diff --git a/.github/workflows/notify.yml b/.github/workflows/notify.yml index e78bfba1..2b1ba765 100644 --- a/.github/workflows/notify.yml +++ b/.github/workflows/notify.yml @@ -1,9 +1,9 @@ -name: Notify Mirror on PR +name: Notify 🪞 on PR on: - pull_request: - types: [opened, synchronize, reopened] - + workflow_dispatch: + #pull_request: + # types: [opened, synchronize, reopened] jobs: trigger-mirror: runs-on: ubuntu-latest @@ -17,11 +17,11 @@ jobs: owner: 'gruntsoftware', repo: 'android', workflow_id: 'sync.yml', - ref: 'main', + ref: 'develop', inputs: { source: '${{ github.event.repository.name }}', pr_number: '${{ github.event.pull_request.number }}', pr_title: '${{ github.event.pull_request.title }}', branch: '${{ github.head_ref }}' } - }) \ No newline at end of file + }) diff --git a/.github/workflows/pr-summary-copilot.yml b/.github/workflows/pr-summary-copilot.yml index f631db1a..cd3c3beb 100644 --- a/.github/workflows/pr-summary-copilot.yml +++ b/.github/workflows/pr-summary-copilot.yml @@ -6,6 +6,7 @@ on: branches: - develop - main + workflow_dispatch: permissions: contents: read diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8d589b90..5e257c02 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,8 +35,8 @@ android { applicationId = "ltd.grunt.brainwallet" minSdk = 29 targetSdk = 35 - versionCode = 202506326 - versionName = "v4.9.1" + versionCode = 202506335 + versionName = "v4.9.3" multiDexEnabled = true base.archivesName.set("${defaultConfig.versionName}(${defaultConfig.versionCode})") testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -86,14 +86,14 @@ android { isDebuggable = true isMinifyEnabled = false } + resValue("string", "firebase_analytics_collection_enabled", "false") } - val release by getting { signingConfig = signingConfigs.getByName("release") isDebuggable = false isMinifyEnabled = true proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") - + resValue("string", "firebase_analytics_collection_enabled", "true") ndk { isDebuggable = false isMinifyEnabled = true @@ -200,6 +200,7 @@ dependencies { implementation(project(":iap")) implementation(project(":core")) implementation("androidx.webkit:webkit:1.9.0") + implementation(libs.androidx.benchmark.traceprocessor) testImplementation(testFixtures(project(":app"))) androidTestImplementation(testFixtures(project(":app"))) implementation(grunt.androidx.core.ktx) @@ -257,6 +258,7 @@ dependencies { implementation(libs.android.installreferrer) implementation("androidx.compose.animation:animation:1.5.0") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7") + implementation("io.coil-kt:coil-compose:2.0.0-rc01") testImplementation(libs.junit) testImplementation(libs.mockk) testImplementation(libs.turbine) diff --git a/app/src/main/java/com/brainwallet/constants/BWConstants.kt b/app/src/main/java/com/brainwallet/constants/BWConstants.kt index c7f5dda4..77c4fef7 100644 --- a/app/src/main/java/com/brainwallet/constants/BWConstants.kt +++ b/app/src/main/java/com/brainwallet/constants/BWConstants.kt @@ -24,8 +24,6 @@ object BWConstants { */ const val NATIVE_LIB_NAME: String = "core-lib" - const val _BW_MAIN_OPEN: String = "bw_main_open" - /** * Permissions */ @@ -106,8 +104,7 @@ object BWConstants { const val SUPPORT_WEB_LINK: String = "https://www.brainwallet.co/support" const val TOS_LINK: String = "https://www.brainwallet.co/privacypolicy" const val LINKTREE_URL: String = "https://linktr.ee/brainwallet" - - const val BITREFILL_URL: String = "https://www.bitrefill.com/?ref=bAshL935" + const val BITREFILL_URL: String = "https://www.bitrefill.com/?ref=bw_tk_26" /** * API Hosts @@ -128,7 +125,7 @@ object BWConstants { const val _20200112_ERR: String = "brainwallet_android_error" const val _20200112_DSR: String = "did_start_resync" const val _20201118_DTGS: String = "did_tap_get_support" - const val _20200217_DUWB: String = "did_unlock_with_biometrics" + const val _20200217_DU: String = "did_unlock" const val _20250303_DSTU: String = "did_skip_top_up" const val _20250517_WCINFO: String = "wallet_callback_info" diff --git a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt index 5d17e732..ddab8a08 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepository.kt @@ -5,11 +5,12 @@ import com.brainwallet.data.model.Fee import com.brainwallet.data.model.LtcStats import com.brainwallet.data.model.MoonpayCurrencyLimit import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow interface LtcRepository { - val rates: StateFlow> + val rates: SharedFlow> val ltcStats: StateFlow suspend fun fetchRates(): List diff --git a/app/src/main/java/com/brainwallet/data/repository/LtcRepositoryImpl.kt b/app/src/main/java/com/brainwallet/data/repository/LtcRepositoryImpl.kt index 0f315d0f..7628af17 100644 --- a/app/src/main/java/com/brainwallet/data/repository/LtcRepositoryImpl.kt +++ b/app/src/main/java/com/brainwallet/data/repository/LtcRepositoryImpl.kt @@ -10,28 +10,56 @@ import com.brainwallet.data.model.LtcStats import com.brainwallet.data.model.MoonpayCurrencyLimit import com.brainwallet.data.repository.LtcRepository.Companion.PREF_KEY_BUY_LIMITS_PREFIX import com.brainwallet.data.repository.LtcRepository.Companion.PREF_KEY_BUY_LIMITS_PREFIX_CACHED_AT +import com.brainwallet.data.source.PeerManagerSource import com.brainwallet.data.source.RemoteApiSource +import com.brainwallet.data.source.RemoteConfigSource import com.brainwallet.data.source.fetchWithCache import com.brainwallet.data.source.response.GetMoonpayBuyQuoteResponse +import com.brainwallet.di.AppModule.json import com.brainwallet.tools.manager.BRSharedPrefs import com.brainwallet.tools.manager.FeeManager import com.brainwallet.tools.sqlite.CurrencyDataSource import com.brainwallet.tools.util.Utils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable import org.koin.core.annotation.Single +import timber.log.Timber @Single(binds = [LtcRepository::class]) class LtcRepositoryImpl( private val context: Context, private val remoteApiSource: RemoteApiSource, + private val remoteConfigSource: RemoteConfigSource, private val currencyDataSource: CurrencyDataSource, + private val peerManagerSource: PeerManagerSource, + private val repositoryScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO), private val sharedPreferences: SharedPreferences, ) : LtcRepository { - private val _rates = MutableStateFlow>(emptyList()) - override val rates: StateFlow> = _rates.asStateFlow() +// private val _rates = MutableStateFlow>( +// currencyDataSource.getAllCurrencies(true) // seed from cache immediately +// ) + private val _rates = MutableSharedFlow>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ).apply { + // seed with cached values immediately, same as before + tryEmit(currencyDataSource.getAllCurrencies(true)) + } + override val rates: SharedFlow> = _rates.asSharedFlow() private val _ltcStats = MutableStateFlow( LtcStats( 0, @@ -40,12 +68,48 @@ class LtcRepositoryImpl( 0 ) ) + + private var syncingDelay = 60_000L + private var nonSyncingDelay = 4_000L + + @Serializable + private data class SyncPoolerContentWrapper( + @SerialName("sync_delay") val syncDelayValue: Long = 60_000L, + @SerialName("non_sync_delay") val nonSyncDelayValue: Long = 4_000L + ) + override val ltcStats: StateFlow = _ltcStats.asStateFlow() + init { + + repositoryScope.launch { + runCatching { remoteConfigSource.fetchAndActivate() } + .onFailure { Timber.e(it, "RemoteConfig fetch failed") } + + val rawJson = remoteConfigSource.getString(RemoteConfigSource.KEY_SYNC_POLLER) + runCatching { + val config = json.decodeFromString(rawJson) + syncingDelay = config.syncDelayValue + nonSyncingDelay = config.nonSyncDelayValue + }.onFailure { Timber.e(it, "RemoteConfig parse failed") } + + while (isActive) { + runCatching { fetchRates() } + .onFailure { Timber.e(it, "fetchRates failed") } + + val delay = if (peerManagerSource.blockInfo.value.syncProgress <= 0.95f) { + syncingDelay + } else { + nonSyncingDelay + } + Timber.d("timber|| duration in fetching $syncingDelay $nonSyncingDelay") + delay(delay) + } + } + } override suspend fun fetchRates(): List { return runCatching { val rates = remoteApiSource.getRates() - FeeManager.updateFeePerKb(context) val selectedISO = BRSharedPrefs.getIsoSymbol(context) rates.forEachIndexed { index, currencyEntity -> @@ -62,7 +126,7 @@ class LtcRepositoryImpl( }.getOrElse { currencyDataSource.getAllCurrencies(true) }.also { result -> - _rates.value = result + _rates.emit(result) } } diff --git a/app/src/main/java/com/brainwallet/data/repository/ShopProxyRepositoryImpl.kt b/app/src/main/java/com/brainwallet/data/repository/ShopProxyRepositoryImpl.kt new file mode 100644 index 00000000..5dbfe795 --- /dev/null +++ b/app/src/main/java/com/brainwallet/data/repository/ShopProxyRepositoryImpl.kt @@ -0,0 +1,50 @@ +package com.brainwallet.data.repository +import com.brainwallet.data.source.RemoteConfigSource +import com.brainwallet.presenter.entities.ShopCard +import com.brainwallet.presenter.entities.ShopConfig +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import timber.log.Timber + +@Serializable +data class ShopProxy( + @SerialName("widget_url") val widget: String = "", + @SerialName("cards") val shopCards: List = emptyList() +) + +interface ShopProxyRepository { + val shopProxy: StateFlow> + suspend fun refresh() +} + +class ShopProxyRepositoryImpl( + private val remoteConfigSource: RemoteConfigSource, + private val json: Json, +) : ShopProxyRepository { + + private val _shopProxy = MutableStateFlow>(emptyList()) + override val shopProxy: StateFlow> = _shopProxy.asStateFlow() + + override suspend fun refresh() { + runCatching { remoteConfigSource.fetchAndActivate() } + .onFailure { Timber.e(it, "RemoteConfig fetch failed") } + + val rawJson = remoteConfigSource.getString(RemoteConfigSource.PATH_SHOP_CONTENT) + Timber.d("ShopProxy rawJson: '$rawJson'") + + runCatching { + val shopConfig = json.decodeFromString(rawJson) + Timber.d("ShopConfig parsed: ${shopConfig.shopData}") + _shopProxy.value = listOf(shopConfig.shopData).map { + ShopProxy( + widget = it.widgetUrl, + shopCards = it.cards + ) + } + }.onFailure { Timber.e(it, "RemoteConfig parse failed") } + } +} diff --git a/app/src/main/java/com/brainwallet/data/repository/SyncAnalyticsRepository.kt b/app/src/main/java/com/brainwallet/data/repository/SyncAnalyticsRepository.kt index 3486dd02..92168928 100644 --- a/app/src/main/java/com/brainwallet/data/repository/SyncAnalyticsRepository.kt +++ b/app/src/main/java/com/brainwallet/data/repository/SyncAnalyticsRepository.kt @@ -40,7 +40,7 @@ class SyncAnalyticsRepository( stopSync() val totalDuration = prefs.getLong(KEY_ACCUMULATED_DURATION, 0L) - if (totalDuration == 0L) return + if (totalDuration <= 8000L) return val endTimestamp = peerManagerSource.getLastBlockTimestamp() val endBlockHeight = peerManagerSource.getCurrentBlockHeight() diff --git a/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt b/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt index 11085d27..7de5a4d8 100644 --- a/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt +++ b/app/src/main/java/com/brainwallet/data/source/RemoteConfigSource.kt @@ -8,6 +8,14 @@ interface RemoteConfigSource { const val KEY_FEATURE_MENU_HIDDEN_EXAMPLE = "feature_menu_hidden_example" const val KEY_FEATURE_SELECTED_PEERS_ENABLED = "feature_selected_peers_enabled" const val KEY_FEATURE_GAMEHUB_CONTENT = "feature_gamehub_content" + const val PATH_SHOP_CONTENT = "path_shop_content" + const val KEY_SYNC_POLLER = "key_sync_poller" + const val KEY_API_BASEURL_PROD_NEW_ENABLED = "key_api_baseurl_prod_new_enabled" + const val KEY_FEATURE_LTC_BROWSER_CONTENT = "feature_ltc_browser_content" + const val KEY_KEYSTORE_MANAGER_ENABLED = "key_keystore_manager_enabled" + const val KEY_DEV_API_BASEURL = "key_dev_api_baseurl" + const val KEY_API_BASEURL_DEV_NEW_ENABLED = "key_api_baseurl_dev_new_enabled" + const val KEY_PROD_API_BASEURL = "key_prod_api_baseurl" } fun initialize() diff --git a/app/src/main/java/com/brainwallet/di/RemoteConfigModule.kt b/app/src/main/java/com/brainwallet/di/RemoteConfigModule.kt index be82670d..26a8a1ca 100644 --- a/app/src/main/java/com/brainwallet/di/RemoteConfigModule.kt +++ b/app/src/main/java/com/brainwallet/di/RemoteConfigModule.kt @@ -3,6 +3,8 @@ package com.brainwallet.di import com.brainwallet.data.repository.FirebaseRemoteConfigRepositoryImpl import com.brainwallet.data.repository.HubContentRepository import com.brainwallet.data.repository.HubContentRepositoryImpl +import com.brainwallet.data.repository.ShopProxyRepository +import com.brainwallet.data.repository.ShopProxyRepositoryImpl import com.brainwallet.data.source.RemoteConfigSource import com.google.firebase.remoteconfig.FirebaseRemoteConfig import org.koin.dsl.module @@ -11,10 +13,14 @@ val remoteConfigModule = module { single { FirebaseRemoteConfig.getInstance().apply { setDefaultsAsync( - mapOf(RemoteConfigSource.KEY_FEATURE_GAMEHUB_CONTENT to "[]") + mapOf( + RemoteConfigSource.KEY_FEATURE_GAMEHUB_CONTENT to "[]", + RemoteConfigSource.PATH_SHOP_CONTENT to "{\"shop_data\":{}}" + ) ) } } single { FirebaseRemoteConfigRepositoryImpl(get()) } single { HubContentRepositoryImpl(get(), get()) } + single { ShopProxyRepositoryImpl(get(), get()) } } diff --git a/app/src/main/java/com/brainwallet/navigation/MainNavigationHost.kt b/app/src/main/java/com/brainwallet/navigation/MainNavigationHost.kt index ef6f005c..e0883d71 100644 --- a/app/src/main/java/com/brainwallet/navigation/MainNavigationHost.kt +++ b/app/src/main/java/com/brainwallet/navigation/MainNavigationHost.kt @@ -148,10 +148,10 @@ fun NavGraphBuilder.mainNavGraph( } composable { navBackStackEntry -> - val route: Route.LinktreeWeb = navBackStackEntry.toRoute() + val route: Route.BitrefillWeb = navBackStackEntry.toRoute() WebModalScreen( onNavigate = onNavigate, - url = BWConstants.BITREFILL_URL + url = route.url ) } } diff --git a/app/src/main/java/com/brainwallet/navigation/Route.kt b/app/src/main/java/com/brainwallet/navigation/Route.kt index f17cdf26..3f2385e4 100644 --- a/app/src/main/java/com/brainwallet/navigation/Route.kt +++ b/app/src/main/java/com/brainwallet/navigation/Route.kt @@ -65,7 +65,7 @@ sealed class Route : JavaSerializable { object MoonPayBuy : Route() @Serializable - object BitrefillWeb : Route() + data class BitrefillWeb(val url: String) : Route() @Serializable object LinktreeWeb : Route() diff --git a/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java index 39ee3517..51828632 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/BreadActivity.java @@ -117,82 +117,15 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_bread); AnalyticsManager.logCustomEvent(BWConstants._HOME_OPEN); - app = this; getWindowManager().getDefaultDisplay().getSize(screenParametersPoint); - initializeViews(); - setPriceTags(BRSharedPrefs.getLTCViewingPreference(BreadActivity.this), false); setListeners(); - setUpBarFlipper(); - checkTransactionDatabase(); primaryPrice.setTextSize(PRIMARY_TEXT_SIZE); secondaryPrice.setTextSize(SECONDARY_TEXT_SIZE); - onConnectionChanged(InternetManager.getInstance().isConnected(this)); - - updateUI(); bottomNav.setSelectedItemId(R.id.nav_history); - - setupNotificationPermission(); - showInAppReviewDialogIfNeeded(); - } - - private void setupNotificationPermission() { - //https://developer.android.com/develop/ui/views/notifications/notification-permission - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { - return; - } - - if (!PermissionUtil.hasPermission(this, Manifest.permission.POST_NOTIFICATIONS)) { - if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.POST_NOTIFICATIONS)) { - new AlertDialog.Builder(this) - .setTitle(R.string.permission_info) - .setMessage(R.string.please_grant_notification_permission) - .setNegativeButton(R.string.cancel, (dialog, which) -> { - dialog.dismiss(); - }) - .setPositiveButton(R.string.ok, (dialog, which) -> { - PermissionUtil.requestPermission(requestNotificationPermissionLauncher, Manifest.permission.POST_NOTIFICATIONS); - }) - .show(); - } else { - PermissionUtil.requestPermission(requestNotificationPermissionLauncher, Manifest.permission.POST_NOTIFICATIONS); - } - } - } - - private void finishActivities(BRActivity... activities) { - for (BRActivity activity : activities) { - if (activity != null) activity.finish(); - } - } - - private void showInAppReviewDialogIfNeeded() { - if (!BRSharedPrefs.isInAppReviewDone(this) && BRSharedPrefs.getSendTransactionCount(this) > 2) { - ReviewManager manager = ReviewManagerFactory.create(this); - Task request = manager.requestReviewFlow(); - request.addOnCompleteListener(task -> { - AnalyticsManager.logCustomEvent(BWConstants._20241006_DRR); - if (task.isSuccessful()) { - ReviewInfo reviewInfo = task.getResult(); - Task flow = manager.launchReviewFlow(BreadActivity.this, reviewInfo); - flow.addOnCompleteListener(task1 -> { - // The flow has finished. The API does not indicate whether the user - // reviewed or not, or even whether the review dialog was shown. Thus, no - // matter the result, we continue our app flow. - Timber.i("timber: In-app LaunchReviewFlow completed successful (%s)", task1.isSuccessful()); - if (task1.isSuccessful()) { - BRSharedPrefs.inAppReviewDone(BreadActivity.this); - AnalyticsManager.logCustomEvent(BWConstants._20241006_UCR); - } - }); - } else { - Timber.e(task.getException(), "In-app request review flow failed"); - } - }); - } } private void addObservers() { @@ -222,8 +155,6 @@ protected void onNewIntent(Intent intent) { private void setListeners() { bottomNav.setOnNavigationItemSelectedListener(item -> handleNavigationItemSelected(item.getItemId())); - primaryPrice.setOnClickListener(v -> swap()); - secondaryPrice.setOnClickListener(v -> swap()); menuBut.setOnClickListener(v -> { if (BRAnimator.isClickAllowed()) { drawerLayout.open(); @@ -240,78 +171,6 @@ public boolean handleNavigationItemSelected(int menuItemId) { return true; } - private void swap() { - if (!BRAnimator.isClickAllowed()) return; - boolean b = !BRSharedPrefs.getLTCViewingPreference(this); - setPriceTags(b, true); - BRSharedPrefs.putLTCViewingPreference(this, b); - } - - private void setPriceTags(boolean ltcPreferred, boolean animate) { - ConstraintSet set = new ConstraintSet(); - set.clone(toolBarConstraintLayout); - - if (animate) { - TransitionSet textSizeTransition = new TransitionSet() - .setOrdering(TransitionSet.ORDERING_TOGETHER) - .addTransition(new TextSizeTransition()) - .addTransition(new ChangeBounds()); - - TransitionSet transition = new TransitionSet() - .setOrdering(TransitionSet.ORDERING_SEQUENTIAL) - .addTransition(new Fade(Fade.OUT)) - .addTransition(textSizeTransition) - .addTransition(new Fade(Fade.IN)); - TransitionManager.beginDelayedTransition(toolBarConstraintLayout, transition); - } - - primaryPrice.setTextSize(ltcPreferred ? PRIMARY_TEXT_SIZE : SECONDARY_TEXT_SIZE); - secondaryPrice.setTextSize(ltcPreferred ? SECONDARY_TEXT_SIZE : PRIMARY_TEXT_SIZE); - - int[] ids = {primaryPrice.getId(), secondaryPrice.getId(), equals.getId()}; - // Clear views constraints - for (int id : ids) { - set.clear(id); - set.constrainWidth(id, ConstraintSet.WRAP_CONTENT); - set.constrainHeight(id, ConstraintSet.WRAP_CONTENT); - } - - int dp16 = Utils.getPixelsFromDps(this, 16); - int dp8 = Utils.getPixelsFromDps(this, 4); - - int leftId = ltcPreferred ? primaryPrice.getId() : secondaryPrice.getId(); - int rightId = ltcPreferred ? secondaryPrice.getId() : primaryPrice.getId(); - - int[] chainViews = {leftId, equals.getId(), rightId}; - - set.connect(leftId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, dp16); - set.connect(leftId, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP); - set.connect(leftId, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM, dp16); - set.setVerticalBias(leftId, 1.0f); - - set.connect(rightId, ConstraintSet.BASELINE, leftId, ConstraintSet.BASELINE); - set.connect(equals.getId(), ConstraintSet.BASELINE, leftId, ConstraintSet.BASELINE); - - set.connect(equals.getId(), ConstraintSet.START, leftId, ConstraintSet.END, dp8); - set.connect(equals.getId(), ConstraintSet.END, rightId, ConstraintSet.START, dp8); - - set.createHorizontalChain(leftId, ConstraintSet.LEFT, equals.getId(), ConstraintSet.RIGHT, chainViews, null, ConstraintSet.CHAIN_PACKED); - - // Apply the changes - set.applyTo(toolBarConstraintLayout); - - mHandler.postDelayed(() -> updateUI(), toolBarConstraintLayout.getLayoutTransition().getDuration(LayoutTransition.CHANGING)); - } - - private void checkTransactionDatabase() { - - } - - private void setUpBarFlipper() { - barFlipper.setInAnimation(AnimationUtils.loadAnimation(this, R.anim.flipper_enter)); - barFlipper.setOutAnimation(AnimationUtils.loadAnimation(this, R.anim.flipper_exit)); - } - @Override protected void onRestart() { super.onRestart(); @@ -325,23 +184,13 @@ protected void onResume() { app = this; addObservers(); - setupNetworking(); - if (!BRWalletManager.getInstance().isCreated()) { BRExecutor.getInstance().forBackgroundTasks().execute(() -> BRWalletManager.getInstance().initWallet(BreadActivity.this)); } - mHandler.postDelayed(() -> updateUI(), 1000); BRWalletManager.getInstance().refreshBalance(this); } - private void setupNetworking() { - if (mConnectionReceiver == null) mConnectionReceiver = InternetManager.getInstance(); - IntentFilter mNetworkStateFilter = new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION); - registerReceiver(mConnectionReceiver, mNetworkStateFilter); - InternetManager.addConnectionListener(this); - } - @Override protected void onPause() { super.onPause(); @@ -355,87 +204,9 @@ protected void onDestroy() { unregisterReceiver(mConnectionReceiver); } - private void initializeViews() { - menuBut = findViewById(R.id.menuBut); - - navigationDrawer = findViewById(R.id.navigationDrawer); - drawerLayout = findViewById(R.id.drawerLayout); - homeSettingDrawerComposeView.observeBus(message -> { - drawerLayout.close(); - if (SettingsViewModel.LEGACY_EFFECT_ON_LOCK.equals(message.getMessage())) { - LegacyNavigation.startBrainwalletActivity(this, true); - } else if (SettingsViewModel.LEGACY_EFFECT_ON_TOGGLE_DARK_MODE.equals(message.getMessage())) { - LegacyNavigation.restartBreadActivity(this); - } else if (SettingsViewModel.LEGACY_EFFECT_ON_SEC_UPDATE_PIN.equals(message.getMessage())) { - Intent intent = BrainwalletActivity.createIntent(this, new Route.UnLock(true)); - intent.putExtra("noPin", true); - startActivity(intent); - } else if (SettingsViewModel.LEGACY_EFFECT_ON_SEED_PHRASE.equals(message.getMessage())) { - PostAuth.getInstance().onPhraseCheckAuth(this, true); - } else if (SettingsViewModel.LEGACY_EFFECT_ON_SHARE_ANALYTICS_DATA_TOGGLE.equals(message.getMessage())) { - boolean currentShareAnalyticsDataEnabled = BRSharedPrefs.getShareData(this); - BRSharedPrefs.putShareData(this, !currentShareAnalyticsDataEnabled); - } else if (SettingsViewModel.LEGACY_EFFECT_ON_SYNC.equals(message.getMessage())) { - Intent intent = new Intent(this, SyncBlockchainActivity.class); - startActivity(intent); - overridePendingTransition(R.anim.enter_from_right, R.anim.exit_to_left); - } - return null; - }); //since we are still using this BreadActivity, need to observe EventBus e.g. lock from [HomeSettingDrawerSheet] - - bottomNav = findViewById(R.id.bottomNav); - bottomNav.getMenu().clear(); - bottomNav.inflateMenu(R.menu.bottom_nav_menu); - balanceTxtV = findViewById(R.id.balanceTxtV); - - primaryPrice = findViewById(R.id.primary_price); - secondaryPrice = findViewById(R.id.secondary_price); - equals = findViewById(R.id.equals); - toolBarConstraintLayout = findViewById(R.id.bread_toolbar); - - barFlipper = findViewById(R.id.tool_bar_flipper); - - final ViewTreeObserver observer = primaryPrice.getViewTreeObserver(); - observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - if (observer.isAlive()) { - observer.removeOnGlobalLayoutListener(this); - } - if (uiIsDone) return; - uiIsDone = true; - setPriceTags(BRSharedPrefs.getLTCViewingPreference(BreadActivity.this), false); - } - }); - - balanceTxtV.append(":"); - } - @Override public void onBalanceChanged(final long balance) { - updateUI(); - } - - public void updateUI() { - BRExecutor.getInstance().forLightWeightBackgroundTasks().execute(() -> { - Thread.currentThread().setName(Thread.currentThread().getName() + ":updateUI"); - //sleep a little in order to make sure all the commits are finished (like SharePreferences commits) - String iso = BRSharedPrefs.getIsoSymbol(BreadActivity.this); - - //current amount in litoshis - final BigDecimal amount = new BigDecimal(BRSharedPrefs.getCachedBalance(BreadActivity.this)); - - //amount in LTC units - BigDecimal btcAmount = BRExchange.getLitecoinForLitoshis(BreadActivity.this, amount); - final String formattedBTCAmount = BRCurrency.getFormattedCurrencyString(BreadActivity.this, "LTC", btcAmount); - final BigDecimal curAmount = BRExchange.getAmountFromLitoshis(BreadActivity.this, iso, amount); - final String formattedCurAmount = BRCurrency.getFormattedCurrencyString(BreadActivity.this, iso, curAmount); - runOnUiThread(() -> { - primaryPrice.setText(formattedBTCAmount); - secondaryPrice.setText(String.format("%s", formattedCurAmount)); - }); - }); } @Override @@ -443,64 +214,11 @@ public void onTxAdded() { BRWalletManager.getInstance().refreshBalance(BreadActivity.this); } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - switch (requestCode) { - case BWConstants.CAMERA_REQUEST_ID: { - // If request is cancelled, the result arrays are empty. - if (grantResults.length > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - BRAnimator.openScanner(this, BWConstants.SCANNER_REQUEST); - // permission was granted, yay! Do the - // contacts-related task you need to do. - } else { - // permission denied, boo! Disable the - // functionality that depends on this permission. - } - return; - } - // other 'case' lines to check for other - // permissions this app might request - } - } - @Override public void onConnectionChanged(boolean isConnected) { Context thisContext = BreadActivity.this; Context app = getApplicationContext(); - if (isConnected) { - if (barFlipper != null) { - if (barFlipper.getDisplayedChild() == 1) { - removeNotificationBar(); - } - } - BRExecutor.getInstance().forLightWeightBackgroundTasks().execute(() -> { - final double progress = BRPeerManager.syncProgress(BRSharedPrefs.getStartHeight(thisContext)); - if (progress > 0 && progress < 1) { - SyncManager.getInstance().startSyncingProgressThread(app); - } - }); - } else { - if (barFlipper != null) { - addNotificationBar(); - } - SyncManager.getInstance().stopSyncingProgressThread(app); - } - } - - public void removeNotificationBar() { - if (barFlipper.getChildCount() == 1) return; - barFlipper.removeViewAt(1); - barFlipper.setDisplayedChild(0); - } - public void addNotificationBar() { - if (barFlipper.getChildCount() == 2) return; - BRNotificationBar view = new BRNotificationBar(this); - barFlipper.addView(view); - barFlipper.setDisplayedChild(1); } } diff --git a/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java b/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java index 6644a386..8f3e93f1 100644 --- a/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java +++ b/app/src/main/java/com/brainwallet/presenter/activities/util/BRActivity.java @@ -9,12 +9,9 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.os.LocaleListCompat; - import com.brainwallet.BrainwalletApp; import com.brainwallet.data.repository.SettingRepository; import com.brainwallet.presenter.activities.DisabledActivity; -import com.brainwallet.presenter.activities.intro.RecoverActivity; -import com.brainwallet.presenter.activities.intro.WriteDownActivity; import com.brainwallet.tools.animation.BRAnimator; import com.brainwallet.tools.manager.InternetManager; import com.brainwallet.tools.security.AuthManager; @@ -24,7 +21,6 @@ import com.brainwallet.tools.threads.BRExecutor; import com.brainwallet.constants.BWConstants; import com.brainwallet.wallet.BRWalletManager; -import com.brainwallet.worker.CurrencyUpdateWorker; import org.koin.java.KoinJavaComponent; @@ -144,12 +140,6 @@ public void run() { public static void init(Activity app) { InternetManager.getInstance(); - - if (!(app instanceof RecoverActivity || app instanceof WriteDownActivity)) { - CurrencyUpdateWorker currencyUpdateWorker = KoinJavaComponent.get(CurrencyUpdateWorker.class); - currencyUpdateWorker.start(); - } - //show wallet locked if it is if (!ActivityUTILS.isAppSafe(app)) if (AuthManager.getInstance().isWalletDisabled(app)) diff --git a/app/src/main/java/com/brainwallet/presenter/customviews/BRNotificationBar.java b/app/src/main/java/com/brainwallet/presenter/customviews/BRNotificationBar.java index 4eee8df8..48551db0 100644 --- a/app/src/main/java/com/brainwallet/presenter/customviews/BRNotificationBar.java +++ b/app/src/main/java/com/brainwallet/presenter/customviews/BRNotificationBar.java @@ -43,9 +43,8 @@ private void init() { @Override public void onClick(View v) { if (getContext() instanceof BreadActivity) { - ((BreadActivity) getContext()).removeNotificationBar(); } } }); } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/brainwallet/presenter/entities/ShopItems.kt b/app/src/main/java/com/brainwallet/presenter/entities/ShopItems.kt new file mode 100644 index 00000000..ac8048ff --- /dev/null +++ b/app/src/main/java/com/brainwallet/presenter/entities/ShopItems.kt @@ -0,0 +1,41 @@ +package com.brainwallet.presenter.entities + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ShopConfig( + @SerialName("shop_data") + val shopData: ShopData +) + +@Serializable +data class ShopData( + @SerialName("widget_url") + val widgetUrl: String, + val cards: List +) + +@Serializable +data class ShopCard( + @SerialName("country_code") + val countryCode: String, + + @SerialName("country_name") + val countryName: String, + + @SerialName("product_slug") + val productSlug: String, + + @SerialName("product_name") + val productName: String, + + @SerialName("product_url") + val productUrl: String, + + @SerialName("card_image_webp") + val cardImageWebP: String +) { + // Computed property — same as Swift's `var id` + val id: String get() = productSlug +} diff --git a/app/src/main/java/com/brainwallet/tools/util/Extension.kt b/app/src/main/java/com/brainwallet/tools/util/Extension.kt index 89a6dc58..16935946 100644 --- a/app/src/main/java/com/brainwallet/tools/util/Extension.kt +++ b/app/src/main/java/com/brainwallet/tools/util/Extension.kt @@ -1,6 +1,7 @@ package com.brainwallet.tools.util import android.content.res.Configuration +import android.net.Uri import android.os.Build import androidx.annotation.StringRes import androidx.fragment.app.Fragment @@ -59,6 +60,20 @@ fun String.Companion.join( return stringBuilder.toString() } +fun String.withTheme(theme: String): String { + val uri = Uri.parse(this) + return uri.buildUpon() + .clearQuery() + .apply { + uri.queryParameterNames.forEach { key -> + val value = if (key == "theme") theme else uri.getQueryParameter(key) + appendQueryParameter(key, value) + } + } + .build() + .toString() +} + /** * TextInputLayout extension */ diff --git a/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt b/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt index 1d9b5c08..64f57370 100644 --- a/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt +++ b/app/src/main/java/com/brainwallet/ui/BrainwalletActivity.kt @@ -97,7 +97,6 @@ class BrainwalletActivity : override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - AnalyticsManager.logCustomEvent(BWConstants._BW_MAIN_OPEN) onConnectionChanged(InternetManager.getInstance().isConnected(this)) BRSharedPrefs.getLTCViewingPreference(application) @@ -364,7 +363,7 @@ class BrainwalletActivity : if (AuthManager.getInstance().checkAuth(passcode.joinToString(""), this)) { AuthManager.getInstance().authSuccess(this) - AnalyticsManager.logCustomEvent(BWConstants._20200217_DUWB) + AnalyticsManager.logCustomEvent(BWConstants._20200217_DU) LegacyNavigation.startBrainwalletActivity(this, false) } else { // Auth fail toast diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoScreen.kt b/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoScreen.kt index 1d443c5a..0785abc0 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoScreen.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp @@ -38,7 +37,6 @@ import com.brainwallet.ui.composable.WheelPickerFocusVertical import com.brainwallet.ui.composable.rememberWheelPickerState import com.brainwallet.ui.theme.IBMPlexSans import com.brainwallet.ui.theme.mainBentoSurface -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -68,18 +66,15 @@ fun LTCPickerBentoScreen( } val wheelPickerState = rememberWheelPickerState(initialIndex = 0) - var resizedFiatFontSize by remember { mutableStateOf(16.sp) } - var resizedCurrencyNameFontSize by remember { mutableStateOf(18.sp) } - var resizedAsOfTimestampFontSize by remember { mutableStateOf(12.sp) } + var resizedFiatFontSize by remember { mutableStateOf(26.sp) } + var resizedAsOfTimestampFontSize by remember { mutableStateOf(13.sp) } val context = LocalContext.current - // Set the initial index to the selected fiat currency - LaunchedEffect(Unit) { - delay(500) - wheelPickerState.scrollToIndex(state.getSelectedFiatRateIndex()) + val selectedIndex = state.getSelectedFiatRateIndex() + LaunchedEffect(selectedIndex) { + wheelPickerState.scrollToIndex(selectedIndex) } - // Listen for changes in the selected fiat currency index LaunchedEffect(wheelPickerState) { snapshotFlow { wheelPickerState.currentIndex } .filter { it > -1 } @@ -117,7 +112,43 @@ fun LTCPickerBentoScreen( ) .padding(horizontal = 6.dp, vertical = 2.dp) ) - Spacer(modifier = Modifier.weight(0.5f)) + Text( + text = state.formattedFiat, + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + onTextLayout = { textLayoutResult -> + if (textLayoutResult.hasVisualOverflow) { + resizedFiatFontSize *= 0.95f + } + }, + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.Normal, + fontSize = resizedFiatFontSize, + color = if (state.darkMode) Color.White else Color.Black + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + Text( + text = state.formattedTimeStamp, + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + onTextLayout = { textLayoutResult -> + if (textLayoutResult.hasVisualOverflow) { + resizedAsOfTimestampFontSize *= 0.95f + } + }, + style = TextStyle( + fontFamily = IBMPlexSans, + fontWeight = FontWeight.Normal, + fontSize = resizedAsOfTimestampFontSize, + color = if (state.darkMode) Color.White else Color.Black + ), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + + Spacer(modifier = Modifier.weight(1f)) VerticalWheelPicker( modifier = Modifier .fillMaxWidth() @@ -151,65 +182,6 @@ fun LTCPickerBentoScreen( ) ) } - Text( - text = "${state.selectedCurrency.name}", - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp), - onTextLayout = { textLayoutResult -> - if (textLayoutResult.hasVisualOverflow) { - resizedCurrencyNameFontSize *= 0.95f - } - }, - style = TextStyle( - fontFamily = IBMPlexSans, - fontWeight = FontWeight.Light, - fontSize = resizedCurrencyNameFontSize, - color = if (state.darkMode) Color.White else Color.Black - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - text = state.formattedFiat, - modifier = Modifier.fillMaxWidth().padding(bottom = 4.dp), - onTextLayout = { textLayoutResult -> - if (textLayoutResult.hasVisualOverflow) { - resizedFiatFontSize *= 0.95f - } - }, - style = TextStyle( - fontFamily = IBMPlexSans, - fontWeight = FontWeight.Normal, - fontSize = resizedFiatFontSize, - color = if (state.darkMode) Color.White else Color.Black - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - text = stringResource( - com.brainwallet.R.string.ltc_ticker_as_of_timestamp, - state.formattedTimeStamp - ), - modifier = Modifier - .fillMaxWidth() - .padding(top = 2.dp, bottom = 4.dp), - onTextLayout = { textLayoutResult -> - if (textLayoutResult.hasVisualOverflow) { - resizedAsOfTimestampFontSize *= 0.95f - } - }, - style = TextStyle( - fontFamily = IBMPlexSans, - fontWeight = FontWeight.Normal, - fontSize = resizedAsOfTimestampFontSize, - color = if (state.darkMode) Color.White else Color.Black, - textAlign = TextAlign.Start - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) } } } diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoViewModel.kt b/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoViewModel.kt index 0254e925..a2128ac6 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoViewModel.kt @@ -1,12 +1,12 @@ package com.brainwallet.ui.bentosections.ltcpickerbento import androidx.lifecycle.viewModelScope +import com.brainwallet.constants.BWConstants import com.brainwallet.data.repository.LtcRepository import com.brainwallet.data.repository.SettingRepository import com.brainwallet.tools.sqlite.CurrencyDataSource import com.brainwallet.ui.BrainwalletViewModel -import com.google.firebase.crashlytics.FirebaseCrashlytics -import kotlinx.coroutines.delay +import com.brainwallet.util.CurrencyDataGetter import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -14,31 +14,24 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.android.annotation.KoinViewModel import timber.log.Timber +import java.math.BigDecimal @KoinViewModel class LTCPickerBentoViewModel( private val settingRepository: SettingRepository, private val currencyDataSource: CurrencyDataSource, + private val currencyDataGetter: CurrencyDataGetter, private val ltcRepository: LtcRepository ) : BrainwalletViewModel() { private val _state = MutableStateFlow(LTCPickerBentoState()) val state: StateFlow = _state.asStateFlow() val formatter = java.text.SimpleDateFormat( - "MMM dd, yyyy h:mm:ss a", + "MMM dd, h:mm:ss a", java.util.Locale.getDefault() ) init { - viewModelScope.launch { - while (true) { - try { fetchAndUpdateRates() } catch (e: Exception) { - Timber.e(e, "fetchRates failed") - FirebaseCrashlytics.getInstance().recordException(e) - } - delay(5000L) - } - } viewModelScope.launch { settingRepository.settings.collect { setting -> _state.update { @@ -60,27 +53,38 @@ class LTCPickerBentoViewModel( } } } - } - private suspend fun fetchAndUpdateRates( - currencyCode: String = settingRepository.currentSettings.value.currency.code - ) { - val rates = ltcRepository.fetchRates() - val selectedFiat = rates.find { it.code == currencyCode } - var formattedFiat = "" - val msg = "||fetchRates ${selectedFiat?.rate} fetch ltc stats: $state.ltcStats.currentBlockHeight" - Timber.d("timber: ltcStats: $state.ltcStats.value") - Timber.d(msg) - FirebaseCrashlytics.getInstance().log(msg) - _state.update { - it.copy( - selectedCurrency = selectedFiat ?: return@update it, - formattedFiat = "${selectedFiat.symbol} ${"%6.2f".format(selectedFiat.rate)}", - formattedTimeStamp = formatter.format(java.util.Date()), - ltcStats = ltcRepository.ltcStats.value - ) + viewModelScope.launch { + ltcRepository.rates.collect { rates -> + runCatching { + Timber.d("timber|| rates in fetching $rates") + val currencyCode = _state.value.selectedCurrency.code + val selectedFiat = rates.find { it.code == currencyCode } + val iso = currencyDataGetter.getIsoSymbol() + val currency = currencyDataGetter.getCurrencyByIso(iso) + + val formattedCurrency = currency?.let { + val rounded = BigDecimal(it.rate.toDouble()) + .multiply(BigDecimal(100)) + .divide(BigDecimal(100), 2, BWConstants.ROUNDING_MODE) + currencyDataGetter.getFormattedCurrencyString(iso, rounded) + } + + if (selectedFiat != null) { + _state.update { + it.copy( + selectedCurrency = selectedFiat, + formattedFiat = formattedCurrency ?: "", + formattedTimeStamp = formatter.format(java.util.Date()), + ltcStats = ltcRepository.ltcStats.value + ) + } + } + }.onFailure { Timber.e(it, "timber|| rates collector crashed") } + } } } + override fun onEvent(event: LTCPickerBentoEvent) { when (event) { is LTCPickerBentoEvent.OnGlobalCurrencyChange -> { @@ -88,7 +92,6 @@ class LTCPickerBentoViewModel( if (newCurrency != null) { viewModelScope.launch { settingRepository.save(settingRepository.currentSettings.value.copy(currency = newCurrency)) - fetchAndUpdateRates(newCurrency.code) } } else { Timber.w("Currency not found for code: ${event.globalCurrency.code}") diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/GiftCardsComposable.kt b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/GiftCardsComposable.kt index e09efb5d..9d40e5d0 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/GiftCardsComposable.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/GiftCardsComposable.kt @@ -12,52 +12,49 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.brainwallet.ui.bentosections.shopbento.cards.CardAmazonComposable -import com.brainwallet.ui.bentosections.shopbento.cards.CardJustEatComposable -import com.brainwallet.ui.bentosections.shopbento.cards.CardVisaComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue +import com.brainwallet.ui.bentosections.shopbento.cards.SingleCardComposable @Composable fun GiftCardsComposable( - state: ShopBentoState, - cardData: String, + imageURLOne: String, + imageURLTwo: String, + imageURLThree: String, modifier: Modifier = Modifier, ) { - val cornerRadius = 4.dp var appeared by remember { mutableStateOf(false) } LaunchedEffect(Unit) { appeared = true } - val offsetVisa by animateFloatAsState( - targetValue = if (appeared) -10f else 100f, + val offsetCardOne by animateFloatAsState( + targetValue = if (appeared) 5f else 100f, animationSpec = tween( 500, - delayMillis = 200 + delayMillis = 400 ), - label = "visaSlide" + label = "cardOneSlide" ) - val offsetJE by animateFloatAsState( + val offsetCardTwo by animateFloatAsState( targetValue = if (appeared) 30f else 100f, animationSpec = tween( 600, - delayMillis = 200 + delayMillis = 400 ), - label = "jeSlide" + label = "cardTwoSlide" ) - val offsetAmazon by animateFloatAsState( - targetValue = if (appeared) -12f else 50f, + val offsetCardThree by animateFloatAsState( + targetValue = if (appeared) 30f else 50f, animationSpec = tween( 400, - delayMillis = 200 + delayMillis = 400 ), - label = "amazonSlide" + label = "cardThreeSlide" ) Box( @@ -68,35 +65,27 @@ fun GiftCardsComposable( Column( modifier = Modifier .fillMaxHeight() - .offset(x = offsetVisa.dp, y = 5.dp) + .offset(x = offsetCardOne.dp, y = 10.dp) ) { - CardVisaComposable(rotation = 20f) + SingleCardComposable(rotation = 20f, modelString = imageURLOne) } Column( modifier = Modifier .fillMaxHeight() - .offset(x = offsetAmazon.dp, y = -15.dp) + .offset(x = offsetCardTwo.dp, y = -20.dp) ) { - CardAmazonComposable(rotation = 20f) + SingleCardComposable(rotation = 20f, modelString = imageURLTwo) } Column( modifier = Modifier .fillMaxHeight() - .offset(x = offsetJE.dp, y = 15.dp) + .offset(x = offsetCardThree.dp, y = 35.dp) ) { - CardJustEatComposable(rotation = -30f) + SingleCardComposable(rotation = -30f, modelString = imageURLThree) } } } - -@Preview -@Composable -private fun GiftCardsComposablePreview() { - Box(modifier = Modifier) { - GiftCardsComposable(state = ShopBentoState(), cardData = "acr") - } -} diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoEvent.kt b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoEvent.kt index fe0a5b02..723c1a29 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoEvent.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoEvent.kt @@ -3,4 +3,6 @@ package com.brainwallet.ui.bentosections.shopbento sealed class ShopBentoEvent { data object OnLoad : ShopBentoEvent() data object OnTapShop : ShopBentoEvent() + + object InvoiceCreated : ShopBentoEvent() } diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoScreen.kt b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoScreen.kt index 037df567..4edc666a 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.runtime.collectAsState import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle @Composable fun ShopBentoScreen( @@ -61,6 +62,8 @@ private fun ShopBentoScreen( val logotypeWhite = R.drawable.logotype_bitrefill_wht val logotypeBitrefill = if (state.darkMode) logotypeWhite else logotypeBlack + val shopBentoState by viewModel.state.collectAsStateWithLifecycle() + Box( modifier = Modifier .fillMaxSize() @@ -118,10 +121,10 @@ private fun ShopBentoScreen( ) { GiftCardsComposable( - state = state, - modifier = - Modifier.fillMaxSize(), - cardData = "test-json" + imageURLOne = shopBentoState.cardImageURL1, + imageURLTwo = shopBentoState.cardImageURL2, + imageURLThree = shopBentoState.cardImageURL3, + modifier = Modifier.fillMaxSize() ) } } diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoState.kt b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoState.kt index cd924d48..4040a562 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoState.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoState.kt @@ -1,9 +1,16 @@ package com.brainwallet.ui.bentosections.shopbento +import com.brainwallet.presenter.entities.ShopCard + data class ShopBentoState( val cardData: String = "", + val shopCards: List = emptyList(), val regionData: String = "", val countryIso: String = "US", val darkMode: Boolean = true, - val shouldSlide: Boolean = false + val shouldSlide: Boolean = false, + val shopBaseUrl: String = "", + val cardImageURL1: String = "", + val cardImageURL2: String = "", + val cardImageURL3: String = "" ) diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModel.kt b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModel.kt index b3d6ce44..9c9f2d59 100644 --- a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModel.kt @@ -1,7 +1,6 @@ package com.brainwallet.ui.bentosections.shopbento import android.app.Application -import android.content.Context import androidx.lifecycle.viewModelScope import com.brainwallet.ui.BrainwalletViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -11,41 +10,60 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.android.annotation.KoinViewModel import java.util.Locale -import android.telephony.TelephonyManager import com.brainwallet.data.repository.SettingRepository +import com.brainwallet.data.repository.ShopProxyRepository +import com.brainwallet.tools.manager.AnalyticsManager @KoinViewModel class ShopBentoViewModel( private val app: Application, private val settingRepository: SettingRepository, + private val shopProxyRepository: ShopProxyRepository ) : BrainwalletViewModel() { private val _state = MutableStateFlow(ShopBentoState()) val state: StateFlow = _state.asStateFlow() + val currentCountryISO: String = Locale.getDefault().country.ifEmpty { "US" } init { viewModelScope.launch { settingRepository.settings.collect { setting -> _state.update { it.copy( darkMode = setting.isDarkMode, - countryIso = getCountryIso() + countryIso = currentCountryISO ) } } } - } - private fun getCountryIso(): String { - val telephonyManager = app.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - val simCountry = telephonyManager.simCountryIso - val networkCountry = telephonyManager.networkCountryIso + viewModelScope.launch { + shopProxyRepository.refresh() + shopProxyRepository.shopProxy.collect { shopList -> + val widget = shopList.firstOrNull()?.widget.orEmpty() + val cards = shopList.firstOrNull()?.shopCards.orEmpty() + .filter { it.countryCode == currentCountryISO } + var imageUrl1 = "" + var imageUrl2 = "" + var imageUrl3 = "" - return when { - simCountry.isNotEmpty() -> simCountry.uppercase() - networkCountry.isNotEmpty() -> networkCountry.uppercase() - else -> Locale.getDefault().country.ifEmpty { "US" } + if (cards.count() >= 3) { + imageUrl1 = cards[0].cardImageWebP + imageUrl2 = cards[1].cardImageWebP + imageUrl3 = cards[2].cardImageWebP + } + _state.update { + it.copy( + shopBaseUrl = widget, + shopCards = cards, + cardImageURL1 = imageUrl1, + cardImageURL2 = imageUrl2, + cardImageURL3 = imageUrl3 + ) + } + } } } + override fun onEvent(event: ShopBentoEvent) { when (event) { is ShopBentoEvent.OnLoad -> { @@ -57,6 +75,13 @@ class ShopBentoViewModel( ) } } + is ShopBentoEvent.InvoiceCreated -> { + AnalyticsManager + .logCustomEventWithParams( + "user_shop_invoice_created", + null + ) + } } } } diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/cards/CardComposables.kt b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/cards/CardComposables.kt new file mode 100644 index 00000000..92e77dc1 --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/cards/CardComposables.kt @@ -0,0 +1,60 @@ +package com.brainwallet.ui.bentosections.shopbento.cards + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.unit.dp +import androidx.compose.ui.draw.rotate +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.size +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import coil.compose.rememberAsyncImagePainter + +@Composable +fun SingleCardComposable( + rotation: Float, + modelString: String, + modifier: Modifier = Modifier, +) { + val cornerRadius = 6.dp + + BoxWithConstraints(modifier = Modifier.fillMaxSize().rotate(rotation)) { + val cardWidth = maxWidth * 0.7f + val cardHeight = cardWidth / 1.586f + + Box( + modifier = Modifier + .size(width = cardWidth, height = cardHeight) + .align(Alignment.Center) + .clip(RoundedCornerShape(cornerRadius)) + ) { + Image( + painter = rememberAsyncImagePainter(modelString), + contentDescription = "card_image", + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + Box( + modifier = Modifier + .fillMaxSize() + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + Color.Black.copy(alpha = 0.15f) + ) + ) + ) + ) + } + } +} diff --git a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/cards/Cards.kt b/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/cards/Cards.kt deleted file mode 100644 index ba5fe454..00000000 --- a/app/src/main/java/com/brainwallet/ui/bentosections/shopbento/cards/Cards.kt +++ /dev/null @@ -1,165 +0,0 @@ -package com.brainwallet.ui.bentosections.shopbento.cards - -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.drawscope.rotate -import androidx.compose.ui.unit.dp -import com.brainwallet.ui.theme.giftCardGradient2 -import com.brainwallet.R -import androidx.compose.ui.draw.rotate -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.size -import androidx.compose.ui.Alignment -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import com.brainwallet.ui.theme.giftCardGradient1 -import com.brainwallet.ui.theme.giftCardGradient3 - -@Composable -fun CardAmazonComposable( - rotation: Float, - modifier: Modifier = Modifier, -) { - val cornerRadius = 4.dp - Box(modifier = Modifier.fillMaxSize()) { - Canvas( - modifier = Modifier - .fillMaxSize() - .offset(x = 30.dp, y = -10.dp) - .clip(RoundedCornerShape(cornerRadius)) - ) { - rotate(degrees = 20f) { - val cardWidth = size.width * 0.7f - val cardHeight = cardWidth / 1.586f - drawRoundRect( - brush = giftCardGradient2(size), - size = Size(cardWidth, cardHeight), - topLeft = Offset( - x = (size.width - cardWidth) / 2f, - y = (size.height - cardHeight) / 2f - ), - cornerRadius = CornerRadius(cornerRadius.toPx()) - ) - } - } - Image( - painter = painterResource(R.drawable.amazon_logo), - contentDescription = null, - modifier = Modifier - .size(50.dp) - .align(Alignment.Center) - .offset(x = 30.dp, y = -10.dp) - .rotate(20f) - ) - } -} - -@Composable -fun CardVisaComposable( - rotation: Float, - modifier: Modifier = Modifier, -) { - val cornerRadius = 4.dp - Box(modifier = Modifier.fillMaxSize()) { - Canvas( - modifier = Modifier - .fillMaxSize() - .clip(RoundedCornerShape(cornerRadius)) - ) { - rotate(degrees = rotation) { - val cardWidth = size.width * 0.7f - val cardHeight = cardWidth / 1.586f - drawRoundRect( - brush = giftCardGradient1(size), - size = Size(cardWidth, cardHeight), - topLeft = Offset( - x = (size.width - cardWidth) / 2f, - y = (size.height - cardHeight) / 2f - ), - cornerRadius = CornerRadius(cornerRadius.toPx()) - ) - } - } - - Image( - painter = painterResource(R.drawable.visa_logo), - contentDescription = null, - modifier = Modifier - .size(40.dp) - .align(Alignment.Center) - .rotate(rotation) - - ) - } -} - -@Composable -fun CardJustEatComposable( - rotation: Float, - modifier: Modifier = Modifier, -) { - val cornerRadius = 4.dp - - Box(modifier = Modifier.fillMaxSize()) { - Canvas( - modifier = Modifier - .fillMaxSize() - .clip(RoundedCornerShape(cornerRadius)) - ) { - rotate(degrees = rotation) { - val cardWidth = size.width * 0.7f - val cardHeight = cardWidth / 1.586f - drawRoundRect( - brush = giftCardGradient3(size), - size = Size(cardWidth, cardHeight), - topLeft = Offset( - x = (size.width - cardWidth) / 2f, - y = (size.height - cardHeight) / 2f - ), - cornerRadius = CornerRadius(cornerRadius.toPx()) - ) - } - } - Image( - painter = painterResource(R.drawable.just_eat_logo), - contentDescription = null, - modifier = Modifier - .size(50.dp) - .align(Alignment.Center) - .rotate(rotation) - ) - } -} - -@Preview -@Composable -private fun CardJustEatComposablePreview() { - Box(modifier = Modifier) { - CardJustEatComposable(rotation = -30f) - } -} - -@Preview -@Composable -private fun CardAmazonComposablePreview() { - Box(modifier = Modifier) { - CardAmazonComposable(rotation = -30f) - } -} - -@Preview -@Composable -private fun CardVisaComposablePreview() { - Box(modifier = Modifier) { - CardVisaComposable(rotation = -30f) - } -} diff --git a/app/src/main/java/com/brainwallet/ui/composable/BrainwalletLoadingIndicator.kt b/app/src/main/java/com/brainwallet/ui/composable/BrainwalletLoadingIndicator.kt new file mode 100644 index 00000000..4708c49f --- /dev/null +++ b/app/src/main/java/com/brainwallet/ui/composable/BrainwalletLoadingIndicator.kt @@ -0,0 +1,34 @@ +package com.brainwallet.ui.composable + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.grunt.brainwallet.core.presentation.theme.BrainwalletTheme +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.PreviewLightDark +import com.brainwallet.ui.theme.lavender +import com.brainwallet.ui.theme.midnight + +@Composable +fun BrainwalletLoadingIndicator( + modifier: Modifier = Modifier, + color: Color = BrainwalletTheme.colors.content +) { + CircularProgressIndicator( + color = lavender, + trackColor = midnight, + strokeWidth = 5.dp, + modifier = Modifier.size(48.dp).then(modifier) + ) +} + +@PreviewLightDark +@Composable +private fun BrainwalletLoadingIndicatorPreview() { + Box(modifier = Modifier) { + BrainwalletLoadingIndicator() + } +} diff --git a/app/src/main/java/com/brainwallet/ui/composable/passcode/PasscodeKeypad.kt b/app/src/main/java/com/brainwallet/ui/composable/passcode/PasscodeKeypad.kt index e32330cf..6017f928 100644 --- a/app/src/main/java/com/brainwallet/ui/composable/passcode/PasscodeKeypad.kt +++ b/app/src/main/java/com/brainwallet/ui/composable/passcode/PasscodeKeypad.kt @@ -12,11 +12,13 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Face import androidx.compose.material3.Icon import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp @@ -90,16 +92,13 @@ fun PasscodeKeypad( _root_ide_package_.com.brainwallet.ui.composable.CircleButton( modifier = modifierCircleButton, - onClick = { - onEvent.invoke(PasscodeKeypadEvent.OnDelete) - }, - colors = IconButtonDefaults.filledIconButtonColors( - containerColor = Color.Transparent - ), + onClick = { onEvent.invoke(PasscodeKeypadEvent.OnDelete) }, ) { Icon( Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Delete", + tint = MaterialTheme.typography.headlineMedium.color + .takeOrElse { LocalContentColor.current } ) } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt index 3c37efeb..4de5a036 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/main/MainScreen.kt @@ -67,11 +67,14 @@ import com.brainwallet.navigation.Route import com.brainwallet.navigation.UiEffect import com.brainwallet.tools.animation.BRAnimator import com.brainwallet.tools.manager.AnalyticsManager +import com.brainwallet.tools.util.withTheme import com.brainwallet.ui.bentosections.balancebento.BalanceBentoScreen import com.brainwallet.ui.bentosections.buyreceivebento.receive.ReceiveDialog import com.brainwallet.ui.bentosections.gamehubbento.GameHubBentoPagerScreen import com.brainwallet.ui.bentosections.ltcpickerbento.LTCPickerBentoScreen +import com.brainwallet.ui.bentosections.shopbento.ShopBentoEvent import com.brainwallet.ui.bentosections.shopbento.ShopBentoScreen +import com.brainwallet.ui.bentosections.shopbento.ShopBentoViewModel import com.brainwallet.ui.bentosections.transactionbento.TransactionsBentoScreen import com.brainwallet.ui.bentosections.tutorials.TutorialsBentoScreen import com.brainwallet.ui.bentosections.tutorials.send.TutorialSendPagerScreen @@ -104,7 +107,8 @@ import timber.log.Timber fun MainScreen( onNavigate: OnNavigate, modifier: Modifier = Modifier, - viewModel: MainViewModel = koinViewModel() + viewModel: MainViewModel = koinViewModel(), + shopBentoViewModel: ShopBentoViewModel = koinViewModel() ) { val context = LocalContext.current var currentRoute by remember { mutableStateOf(Route.Main) } @@ -113,6 +117,7 @@ fun MainScreen( val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var isSheetOpen by remember { mutableStateOf(false) } var modalContentRoute by remember { mutableStateOf(null) } + val shopBentoState by shopBentoViewModel.state.collectAsStateWithLifecycle() val appSetting by viewModel.appSetting.collectAsStateWithLifecycle() val isDarkMode = appSetting.isDarkMode val state by viewModel.state.collectAsStateWithLifecycle() @@ -329,7 +334,7 @@ fun MainScreen( } Box(modifier = Modifier.height(storeBentoHeight)) { ShopBentoScreen(onClick = { - modalContentRoute = Route.BitrefillWeb + modalContentRoute = Route.BitrefillWeb(url = shopBentoState.shopBaseUrl) isSheetOpen = true }) } @@ -390,7 +395,7 @@ fun MainScreen( .fillMaxHeight( if (modalContentRoute == Route.TutorialSend || modalContentRoute == Route.TutorialWalkthrough || - modalContentRoute == Route.BitrefillWeb || + modalContentRoute is Route.BitrefillWeb || modalContentRoute == Route.LinktreeWeb ) { 1f @@ -452,10 +457,15 @@ fun MainScreen( onNavigate = onNavigate, url = BWConstants.LINKTREE_URL ) - - Route.BitrefillWeb -> WebModalScreen( + is Route.BitrefillWeb -> WebModalScreen( + modifier = Modifier.fillMaxSize(), onNavigate = onNavigate, - url = BWConstants.BITREFILL_URL + url = (modalContentRoute as Route.BitrefillWeb).url.withTheme( + if (isDarkMode) "dark" else "light" + ), + invoiceCreated = { _, _ -> + shopBentoViewModel.onEvent(ShopBentoEvent.InvoiceCreated) + } ) else -> {} } diff --git a/app/src/main/java/com/brainwallet/ui/screens/main/WebModalScreen.kt b/app/src/main/java/com/brainwallet/ui/screens/main/WebModalScreen.kt index 58c091c5..d9e761bb 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/main/WebModalScreen.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/main/WebModalScreen.kt @@ -1,52 +1,136 @@ package com.brainwallet.ui.screens.main +import android.graphics.Bitmap +import android.view.ViewGroup +import android.webkit.JavascriptInterface import android.webkit.WebView import android.webkit.WebViewClient import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.brainwallet.navigation.OnNavigate import com.brainwallet.tools.manager.AnalyticsManager +import com.brainwallet.ui.composable.BrainwalletLoadingIndicator import com.brainwallet.ui.theme.bentoDarkSurfaceGradient - @Composable fun WebModalScreen( onNavigate: OnNavigate, url: String, modifier: Modifier = Modifier, + invoiceCreated: (invoiceId: String, paymentUri: String) -> Unit = { _, _ -> } + ) { val eventString = if (url.contains("bitrefill")) { - "user_did_tap_bitrefill" + "user_did_tap_shop_bento" } else { "user_did_tap_linktree" } LaunchedEffect(Unit) { AnalyticsManager.logCustomEvent(eventString) } - val context = LocalContext.current + + var isLoading by remember { mutableStateOf(true) } + Card( - modifier = modifier - .fillMaxSize() - .background(brush = bentoDarkSurfaceGradient), + modifier = modifier.fillMaxSize(), shape = RoundedCornerShape(18.dp), + colors = CardDefaults.cardColors(containerColor = Color.Transparent), ) { - AndroidView( - factory = { context -> - WebView(context).apply { - webViewClient = WebViewClient() - loadUrl(url) - } - }, - modifier = Modifier.fillMaxSize() - ) + Box( + modifier = Modifier + .fillMaxSize() + .background(brush = bentoDarkSurfaceGradient), + ) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + WebView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + setBackgroundColor(0) + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.useWideViewPort = true + webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + isLoading = true + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + isLoading = false + + view?.evaluateJavascript( + """ + (function() { + function handleMessage(event) { + try { + var data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; + if (data && data.event === 'invoice_created') { + BitrefillAndroid.invoiceCreated(JSON.stringify(data)); + } + } catch(e) { + console.log('parse error:', e); + } + } + window.addEventListener('message', handleMessage); + document.addEventListener('message', handleMessage); + })(); + """.trimIndent(), + null + ) + } + } + + addJavascriptInterface( + WebAppInterface { invoiceId, paymentUri -> + invoiceCreated(invoiceId, paymentUri) + }, + "BitrefillAndroid" + ) + } + }, + update = { it.loadUrl(url) } + ) + + if (isLoading) { + BrainwalletLoadingIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } + } + } +} + +class WebAppInterface( + private val onInvoiceCreated: (invoiceId: String, paymentUri: String) -> Unit +) { + @JavascriptInterface + fun invoiceCreated(dataJson: String) { + try { + val obj = org.json.JSONObject(dataJson) + val invoiceId = obj.getString("invoiceId") + val paymentUri = obj.getString("paymentUri") + onInvoiceCreated(invoiceId, paymentUri) + } catch (e: Exception) { + android.util.Log.e("WebAppInterface", "Failed to parse invoice data", e) + } } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/send/SendViewModel.kt b/app/src/main/java/com/brainwallet/ui/screens/send/SendViewModel.kt index de7c2209..a3dc3275 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/send/SendViewModel.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/send/SendViewModel.kt @@ -15,9 +15,11 @@ import com.brainwallet.tools.manager.BRSharedPrefs import com.brainwallet.tools.manager.FeeManager import com.brainwallet.tools.security.BRKeyStore import com.brainwallet.tools.util.BRExchange +import com.brainwallet.tools.util.BRExchange.ONE_LITECOIN_OF_LITOSHIS import com.brainwallet.tools.util.Utils import com.brainwallet.ui.BrainwalletViewModel import com.brainwallet.ui.screens.send.BWSendResult.Error +import com.brainwallet.util.CurrencyDataGetter import com.brainwallet.util.EventBus import com.brainwallet.wallet.BRWalletManager import kotlinx.coroutines.CoroutineDispatcher @@ -43,6 +45,7 @@ class SendViewModel( private val bwSender: BWSender, private val txRepository: TxRepository, private val settingRepository: SettingRepository, + private val currencyDataGetter: CurrencyDataGetter, private val isWalletCreated: () -> Boolean = { BRWalletManager.getInstance().isCreated() }, private val validateAddress: (String) -> Boolean = { BRWalletManager.getInstance().validateAddress(it) }, private val getBalance: () -> Long = { BRWalletManager.getInstance().getBalance(app) }, @@ -138,13 +141,19 @@ class SendViewModel( } is SendEvent.OnToggleFiatOrLTC -> { val currentState = _state.value - val rate = _state.value.selectedCurrency.rate + val rate = currencyDataGetter + .getCurrencyByIso(currentState.selectedCurrency.code)?.rate + ?.takeIf { it > 0f } val convertedAmount = if (rate != null && currentState.amountString.isNotBlank()) { val current = currentState.amountString.toBigDecimalOrNull() ?: BigDecimal.ZERO if (currentState.userViewsFiat) { // switching fiat → LTC: divide by rate - current.divide(BigDecimal(rate.toString()), 8, BWConstants.ROUNDING_MODE) + current.divide( + BigDecimal(rate.toString()), + 8, + BWConstants.ROUNDING_MODE + ) } else { // switching LTC → fiat: multiply by rate current.multiply(BigDecimal(rate.toString())) @@ -153,19 +162,24 @@ class SendViewModel( } else { null } + + val newAmountInLitoshi = when { + currentState.userViewsFiat && convertedAmount != null -> { + // was fiat, now LTC — convertedAmount is LTC + convertedAmount + .multiply(BigDecimal(ONE_LITECOIN_OF_LITOSHIS)) + } + !currentState.userViewsFiat -> { + // was LTC, now fiat — litoshi amount is unchanged + currentState.amountInLitoshi + } + else -> BigDecimal.ZERO + } _state.update { it.copy( userViewsFiat = !it.userViewsFiat, amountString = convertedAmount?.toPlainString() ?: it.amountString, - amountInLitoshi = if (currentState.userViewsFiat) { - BigDecimal(rate.toDouble()).divide( - convertedAmount, - 8, - BWConstants.ROUNDING_MODE - ) - } else { - convertedAmount ?: BigDecimal.ZERO - } + amountInLitoshi = newAmountInLitoshi ) } } diff --git a/app/src/main/java/com/brainwallet/ui/screens/unlock/components/UnLockScreenBody.kt b/app/src/main/java/com/brainwallet/ui/screens/unlock/components/UnLockScreenBody.kt index 8a47a525..9ec5186d 100644 --- a/app/src/main/java/com/brainwallet/ui/screens/unlock/components/UnLockScreenBody.kt +++ b/app/src/main/java/com/brainwallet/ui/screens/unlock/components/UnLockScreenBody.kt @@ -68,7 +68,7 @@ fun UnLockScreenBody( AuthManager.getInstance().checkAuth(pin, context).also { isValid -> if (isValid) { AuthManager.getInstance().authSuccess(context) - AnalyticsManager.logCustomEvent(BWConstants._20200217_DUWB) + AnalyticsManager.logCustomEvent(BWConstants._20200217_DU) } else { Toast.makeText(context, R.string.incorrect_passcode, Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt b/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt deleted file mode 100644 index c2f1f6ce..00000000 --- a/app/src/main/java/com/brainwallet/worker/CurrencyUpdateWorker.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.brainwallet.worker - -import com.brainwallet.data.repository.LtcRepository -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import org.koin.core.annotation.Single - -@Single -class CurrencyUpdateWorker( - private val ltcRepository: LtcRepository, - private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, - private val workerScope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) -) { - private var job: Job? = null - - fun start() { - if (job?.isActive == true) { - job?.cancel() - } - job = workerScope.launch(ioDispatcher) { - while (isActive) { - ltcRepository.fetchRates() - delay(4000L) - } - } - } -} diff --git a/app/src/main/jni/core b/app/src/main/jni/core index 28d7d31a..fcbf4ae6 160000 --- a/app/src/main/jni/core +++ b/app/src/main/jni/core @@ -1 +1 @@ -Subproject commit 28d7d31ae057bdde33b1d19e6b7af3ba07710c89 +Subproject commit fcbf4ae60b770463a2a4c2d8b585cb3b498f25c0 diff --git a/app/src/main/res/drawable/amazon_logo.png b/app/src/main/res/drawable/amazon_logo.png deleted file mode 100644 index b5c7a734..00000000 Binary files a/app/src/main/res/drawable/amazon_logo.png and /dev/null differ diff --git a/app/src/main/res/drawable/just_eat_logo.png b/app/src/main/res/drawable/just_eat_logo.png deleted file mode 100644 index 91cac7e5..00000000 Binary files a/app/src/main/res/drawable/just_eat_logo.png and /dev/null differ diff --git a/app/src/main/res/drawable/visa_logo.png b/app/src/main/res/drawable/visa_logo.png deleted file mode 100644 index 80f1b6f7..00000000 Binary files a/app/src/main/res/drawable/visa_logo.png and /dev/null differ diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index a9072767..f55a3768 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -304,4 +304,6 @@ تحقق قبل الإرسال! امسح أو الصق عنوان الوجهة. تحقق مرتين! غيّر قيمتك بالعملة المحلية لمعرفة المبلغ الذي يمكنك إرساله. أضف ملاحظة كتذكير مفيد. اضغط متابعة. السماح بالوصول إلى الكاميرا من الإعدادات - + المتجر + اشترِ بطاقات هدايا باستخدام LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 930815f2..c4759610 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -434,4 +434,6 @@ Überprüfe vor dem Senden! Scanne oder füge die Zieladresse ein. Überprüfe sie genau! Ändere deine lokale Währung, um herauszufinden, wie viel du senden kannst. Füge eine Notiz als praktische Erinnerung hinzu. Tippe auf Weiter. Kamerazugriff in den Einstellungen aktivieren - + Shop + Kaufe Geschenkgutscheine mit LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9daa34b9..b83dbf63 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -509,4 +509,6 @@ ¡Verifica antes de enviar! Escanea o pega la dirección de destino. ¡Verifica dos veces! Cambia tu moneda fiduciaria local para determinar cuánto puedes enviar. Añade una nota como recordatorio útil. Toca Continuar. Permitir acceso a la cámara en Configuración - + Tienda + ¡Compra Tarjetas de Regalo con LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 58af62d4..7ca6cee3 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -645,4 +645,6 @@ قبل از ارسال تأیید کنید! آدرس مقصد را اسکن یا جایگذاری کنید. دوبار بررسی کنید! ارز محلی خود را تغییر دهید تا بفهمید چقدر می‌توانید ارسال کنید. یادداشتی به عنوان یادآوری اضافه کنید. بر روی ادامه ضربه بزنید. دسترسی دوربین را در تنظیمات فعال کنید - + فروشگاه + خرید کارت هدیه با LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 911a9499..e05eb164 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -660,4 +660,6 @@ Vérifiez avant d\'envoyer! Scannez ou collez l\'adresse de destination. Vérifiez bien! Convertissez en devise locale pour déterminer le montant à envoyer. Ajoutez un mémo comme aide-mémoire. Appuyez sur Continuer. Autoriser l\'accès à la caméra dans les Paramètres - + Boutique + Achetez des Cartes Cadeaux avec LTC ! + \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e0f839d5..b75967b5 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -304,4 +304,6 @@ भेजने से पहले सत्यापित करें! गंतव्य पता स्कैन या पेस्ट करें। दोबारा जांचें! आप कितना भेज सकते हैं यह जानने के लिए अपनी स्थानीय फिएट को बदलें। एक सहायक स्मरणीय के रूप में एक मेमो जोड़ें। जारी रखें पर टैप करें। सेटिंग्स में कैमरा एक्सेस की अनुमति दें - + दुकान + LTC के साथ गिफ्ट कार्ड खरीदें! + \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 0cd1aece..8e91ddc1 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -660,4 +660,6 @@ Verifikasi sebelum mengirim! Pindai atau tempel alamat tujuan. Periksa dua kali! Ubah fiat lokal Anda untuk mengetahui berapa banyak yang dapat Anda kirim. Tambahkan memo sebagai pengingat praktis. Ketuk Lanjutkan. Izinkan akses kamera di Pengaturan - + Toko + Beli Kartu Hadiah dengan LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index ccf1ad45..4b39065e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -660,4 +660,6 @@ Verifica prima di inviare! Scansiona o incolla l\'indirizzo di destinazione. Verifica! Cambia la tua valuta fiat locale per capire quanto puoi inviare. Aggiungi una nota come promemoria utile. Tocca Continua. Consenti l\'accesso alla fotocamera in Impostazioni - + Negozio + Acquista Carte Regalo con LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index da1398a3..ec3d2212 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -660,4 +660,6 @@ 送信前に確認! 宛先アドレスをスキャンまたは貼り付けます。よく確認してください!現地通貨で計算して送信可能な金額を確認します。メモを追加して便利に管理できます。続けるをタップします。 設定からカメラへのアクセスを許可してください - + ショップ + LTCでギフトカードを購入! + \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 0ca33d4c..98d1ddbb 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -660,4 +660,6 @@ 보내기 전에 확인하세요! 수신자 주소를 스캔하거나 붙여넣기하세요. 확인하세요! 현지 통화로 변경하여 보낼 수 있는 금액을 계산하세요. 편리한 참고를 위해 메모를 추가하세요. 계속을 탭하세요. 설정에서 카메라 접근을 허용해주세요 - + 상점 + LTC로 기프트 카드를 구매하세요! + \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index a97d6571..23850a43 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -304,4 +304,6 @@ Verifieer voordat u verzendt! Scan of plak het doeladdres. Dubbel controleren! Wijzig uw lokale fiat om uit te rekenen hoeveel u kunt verzenden. Voeg een memo toe als handige herinnering. Tik op Doorgaan. Sta cameratoegang toe in Instellingen - + Winkel + Koop cadeaukaarten met LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 8860f8b3..72fce716 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -670,4 +670,6 @@ ਭੇਜਣ ਤੋਂ ਪਹਿਲਾਂ ਤਸਦੀਕ ਕਰੋ! ਮੰਜ਼ਿਲ ਦਾ ਪਤਾ ਸਕੈਨ ਕਰੋ ਜਾਂ ਚਿਪਕਾਓ। ਦੋ ਵਾਰ ਚੈਕ ਕਰੋ! ਇਹ ਪਤਾ ਲਗਾਉਣ ਲਈ ਆਪਣੀ ਸਥਾਨਕ ਫਿਆਟ ਨੂੰ ਬਦਲੋ ਕਿ ਤੁਸੀਂ ਕਿੰਨਾ ਭੇਜ ਸਕਦੇ ਹੋ। ਆਸਾਨ ਯਾਦ ਦਿਖਾਉਣ ਲਈ ਇੱਕ ਯਾਦ ਸ਼ਾਮਲ ਕਰੋ। ਜਾਰੀ ਰੱਖੋ \'ਤੇ ਟੈਪ ਕਰੋ। ਸੈਟਿੰਗਜ਼ ਵਿੱਚ ਕੈਮਰੇ ਦੀ ਅਨੁਮਤੀ ਦਿਓ - + ਦੁਕਾਨ + LTC ਨਾਲ ਗਿਫ਼ਟ ਕਾਰਡ ਖਰੀਦੋ! + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index b034e957..ea238811 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -665,4 +665,6 @@ Sprawdź przed wysłaniem! Zeskanuj lub wklej adres docelowy. Sprawdź dokładnie! Zmień swoją lokalną walutę fiat, aby sprawdzić, ile możesz wysłać. Dodaj notatkę jako przydatne przypomnienie. Dotknij Kontynuuj. Zezwól na dostęp do aparatu w Ustawieniach - + Sklep + Kup karty podarunkowe za LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 07c5db4b..f1e88df2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -304,4 +304,6 @@ Verifique antes de enviar! Escaneie ou cole o endereço de destino. Verifique bem! Mude sua moeda fiduciária local para calcular quanto pode enviar. Adicione um memorando como lembrete útil. Toque em Continuar. Permita o acesso à câmera nas Configurações - + Loja + Compre Cartões Presentes com LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 08c2779e..c45af3d4 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -660,4 +660,6 @@ Проверьте перед отправкой! Отсканируйте или вставьте адрес получателя. Проверьте дважды! Конвертируйте между фиатом и LTC, чтобы узнать сумму отправки. Добавьте заметку в качестве напоминания. Нажмите Продолжить. Разрешите доступ к камере в Настройках - + Магазин + Покупайте подарочные карты за LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index efbb0b93..a655a7c6 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -304,4 +304,6 @@ Verifiera innan du skickar! Skanna eller klistra in destinationsadressen. Dubbelkolla! Ändra din lokala fiat-valuta för att räkna ut hur mycket du kan skicka. Lägg till en anteckning som en praktisk påminnelse. Tryck Fortsätt. Tillåt kameraåtkomst i Inställningar - + Butik + Köp presentkort med LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 858b18f3..0c90e5ce 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -304,4 +304,6 @@ ตรวจสอบก่อนส่ง! สแกนหรือวาง address ปลายทาง ตรวจสอบอีกครั้ง! เปลี่ยนตัวเลขเงินฟิแอตท้องถิ่นของคุณเพื่อคำนวณจำนวนที่คุณสามารถส่งได้ เพิ่มบันทึกเป็นการเตือนที่มีประโยชน์ แตะ ดำเนินการต่อ อนุญาตการเข้าถึงกล้องในการตั้งค่า - + ร้านค้า + ซื้อบัตรของขวัญด้วย LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 866f998c..02bc40cf 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -679,4 +679,6 @@ Göndermeden Önce Doğrulayın! Hedef adresini tarayın veya yapıştırın. İki kez kontrol edin! Ne kadar gönderebileceğinizi öğrenmek için yerel para biriminizi değiştirin. Hatırlatıcı olması için not ekleyin. Devam Et\'e dokunun. Ayarlar\'da kamera erişimine izin verin - + Mağaza + LTC ile Hediye Kartı Satın Al! + \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d2ef237a..81d90e99 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -662,4 +662,6 @@ Перевірте перед відправленням! Відсканюйте або вставте адресу призначення. Перевірте двічі! Змініть вашу місцеву валюту на фіатну, щоб дізнатися, скільки можна відправити. Додайте замітку як корисне нагадування. Натисніть «Продовжити». Дозвольте доступ до камери в Налаштуваннях - + Магазин + Купуйте подарункові карти за LTC! + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 2d4b4cf8..90a78ab7 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -660,4 +660,6 @@ 发送前验证! 扫描或粘贴目标地址。仔细检查!更改您的本地法币以计算可以发送的金额。添加备忘录作为便捷提醒。点击继续。 在设置中允许摄像头访问 - + 商店 + 用 LTC 购买礼品卡! + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1a746ae4..08c5214d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -660,4 +660,6 @@ 發送前驗證! 掃描或貼上目標地址。仔細核實!將本地法幣轉換為LTC以確定可發送金額。新增備忘錄作為方便的提醒。點擊繼續。 在設定中允許相機存取 - + 商店 + 用 LTC 購買禮品卡! + \ No newline at end of file diff --git a/app/src/main/res/xml/remote_config_defaults.xml b/app/src/main/res/xml/remote_config_defaults.xml index 6852653a..8ad710bb 100644 --- a/app/src/main/res/xml/remote_config_defaults.xml +++ b/app/src/main/res/xml/remote_config_defaults.xml @@ -1,19 +1,3255 @@ - + feature_selected_peers_enabled true - - + + update_debounce_interval 3.0 - - + + progress_update_interval 1.0 - - + + feature_menu_hidden_example {"enabled":true,"title":"brainwallet-android repository","url":"https://github.com/brainwallet-co/android"} - - \ No newline at end of file + + + key_sync_poller + {"delays":{"sync_delay":60000,"non_sync_delay":4000} + + + path_shop_content + { + "shop_data": { + "widget_url": "https://www.bitrefill.com/?ref=bw_tk_26", + "cards": [ + { + "country_code": "AD", + "country_name": "Andorra", + "product_slug": "carrefour-france", + "product_name": "Carrefour France", + "product_url": "https://www.bitrefill.com/carrefour-france/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/carrefour-france.webp" + }, + { + "country_code": "AD", + "country_name": "Andorra", + "product_slug": "amazon_fr-france", + "product_name": "Amazon.fr France", + "product_url": "https://www.bitrefill.com/amazon_fr-france/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/amazon_fr-france.webp" + }, + { + "country_code": "AD", + "country_name": "Andorra", + "product_slug": "orange-spain", + "product_name": "Orange Spain", + "product_url": "https://www.bitrefill.com/orange-spain/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/orange-spain.webp" + }, + { + "country_code": "AE", + "country_name": "United Arab Emirates", + "product_slug": "noon-uae", + "product_name": "Noon.com UAE", + "product_url": "https://www.bitrefill.com/noon-uae/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/noon-uae.webp" + }, + { + "country_code": "AE", + "country_name": "United Arab Emirates", + "product_slug": "talabat-uae", + "product_name": "Talabat UAE", + "product_url": "https://www.bitrefill.com/talabat-uae/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/talabat-uae.webp" + }, + { + "country_code": "AE", + "country_name": "United Arab Emirates", + "product_slug": "lulu-hypermarket-uae", + "product_name": "LuLu Hypermarket UAE", + "product_url": "https://www.bitrefill.com/lulu-hypermarket-uae/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/lulu-hypermarket-uae.webp" + }, + { + "country_code": "AG", + "country_name": "Antigua and Barbuda", + "product_slug": "digicel-antigua-and-barbuda", + "product_name": "Digicel Antigua", + "product_url": "https://www.bitrefill.com/digicel-antigua-and-barbuda/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-antigua-and-barbuda.webp" + }, + { + "country_code": "AG", + "country_name": "Antigua and Barbuda", + "product_slug": "flow-antigua-and-barbuda", + "product_name": "Flow Antigua", + "product_url": "https://www.bitrefill.com/flow-antigua-and-barbuda/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-antigua-and-barbuda.webp" + }, + { + "country_code": "AG", + "country_name": "Antigua and Barbuda", + "product_slug": "bitrefill-esim-caribbean", + "product_name": "eSIM Caribbean", + "product_url": "https://www.bitrefill.com/bitrefill-esim-caribbean/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-caribbean.webp" + }, + { + "country_code": "AI", + "country_name": "Anguilla", + "product_slug": "digicel-anguilla", + "product_name": "Digicel Anguilla", + "product_url": "https://www.bitrefill.com/digicel-anguilla/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-anguilla.webp" + }, + { + "country_code": "AI", + "country_name": "Anguilla", + "product_slug": "flow-anguilla", + "product_name": "Flow Anguilla", + "product_url": "https://www.bitrefill.com/flow-anguilla/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-anguilla.webp" + }, + { + "country_code": "AI", + "country_name": "Anguilla", + "product_slug": "bitrefill-esim-caribbean", + "product_name": "eSIM Caribbean", + "product_url": "https://www.bitrefill.com/bitrefill-esim-caribbean/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-caribbean.webp" + }, + { + "country_code": "AL", + "country_name": "Albania", + "product_slug": "eneba-gift-card-eu", + "product_name": "Eneba EUR", + "product_url": "https://www.bitrefill.com/eneba-gift-card-eu/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/eneba-gift-card-eu.webp" + }, + { + "country_code": "AL", + "country_name": "Albania", + "product_slug": "valorant-eu", + "product_name": "Valorant EU", + "product_url": "https://www.bitrefill.com/valorant-eu/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/valorant-eu.webp" + }, + { + "country_code": "AL", + "country_name": "Albania", + "product_slug": "league-of-legends-eu", + "product_name": "League of Legends EU", + "product_url": "https://www.bitrefill.com/league-of-legends-eu/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/league-of-legends-eu.webp" + }, + { + "country_code": "AM", + "country_name": "Armenia", + "product_slug": "ucom-armenia", + "product_name": "UCom Armenia", + "product_url": "https://www.bitrefill.com/ucom-armenia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ucom-armenia.webp" + }, + { + "country_code": "AM", + "country_name": "Armenia", + "product_slug": "vivacell-mts-armenia", + "product_name": "VivaCell-MTS Armenia", + "product_url": "https://www.bitrefill.com/vivacell-mts-armenia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/vivacell-mts-armenia.webp" + }, + { + "country_code": "AM", + "country_name": "Armenia", + "product_slug": "bitrefill-esim-armenia", + "product_name": "eSIM Armenia", + "product_url": "https://www.bitrefill.com/bitrefill-esim-armenia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-armenia.webp" + }, + { + "country_code": "AN", + "country_name": "Netherlands Antilles", + "product_slug": "digicel-netherlands-antilles", + "product_name": "Digicel Netherlands Antilles", + "product_url": "https://www.bitrefill.com/digicel-netherlands-antilles/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-netherlands-antilles.webp" + }, + { + "country_code": "AN", + "country_name": "Netherlands Antilles", + "product_slug": "flow-netherlands-antilles", + "product_name": "Flow Netherlands Antilles", + "product_url": "https://www.bitrefill.com/flow-netherlands-antilles/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-netherlands-antilles.webp" + }, + { + "country_code": "AN", + "country_name": "Netherlands Antilles", + "product_slug": "bitrefill-esim-caribbean", + "product_name": "eSIM Caribbean", + "product_url": "https://www.bitrefill.com/bitrefill-esim-caribbean/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-caribbean.webp" + }, + { + "country_code": "AR", + "country_name": "Argentina", + "product_slug": "eneba-gift-card-ars-international", + "product_name": "Eneba ARS", + "product_url": "https://www.bitrefill.com/eneba-gift-card-ars-international/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/eneba-gift-card-ars-international.webp" + }, + { + "country_code": "AR", + "country_name": "Argentina", + "product_slug": "dia-argentina", + "product_name": "Día Argentina", + "product_url": "https://www.bitrefill.com/dia-argentina/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/dia-argentina.webp" + }, + { + "country_code": "AR", + "country_name": "Argentina", + "product_slug": "claro-argentina", + "product_name": "Claro Argentina", + "product_url": "https://www.bitrefill.com/claro-argentina/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/claro-argentina.webp" + }, + { + "country_code": "AT", + "country_name": "Austria", + "product_slug": "interspar-austria", + "product_name": "INTERSPAR Austria", + "product_url": "https://www.bitrefill.com/interspar-austria/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/interspar-austria.webp" + }, + { + "country_code": "AT", + "country_name": "Austria", + "product_slug": "billa-austria", + "product_name": "Billa Austria", + "product_url": "https://www.bitrefill.com/billa-austria/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/billa-austria.webp" + }, + { + "country_code": "AT", + "country_name": "Austria", + "product_slug": "drei-austria", + "product_name": "Drei Austria", + "product_url": "https://www.bitrefill.com/drei-austria/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/drei-austria.webp" + }, + { + "country_code": "AU", + "country_name": "Australia", + "product_slug": "the-visa-digital-gift-card-australia", + "product_name": "Visa Gift Card AU", + "product_url": "https://www.bitrefill.com/the-visa-digital-gift-card-australia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/the-visa-digital-gift-card-australia.webp" + }, + { + "country_code": "AU", + "country_name": "Australia", + "product_slug": "woolworths-australia", + "product_name": "Woolworths Australia", + "product_url": "https://www.bitrefill.com/woolworths-australia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/woolworths-australia.webp" + }, + { + "country_code": "AU", + "country_name": "Australia", + "product_slug": "jb-hi-fi-australia", + "product_name": "JB Hi-Fi Australia", + "product_url": "https://www.bitrefill.com/jb-hi-fi-australia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/jb-hi-fi-australia.webp" + }, + { + "country_code": "AX", + "country_name": "Åland Islands", + "product_slug": "k-ruoka-finland", + "product_name": "K-Ruoka Finland", + "product_url": "https://www.bitrefill.com/k-ruoka-finland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/k-ruoka-finland.webp" + }, + { + "country_code": "AX", + "country_name": "Åland Islands", + "product_slug": "wolt-fi", + "product_name": "Wolt Finland", + "product_url": "https://www.bitrefill.com/wolt-fi/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/wolt-fi.webp" + }, + { + "country_code": "AX", + "country_name": "Åland Islands", + "product_slug": "s-group-finland", + "product_name": "S-Group Finland", + "product_url": "https://www.bitrefill.com/s-group-finland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/s-group-finland.webp" + }, + { + "country_code": "BA", + "country_name": "Bosnia and Herzegovina", + "product_slug": "league-of-legends-eu", + "product_name": "League of Legends EU", + "product_url": "https://www.bitrefill.com/league-of-legends-eu/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/league-of-legends-eu.webp" + }, + { + "country_code": "BA", + "country_name": "Bosnia and Herzegovina", + "product_slug": "valorant-eu", + "product_name": "Valorant EU", + "product_url": "https://www.bitrefill.com/valorant-eu/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/valorant-eu.webp" + }, + { + "country_code": "BA", + "country_name": "Bosnia and Herzegovina", + "product_slug": "eneba-gift-card-eu", + "product_name": "Eneba EUR", + "product_url": "https://www.bitrefill.com/eneba-gift-card-eu/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/eneba-gift-card-eu.webp" + }, + { + "country_code": "BB", + "country_name": "Barbados", + "product_slug": "digicel-barbados", + "product_name": "Digicel Barbados", + "product_url": "https://www.bitrefill.com/digicel-barbados/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-barbados.webp" + }, + { + "country_code": "BB", + "country_name": "Barbados", + "product_slug": "flow-barbados", + "product_name": "Flow Barbados", + "product_url": "https://www.bitrefill.com/flow-barbados/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-barbados.webp" + }, + { + "country_code": "BB", + "country_name": "Barbados", + "product_slug": "bitrefill-esim-caribbean", + "product_name": "eSIM Caribbean", + "product_url": "https://www.bitrefill.com/bitrefill-esim-caribbean/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-caribbean.webp" + }, + { + "country_code": "BE", + "country_name": "Belgium", + "product_slug": "itunes-belgium", + "product_name": "Apple Belgium", + "product_url": "https://www.bitrefill.com/itunes-belgium/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/itunes-belgium.webp" + }, + { + "country_code": "BE", + "country_name": "Belgium", + "product_slug": "bol-eu", + "product_name": "Bol.com", + "product_url": "https://www.bitrefill.com/bol-eu/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bol-eu.webp" + }, + { + "country_code": "BE", + "country_name": "Belgium", + "product_slug": "zalando-belgium", + "product_name": "Zalando Belgium", + "product_url": "https://www.bitrefill.com/zalando-belgium/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/zalando-belgium.webp" + }, + { + "country_code": "BG", + "country_name": "Bulgaria", + "product_slug": "ozone_bg", + "product_name": "Ozone.bg Bulgaria", + "product_url": "https://www.bitrefill.com/ozone_bg/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ozone_bg.webp" + }, + { + "country_code": "BG", + "country_name": "Bulgaria", + "product_slug": "eMAG-bulgaria", + "product_name": "eMAG Bulgaria", + "product_url": "https://www.bitrefill.com/eMAG-bulgaria/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/eMAG-bulgaria.webp" + }, + { + "country_code": "BG", + "country_name": "Bulgaria", + "product_slug": "decathlon-bulgaria", + "product_name": "Decathlon Bulgaria", + "product_url": "https://www.bitrefill.com/decathlon-bulgaria/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/decathlon-bulgaria.webp" + }, + { + "country_code": "BO", + "country_name": "Bolivia", + "product_slug": "tigo-bolivia", + "product_name": "Tigo Bolivia", + "product_url": "https://www.bitrefill.com/tigo-bolivia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/tigo-bolivia.webp" + }, + { + "country_code": "BO", + "country_name": "Bolivia", + "product_slug": "entel-bolivia", + "product_name": "Entel Bolivia", + "product_url": "https://www.bitrefill.com/entel-bolivia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/entel-bolivia.webp" + }, + { + "country_code": "BO", + "country_name": "Bolivia", + "product_slug": "viva-bolivia", + "product_name": "Viva Bolivia", + "product_url": "https://www.bitrefill.com/viva-bolivia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/viva-bolivia.webp" + }, + { + "country_code": "BR", + "country_name": "Brazil", + "product_slug": "uber-brazil", + "product_name": "Uber Brazil", + "product_url": "https://www.bitrefill.com/uber-brazil/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/uber-brazil.webp" + }, + { + "country_code": "BR", + "country_name": "Brazil", + "product_slug": "ifood-br", + "product_name": "iFood Brazil", + "product_url": "https://www.bitrefill.com/ifood-br/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ifood-br.webp" + }, + { + "country_code": "BR", + "country_name": "Brazil", + "product_slug": "submarino-brazil", + "product_name": "Submarino Brazil", + "product_url": "https://www.bitrefill.com/submarino-brazil/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/submarino-brazil.webp" + }, + { + "country_code": "BS", + "country_name": "Bahamas", + "product_slug": "batelco-bahamas", + "product_name": "Batelco Bahamas", + "product_url": "https://www.bitrefill.com/batelco-bahamas/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/batelco-bahamas.webp" + }, + { + "country_code": "BS", + "country_name": "Bahamas", + "product_slug": "aliv-bahamas", + "product_name": "Aliv Bahamas", + "product_url": "https://www.bitrefill.com/aliv-bahamas/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/aliv-bahamas.webp" + }, + { + "country_code": "BS", + "country_name": "Bahamas", + "product_slug": "bitrefill-esim-bahamas", + "product_name": "eSIM Bahamas", + "product_url": "https://www.bitrefill.com/bitrefill-esim-bahamas/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-bahamas.webp" + }, + { + "country_code": "BZ", + "country_name": "Belize", + "product_slug": "digicel-belize", + "product_name": "Digicel Belize", + "product_url": "https://www.bitrefill.com/digicel-belize/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-belize.webp" + }, + { + "country_code": "BZ", + "country_name": "Belize", + "product_slug": "smart-belize", + "product_name": "Smart Belize", + "product_url": "https://www.bitrefill.com/smart-belize/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/smart-belize.webp" + }, + { + "country_code": "BZ", + "country_name": "Belize", + "product_slug": "bitrefill-esim-belize", + "product_name": "eSIM Belize", + "product_url": "https://www.bitrefill.com/bitrefill-esim-belize/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-belize.webp" + }, + { + "country_code": "CA", + "country_name": "Canada", + "product_slug": "tim-hortons-canada", + "product_name": "Tim Hortons Canada", + "product_url": "https://www.bitrefill.com/tim-hortons-canada/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/tim-hortons-canada.webp" + }, + { + "country_code": "CA", + "country_name": "Canada", + "product_slug": "uber-eats-canada", + "product_name": "Uber Eats Canada", + "product_url": "https://www.bitrefill.com/uber-eats-canada/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/uber-eats-canada.webp" + }, + { + "country_code": "CA", + "country_name": "Canada", + "product_slug": "walmart-canada", + "product_name": "Walmart Canada", + "product_url": "https://www.bitrefill.com/walmart-canada/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/walmart-canada.webp" + }, + { + "country_code": "CH", + "country_name": "Switzerland", + "product_slug": "coop-switzerland", + "product_name": "Coop Switzerland", + "product_url": "https://www.bitrefill.com/coop-switzerland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/coop-switzerland.webp" + }, + { + "country_code": "CH", + "country_name": "Switzerland", + "product_slug": "migros-switzerland", + "product_name": "Migros Switzerland", + "product_url": "https://www.bitrefill.com/migros-switzerland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/migros-switzerland.webp" + }, + { + "country_code": "CH", + "country_name": "Switzerland", + "product_slug": "digitec-switzerland", + "product_name": "Digitec Switzerland", + "product_url": "https://www.bitrefill.com/digitec-switzerland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digitec-switzerland.webp" + }, + { + "country_code": "CL", + "country_name": "Chile", + "product_slug": "uber-and-uber-eats-voucher-chile", + "product_name": "Uber & Uber Eats CLP", + "product_url": "https://www.bitrefill.com/uber-and-uber-eats-voucher-chile/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/uber-and-uber-eats-voucher-chile.webp" + }, + { + "country_code": "CL", + "country_name": "Chile", + "product_slug": "entel-chile", + "product_name": "Entel Chile", + "product_url": "https://www.bitrefill.com/entel-chile/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/entel-chile.webp" + }, + { + "country_code": "CL", + "country_name": "Chile", + "product_slug": "falabella-chile", + "product_name": "Falabella Chile", + "product_url": "https://www.bitrefill.com/falabella-chile/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/falabella-chile.webp" + }, + { + "country_code": "CM", + "country_name": "Cameroon", + "product_slug": "mtn-cameroon", + "product_name": "MTN Cameroon", + "product_url": "https://www.bitrefill.com/mtn-cameroon/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/mtn-cameroon.webp" + }, + { + "country_code": "CM", + "country_name": "Cameroon", + "product_slug": "orange-cameroon", + "product_name": "Orange Cameroon", + "product_url": "https://www.bitrefill.com/orange-cameroon/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/orange-cameroon.webp" + }, + { + "country_code": "CM", + "country_name": "Cameroon", + "product_slug": "bitrefill-esim-cameroon", + "product_name": "eSIM Cameroon", + "product_url": "https://www.bitrefill.com/bitrefill-esim-cameroon/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-cameroon.webp" + }, + { + "country_code": "CN", + "country_name": "China", + "product_slug": "alipay-china", + "product_name": "Alipay China", + "product_url": "https://www.bitrefill.com/alipay-china/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/alipay-china.webp" + }, + { + "country_code": "CN", + "country_name": "China", + "product_slug": "jd-com-china", + "product_name": "JD.com China", + "product_url": "https://www.bitrefill.com/jd-com-china/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/jd-com-china.webp" + }, + { + "country_code": "CN", + "country_name": "China", + "product_slug": "bitrefill-esim-china", + "product_name": "eSIM China", + "product_url": "https://www.bitrefill.com/bitrefill-esim-china/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-china.webp" + }, + { + "country_code": "CO", + "country_name": "Colombia", + "product_slug": "tiendasd1-colombia", + "product_name": "Tiendas D1 Colombia", + "product_url": "https://www.bitrefill.com/tiendasd1-colombia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/tiendasd1-colombia.webp" + }, + { + "country_code": "CO", + "country_name": "Colombia", + "product_slug": "claro-colombia", + "product_name": "Claro Colombia", + "product_url": "https://www.bitrefill.com/claro-colombia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/claro-colombia.webp" + }, + { + "country_code": "CO", + "country_name": "Colombia", + "product_slug": "exito-colombia", + "product_name": "Éxito Colombia", + "product_url": "https://www.bitrefill.com/exito-colombia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/exito-colombia.webp" + }, + { + "country_code": "CR", + "country_name": "Costa Rica", + "product_slug": "kolbi-costa-rica", + "product_name": "Kolbi Costa Rica", + "product_url": "https://www.bitrefill.com/kolbi-costa-rica/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/kolbi-costa-rica.webp" + }, + { + "country_code": "CR", + "country_name": "Costa Rica", + "product_slug": "claro-costa-rica", + "product_name": "Claro Costa Rica", + "product_url": "https://www.bitrefill.com/claro-costa-rica/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/claro-costa-rica.webp" + }, + { + "country_code": "CR", + "country_name": "Costa Rica", + "product_slug": "bitrefill-esim-costa-rica", + "product_name": "eSIM Costa Rica", + "product_url": "https://www.bitrefill.com/bitrefill-esim-costa-rica/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-costa-rica.webp" + }, + { + "country_code": "CU", + "country_name": "Cuba", + "product_slug": "bitrefill-esim-cuba", + "product_name": "eSIM Cuba", + "product_url": "https://www.bitrefill.com/bitrefill-esim-cuba/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-cuba.webp" + }, + { + "country_code": "CU", + "country_name": "Cuba", + "product_slug": "nauta-cuba", + "product_name": "Nauta Cuba", + "product_url": "https://www.bitrefill.com/nauta-cuba/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/nauta-cuba.webp" + }, + { + "country_code": "CU", + "country_name": "Cuba", + "product_slug": "cubacel-cuba", + "product_name": "Cubacel Cuba", + "product_url": "https://www.bitrefill.com/cubacel-cuba/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/cubacel-cuba.webp" + }, + { + "country_code": "CY", + "country_name": "Cyprus", + "product_slug": "wolt-cy", + "product_name": "Wolt Cyprus", + "product_url": "https://www.bitrefill.com/wolt-cy/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/wolt-cy.webp" + }, + { + "country_code": "CY", + "country_name": "Cyprus", + "product_slug": "ikea-cy", + "product_name": "IKEA Cyprus", + "product_url": "https://www.bitrefill.com/ikea-cy/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ikea-cy.webp" + }, + { + "country_code": "CY", + "country_name": "Cyprus", + "product_slug": "bitrefill-esim-cyprus", + "product_name": "eSIM Cyprus", + "product_url": "https://www.bitrefill.com/bitrefill-esim-cyprus/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-cyprus.webp" + }, + { + "country_code": "CZ", + "country_name": "Czech Republic", + "product_slug": "kaufland-czech-republic", + "product_name": "Kaufland CZ", + "product_url": "https://www.bitrefill.com/kaufland-czech-republic/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/kaufland-czech-republic.webp" + }, + { + "country_code": "CZ", + "country_name": "Czech Republic", + "product_slug": "alza-cz", + "product_name": "Alza CZ", + "product_url": "https://www.bitrefill.com/alza-cz/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/alza-cz.webp" + }, + { + "country_code": "CZ", + "country_name": "Czech Republic", + "product_slug": "wolt-cz", + "product_name": "Wolt CZ", + "product_url": "https://www.bitrefill.com/wolt-cz/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/wolt-cz.webp" + }, + { + "country_code": "DE", + "country_name": "Germany", + "product_slug": "rewe-germany", + "product_name": "Rewe Germany", + "product_url": "https://www.bitrefill.com/rewe-germany/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/rewe-germany.webp" + }, + { + "country_code": "DE", + "country_name": "Germany", + "product_slug": "kaufland-germany", + "product_name": "Kaufland Germany", + "product_url": "https://www.bitrefill.com/kaufland-germany/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/kaufland-germany.webp" + }, + { + "country_code": "DE", + "country_name": "Germany", + "product_slug": "media-markt-germany", + "product_name": "MediaMarkt Germany", + "product_url": "https://www.bitrefill.com/media-markt-germany/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/media-markt-germany.webp" + }, + { + "country_code": "DK", + "country_name": "Denmark", + "product_slug": "wolt-dk", + "product_name": "Wolt Denmark", + "product_url": "https://www.bitrefill.com/wolt-dk/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/wolt-dk.webp" + }, + { + "country_code": "DK", + "country_name": "Denmark", + "product_slug": "zalando-denmark", + "product_name": "Zalando Denmark", + "product_url": "https://www.bitrefill.com/zalando-denmark/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/zalando-denmark.webp" + }, + { + "country_code": "DK", + "country_name": "Denmark", + "product_slug": "salling-denmark", + "product_name": "Salling Denmark", + "product_url": "https://www.bitrefill.com/salling-denmark/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/salling-denmark.webp" + }, + { + "country_code": "DO", + "country_name": "Dominican Republic", + "product_slug": "claro-dominicana", + "product_name": "Claro Dominicana", + "product_url": "https://www.bitrefill.com/claro-dominicana/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/claro-dominicana.webp" + }, + { + "country_code": "DO", + "country_name": "Dominican Republic", + "product_slug": "altice-dominicana", + "product_name": "Altice Dominicana", + "product_url": "https://www.bitrefill.com/altice-dominicana/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/altice-dominicana.webp" + }, + { + "country_code": "DO", + "country_name": "Dominican Republic", + "product_slug": "bitrefill-esim-dominican-republic", + "product_name": "eSIM Dominican Republic", + "product_url": "https://www.bitrefill.com/bitrefill-esim-dominican-republic/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-dominican-republic.webp" + }, + { + "country_code": "EC", + "country_name": "Ecuador", + "product_slug": "digicel-ecuador", + "product_name": "Digicel Ecuador", + "product_url": "https://www.bitrefill.com/digicel-ecuador/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-ecuador.webp" + }, + { + "country_code": "EC", + "country_name": "Ecuador", + "product_slug": "claro-ecuador", + "product_name": "Claro Ecuador", + "product_url": "https://www.bitrefill.com/claro-ecuador/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/claro-ecuador.webp" + }, + { + "country_code": "EC", + "country_name": "Ecuador", + "product_slug": "bitrefill-esim-ecuador", + "product_name": "eSIM Ecuador", + "product_url": "https://www.bitrefill.com/bitrefill-esim-ecuador/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-ecuador.webp" + }, + { + "country_code": "EE", + "country_name": "Estonia", + "product_slug": "circle-k-estonia", + "product_name": "Circle K Estonia", + "product_url": "https://www.bitrefill.com/circle-k-estonia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/circle-k-estonia.webp" + }, + { + "country_code": "EE", + "country_name": "Estonia", + "product_slug": "handm-estonia", + "product_name": "H&M Estonia", + "product_url": "https://www.bitrefill.com/handm-estonia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/handm-estonia.webp" + }, + { + "country_code": "EE", + "country_name": "Estonia", + "product_slug": "telia-estonia", + "product_name": "Telia Estonia", + "product_url": "https://www.bitrefill.com/telia-estonia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/telia-estonia.webp" + }, + { + "country_code": "EG", + "country_name": "Egypt", + "product_slug": "vodafone-pin-egypt", + "product_name": "Vodafone Egypt", + "product_url": "https://www.bitrefill.com/vodafone-pin-egypt/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/vodafone-pin-egypt.webp" + }, + { + "country_code": "EG", + "country_name": "Egypt", + "product_slug": "etisalat-egypt", + "product_name": "Etisalat Egypt", + "product_url": "https://www.bitrefill.com/etisalat-egypt/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/etisalat-egypt.webp" + }, + { + "country_code": "EG", + "country_name": "Egypt", + "product_slug": "jumia-egypt", + "product_name": "Jumia Egypt", + "product_url": "https://www.bitrefill.com/jumia-egypt/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/jumia-egypt.webp" + }, + { + "country_code": "ES", + "country_name": "Spain", + "product_slug": "carrefour-es", + "product_name": "Carrefour Spain", + "product_url": "https://www.bitrefill.com/carrefour-es/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/carrefour-es.webp" + }, + { + "country_code": "ES", + "country_name": "Spain", + "product_slug": "el-corte-ingles-spain", + "product_name": "El Corte Inglés", + "product_url": "https://www.bitrefill.com/el-corte-ingles-spain/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/el-corte-ingles-spain.webp" + }, + { + "country_code": "ES", + "country_name": "Spain", + "product_slug": "mercadona-spain", + "product_name": "Mercadona Spain", + "product_url": "https://www.bitrefill.com/mercadona-spain/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/mercadona-spain.webp" + }, + { + "country_code": "ET", + "country_name": "Ethiopia", + "product_slug": "ethio-telecom-ethiopia", + "product_name": "Ethio Telecom", + "product_url": "https://www.bitrefill.com/ethio-telecom-ethiopia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ethio-telecom-ethiopia.webp" + }, + { + "country_code": "ET", + "country_name": "Ethiopia", + "product_slug": "bitrefill-esim-ethiopia", + "product_name": "eSIM Ethiopia", + "product_url": "https://www.bitrefill.com/bitrefill-esim-ethiopia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-ethiopia.webp" + }, + { + "country_code": "ET", + "country_name": "Ethiopia", + "product_slug": "cbe-birr-ethiopia", + "product_name": "CBE Birr Ethiopia", + "product_url": "https://www.bitrefill.com/cbe-birr-ethiopia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/cbe-birr-ethiopia.webp" + }, + { + "country_code": "FI", + "country_name": "Finland", + "product_slug": "wolt-fi", + "product_name": "Wolt Finland", + "product_url": "https://www.bitrefill.com/wolt-fi/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/wolt-fi.webp" + }, + { + "country_code": "FI", + "country_name": "Finland", + "product_slug": "k-ruoka-finland", + "product_name": "K-Ruoka Finland", + "product_url": "https://www.bitrefill.com/k-ruoka-finland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/k-ruoka-finland.webp" + }, + { + "country_code": "FI", + "country_name": "Finland", + "product_slug": "s-group-finland", + "product_name": "S-Group Finland", + "product_url": "https://www.bitrefill.com/s-group-finland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/s-group-finland.webp" + }, + { + "country_code": "FR", + "country_name": "France", + "product_slug": "carrefour-france", + "product_name": "Carrefour France", + "product_url": "https://www.bitrefill.com/carrefour-france/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/carrefour-france.webp" + }, + { + "country_code": "FR", + "country_name": "France", + "product_slug": "fnac-france", + "product_name": "Fnac France", + "product_url": "https://www.bitrefill.com/fnac-france/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/fnac-france.webp" + }, + { + "country_code": "FR", + "country_name": "France", + "product_slug": "leclerc-france", + "product_name": "E.Leclerc France", + "product_url": "https://www.bitrefill.com/leclerc-france/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/leclerc-france.webp" + }, + { + "country_code": "GB", + "country_name": "United Kingdom", + "product_slug": "uber-rides-uk", + "product_name": "Uber Money GBP", + "product_url": "https://www.bitrefill.com/uber-rides-uk/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/uber-rides-uk.webp" + }, + { + "country_code": "GB", + "country_name": "United Kingdom", + "product_slug": "deliveroo-uk", + "product_name": "Deliveroo UK", + "product_url": "https://www.bitrefill.com/deliveroo-uk/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/deliveroo-uk.webp" + }, + { + "country_code": "GB", + "country_name": "United Kingdom", + "product_slug": "sainsburys-in-store-digital-uk", + "product_name": "Sainsbury's UK", + "product_url": "https://www.bitrefill.com/sainsburys-in-store-digital-uk/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/sainsburys-in-store-digital-uk.webp" + }, + { + "country_code": "GD", + "country_name": "Grenada", + "product_slug": "digicel-grenada", + "product_name": "Digicel Grenada", + "product_url": "https://www.bitrefill.com/digicel-grenada/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-grenada.webp" + }, + { + "country_code": "GD", + "country_name": "Grenada", + "product_slug": "flow-grenada", + "product_name": "Flow Grenada", + "product_url": "https://www.bitrefill.com/flow-grenada/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-grenada.webp" + }, + { + "country_code": "GD", + "country_name": "Grenada", + "product_slug": "bitrefill-esim-caribbean", + "product_name": "eSIM Caribbean", + "product_url": "https://www.bitrefill.com/bitrefill-esim-caribbean/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-caribbean.webp" + }, + { + "country_code": "GE", + "country_name": "Georgia", + "product_slug": "geocell-georgia", + "product_name": "Geocell Georgia", + "product_url": "https://www.bitrefill.com/geocell-georgia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/geocell-georgia.webp" + }, + { + "country_code": "GE", + "country_name": "Georgia", + "product_slug": "magti-georgia", + "product_name": "Magti Georgia", + "product_url": "https://www.bitrefill.com/magti-georgia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/magti-georgia.webp" + }, + { + "country_code": "GE", + "country_name": "Georgia", + "product_slug": "bitrefill-esim-georgia", + "product_name": "eSIM Georgia", + "product_url": "https://www.bitrefill.com/bitrefill-esim-georgia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-georgia.webp" + }, + { + "country_code": "GG", + "country_name": "Guernsey", + "product_slug": "deliveroo-uk", + "product_name": "Deliveroo UK", + "product_url": "https://www.bitrefill.com/deliveroo-uk/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/deliveroo-uk.webp" + }, + { + "country_code": "GG", + "country_name": "Guernsey", + "product_slug": "o2-pin-united-kingdom", + "product_name": "O2 UK", + "product_url": "https://www.bitrefill.com/o2-pin-united-kingdom/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/o2-pin-united-kingdom.webp" + }, + { + "country_code": "GG", + "country_name": "Guernsey", + "product_slug": "ee-pin-united-kingdom", + "product_name": "EE UK", + "product_url": "https://www.bitrefill.com/ee-pin-united-kingdom/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ee-pin-united-kingdom.webp" + }, + { + "country_code": "GH", + "country_name": "Ghana", + "product_slug": "mtn-ghana", + "product_name": "MTN Ghana", + "product_url": "https://www.bitrefill.com/mtn-ghana/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/mtn-ghana.webp" + }, + { + "country_code": "GH", + "country_name": "Ghana", + "product_slug": "vodafone-ghana", + "product_name": "Vodafone Ghana", + "product_url": "https://www.bitrefill.com/vodafone-ghana/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/vodafone-ghana.webp" + }, + { + "country_code": "GH", + "country_name": "Ghana", + "product_slug": "bitrefill-esim-ghana", + "product_name": "eSIM Ghana", + "product_url": "https://www.bitrefill.com/bitrefill-esim-ghana/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-ghana.webp" + }, + { + "country_code": "GI", + "country_name": "Gibraltar", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "GI", + "country_name": "Gibraltar", + "product_slug": "gibtel-gibraltar", + "product_name": "Gibtel Gibraltar", + "product_url": "https://www.bitrefill.com/gibtel-gibraltar/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/gibtel-gibraltar.webp" + }, + { + "country_code": "GI", + "country_name": "Gibraltar", + "product_slug": "sure-gibraltar", + "product_name": "Sure Gibraltar", + "product_url": "https://www.bitrefill.com/sure-gibraltar/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/sure-gibraltar.webp" + }, + { + "country_code": "GM", + "country_name": "Gambia", + "product_slug": "africell-gambia", + "product_name": "Africell Gambia", + "product_url": "https://www.bitrefill.com/africell-gambia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/africell-gambia.webp" + }, + { + "country_code": "GM", + "country_name": "Gambia", + "product_slug": "qcell-gambia", + "product_name": "QCell Gambia", + "product_url": "https://www.bitrefill.com/qcell-gambia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/qcell-gambia.webp" + }, + { + "country_code": "GM", + "country_name": "Gambia", + "product_slug": "bitrefill-esim-gambia", + "product_name": "eSIM Gambia", + "product_url": "https://www.bitrefill.com/bitrefill-esim-gambia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-gambia.webp" + }, + { + "country_code": "GP", + "country_name": "Guadeloupe", + "product_slug": "orange-guadeloupe", + "product_name": "Orange Guadeloupe", + "product_url": "https://www.bitrefill.com/orange-guadeloupe/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/orange-guadeloupe.webp" + }, + { + "country_code": "GP", + "country_name": "Guadeloupe", + "product_slug": "digicel-guadeloupe", + "product_name": "Digicel Guadeloupe", + "product_url": "https://www.bitrefill.com/digicel-guadeloupe/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-guadeloupe.webp" + }, + { + "country_code": "GP", + "country_name": "Guadeloupe", + "product_slug": "bitrefill-esim-guadeloupe", + "product_name": "eSIM Guadeloupe", + "product_url": "https://www.bitrefill.com/bitrefill-esim-guadeloupe/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-guadeloupe.webp" + }, + { + "country_code": "GR", + "country_name": "Greece", + "product_slug": "cosmote-pin-greece", + "product_name": "Cosmote Greece", + "product_url": "https://www.bitrefill.com/cosmote-pin-greece/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/cosmote-pin-greece.webp" + }, + { + "country_code": "GR", + "country_name": "Greece", + "product_slug": "wind-greece", + "product_name": "Wind Hellas Greece", + "product_url": "https://www.bitrefill.com/wind-greece/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/wind-greece.webp" + }, + { + "country_code": "GR", + "country_name": "Greece", + "product_slug": "public-greece", + "product_name": "Public Greece", + "product_url": "https://www.bitrefill.com/public-greece/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/public-greece.webp" + }, + { + "country_code": "GT", + "country_name": "Guatemala", + "product_slug": "tigo-guatemala", + "product_name": "Tigo Guatemala", + "product_url": "https://www.bitrefill.com/tigo-guatemala/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/tigo-guatemala.webp" + }, + { + "country_code": "GT", + "country_name": "Guatemala", + "product_slug": "claro-guatemala", + "product_name": "Claro Guatemala", + "product_url": "https://www.bitrefill.com/claro-guatemala/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/claro-guatemala.webp" + }, + { + "country_code": "GT", + "country_name": "Guatemala", + "product_slug": "bitrefill-esim-guatemala", + "product_name": "eSIM Guatemala", + "product_url": "https://www.bitrefill.com/bitrefill-esim-guatemala/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-guatemala.webp" + }, + { + "country_code": "GU", + "country_name": "Guam", + "product_slug": "it-e-t-mobile-guam", + "product_name": "IT&E / T-Mobile Guam", + "product_url": "https://www.bitrefill.com/it-e-t-mobile-guam/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/it-e-t-mobile-guam.webp" + }, + { + "country_code": "GU", + "country_name": "Guam", + "product_slug": "docomo-guam", + "product_name": "Docomo Pacific Guam", + "product_url": "https://www.bitrefill.com/docomo-guam/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/docomo-guam.webp" + }, + { + "country_code": "GU", + "country_name": "Guam", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "HK", + "country_name": "Hong Kong", + "product_slug": "steam-hong-kong", + "product_name": "Steam HK", + "product_url": "https://www.bitrefill.com/steam-hong-kong/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/steam-hong-kong.webp" + }, + { + "country_code": "HK", + "country_name": "Hong Kong", + "product_slug": "itunes-hong-kong", + "product_name": "Apple HK", + "product_url": "https://www.bitrefill.com/itunes-hong-kong/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/itunes-hong-kong.webp" + }, + { + "country_code": "HK", + "country_name": "Hong Kong", + "product_slug": "watsons-hong-kong", + "product_name": "Watsons HK", + "product_url": "https://www.bitrefill.com/watsons-hong-kong/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/watsons-hong-kong.webp" + }, + { + "country_code": "HN", + "country_name": "Honduras", + "product_slug": "tigo-honduras", + "product_name": "Tigo Honduras", + "product_url": "https://www.bitrefill.com/tigo-honduras/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/tigo-honduras.webp" + }, + { + "country_code": "HN", + "country_name": "Honduras", + "product_slug": "claro-honduras", + "product_name": "Claro Honduras", + "product_url": "https://www.bitrefill.com/claro-honduras/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/claro-honduras.webp" + }, + { + "country_code": "HN", + "country_name": "Honduras", + "product_slug": "bitrefill-esim-honduras", + "product_name": "eSIM Honduras", + "product_url": "https://www.bitrefill.com/bitrefill-esim-honduras/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-honduras.webp" + }, + { + "country_code": "HR", + "country_name": "Croatia", + "product_slug": "wolthr-croatia", + "product_name": "Wolt Croatia", + "product_url": "https://www.bitrefill.com/wolthr-croatia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/wolthr-croatia.webp" + }, + { + "country_code": "HR", + "country_name": "Croatia", + "product_slug": "mall-hr-croatia", + "product_name": "MALL.HR Croatia", + "product_url": "https://www.bitrefill.com/mall-hr-croatia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/mall-hr-croatia.webp" + }, + { + "country_code": "HR", + "country_name": "Croatia", + "product_slug": "playstation-croatia", + "product_name": "PlayStation Croatia", + "product_url": "https://www.bitrefill.com/playstation-croatia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/playstation-croatia.webp" + }, + { + "country_code": "HU", + "country_name": "Hungary", + "product_slug": "alza-hungary", + "product_name": "Alza Hungary", + "product_url": "https://www.bitrefill.com/alza-hungary/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/alza-hungary.webp" + }, + { + "country_code": "HU", + "country_name": "Hungary", + "product_slug": "tesco-hungary", + "product_name": "Tesco Hungary", + "product_url": "https://www.bitrefill.com/tesco-hungary/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/tesco-hungary.webp" + }, + { + "country_code": "HU", + "country_name": "Hungary", + "product_slug": "telekom-hungary", + "product_name": "Telekom Hungary", + "product_url": "https://www.bitrefill.com/telekom-hungary/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/telekom-hungary.webp" + }, + { + "country_code": "ID", + "country_name": "Indonesia", + "product_slug": "dana-cash-indonesia", + "product_name": "DANA Cash Indonesia", + "product_url": "https://www.bitrefill.com/dana-cash-indonesia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/dana-cash-indonesia.webp" + }, + { + "country_code": "ID", + "country_name": "Indonesia", + "product_slug": "gopay-indonesia", + "product_name": "GoPay Indonesia", + "product_url": "https://www.bitrefill.com/gopay-indonesia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/gopay-indonesia.webp" + }, + { + "country_code": "ID", + "country_name": "Indonesia", + "product_slug": "ovo-indonesia", + "product_name": "OVO Indonesia", + "product_url": "https://www.bitrefill.com/ovo-indonesia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ovo-indonesia.webp" + }, + { + "country_code": "IE", + "country_name": "Ireland", + "product_slug": "an-post-ireland", + "product_name": "An Post Ireland", + "product_url": "https://www.bitrefill.com/an-post-ireland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/an-post-ireland.webp" + }, + { + "country_code": "IE", + "country_name": "Ireland", + "product_slug": "tesco-ireland", + "product_name": "Tesco Ireland", + "product_url": "https://www.bitrefill.com/tesco-ireland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/tesco-ireland.webp" + }, + { + "country_code": "IE", + "country_name": "Ireland", + "product_slug": "three-ireland", + "product_name": "Three Ireland", + "product_url": "https://www.bitrefill.com/three-ireland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/three-ireland.webp" + }, + { + "country_code": "IL", + "country_name": "Israel", + "product_slug": "cellcom-israel", + "product_name": "Cellcom Israel", + "product_url": "https://www.bitrefill.com/cellcom-israel/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/cellcom-israel.webp" + }, + { + "country_code": "IL", + "country_name": "Israel", + "product_slug": "partner-israel", + "product_name": "Partner Israel", + "product_url": "https://www.bitrefill.com/partner-israel/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/partner-israel.webp" + }, + { + "country_code": "IL", + "country_name": "Israel", + "product_slug": "bezeq-israel", + "product_name": "Bezeq Israel", + "product_url": "https://www.bitrefill.com/bezeq-israel/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bezeq-israel.webp" + }, + { + "country_code": "IM", + "country_name": "Isle of Man", + "product_slug": "deliveroo-uk", + "product_name": "Deliveroo UK", + "product_url": "https://www.bitrefill.com/deliveroo-uk/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/deliveroo-uk.webp" + }, + { + "country_code": "IM", + "country_name": "Isle of Man", + "product_slug": "o2-pin-united-kingdom", + "product_name": "O2 UK", + "product_url": "https://www.bitrefill.com/o2-pin-united-kingdom/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/o2-pin-united-kingdom.webp" + }, + { + "country_code": "IM", + "country_name": "Isle of Man", + "product_slug": "ee-pin-united-kingdom", + "product_name": "EE UK", + "product_url": "https://www.bitrefill.com/ee-pin-united-kingdom/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ee-pin-united-kingdom.webp" + }, + { + "country_code": "IN", + "country_name": "India", + "product_slug": "phonepe-india", + "product_name": "PhonePe India", + "product_url": "https://www.bitrefill.com/phonepe-india/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/phonepe-india.webp" + }, + { + "country_code": "IN", + "country_name": "India", + "product_slug": "jio-india", + "product_name": "Jio India", + "product_url": "https://www.bitrefill.com/jio-india/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/jio-india.webp" + }, + { + "country_code": "IN", + "country_name": "India", + "product_slug": "airtel-india", + "product_name": "Airtel India", + "product_url": "https://www.bitrefill.com/airtel-india/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/airtel-india.webp" + }, + { + "country_code": "IQ", + "country_name": "Iraq", + "product_slug": "zain-iraq", + "product_name": "Zain Iraq", + "product_url": "https://www.bitrefill.com/zain-iraq/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/zain-iraq.webp" + }, + { + "country_code": "IQ", + "country_name": "Iraq", + "product_slug": "asia-cell-iraq", + "product_name": "Asia Cell Iraq", + "product_url": "https://www.bitrefill.com/asia-cell-iraq/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/asia-cell-iraq.webp" + }, + { + "country_code": "IQ", + "country_name": "Iraq", + "product_slug": "bitrefill-esim-iraq", + "product_name": "eSIM Iraq", + "product_url": "https://www.bitrefill.com/bitrefill-esim-iraq/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-iraq.webp" + }, + { + "country_code": "IS", + "country_name": "Iceland", + "product_slug": "siminn-iceland", + "product_name": "Síminn Iceland", + "product_url": "https://www.bitrefill.com/siminn-iceland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/siminn-iceland.webp" + }, + { + "country_code": "IS", + "country_name": "Iceland", + "product_slug": "nova-iceland", + "product_name": "Nova Iceland", + "product_url": "https://www.bitrefill.com/nova-iceland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/nova-iceland.webp" + }, + { + "country_code": "IS", + "country_name": "Iceland", + "product_slug": "bitrefill-esim-europe", + "product_name": "eSIM Europe", + "product_url": "https://www.bitrefill.com/bitrefill-esim-europe/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-europe.webp" + }, + { + "country_code": "IT", + "country_name": "Italy", + "product_slug": "q8-italy", + "product_name": "Q8 Italy", + "product_url": "https://www.bitrefill.com/q8-italy/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/q8-italy.webp" + }, + { + "country_code": "IT", + "country_name": "Italy", + "product_slug": "esselunga-italy", + "product_name": "Esselunga Italy", + "product_url": "https://www.bitrefill.com/esselunga-italy/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/esselunga-italy.webp" + }, + { + "country_code": "IT", + "country_name": "Italy", + "product_slug": "trenitalia-italy", + "product_name": "Trenitalia Italy", + "product_url": "https://www.bitrefill.com/trenitalia-italy/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/trenitalia-italy.webp" + }, + { + "country_code": "JE", + "country_name": "Jersey", + "product_slug": "deliveroo-uk", + "product_name": "Deliveroo UK", + "product_url": "https://www.bitrefill.com/deliveroo-uk/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/deliveroo-uk.webp" + }, + { + "country_code": "JE", + "country_name": "Jersey", + "product_slug": "o2-pin-united-kingdom", + "product_name": "O2 UK", + "product_url": "https://www.bitrefill.com/o2-pin-united-kingdom/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/o2-pin-united-kingdom.webp" + }, + { + "country_code": "JE", + "country_name": "Jersey", + "product_slug": "ee-pin-united-kingdom", + "product_name": "EE UK", + "product_url": "https://www.bitrefill.com/ee-pin-united-kingdom/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ee-pin-united-kingdom.webp" + }, + { + "country_code": "JM", + "country_name": "Jamaica", + "product_slug": "digicel-jamaica", + "product_name": "Digicel Jamaica", + "product_url": "https://www.bitrefill.com/digicel-jamaica/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-jamaica.webp" + }, + { + "country_code": "JM", + "country_name": "Jamaica", + "product_slug": "flow-jamaica", + "product_name": "Flow Jamaica", + "product_url": "https://www.bitrefill.com/flow-jamaica/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-jamaica.webp" + }, + { + "country_code": "JM", + "country_name": "Jamaica", + "product_slug": "bitrefill-esim-jamaica", + "product_name": "eSIM Jamaica", + "product_url": "https://www.bitrefill.com/bitrefill-esim-jamaica/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-jamaica.webp" + }, + { + "country_code": "JO", + "country_name": "Jordan", + "product_slug": "zain-jordan", + "product_name": "Zain Jordan", + "product_url": "https://www.bitrefill.com/zain-jordan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/zain-jordan.webp" + }, + { + "country_code": "JO", + "country_name": "Jordan", + "product_slug": "orange-jordan", + "product_name": "Orange Jordan", + "product_url": "https://www.bitrefill.com/orange-jordan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/orange-jordan.webp" + }, + { + "country_code": "JO", + "country_name": "Jordan", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "JP", + "country_name": "Japan", + "product_slug": "au-pay-gift-card-japan", + "product_name": "au PAY Japan", + "product_url": "https://www.bitrefill.com/au-pay-gift-card-japan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/au-pay-gift-card-japan.webp" + }, + { + "country_code": "JP", + "country_name": "Japan", + "product_slug": "nintendo-japan", + "product_name": "Nintendo Japan", + "product_url": "https://www.bitrefill.com/nintendo-japan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/nintendo-japan.webp" + }, + { + "country_code": "JP", + "country_name": "Japan", + "product_slug": "starbucks-japan", + "product_name": "Starbucks Japan", + "product_url": "https://www.bitrefill.com/starbucks-japan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/starbucks-japan.webp" + }, + { + "country_code": "KE", + "country_name": "Kenya", + "product_slug": "naivas-pin-kenya", + "product_name": "Naivas Kenya", + "product_url": "https://www.bitrefill.com/naivas-pin-kenya/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/naivas-pin-kenya.webp" + }, + { + "country_code": "KE", + "country_name": "Kenya", + "product_slug": "safaricom-kenya", + "product_name": "Safaricom Kenya", + "product_url": "https://www.bitrefill.com/safaricom-kenya/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/safaricom-kenya.webp" + }, + { + "country_code": "KE", + "country_name": "Kenya", + "product_slug": "mtn-kenya", + "product_name": "MTN Kenya", + "product_url": "https://www.bitrefill.com/mtn-kenya/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/mtn-kenya.webp" + }, + { + "country_code": "KN", + "country_name": "Saint Kitts and Nevis", + "product_slug": "digicel-st-kitts-and-nevis", + "product_name": "Digicel St Kitts", + "product_url": "https://www.bitrefill.com/digicel-st-kitts-and-nevis/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-st-kitts-and-nevis.webp" + }, + { + "country_code": "KN", + "country_name": "Saint Kitts and Nevis", + "product_slug": "flow-st-kitts", + "product_name": "Flow St Kitts", + "product_url": "https://www.bitrefill.com/flow-st-kitts/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-st-kitts.webp" + }, + { + "country_code": "KN", + "country_name": "Saint Kitts and Nevis", + "product_slug": "bitrefill-esim-caribbean", + "product_name": "eSIM Caribbean", + "product_url": "https://www.bitrefill.com/bitrefill-esim-caribbean/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-caribbean.webp" + }, + { + "country_code": "KR", + "country_name": "South Korea", + "product_slug": "naver-pay-korea", + "product_name": "Naver Pay", + "product_url": "https://www.bitrefill.com/naver-pay-korea/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/naver-pay-korea.webp" + }, + { + "country_code": "KR", + "country_name": "South Korea", + "product_slug": "kakao-korea", + "product_name": "Kakao Korea", + "product_url": "https://www.bitrefill.com/kakao-korea/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/kakao-korea.webp" + }, + { + "country_code": "KR", + "country_name": "South Korea", + "product_slug": "coupang-korea", + "product_name": "Coupang Korea", + "product_url": "https://www.bitrefill.com/coupang-korea/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/coupang-korea.webp" + }, + { + "country_code": "KW", + "country_name": "Kuwait", + "product_slug": "viva-kuwait", + "product_name": "Viva Kuwait", + "product_url": "https://www.bitrefill.com/viva-kuwait/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/viva-kuwait.webp" + }, + { + "country_code": "KW", + "country_name": "Kuwait", + "product_slug": "zain-kuwait", + "product_name": "Zain Kuwait", + "product_url": "https://www.bitrefill.com/zain-kuwait/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/zain-kuwait.webp" + }, + { + "country_code": "KW", + "country_name": "Kuwait", + "product_slug": "ooredoo-kuwait", + "product_name": "Ooredoo Kuwait", + "product_url": "https://www.bitrefill.com/ooredoo-kuwait/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ooredoo-kuwait.webp" + }, + { + "country_code": "KY", + "country_name": "Cayman Islands", + "product_slug": "digicel-cayman-islands", + "product_name": "Digicel Cayman Islands", + "product_url": "https://www.bitrefill.com/digicel-cayman-islands/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-cayman-islands.webp" + }, + { + "country_code": "KY", + "country_name": "Cayman Islands", + "product_slug": "flow-cayman", + "product_name": "Flow Cayman", + "product_url": "https://www.bitrefill.com/flow-cayman/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-cayman.webp" + }, + { + "country_code": "KY", + "country_name": "Cayman Islands", + "product_slug": "bitrefill-esim-caribbean", + "product_name": "eSIM Caribbean", + "product_url": "https://www.bitrefill.com/bitrefill-esim-caribbean/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-caribbean.webp" + }, + { + "country_code": "KZ", + "country_name": "Kazakhstan", + "product_slug": "beeline-kazakhstan", + "product_name": "Beeline Kazakhstan", + "product_url": "https://www.bitrefill.com/beeline-kazakhstan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/beeline-kazakhstan.webp" + }, + { + "country_code": "KZ", + "country_name": "Kazakhstan", + "product_slug": "kcell-kazakhstan", + "product_name": "Kcell Kazakhstan", + "product_url": "https://www.bitrefill.com/kcell-kazakhstan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/kcell-kazakhstan.webp" + }, + { + "country_code": "KZ", + "country_name": "Kazakhstan", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "LC", + "country_name": "Saint Lucia", + "product_slug": "digicel-saint-lucia", + "product_name": "Digicel Saint Lucia", + "product_url": "https://www.bitrefill.com/digicel-saint-lucia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-saint-lucia.webp" + }, + { + "country_code": "LC", + "country_name": "Saint Lucia", + "product_slug": "flow-saint-lucia", + "product_name": "Flow Saint Lucia", + "product_url": "https://www.bitrefill.com/flow-saint-lucia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-saint-lucia.webp" + }, + { + "country_code": "LC", + "country_name": "Saint Lucia", + "product_slug": "bitrefill-esim-caribbean", + "product_name": "eSIM Caribbean", + "product_url": "https://www.bitrefill.com/bitrefill-esim-caribbean/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-caribbean.webp" + }, + { + "country_code": "LI", + "country_name": "Liechtenstein", + "product_slug": "post-liechtenstein", + "product_name": "Post Liechtenstein", + "product_url": "https://www.bitrefill.com/post-liechtenstein/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/post-liechtenstein.webp" + }, + { + "country_code": "LI", + "country_name": "Liechtenstein", + "product_slug": "bitrefill-esim-europe", + "product_name": "eSIM Europe", + "product_url": "https://www.bitrefill.com/bitrefill-esim-europe/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-europe.webp" + }, + { + "country_code": "LI", + "country_name": "Liechtenstein", + "product_slug": "digitec-switzerland", + "product_name": "Digitec Switzerland", + "product_url": "https://www.bitrefill.com/digitec-switzerland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digitec-switzerland.webp" + }, + { + "country_code": "LK", + "country_name": "Sri Lanka", + "product_slug": "dialog-sri-lanka", + "product_name": "Dialog Sri Lanka", + "product_url": "https://www.bitrefill.com/dialog-sri-lanka/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/dialog-sri-lanka.webp" + }, + { + "country_code": "LK", + "country_name": "Sri Lanka", + "product_slug": "mobitel-sri-lanka", + "product_name": "Mobitel Sri Lanka", + "product_url": "https://www.bitrefill.com/mobitel-sri-lanka/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/mobitel-sri-lanka.webp" + }, + { + "country_code": "LK", + "country_name": "Sri Lanka", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "LT", + "country_name": "Lithuania", + "product_slug": "maxima-lithuania", + "product_name": "Maxima Lithuania", + "product_url": "https://www.bitrefill.com/maxima-lithuania/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/maxima-lithuania.webp" + }, + { + "country_code": "LT", + "country_name": "Lithuania", + "product_slug": "telia-lithuania", + "product_name": "Telia Lithuania", + "product_url": "https://www.bitrefill.com/telia-lithuania/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/telia-lithuania.webp" + }, + { + "country_code": "LT", + "country_name": "Lithuania", + "product_slug": "bitrefill-esim-lithuania", + "product_name": "eSIM Lithuania", + "product_url": "https://www.bitrefill.com/bitrefill-esim-lithuania/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-lithuania.webp" + }, + { + "country_code": "LU", + "country_name": "Luxembourg", + "product_slug": "deliveroo-be", + "product_name": "Deliveroo Belgium", + "product_url": "https://www.bitrefill.com/deliveroo-be/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/deliveroo-be.webp" + }, + { + "country_code": "LU", + "country_name": "Luxembourg", + "product_slug": "bol-eu", + "product_name": "Bol.com", + "product_url": "https://www.bitrefill.com/bol-eu/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bol-eu.webp" + }, + { + "country_code": "LU", + "country_name": "Luxembourg", + "product_slug": "bitrefill-esim-europe", + "product_name": "eSIM Europe", + "product_url": "https://www.bitrefill.com/bitrefill-esim-europe/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-europe.webp" + }, + { + "country_code": "LV", + "country_name": "Latvia", + "product_slug": "rimi-latvia", + "product_name": "Rimi Latvia", + "product_url": "https://www.bitrefill.com/rimi-latvia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/rimi-latvia.webp" + }, + { + "country_code": "LV", + "country_name": "Latvia", + "product_slug": "tele2-latvia", + "product_name": "Tele2 Latvia", + "product_url": "https://www.bitrefill.com/tele2-latvia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/tele2-latvia.webp" + }, + { + "country_code": "LV", + "country_name": "Latvia", + "product_slug": "bitrefill-esim-latvia", + "product_name": "eSIM Latvia", + "product_url": "https://www.bitrefill.com/bitrefill-esim-latvia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-latvia.webp" + }, + { + "country_code": "MA", + "country_name": "Morocco", + "product_slug": "maroc-telecom-morocco", + "product_name": "Maroc Telecom", + "product_url": "https://www.bitrefill.com/maroc-telecom-morocco/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/maroc-telecom-morocco.webp" + }, + { + "country_code": "MA", + "country_name": "Morocco", + "product_slug": "orange-maroc", + "product_name": "Orange Maroc", + "product_url": "https://www.bitrefill.com/orange-maroc/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/orange-maroc.webp" + }, + { + "country_code": "MA", + "country_name": "Morocco", + "product_slug": "bitrefill-esim-morocco", + "product_name": "eSIM Morocco", + "product_url": "https://www.bitrefill.com/bitrefill-esim-morocco/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-morocco.webp" + }, + { + "country_code": "MD", + "country_name": "Moldova", + "product_slug": "moldcell-moldova", + "product_name": "Moldcell Moldova", + "product_url": "https://www.bitrefill.com/moldcell-moldova/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/moldcell-moldova.webp" + }, + { + "country_code": "MD", + "country_name": "Moldova", + "product_slug": "orange-moldova", + "product_name": "Orange Moldova", + "product_url": "https://www.bitrefill.com/orange-moldova/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/orange-moldova.webp" + }, + { + "country_code": "MD", + "country_name": "Moldova", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "ME", + "country_name": "Montenegro", + "product_slug": "telenor-montenegro", + "product_name": "Telenor Montenegro", + "product_url": "https://www.bitrefill.com/telenor-montenegro/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/telenor-montenegro.webp" + }, + { + "country_code": "ME", + "country_name": "Montenegro", + "product_slug": "m-tel-montenegro", + "product_name": "m:tel Montenegro", + "product_url": "https://www.bitrefill.com/m-tel-montenegro/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/m-tel-montenegro.webp" + }, + { + "country_code": "ME", + "country_name": "Montenegro", + "product_slug": "bitrefill-esim-montenegro", + "product_name": "eSIM Montenegro", + "product_url": "https://www.bitrefill.com/bitrefill-esim-montenegro/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-montenegro.webp" + }, + { + "country_code": "MF", + "country_name": "Saint Martin", + "product_slug": "orange-saint-martin", + "product_name": "Orange Saint Martin", + "product_url": "https://www.bitrefill.com/orange-saint-martin/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/orange-saint-martin.webp" + }, + { + "country_code": "MF", + "country_name": "Saint Martin", + "product_slug": "digicel-saint-martin", + "product_name": "Digicel Saint Martin", + "product_url": "https://www.bitrefill.com/digicel-saint-martin/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-saint-martin.webp" + }, + { + "country_code": "MF", + "country_name": "Saint Martin", + "product_slug": "bitrefill-esim-saint-martin", + "product_name": "eSIM Saint Martin", + "product_url": "https://www.bitrefill.com/bitrefill-esim-saint-martin/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-saint-martin.webp" + }, + { + "country_code": "MG", + "country_name": "Madagascar", + "product_slug": "airtel-madagascar", + "product_name": "Airtel Madagascar", + "product_url": "https://www.bitrefill.com/airtel-madagascar/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/airtel-madagascar.webp" + }, + { + "country_code": "MG", + "country_name": "Madagascar", + "product_slug": "telma-madagascar", + "product_name": "Telma Madagascar", + "product_url": "https://www.bitrefill.com/telma-madagascar/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/telma-madagascar.webp" + }, + { + "country_code": "MG", + "country_name": "Madagascar", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "MH", + "country_name": "Marshall Islands", + "product_slug": "it-e-t-mobile-guam", + "product_name": "IT&E / T-Mobile", + "product_url": "https://www.bitrefill.com/it-e-t-mobile-guam/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/it-e-t-mobile-guam.webp" + }, + { + "country_code": "MH", + "country_name": "Marshall Islands", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "MH", + "country_name": "Marshall Islands", + "product_slug": "docomo-guam", + "product_name": "Docomo Pacific", + "product_url": "https://www.bitrefill.com/docomo-guam/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/docomo-guam.webp" + }, + { + "country_code": "MK", + "country_name": "North Macedonia", + "product_slug": "a1-north-macedonia", + "product_name": "A1 North Macedonia", + "product_url": "https://www.bitrefill.com/a1-north-macedonia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/a1-north-macedonia.webp" + }, + { + "country_code": "MK", + "country_name": "North Macedonia", + "product_slug": "telekom-mk", + "product_name": "Telekom MK", + "product_url": "https://www.bitrefill.com/telekom-mk/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/telekom-mk.webp" + }, + { + "country_code": "MK", + "country_name": "North Macedonia", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "MO", + "country_name": "Macau", + "product_slug": "ctm-macau", + "product_name": "CTM Macau", + "product_url": "https://www.bitrefill.com/ctm-macau/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ctm-macau.webp" + }, + { + "country_code": "MO", + "country_name": "Macau", + "product_slug": "three-macau", + "product_name": "3 Macau", + "product_url": "https://www.bitrefill.com/three-macau/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/three-macau.webp" + }, + { + "country_code": "MO", + "country_name": "Macau", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "MS", + "country_name": "Montserrat", + "product_slug": "digicel-montserrat", + "product_name": "Digicel Montserrat", + "product_url": "https://www.bitrefill.com/digicel-montserrat/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-montserrat.webp" + }, + { + "country_code": "MS", + "country_name": "Montserrat", + "product_slug": "flow-montserrat", + "product_name": "Flow Montserrat", + "product_url": "https://www.bitrefill.com/flow-montserrat/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-montserrat.webp" + }, + { + "country_code": "MS", + "country_name": "Montserrat", + "product_slug": "bitrefill-esim-caribbean", + "product_name": "eSIM Caribbean", + "product_url": "https://www.bitrefill.com/bitrefill-esim-caribbean/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-caribbean.webp" + }, + { + "country_code": "MT", + "country_name": "Malta", + "product_slug": "melita-malta", + "product_name": "Melita Malta", + "product_url": "https://www.bitrefill.com/melita-malta/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/melita-malta.webp" + }, + { + "country_code": "MT", + "country_name": "Malta", + "product_slug": "vodafone-malta", + "product_name": "Vodafone Malta", + "product_url": "https://www.bitrefill.com/vodafone-malta/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/vodafone-malta.webp" + }, + { + "country_code": "MT", + "country_name": "Malta", + "product_slug": "bitrefill-esim-europe", + "product_name": "eSIM Europe", + "product_url": "https://www.bitrefill.com/bitrefill-esim-europe/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-europe.webp" + }, + { + "country_code": "MU", + "country_name": "Mauritius", + "product_slug": "emtel-mauritius", + "product_name": "Emtel Mauritius", + "product_url": "https://www.bitrefill.com/emtel-mauritius/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/emtel-mauritius.webp" + }, + { + "country_code": "MU", + "country_name": "Mauritius", + "product_slug": "orange-mauritius", + "product_name": "Orange Mauritius", + "product_url": "https://www.bitrefill.com/orange-mauritius/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/orange-mauritius.webp" + }, + { + "country_code": "MU", + "country_name": "Mauritius", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "MX", + "country_name": "Mexico", + "product_slug": "telcel-mexico", + "product_name": "Telcel Mexico", + "product_url": "https://www.bitrefill.com/telcel-mexico/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/telcel-mexico.webp" + }, + { + "country_code": "MX", + "country_name": "Mexico", + "product_slug": "uber-mexico", + "product_name": "Uber Mexico", + "product_url": "https://www.bitrefill.com/uber-mexico/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/uber-mexico.webp" + }, + { + "country_code": "MX", + "country_name": "Mexico", + "product_slug": "cashi-walmart-mexico", + "product_name": "Cashi Walmart Mexico", + "product_url": "https://www.bitrefill.com/cashi-walmart-mexico/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/cashi-walmart-mexico.webp" + }, + { + "country_code": "MY", + "country_name": "Malaysia", + "product_slug": "digi-pin-malaysia", + "product_name": "Digi Malaysia", + "product_url": "https://www.bitrefill.com/digi-pin-malaysia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digi-pin-malaysia.webp" + }, + { + "country_code": "MY", + "country_name": "Malaysia", + "product_slug": "grabfood-my", + "product_name": "GrabFood Malaysia", + "product_url": "https://www.bitrefill.com/grabfood-my/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/grabfood-my.webp" + }, + { + "country_code": "MY", + "country_name": "Malaysia", + "product_slug": "touch-n-go-malaysia", + "product_name": "Touch 'n Go Malaysia", + "product_url": "https://www.bitrefill.com/touch-n-go-malaysia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/touch-n-go-malaysia.webp" + }, + { + "country_code": "NG", + "country_name": "Nigeria", + "product_slug": "mtn-nigeria", + "product_name": "MTN Nigeria", + "product_url": "https://www.bitrefill.com/mtn-nigeria/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/mtn-nigeria.webp" + }, + { + "country_code": "NG", + "country_name": "Nigeria", + "product_slug": "airtel-nigeria", + "product_name": "Airtel Nigeria", + "product_url": "https://www.bitrefill.com/airtel-nigeria/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/airtel-nigeria.webp" + }, + { + "country_code": "NG", + "country_name": "Nigeria", + "product_slug": "glo-nigeria", + "product_name": "Glo Nigeria", + "product_url": "https://www.bitrefill.com/glo-nigeria/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/glo-nigeria.webp" + }, + { + "country_code": "NI", + "country_name": "Nicaragua", + "product_slug": "claro-nicaragua", + "product_name": "Claro Nicaragua", + "product_url": "https://www.bitrefill.com/claro-nicaragua/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/claro-nicaragua.webp" + }, + { + "country_code": "NI", + "country_name": "Nicaragua", + "product_slug": "tigo-nicaragua", + "product_name": "Tigo Nicaragua", + "product_url": "https://www.bitrefill.com/tigo-nicaragua/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/tigo-nicaragua.webp" + }, + { + "country_code": "NI", + "country_name": "Nicaragua", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "NL", + "country_name": "Netherlands", + "product_slug": "thuisbezorgd-netherlands", + "product_name": "Thuisbezorgd NL", + "product_url": "https://www.bitrefill.com/thuisbezorgd-netherlands/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/thuisbezorgd-netherlands.webp" + }, + { + "country_code": "NL", + "country_name": "Netherlands", + "product_slug": "bol-eu", + "product_name": "Bol.com", + "product_url": "https://www.bitrefill.com/bol-eu/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bol-eu.webp" + }, + { + "country_code": "NL", + "country_name": "Netherlands", + "product_slug": "albert-heijn-netherlands", + "product_name": "Albert Heijn", + "product_url": "https://www.bitrefill.com/albert-heijn-netherlands/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/albert-heijn-netherlands.webp" + }, + { + "country_code": "NO", + "country_name": "Norway", + "product_slug": "wolt-no", + "product_name": "Wolt Norway", + "product_url": "https://www.bitrefill.com/wolt-no/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/wolt-no.webp" + }, + { + "country_code": "NO", + "country_name": "Norway", + "product_slug": "telenor-norway", + "product_name": "Telenor Norway", + "product_url": "https://www.bitrefill.com/telenor-norway/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/telenor-norway.webp" + }, + { + "country_code": "NO", + "country_name": "Norway", + "product_slug": "norwegian-air", + "product_name": "Norwegian Air", + "product_url": "https://www.bitrefill.com/norwegian-air/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/norwegian-air.webp" + }, + { + "country_code": "NZ", + "country_name": "New Zealand", + "product_slug": "steam-new-zealand", + "product_name": "Steam NZ", + "product_url": "https://www.bitrefill.com/steam-new-zealand/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/steam-new-zealand.webp" + }, + { + "country_code": "NZ", + "country_name": "New Zealand", + "product_slug": "countdown-nz", + "product_name": "Countdown NZ", + "product_url": "https://www.bitrefill.com/countdown-nz/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/countdown-nz.webp" + }, + { + "country_code": "NZ", + "country_name": "New Zealand", + "product_slug": "vodafone-new-zealand", + "product_name": "Vodafone NZ", + "product_url": "https://www.bitrefill.com/vodafone-new-zealand/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/vodafone-new-zealand.webp" + }, + { + "country_code": "OM", + "country_name": "Oman", + "product_slug": "omantel-oman", + "product_name": "Omantel Oman", + "product_url": "https://www.bitrefill.com/omantel-oman/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/omantel-oman.webp" + }, + { + "country_code": "OM", + "country_name": "Oman", + "product_slug": "ooredoo-oman", + "product_name": "Ooredoo Oman", + "product_url": "https://www.bitrefill.com/ooredoo-oman/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ooredoo-oman.webp" + }, + { + "country_code": "OM", + "country_name": "Oman", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "PA", + "country_name": "Panama", + "product_slug": "cable-onda-panama", + "product_name": "Cable Onda Panama", + "product_url": "https://www.bitrefill.com/cable-onda-panama/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/cable-onda-panama.webp" + }, + { + "country_code": "PA", + "country_name": "Panama", + "product_slug": "claro-panama", + "product_name": "Claro Panama", + "product_url": "https://www.bitrefill.com/claro-panama/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/claro-panama.webp" + }, + { + "country_code": "PA", + "country_name": "Panama", + "product_slug": "bitrefill-esim-panama", + "product_name": "eSIM Panama", + "product_url": "https://www.bitrefill.com/bitrefill-esim-panama/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-panama.webp" + }, + { + "country_code": "PE", + "country_name": "Peru", + "product_slug": "claro-peru", + "product_name": "Claro Peru", + "product_url": "https://www.bitrefill.com/claro-peru/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/claro-peru.webp" + }, + { + "country_code": "PE", + "country_name": "Peru", + "product_slug": "bcp-peru", + "product_name": "BCP Peru", + "product_url": "https://www.bitrefill.com/bcp-peru/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bcp-peru.webp" + }, + { + "country_code": "PE", + "country_name": "Peru", + "product_slug": "ripley-peru", + "product_name": "Ripley Peru", + "product_url": "https://www.bitrefill.com/ripley-peru/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ripley-peru.webp" + }, + { + "country_code": "PK", + "country_name": "Pakistan", + "product_slug": "jazz-pakistan", + "product_name": "Jazz Pakistan", + "product_url": "https://www.bitrefill.com/jazz-pakistan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/jazz-pakistan.webp" + }, + { + "country_code": "PK", + "country_name": "Pakistan", + "product_slug": "telenor-pakistan", + "product_name": "Telenor Pakistan", + "product_url": "https://www.bitrefill.com/telenor-pakistan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/telenor-pakistan.webp" + }, + { + "country_code": "PK", + "country_name": "Pakistan", + "product_slug": "ufone-pakistan", + "product_name": "Ufone Pakistan", + "product_url": "https://www.bitrefill.com/ufone-pakistan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ufone-pakistan.webp" + }, + { + "country_code": "PL", + "country_name": "Poland", + "product_slug": "allegro-pl-egift", + "product_name": "Allegro Poland", + "product_url": "https://www.bitrefill.com/allegro-pl-egift/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/allegro-pl-egift.webp" + }, + { + "country_code": "PL", + "country_name": "Poland", + "product_slug": "biedronka-poland", + "product_name": "Biedronka Poland", + "product_url": "https://www.bitrefill.com/biedronka-poland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/biedronka-poland.webp" + }, + { + "country_code": "PL", + "country_name": "Poland", + "product_slug": "orlen-poland", + "product_name": "Orlen Poland", + "product_url": "https://www.bitrefill.com/orlen-poland/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/orlen-poland.webp" + }, + { + "country_code": "PR", + "country_name": "Puerto Rico", + "product_slug": "virtual-prepaid-visa-usa", + "product_name": "Prepaid Visa USA", + "product_url": "https://www.bitrefill.com/virtual-prepaid-visa-usa/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/virtual-prepaid-visa-usa.webp" + }, + { + "country_code": "PR", + "country_name": "Puerto Rico", + "product_slug": "doordash-usa", + "product_name": "DoorDash USA", + "product_url": "https://www.bitrefill.com/doordash-usa/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/doordash-usa.webp" + }, + { + "country_code": "PR", + "country_name": "Puerto Rico", + "product_slug": "t-mobile-usa", + "product_name": "T-Mobile USA", + "product_url": "https://www.bitrefill.com/t-mobile-usa/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/t-mobile-usa.webp" + }, + { + "country_code": "PT", + "country_name": "Portugal", + "product_slug": "cartao-da-portugal", + "product_name": "Cartão Dá Portugal", + "product_url": "https://www.bitrefill.com/cartao-da-portugal/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/cartao-da-portugal.webp" + }, + { + "country_code": "PT", + "country_name": "Portugal", + "product_slug": "continente-portugal", + "product_name": "Continente Portugal", + "product_url": "https://www.bitrefill.com/continente-portugal/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/continente-portugal.webp" + }, + { + "country_code": "PT", + "country_name": "Portugal", + "product_slug": "nos-portugal", + "product_name": "NOS Portugal", + "product_url": "https://www.bitrefill.com/nos-portugal/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/nos-portugal.webp" + }, + { + "country_code": "PY", + "country_name": "Paraguay", + "product_slug": "tigo-paraguay", + "product_name": "Tigo Paraguay", + "product_url": "https://www.bitrefill.com/tigo-paraguay/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/tigo-paraguay.webp" + }, + { + "country_code": "PY", + "country_name": "Paraguay", + "product_slug": "personal-paraguay", + "product_name": "Personal Paraguay", + "product_url": "https://www.bitrefill.com/personal-paraguay/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/personal-paraguay.webp" + }, + { + "country_code": "PY", + "country_name": "Paraguay", + "product_slug": "bitrefill-esim-paraguay", + "product_name": "eSIM Paraguay", + "product_url": "https://www.bitrefill.com/bitrefill-esim-paraguay/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-paraguay.webp" + }, + { + "country_code": "QA", + "country_name": "Qatar", + "product_slug": "ooredoo-qatar", + "product_name": "Ooredoo Qatar", + "product_url": "https://www.bitrefill.com/ooredoo-qatar/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ooredoo-qatar.webp" + }, + { + "country_code": "QA", + "country_name": "Qatar", + "product_slug": "vodafone-qatar", + "product_name": "Vodafone Qatar", + "product_url": "https://www.bitrefill.com/vodafone-qatar/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/vodafone-qatar.webp" + }, + { + "country_code": "QA", + "country_name": "Qatar", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "RO", + "country_name": "Romania", + "product_slug": "emagro-dir-romania", + "product_name": "eMAG Romania", + "product_url": "https://www.bitrefill.com/emagro-dir-romania/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/emagro-dir-romania.webp" + }, + { + "country_code": "RO", + "country_name": "Romania", + "product_slug": "kaufland-romania", + "product_name": "Kaufland Romania", + "product_url": "https://www.bitrefill.com/kaufland-romania/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/kaufland-romania.webp" + }, + { + "country_code": "RO", + "country_name": "Romania", + "product_slug": "digi-romania", + "product_name": "Digi Romania", + "product_url": "https://www.bitrefill.com/digi-romania/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digi-romania.webp" + }, + { + "country_code": "RS", + "country_name": "Serbia", + "product_slug": "delfi-serbia", + "product_name": "Delfi Serbia", + "product_url": "https://www.bitrefill.com/delfi-serbia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/delfi-serbia.webp" + }, + { + "country_code": "RS", + "country_name": "Serbia", + "product_slug": "mts-serbia", + "product_name": "MTS Serbia", + "product_url": "https://www.bitrefill.com/mts-serbia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/mts-serbia.webp" + }, + { + "country_code": "RS", + "country_name": "Serbia", + "product_slug": "bitrefill-esim-serbia", + "product_name": "eSIM Serbia", + "product_url": "https://www.bitrefill.com/bitrefill-esim-serbia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-serbia.webp" + }, + { + "country_code": "SA", + "country_name": "Saudi Arabia", + "product_slug": "hungerstation-sa", + "product_name": "HungerStation SA", + "product_url": "https://www.bitrefill.com/hungerstation-sa/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/hungerstation-sa.webp" + }, + { + "country_code": "SA", + "country_name": "Saudi Arabia", + "product_slug": "stc-saudi-arabia", + "product_name": "STC Saudi Arabia", + "product_url": "https://www.bitrefill.com/stc-saudi-arabia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/stc-saudi-arabia.webp" + }, + { + "country_code": "SA", + "country_name": "Saudi Arabia", + "product_slug": "noon-saudi-arabia", + "product_name": "Noon.com Saudi Arabia", + "product_url": "https://www.bitrefill.com/noon-saudi-arabia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/noon-saudi-arabia.webp" + }, + { + "country_code": "SC", + "country_name": "Seychelles", + "product_slug": "airtel-seychelles", + "product_name": "Airtel Seychelles", + "product_url": "https://www.bitrefill.com/airtel-seychelles/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/airtel-seychelles.webp" + }, + { + "country_code": "SC", + "country_name": "Seychelles", + "product_slug": "cable-and-wireless-seychelles", + "product_name": "Cable & Wireless SC", + "product_url": "https://www.bitrefill.com/cable-and-wireless-seychelles/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/cable-and-wireless-seychelles.webp" + }, + { + "country_code": "SC", + "country_name": "Seychelles", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "SE", + "country_name": "Sweden", + "product_slug": "superpresentkortet-sweden", + "product_name": "SuperPresentkortet", + "product_url": "https://www.bitrefill.com/superpresentkortet-sweden/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/superpresentkortet-sweden.webp" + }, + { + "country_code": "SE", + "country_name": "Sweden", + "product_slug": "foodora-se", + "product_name": "Foodora SE", + "product_url": "https://www.bitrefill.com/foodora-se/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/foodora-se.webp" + }, + { + "country_code": "SE", + "country_name": "Sweden", + "product_slug": "zalando-sweden", + "product_name": "Zalando Sweden", + "product_url": "https://www.bitrefill.com/zalando-sweden/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/zalando-sweden.webp" + }, + { + "country_code": "SG", + "country_name": "Singapore", + "product_slug": "foodpanda-singapore", + "product_name": "Foodpanda Singapore", + "product_url": "https://www.bitrefill.com/foodpanda-singapore/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/foodpanda-singapore.webp" + }, + { + "country_code": "SG", + "country_name": "Singapore", + "product_slug": "grab-singapore", + "product_name": "Grab Singapore", + "product_url": "https://www.bitrefill.com/grab-singapore/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/grab-singapore.webp" + }, + { + "country_code": "SG", + "country_name": "Singapore", + "product_slug": "fairprice-singapore", + "product_name": "FairPrice Singapore", + "product_url": "https://www.bitrefill.com/fairprice-singapore/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/fairprice-singapore.webp" + }, + { + "country_code": "SI", + "country_name": "Slovenia", + "product_slug": "mercator-slovenia", + "product_name": "Mercator Slovenia", + "product_url": "https://www.bitrefill.com/mercator-slovenia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/mercator-slovenia.webp" + }, + { + "country_code": "SI", + "country_name": "Slovenia", + "product_slug": "petrol-slovenia", + "product_name": "Petrol Slovenia", + "product_url": "https://www.bitrefill.com/petrol-slovenia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/petrol-slovenia.webp" + }, + { + "country_code": "SI", + "country_name": "Slovenia", + "product_slug": "bitrefill-esim-europe", + "product_name": "eSIM Europe", + "product_url": "https://www.bitrefill.com/bitrefill-esim-europe/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-europe.webp" + }, + { + "country_code": "SK", + "country_name": "Slovakia", + "product_slug": "tesco-slovakia", + "product_name": "Tesco Slovakia", + "product_url": "https://www.bitrefill.com/tesco-slovakia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/tesco-slovakia.webp" + }, + { + "country_code": "SK", + "country_name": "Slovakia", + "product_slug": "mall-sk", + "product_name": "MALL.SK Slovakia", + "product_url": "https://www.bitrefill.com/mall-sk/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/mall-sk.webp" + }, + { + "country_code": "SK", + "country_name": "Slovakia", + "product_slug": "bitrefill-esim-europe", + "product_name": "eSIM Europe", + "product_url": "https://www.bitrefill.com/bitrefill-esim-europe/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-europe.webp" + }, + { + "country_code": "SR", + "country_name": "Suriname", + "product_slug": "telesur-suriname", + "product_name": "Telesur Suriname", + "product_url": "https://www.bitrefill.com/telesur-suriname/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/telesur-suriname.webp" + }, + { + "country_code": "SR", + "country_name": "Suriname", + "product_slug": "digicel-suriname", + "product_name": "Digicel Suriname", + "product_url": "https://www.bitrefill.com/digicel-suriname/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-suriname.webp" + }, + { + "country_code": "SR", + "country_name": "Suriname", + "product_slug": "bitrefill-esim-suriname", + "product_name": "eSIM Suriname", + "product_url": "https://www.bitrefill.com/bitrefill-esim-suriname/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-suriname.webp" + }, + { + "country_code": "SV", + "country_name": "El Salvador", + "product_slug": "tigo-el-salvador", + "product_name": "Tigo El Salvador", + "product_url": "https://www.bitrefill.com/tigo-el-salvador/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/tigo-el-salvador.webp" + }, + { + "country_code": "SV", + "country_name": "El Salvador", + "product_slug": "claro-el-salvador", + "product_name": "Claro El Salvador", + "product_url": "https://www.bitrefill.com/claro-el-salvador/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/claro-el-salvador.webp" + }, + { + "country_code": "SV", + "country_name": "El Salvador", + "product_slug": "bitrefill-esim-el-salvador", + "product_name": "eSIM El Salvador", + "product_url": "https://www.bitrefill.com/bitrefill-esim-el-salvador/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-el-salvador.webp" + }, + { + "country_code": "TC", + "country_name": "Turks and Caicos", + "product_slug": "digicel-turks-and-caicos", + "product_name": "Digicel T&C", + "product_url": "https://www.bitrefill.com/digicel-turks-and-caicos/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-turks-and-caicos.webp" + }, + { + "country_code": "TC", + "country_name": "Turks and Caicos", + "product_slug": "flow-turks-and-caicos", + "product_name": "Flow T&C", + "product_url": "https://www.bitrefill.com/flow-turks-and-caicos/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-turks-and-caicos.webp" + }, + { + "country_code": "TC", + "country_name": "Turks and Caicos", + "product_slug": "bitrefill-esim-turks-and-caicos", + "product_name": "eSIM T&C", + "product_url": "https://www.bitrefill.com/bitrefill-esim-turks-and-caicos/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-turks-and-caicos.webp" + }, + { + "country_code": "TH", + "country_name": "Thailand", + "product_slug": "grabfood-thailand", + "product_name": "GrabFood Thailand", + "product_url": "https://www.bitrefill.com/grabfood-thailand/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/grabfood-thailand.webp" + }, + { + "country_code": "TH", + "country_name": "Thailand", + "product_slug": "true-move-h-thailand", + "product_name": "True Move H Thailand", + "product_url": "https://www.bitrefill.com/true-move-h-thailand/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/true-move-h-thailand.webp" + }, + { + "country_code": "TH", + "country_name": "Thailand", + "product_slug": "central-gift-card-thailand", + "product_name": "Central Gift Card", + "product_url": "https://www.bitrefill.com/central-gift-card-thailand/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/central-gift-card-thailand.webp" + }, + { + "country_code": "TN", + "country_name": "Tunisia", + "product_slug": "ooredoo-tunisia", + "product_name": "Ooredoo Tunisia", + "product_url": "https://www.bitrefill.com/ooredoo-tunisia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ooredoo-tunisia.webp" + }, + { + "country_code": "TN", + "country_name": "Tunisia", + "product_slug": "orange-tunisia", + "product_name": "Orange Tunisia", + "product_url": "https://www.bitrefill.com/orange-tunisia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/orange-tunisia.webp" + }, + { + "country_code": "TN", + "country_name": "Tunisia", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "TR", + "country_name": "Turkey", + "product_slug": "google-play-turkey", + "product_name": "Google Play Turkey", + "product_url": "https://www.bitrefill.com/google-play-turkey/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/google-play-turkey.webp" + }, + { + "country_code": "TR", + "country_name": "Turkey", + "product_slug": "migros-turkey", + "product_name": "Migros Turkey", + "product_url": "https://www.bitrefill.com/migros-turkey/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/migros-turkey.webp" + }, + { + "country_code": "TR", + "country_name": "Turkey", + "product_slug": "getir-turkey", + "product_name": "Getir Turkey", + "product_url": "https://www.bitrefill.com/getir-turkey/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/getir-turkey.webp" + }, + { + "country_code": "TT", + "country_name": "Trinidad and Tobago", + "product_slug": "digicel-trinidad-tobago", + "product_name": "Digicel T&T", + "product_url": "https://www.bitrefill.com/digicel-trinidad-tobago/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-trinidad-tobago.webp" + }, + { + "country_code": "TT", + "country_name": "Trinidad and Tobago", + "product_slug": "bmobile-tt", + "product_name": "bmobile T&T", + "product_url": "https://www.bitrefill.com/bmobile-tt/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bmobile-tt.webp" + }, + { + "country_code": "TT", + "country_name": "Trinidad and Tobago", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "TW", + "country_name": "Taiwan", + "product_slug": "steam-taiwan", + "product_name": "Steam Taiwan", + "product_url": "https://www.bitrefill.com/steam-taiwan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/steam-taiwan.webp" + }, + { + "country_code": "TW", + "country_name": "Taiwan", + "product_slug": "7-eleven-taiwan", + "product_name": "7-Eleven Taiwan", + "product_url": "https://www.bitrefill.com/7-eleven-taiwan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/7-eleven-taiwan.webp" + }, + { + "country_code": "TW", + "country_name": "Taiwan", + "product_slug": "famiport-taiwan", + "product_name": "FamiPort Taiwan", + "product_url": "https://www.bitrefill.com/famiport-taiwan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/famiport-taiwan.webp" + }, + { + "country_code": "TZ", + "country_name": "Tanzania", + "product_slug": "vodacom-tanzania", + "product_name": "Vodacom Tanzania", + "product_url": "https://www.bitrefill.com/vodacom-tanzania/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/vodacom-tanzania.webp" + }, + { + "country_code": "TZ", + "country_name": "Tanzania", + "product_slug": "airtel-tanzania", + "product_name": "Airtel Tanzania", + "product_url": "https://www.bitrefill.com/airtel-tanzania/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/airtel-tanzania.webp" + }, + { + "country_code": "TZ", + "country_name": "Tanzania", + "product_slug": "bitrefill-esim-tanzania", + "product_name": "eSIM Tanzania", + "product_url": "https://www.bitrefill.com/bitrefill-esim-tanzania/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-tanzania.webp" + }, + { + "country_code": "UA", + "country_name": "Ukraine", + "product_slug": "silpo-ukraine", + "product_name": "Сільпо Ukraine", + "product_url": "https://www.bitrefill.com/silpo-ukraine/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/silpo-ukraine.webp" + }, + { + "country_code": "UA", + "country_name": "Ukraine", + "product_slug": "lifecell-ukraine", + "product_name": "lifecell Ukraine", + "product_url": "https://www.bitrefill.com/lifecell-ukraine/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/lifecell-ukraine.webp" + }, + { + "country_code": "UA", + "country_name": "Ukraine", + "product_slug": "kyivstar-ukraine", + "product_name": "Kyivstar Ukraine", + "product_url": "https://www.bitrefill.com/kyivstar-ukraine/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/kyivstar-ukraine.webp" + }, + { + "country_code": "UG", + "country_name": "Uganda", + "product_slug": "mtn-uganda", + "product_name": "MTN Uganda", + "product_url": "https://www.bitrefill.com/mtn-uganda/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/mtn-uganda.webp" + }, + { + "country_code": "UG", + "country_name": "Uganda", + "product_slug": "airtel-uganda", + "product_name": "Airtel Uganda", + "product_url": "https://www.bitrefill.com/airtel-uganda/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/airtel-uganda.webp" + }, + { + "country_code": "UG", + "country_name": "Uganda", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + }, + { + "country_code": "US", + "country_name": "United States", + "product_slug": "doordash-usa", + "product_name": "DoorDash USA", + "product_url": "https://www.bitrefill.com/doordash-usa/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/doordash-usa.webp" + }, + { + "country_code": "US", + "country_name": "United States", + "product_slug": "target-usa", + "product_name": "Target USA", + "product_url": "https://www.bitrefill.com/target-usa/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/target-usa.webp" + }, + { + "country_code": "US", + "country_name": "United States", + "product_slug": "dunkin-donuts-usa", + "product_name": "Dunkin USA", + "product_url": "https://www.bitrefill.com/dunkin-donuts-usa/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w360h216/dunkin-donuts-usa.webp" + }, + { + "country_code": "UY", + "country_name": "Uruguay", + "product_slug": "abitab-uruguay", + "product_name": "Abitab Uruguay", + "product_url": "https://www.bitrefill.com/abitab-uruguay/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/abitab-uruguay.webp" + }, + { + "country_code": "UY", + "country_name": "Uruguay", + "product_slug": "antel-uruguay", + "product_name": "Antel Uruguay", + "product_url": "https://www.bitrefill.com/antel-uruguay/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/antel-uruguay.webp" + }, + { + "country_code": "UY", + "country_name": "Uruguay", + "product_slug": "bitrefill-esim-uruguay", + "product_name": "eSIM Uruguay", + "product_url": "https://www.bitrefill.com/bitrefill-esim-uruguay/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-uruguay.webp" + }, + { + "country_code": "UZ", + "country_name": "Uzbekistan", + "product_slug": "ucell-uzbekistan", + "product_name": "Ucell Uzbekistan", + "product_url": "https://www.bitrefill.com/ucell-uzbekistan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/ucell-uzbekistan.webp" + }, + { + "country_code": "UZ", + "country_name": "Uzbekistan", + "product_slug": "beeline-uzbekistan", + "product_name": "Beeline Uzbekistan", + "product_url": "https://www.bitrefill.com/beeline-uzbekistan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/beeline-uzbekistan.webp" + }, + { + "country_code": "UZ", + "country_name": "Uzbekistan", + "product_slug": "bitrefill-esim-uzbekistan", + "product_name": "eSIM Uzbekistan", + "product_url": "https://www.bitrefill.com/bitrefill-esim-uzbekistan/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-uzbekistan.webp" + }, + { + "country_code": "VA", + "country_name": "Vatican City", + "product_slug": "eneba-gift-card-eu", + "product_name": "Eneba EUR", + "product_url": "https://www.bitrefill.com/eneba-gift-card-eu/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/eneba-gift-card-eu.webp" + }, + { + "country_code": "VA", + "country_name": "Vatican City", + "product_slug": "trenitalia-italy", + "product_name": "Trenitalia Italy", + "product_url": "https://www.bitrefill.com/trenitalia-italy/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/trenitalia-italy.webp" + }, + { + "country_code": "VA", + "country_name": "Vatican City", + "product_slug": "esselunga-italy", + "product_name": "Esselunga Italy", + "product_url": "https://www.bitrefill.com/esselunga-italy/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/esselunga-italy.webp" + }, + { + "country_code": "VC", + "country_name": "Saint Vincent", + "product_slug": "digicel-st-vincent", + "product_name": "Digicel St Vincent", + "product_url": "https://www.bitrefill.com/digicel-st-vincent/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-st-vincent.webp" + }, + { + "country_code": "VC", + "country_name": "Saint Vincent", + "product_slug": "flow-st-vincent", + "product_name": "Flow St Vincent", + "product_url": "https://www.bitrefill.com/flow-st-vincent/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-st-vincent.webp" + }, + { + "country_code": "VC", + "country_name": "Saint Vincent", + "product_slug": "bitrefill-esim-caribbean", + "product_name": "eSIM Caribbean", + "product_url": "https://www.bitrefill.com/bitrefill-esim-caribbean/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-caribbean.webp" + }, + { + "country_code": "VG", + "country_name": "British Virgin Islands", + "product_slug": "digicel-british-virgin-islands", + "product_name": "Digicel BVI", + "product_url": "https://www.bitrefill.com/digicel-british-virgin-islands/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/digicel-british-virgin-islands.webp" + }, + { + "country_code": "VG", + "country_name": "British Virgin Islands", + "product_slug": "flow-british-virgin-islands", + "product_name": "Flow BVI", + "product_url": "https://www.bitrefill.com/flow-british-virgin-islands/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/flow-british-virgin-islands.webp" + }, + { + "country_code": "VG", + "country_name": "British Virgin Islands", + "product_slug": "bitrefill-esim-caribbean", + "product_name": "eSIM Caribbean", + "product_url": "https://www.bitrefill.com/bitrefill-esim-caribbean/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-caribbean.webp" + }, + { + "country_code": "VN", + "country_name": "Vietnam", + "product_slug": "viettel-mobile-vietnam", + "product_name": "Viettel Vietnam", + "product_url": "https://www.bitrefill.com/viettel-mobile-vietnam/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/viettel-mobile-vietnam.webp" + }, + { + "country_code": "VN", + "country_name": "Vietnam", + "product_slug": "momo-vietnam", + "product_name": "MoMo Vietnam", + "product_url": "https://www.bitrefill.com/momo-vietnam/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/momo-vietnam.webp" + }, + { + "country_code": "VN", + "country_name": "Vietnam", + "product_slug": "vinid-vietnam", + "product_name": "VinID Vietnam", + "product_url": "https://www.bitrefill.com/vinid-vietnam/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/vinid-vietnam.webp" + }, + { + "country_code": "ZA", + "country_name": "South Africa", + "product_slug": "1voucher-south-africa", + "product_name": "1Voucher South Africa", + "product_url": "https://www.bitrefill.com/1voucher-south-africa/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/1voucher-south-africa.webp" + }, + { + "country_code": "ZA", + "country_name": "South Africa", + "product_slug": "vodacom-south-africa", + "product_name": "Vodacom South Africa", + "product_url": "https://www.bitrefill.com/vodacom-south-africa/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/vodacom-south-africa.webp" + }, + { + "country_code": "ZA", + "country_name": "South Africa", + "product_slug": "takealot-south-africa", + "product_name": "Takealot South Africa", + "product_url": "https://www.bitrefill.com/takealot-south-africa/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/takealot-south-africa.webp" + }, + { + "country_code": "ZM", + "country_name": "Zambia", + "product_slug": "mtn-zambia", + "product_name": "MTN Zambia", + "product_url": "https://www.bitrefill.com/mtn-zambia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/mtn-zambia.webp" + }, + { + "country_code": "ZM", + "country_name": "Zambia", + "product_slug": "airtel-zambia", + "product_name": "Airtel Zambia", + "product_url": "https://www.bitrefill.com/airtel-zambia/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/airtel-zambia.webp" + }, + { + "country_code": "ZM", + "country_name": "Zambia", + "product_slug": "bitrefill-esim-global", + "product_name": "eSIM Global", + "product_url": "https://www.bitrefill.com/bitrefill-esim-global/", + "card_image_webp": "https://cdn.bitrefill.com/primg/w720h432/bitrefill-esim-global.webp" + } + ] + } + } + + + + + key_api_baseurl_prod_new_enabled + true + + + feature_ltc_browser_content + { "browsers": [ { "name" : "blockchair", "urlstub" : "https://blockchair.com/litecoin/transaction/" }, { "name" : "blockcypher", "urlstub" : "https://blockchair.com/litecoin/transaction/" }, { "name" : "litecoinspace", "urlstub" : "https://litecoinspace.org/address/" }, { "name" : "sochain", "urlstub" : "https://chain.so/address/LTC/" } ] } + + + feature_gamehub_content + { "hubcontent": [ { "content_name": "rice_partner_05", "asset_url": "https://storage.googleapis.com/1756210308-bw-public/rice_partner_05.jpg", "title": "MoonPay", "subtitle": "Top up in 5 minutes!", "id": 10000 }, { "content_name": "unagi_social_02", "asset_url": "https://storage.googleapis.com/1756210308-bw-public/unagi_social_02.jpg", "title": "Social media background", "subtitle": "Follow us on socials!", "id": 20000 } ] } + + + key_keystore_manager_enabled + true + + + key_dev_api_baseurl + https://dev.apigsltd.net/ + + + key_api_baseurl_dev_new_enabled + true + + + key_prod_api_baseurl + https://prod.apigsltd.net/ + + diff --git a/app/src/test/java/com/brainwallet/data/repository/SyncAnalyticsRepositoryTest.kt b/app/src/test/java/com/brainwallet/data/repository/SyncAnalyticsRepositoryTest.kt index 0167e25f..c6831137 100644 --- a/app/src/test/java/com/brainwallet/data/repository/SyncAnalyticsRepositoryTest.kt +++ b/app/src/test/java/com/brainwallet/data/repository/SyncAnalyticsRepositoryTest.kt @@ -83,7 +83,7 @@ class SyncAnalyticsRepositoryTest { @Test fun `given accumulated duration when completeSync called then metadata persisted and event logged`() { - prefs.edit().putLong("accumulated_sync_duration", 5000L).apply() + prefs.edit().putLong("accumulated_sync_duration", 5000000L).apply() every { peerManagerSource.getLastBlockTimestamp() } returns 6000L every { peerManagerSource.getCurrentBlockHeight() } returns 456 @@ -98,7 +98,8 @@ class SyncAnalyticsRepositoryTest { val accumulatedCleared = prefs.getLong("accumulated_sync_duration", -1L) assert(lastUuid == "uuid-123") { "expected uuid-123 but was $lastUuid" } - assert(lastDuration == 5000000L) { "expected duration=5000000 but was $lastDuration" } + assert(lastDuration == 5_000_000_000L) { "expected duration=5000000000 but was $lastDuration" } + assert(lastEnd == 6000L) { "expected end=6000 but was $lastEnd" } assert(accumulatedCleared == -1L) { "expected accumulated_sync_duration removed" } @@ -115,7 +116,7 @@ class SyncAnalyticsRepositoryTest { println("captured: $captured") assert(captured["uuid"] == "uuid-123") { "uuid should match" } - assert(captured["duration_millis"] == 5000000L) { "duration should match" } + assert(captured["duration_millis"] == 5_000_000_000L) { "duration should match" } assert(captured["end_timestamp"] == 6000L) { "end timestamp should match" } assert(captured["end_block_height"] == 456) { "end block height should match" } } diff --git a/app/src/test/java/com/brainwallet/ui/screens/send/SendViewModelTest.kt b/app/src/test/java/com/brainwallet/ui/screens/send/SendViewModelTest.kt index 25e0eda1..eb16f87a 100644 --- a/app/src/test/java/com/brainwallet/ui/screens/send/SendViewModelTest.kt +++ b/app/src/test/java/com/brainwallet/ui/screens/send/SendViewModelTest.kt @@ -9,6 +9,7 @@ import com.brainwallet.presenter.entities.TransactionItem import com.brainwallet.tools.manager.AnalyticsManager import com.brainwallet.tools.security.BRKeyStore import com.brainwallet.tools.util.Utils +import com.brainwallet.util.CurrencyDataGetter import com.brainwallet.util.EventBus import io.mockk.coEvery import io.mockk.coVerify @@ -33,15 +34,6 @@ import org.junit.Before import org.junit.Test import java.math.BigDecimal -/** - * Regression tests for the Send flow. These cover the state transitions that, - * if broken, would leave the Send button disabled or cause a crash before a - * transaction could be submitted — matching the production incident. - * - * JNI-backed methods (BRWalletManager.validateAddress, getBalance, FeeManager, - * Utils.tieredOpsFee) are injected as lambdas so these tests never trigger the - * native linker. - */ @OptIn(ExperimentalCoroutinesApi::class) class SendViewModelTest { @@ -51,21 +43,16 @@ class SendViewModelTest { private lateinit var bwSender: BWSender private lateinit var txRepository: TxRepository private lateinit var settingRepository: SettingRepository + private lateinit var currencyDataGetter: CurrencyDataGetter + + private val usdCurrency = CurrencyEntity("USD", "US Dollar", 100f, "$") private val settingsFlow = MutableStateFlow( - AppSetting( - isDarkMode = false, - currency = CurrencyEntity("USD", "US Dollar", 100f, "$") - ) + AppSetting(isDarkMode = false, currency = usdCurrency) ) - /** - * Build a ViewModel with sensible test defaults. Individual tests override - * whatever they need to exercise specific branches. - */ - private fun TestScope.buildViewModel( - getBalance: () -> Long = { 1_000_000_000L }, // 10 LTC in litoshis + getBalance: () -> Long = { 1_000_000_000L }, validateAddress: (String) -> Boolean = { it.startsWith("L") }, getCurrentFee: () -> Long = { 2_000L }, getOpsFee: (Long) -> Long = { 500L }, @@ -80,6 +67,7 @@ class SendViewModelTest { getBalance = getBalance, getCurrentFee = getCurrentFee, getOpsFee = getOpsFee, + currencyDataGetter = currencyDataGetter ).also { advanceUntilIdle() } @Before @@ -96,10 +84,14 @@ class SendViewModelTest { every { currentSettings } returns settingsFlow } + // Default: getCurrencyByIso returns USD at 100f + currencyDataGetter = mockk { + every { getCurrencyByIso("USD") } returns usdCurrency + } + mockkStatic(BRKeyStore::class) every { BRKeyStore.getPinCode(any()) } returns "1234" - // Stub the asset/Firebase boundary that OnSend hits. mockkStatic(Utils::class) every { Utils.fetchServiceItem(any(), any()) } returns "LOpsAddr" every { Utils.tieredOpsFee(any(), any()) } returns 500L @@ -133,6 +125,7 @@ class SendViewModelTest { fun `settings update propagates currency and darkMode`() = runTest { val vm = buildViewModel() val newCurrency = CurrencyEntity("GBP", "Pound", 80f, "£") + every { currencyDataGetter.getCurrencyByIso("GBP") } returns newCurrency settingsFlow.value = AppSetting(isDarkMode = true, currency = newCurrency) advanceUntilIdle() @@ -142,35 +135,45 @@ class SendViewModelTest { } // ------------------------------------------------------------------------- - // REGRESSION: OnToggleFiatOrLTC null / negative rate handling + // OnToggleFiatOrLTC — uses currencyDataGetter (not selectedCurrency.rate) // ------------------------------------------------------------------------- @Test - fun `toggle does not crash when rate is negative sentinel`() = runTest { - // The default CurrencyEntity in SendState has rate = -1f. If the user - // toggles before rates load, the VM must not crash. - settingsFlow.value = AppSetting( - currency = CurrencyEntity("USD", "US Dollar", -1f, "$") - ) - val vm = buildViewModel() + fun `toggle does not crash when currencyDataGetter returns no rate`() = runTest { + // Simulates the case where rates haven't loaded yet — getCurrencyByIso + // returns null, so the takeIf guard fires and convertedAmount stays null. + every { currencyDataGetter.getCurrencyByIso(any()) } returns null + val vm = buildViewModel() vm.onEvent(SendEvent.OnAmountChanged("0.5")) advanceUntilIdle() vm.onEvent(SendEvent.OnToggleFiatOrLTC) advanceUntilIdle() - // Expectation: no crash. The amount string should either stay put or - // clear — but the VM must remain functional. + // amountString must be unchanged; VM must remain functional + assertEquals("0.5", vm.state.value.amountString) assertFalse(vm.state.value.brainwalletIsPublishing) } + @Test + fun `toggle does not crash when rate is zero`() = runTest { + // rate = 0f fails the takeIf { it > 0f } guard — same safe path as null + every { currencyDataGetter.getCurrencyByIso("USD") } returns + CurrencyEntity("USD", "US Dollar", 0f, "$") + + val vm = buildViewModel() + vm.onEvent(SendEvent.OnAmountChanged("0.5")) + advanceUntilIdle() + vm.onEvent(SendEvent.OnToggleFiatOrLTC) + advanceUntilIdle() + + assertEquals("0.5", vm.state.value.amountString) + } + @Test fun `toggle LTC to fiat multiplies by rate with 2dp rounding`() = runTest { - settingsFlow.value = AppSetting( - currency = CurrencyEntity("USD", "US Dollar", 100f, "$") - ) val vm = buildViewModel() - // Start in LTC view, enter 0.5 LTC. + // Start in LTC view, enter 0.5 LTC vm.onEvent(SendEvent.OnAmountChanged("0.5")) advanceUntilIdle() @@ -184,11 +187,9 @@ class SendViewModelTest { @Test fun `toggle fiat to LTC divides by rate with 8dp precision`() = runTest { - settingsFlow.value = AppSetting( - currency = CurrencyEntity("USD", "US Dollar", 100f, "$") - ) val vm = buildViewModel() vm.onEvent(SendEvent.OnToggleFiatOrLTC) // → fiat view + advanceUntilIdle() vm.onEvent(SendEvent.OnAmountChanged("50")) advanceUntilIdle() @@ -200,14 +201,44 @@ class SendViewModelTest { assertEquals(BigDecimal("0.50000000"), BigDecimal(vm.state.value.amountString)) } + @Test + fun `toggle LTC to fiat preserves amountInLitoshi`() = runTest { + val vm = buildViewModel() + vm.onEvent(SendEvent.OnAmountChanged("1")) // 1 LTC = 100_000_000 litoshis + advanceUntilIdle() + val litoshiBefore = vm.state.value.amountInLitoshi + + vm.onEvent(SendEvent.OnToggleFiatOrLTC) // → fiat, litoshi unchanged + advanceUntilIdle() + + assertEquals(litoshiBefore, vm.state.value.amountInLitoshi) + } + + @Test + fun `toggle fiat to LTC updates amountInLitoshi`() = runTest { + val vm = buildViewModel() + vm.onEvent(SendEvent.OnToggleFiatOrLTC) // → fiat + advanceUntilIdle() + vm.onEvent(SendEvent.OnAmountChanged("100")) // 100 USD / 100 rate = 1 LTC + advanceUntilIdle() + + vm.onEvent(SendEvent.OnToggleFiatOrLTC) // → LTC + advanceUntilIdle() + + // 1 LTC = 100_000_000 litoshis + assertEquals( + 0, + BigDecimal("100000000").compareTo(vm.state.value.amountInLitoshi) + ) + } + // ------------------------------------------------------------------------- - // REGRESSION: Amount validation drives isReadyToSend + // OnAmountChanged — uses selectedCurrency.rate (Float, always present) // ------------------------------------------------------------------------- @Test fun `valid address alone does not enable send`() = runTest { val vm = buildViewModel() - vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) advanceUntilIdle() @@ -221,23 +252,19 @@ class SendViewModelTest { @Test fun `valid address plus valid amount enables send`() = runTest { val vm = buildViewModel() - vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) vm.onEvent(SendEvent.OnAmountChanged("0.1")) advanceUntilIdle() - // Note: this asserts the intended contract. It will currently fail - // unless the getBalance/getCurrentFee/getOpsFee lambdas are wired into - // the ViewModel — which is exactly the point of this regression test. assertTrue(vm.state.value.isReadyToSend) } @Test fun `amount exceeding balance disables send`() = runTest { - val vm = buildViewModel(getBalance = { 1000L }) // 1000 litoshis ~ tiny + val vm = buildViewModel(getBalance = { 1000L }) vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) - vm.onEvent(SendEvent.OnAmountChanged("100")) // 100 LTC — way over + vm.onEvent(SendEvent.OnAmountChanged("100")) advanceUntilIdle() assertFalse(vm.state.value.isAmountBelowBalance) @@ -247,7 +274,6 @@ class SendViewModelTest { @Test fun `invalid address disables send even with valid amount`() = runTest { val vm = buildViewModel(validateAddress = { false }) - vm.onEvent(SendEvent.OnRecipientAddressChanged("not-a-real-addr")) vm.onEvent(SendEvent.OnAmountChanged("0.1")) advanceUntilIdle() @@ -259,7 +285,6 @@ class SendViewModelTest { @Test fun `zero amount does not enable send`() = runTest { val vm = buildViewModel() - vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) vm.onEvent(SendEvent.OnAmountChanged("0")) advanceUntilIdle() @@ -273,7 +298,6 @@ class SendViewModelTest { @Test fun `empty amount string does not enable send`() = runTest { val vm = buildViewModel() - vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) vm.onEvent(SendEvent.OnAmountChanged("")) advanceUntilIdle() @@ -281,8 +305,23 @@ class SendViewModelTest { assertFalse(vm.state.value.isReadyToSend) } + @Test + fun `fiat amount converts to litoshis correctly for balance check`() = runTest { + // 50 USD / 100 rate = 0.5 LTC = 50_000_000 litoshis + // balance = 1_000_000_000 litoshis (10 LTC) → should be valid + val vm = buildViewModel() + vm.onEvent(SendEvent.OnToggleFiatOrLTC) // → fiat + advanceUntilIdle() + vm.onEvent(SendEvent.OnRecipientAddressChanged("LValidAddr")) + vm.onEvent(SendEvent.OnAmountChanged("50")) + advanceUntilIdle() + + assertTrue(vm.state.value.isAmountBelowBalance) + assertTrue(vm.state.value.isReadyToSend) + } + // ------------------------------------------------------------------------- - // REGRESSION: Send button lock-up (isSending never clears) + // Send result handling — brainwalletIsPublishing always clears // ------------------------------------------------------------------------- @Test @@ -293,17 +332,11 @@ class SendViewModelTest { vm.onEvent(SendEvent.OnSend(dummyTransactionItem())) advanceUntilIdle() - assertFalse( - "brainwalletIsPublishing must be cleared after success", - vm.state.value.brainwalletIsPublishing - ) + assertFalse(vm.state.value.brainwalletIsPublishing) } @Test fun `send AlreadySending clears publishing flag`() = runTest { - // This is the core regression: if AlreadySending leaves the VM in - // brainwalletIsPublishing = true, the Send button never re-enables - // and the user is permanently blocked. coEvery { bwSender.prepareTransaction(any()) } returns BWSendResult.Error.AlreadySending val vm = buildViewModel() @@ -351,6 +384,19 @@ class SendViewModelTest { assertTrue(vm.state.value.errorResultString.isNotEmpty()) } + @Test + fun `send InsufficientFunds surfaces error and clears publishing`() = runTest { + coEvery { bwSender.prepareTransaction(any()) } returns + BWSendResult.Error.InsufficientFunds + val vm = buildViewModel() + + vm.onEvent(SendEvent.OnSend(dummyTransactionItem())) + advanceUntilIdle() + + assertFalse(vm.state.value.brainwalletIsPublishing) + assertTrue(vm.state.value.errorResultString.isNotEmpty()) + } + @Test fun `onSend calls bwSender exactly once`() = runTest { coEvery { bwSender.prepareTransaction(any()) } returns BWSendResult.Success @@ -417,13 +463,12 @@ class SendViewModelTest { } // ------------------------------------------------------------------------- - // QR scan / EventBus wiring + // QR scan / EventBus // ------------------------------------------------------------------------- @Test fun `QR scan event populates recipient address`() = runTest { val vm = buildViewModel() - EventBus.emit(EventBus.Event.QRCodeScanned(url = "LQRScannedAddr")) advanceUntilIdle() @@ -433,7 +478,6 @@ class SendViewModelTest { @Test fun `QR scan with null url sets empty string not crash`() = runTest { val vm = buildViewModel() - EventBus.emit(EventBus.Event.QRCodeScanned(url = null)) advanceUntilIdle() diff --git a/app/src/test/kotlin/com/brainwallet/consants/BWConstantsTest.kt b/app/src/test/kotlin/com/brainwallet/consants/BWConstantsTest.kt index 839c8b15..8353f12d 100644 --- a/app/src/test/kotlin/com/brainwallet/consants/BWConstantsTest.kt +++ b/app/src/test/kotlin/com/brainwallet/consants/BWConstantsTest.kt @@ -289,7 +289,6 @@ class BWConstantsTest { @Test fun `all analytics event keys are non-empty`() { listOf( - BWConstants._BW_MAIN_OPEN, BWConstants._20191105_AL, BWConstants._20191105_VSC, BWConstants._20202116_VRC, @@ -301,7 +300,7 @@ class BWConstantsTest { BWConstants._20200112_ERR, BWConstants._20200112_DSR, BWConstants._20201118_DTGS, - BWConstants._20200217_DUWB, + BWConstants._20200217_DU, BWConstants._20250303_DSTU, BWConstants._20250517_WCINFO, BWConstants._20241006_DRR, @@ -326,7 +325,7 @@ class BWConstantsTest { BWConstants._20200112_ERR, BWConstants._20200112_DSR, BWConstants._20201118_DTGS, - BWConstants._20200217_DUWB, + BWConstants._20200217_DU, BWConstants._20250303_DSTU, BWConstants._20250517_WCINFO, BWConstants._20241006_DRR, diff --git a/app/src/test/kotlin/com/brainwallet/testing/FlakyTest.kt b/app/src/test/kotlin/com/brainwallet/testing/FlakyTest.kt new file mode 100644 index 00000000..e6005238 --- /dev/null +++ b/app/src/test/kotlin/com/brainwallet/testing/FlakyTest.kt @@ -0,0 +1,4 @@ +package com.brainwallet.testing +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +annotation class FlakyTest(val reason: String = "") diff --git a/app/src/test/kotlin/com/brainwallet/tools/constants/BWConstantTests.kt b/app/src/test/kotlin/com/brainwallet/tools/constants/BWConstantTests.kt index 7e47116b..6bdadc45 100644 --- a/app/src/test/kotlin/com/brainwallet/tools/constants/BWConstantTests.kt +++ b/app/src/test/kotlin/com/brainwallet/tools/constants/BWConstantTests.kt @@ -168,7 +168,7 @@ class BWConstantsTests { assertSame(BWConstants._20200112_ERR, "brainwallet_android_error") assertSame(BWConstants._20200112_DSR, "did_start_resync") assertSame(BWConstants._20201118_DTGS, "did_tap_get_support") - assertSame(BWConstants._20200217_DUWB, "did_unlock_with_biometrics") + assertSame(BWConstants._20200217_DU, "did_unlock") } @Test diff --git a/app/src/test/kotlin/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoViewModelTests.kt b/app/src/test/kotlin/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoViewModelTests.kt index 3d770bf3..fd98fb6b 100644 --- a/app/src/test/kotlin/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoViewModelTests.kt +++ b/app/src/test/kotlin/com/brainwallet/ui/bentosections/ltcpickerbento/LTCPickerBentoViewModelTests.kt @@ -5,6 +5,7 @@ import com.brainwallet.data.model.GlobalCurrency import com.brainwallet.data.repository.LtcRepository import com.brainwallet.data.repository.SettingRepository import com.brainwallet.tools.sqlite.CurrencyDataSource +import com.brainwallet.util.CurrencyDataGetter import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -38,6 +39,7 @@ class LTCPickerBentoViewModelTest { every { settings } returns settingsFlow } private val currencyDataSource: CurrencyDataSource = mockk() + private val currencyDataGetter: CurrencyDataGetter = mockk() private lateinit var viewModel: LTCPickerBentoViewModel private val mockCrashlytics: FirebaseCrashlytics = mockk(relaxed = true) @@ -52,6 +54,7 @@ class LTCPickerBentoViewModelTest { viewModel = LTCPickerBentoViewModel( settingRepository, currencyDataSource, + currencyDataGetter, ltcRepository ) } diff --git a/app/src/test/kotlin/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModelTest.kt b/app/src/test/kotlin/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModelTest.kt index 70d5a71a..cd85b224 100644 --- a/app/src/test/kotlin/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModelTest.kt +++ b/app/src/test/kotlin/com/brainwallet/ui/bentosections/shopbento/ShopBentoViewModelTest.kt @@ -1,12 +1,12 @@ package com.brainwallet.ui.bentosections.shopbento import android.app.Application -import android.content.Context -import android.telephony.TelephonyManager -import app.cash.turbine.testIn import app.cash.turbine.turbineScope import com.brainwallet.data.model.AppSetting import com.brainwallet.data.repository.SettingRepository +import com.brainwallet.data.repository.ShopProxy +import com.brainwallet.data.repository.ShopProxyRepository +import com.brainwallet.testing.FlakyTest import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow @@ -15,53 +15,54 @@ import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test +import io.mockk.coEvery class ShopBentoViewModelTest { private lateinit var app: Application private lateinit var settingRepository: SettingRepository - private lateinit var telephonyManager: TelephonyManager + private lateinit var shopProxyRepository: ShopProxyRepository private lateinit var settingsFlow: MutableStateFlow + private lateinit var shopProxyFlow: MutableStateFlow> private fun buildViewModel() = ShopBentoViewModel( app = app, settingRepository = settingRepository, + shopProxyRepository = shopProxyRepository ) @Before fun setup() { app = mockk() settingRepository = mockk() - telephonyManager = mockk() + shopProxyRepository = mockk() settingsFlow = MutableStateFlow(AppSetting()) + shopProxyFlow = MutableStateFlow(emptyList()) every { settingRepository.settings } returns settingsFlow - every { app.getSystemService(Context.TELEPHONY_SERVICE) } returns telephonyManager - every { telephonyManager.simCountryIso } returns "gb" - every { telephonyManager.networkCountryIso } returns "" + coEvery { shopProxyRepository.refresh() } returns Unit + every { shopProxyRepository.shopProxy } returns shopProxyFlow } @Test - fun `init - uses simCountryIso when available`() = runTest { + fun `init - countryIso defaults to Locale`() = runTest { turbineScope { - every { telephonyManager.simCountryIso } returns "us" - val viewModel = buildViewModel() val turbine = viewModel.state.testIn(backgroundScope) settingsFlow.emit(AppSetting()) advanceTimeBy(100) - assertEquals("US", turbine.expectMostRecentItem().countryIso) + val countryIso = turbine.expectMostRecentItem().countryIso + assert(countryIso.isNotEmpty()) turbine.cancelAndIgnoreRemainingEvents() } } @Test - fun `init - falls back to Locale default when both sim and network are empty`() = runTest { + fun `init - sets shopBaseUrl from widget`() = runTest { turbineScope { - every { telephonyManager.simCountryIso } returns "" - every { telephonyManager.networkCountryIso } returns "" + shopProxyFlow.emit(listOf(ShopProxy(widget = "https://shop.example.com", shopCards = emptyList()))) val viewModel = buildViewModel() val turbine = viewModel.state.testIn(backgroundScope) @@ -69,12 +70,13 @@ class ShopBentoViewModelTest { settingsFlow.emit(AppSetting()) advanceTimeBy(100) - val countryIso = turbine.expectMostRecentItem().countryIso - assert(countryIso.isNotEmpty()) + val state = turbine.expectMostRecentItem() + assertEquals("https://shop.example.com", state.shopBaseUrl) turbine.cancelAndIgnoreRemainingEvents() } } + @FlakyTest(reason = "advanceTimeBy timing sensitive under load and in remote test runner server") @Test fun `onEvent OnTapShop - updates shouldSlide to true`() = runTest { turbineScope { @@ -82,10 +84,10 @@ class ShopBentoViewModelTest { val turbine = viewModel.state.testIn(backgroundScope) settingsFlow.emit(AppSetting()) - advanceTimeBy(200) + advanceTimeBy(500) viewModel.onEvent(ShopBentoEvent.OnTapShop) - advanceTimeBy(200) + advanceTimeBy(500) assertEquals(true, turbine.expectMostRecentItem().shouldSlide) turbine.cancelAndIgnoreRemainingEvents() @@ -98,19 +100,15 @@ class ShopBentoViewModelTest { val viewModel = buildViewModel() val turbine = viewModel.state.testIn(backgroundScope) - turbine.awaitItem() - settingsFlow.emit(AppSetting(isDarkMode = true)) advanceTimeBy(100) - val stateBefore = turbine.awaitItem() + val stateBefore = turbine.expectMostRecentItem() viewModel.onEvent(ShopBentoEvent.OnLoad) advanceTimeBy(100) - turbine.expectNoEvents() assertEquals(stateBefore, viewModel.state.value) - turbine.cancelAndIgnoreRemainingEvents() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dd78680f..f74782b1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ runtime = "1.10.6" foundation = "1.10.6" animation = "1.10.6" play-services-games = "24.0.0" +benchmark-traceprocessor = "1.4.1" [libraries] androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } @@ -107,6 +108,7 @@ androidx-runtime = { group = "androidx.compose.runtime", name = "runtime", versi androidx-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } androidx-animation = { group = "androidx.compose.animation", name = "animation", version.ref = "animation" } play-services-games = { group = "com.google.android.gms", name = "play-services-games", version.ref = "play-services-games" } +androidx-benchmark-traceprocessor = { group = "androidx.benchmark", name = "benchmark-traceprocessor", version.ref = "benchmark-traceprocessor" } [bundles] androidx-lifecycle = ["androidx-lifecycle-runtime", "androidx-lifecycle-viewmodel", "androidx-lifecycle-viewmodel-compose"]