diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d63ff6b227..7da64b0215 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -223,7 +223,6 @@ dependencies { implementation(deps.lottie) implementation(deps.compose.accompanist.appCompatTheme) implementation(deps.compose.accompanist.systemUiController) - implementation(deps.compose.accompanist.webView) implementation(deps.xmlShimmer) implementation(deps.viewBindingDelegate) implementation(deps.armadillo) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b13606584b..b9db373358 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ + diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 6d42707997..e0a2bf62d7 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -6,6 +6,7 @@ import android.content.pm.ActivityInfo import android.content.res.Configuration import android.os.Build import android.os.Bundle +import android.view.MotionEvent import android.view.View import android.view.WindowManager import androidx.activity.SystemBarStyle @@ -61,6 +62,7 @@ import com.tangem.features.send.api.navigation.SendRouter import com.tangem.features.staking.api.navigation.StakingRouter import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.features.wallet.navigation.WalletRouter +import com.tangem.google.GoogleServicesHelper import com.tangem.operations.backup.BackupService import com.tangem.sdk.api.BackupServiceHolder import com.tangem.sdk.api.TangemSdkManager @@ -72,7 +74,6 @@ import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss -import com.tangem.google.GoogleServicesHelper import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.features.intentHandler.IntentProcessor @@ -245,6 +246,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_NOTHING) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + window.setHideOverlayWindows(true) + } + splashScreen.setKeepOnScreenCondition { viewModel.isSplashScreenShown } installActivityDependencies() @@ -518,6 +523,23 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lockUserWalletsTimer?.restart() } + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + val isPartiallyObscured = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + event.flags and MotionEvent.FLAG_WINDOW_IS_PARTIALLY_OBSCURED != 0 + } else { + false + } + + val isFullyObscured = event.flags and MotionEvent.FLAG_WINDOW_IS_OBSCURED != 0 + + if (isPartiallyObscured || isFullyObscured) { + Timber.e("Window is partially or fully obscured") + return false + } + + return super.dispatchTouchEvent(event) + } + private fun showSnackbar(text: String, length: Int, buttonTitle: String?, action: View.OnClickListener?) { if (snackbar != null) return diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index e61ff4a8d6..85d5a6630b 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -1,7 +1,7 @@ package com.tangem.tap.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader +import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository @@ -49,7 +49,7 @@ internal object ActivityModule { @Singleton fun provideDefaultRampManager( appStateHolder: AppStateHolder, - swapServiceLoader: SwapServiceLoader, + expressServiceLoader: ExpressServiceLoader, currenciesRepository: CurrenciesRepository, getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, excludedBlockchains: ExcludedBlockchains, @@ -61,7 +61,7 @@ internal object ActivityModule { exchangeService = appStateHolder.exchangeService, buyService = Provider { requireNotNull(appStateHolder.buyService) }, sellService = Provider { requireNotNull(appStateHolder.sellService) }, - swapServiceLoader = swapServiceLoader, + expressServiceLoader = expressServiceLoader, currenciesRepository = currenciesRepository, getNetworkCoinStatusUseCase = getNetworkCoinStatusUseCase, excludedBlockchains = excludedBlockchains, diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index b5f053bc34..ca9f43d672 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -82,15 +82,6 @@ internal object OnrampDomainModule { ) } - @Provides - @Singleton - fun provideGetOnrampCurrencyUseCase( - onrampRepository: OnrampRepository, - onrampErrorResolver: OnrampErrorResolver, - ): GetOnrampCurrencyUseCase { - return GetOnrampCurrencyUseCase(onrampRepository, onrampErrorResolver) - } - @Provides @Singleton fun provideGetOnrampTransactionsUseCase( diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt index 8ee50fc80d..ec80c29c24 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/UnfinishedBackupFoundDialog.kt @@ -3,7 +3,9 @@ package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs import android.content.Context import androidx.appcompat.app.AlertDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.tangem.core.analytics.Analytics import com.tangem.domain.models.scan.ScanResponse +import com.tangem.features.onboarding.v2.multiwallet.impl.analytics.OnboardingEvent import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog @@ -16,10 +18,12 @@ object UnfinishedBackupFoundDialog { setTitle(R.string.common_warning) setMessage(R.string.welcome_interrupted_backup_alert_message) setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ -> + Analytics.send(OnboardingEvent.Backup.ResumeInterruptedBackup) store.dispatch(GlobalAction.HideDialog) store.dispatch(BackupAction.ResumeFoundUnfinishedBackup(scanResponse)) } setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ -> + Analytics.send(OnboardingEvent.Backup.CancelInterruptedBackup) store.dispatch(GlobalAction.HideDialog) store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup(scanResponse))) } diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 8617e45745..7f8f63e344 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -3,7 +3,7 @@ package com.tangem.tap.network.exchangeServices import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE import com.tangem.datasource.api.express.models.response.Asset -import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader +import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.scan.ScanResponse @@ -16,6 +16,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext @@ -25,7 +26,7 @@ internal class DefaultRampManager( private val exchangeService: ExchangeService?, private val buyService: Provider, private val sellService: Provider, - private val swapServiceLoader: SwapServiceLoader, + private val expressServiceLoader: ExpressServiceLoader, private val currenciesRepository: CurrenciesRepository, private val getNetworkCoinStatusUseCase: GetNetworkCoinStatusUseCase, private val dispatchers: CoroutineDispatcherProvider, @@ -37,9 +38,13 @@ internal class DefaultRampManager( private val cryptoCurrencyConverter = CryptoCurrencyConverter(excludedBlockchains) override fun isSellSupportedByService(cryptoCurrency: CryptoCurrency): Boolean { - return exchangeService?.availableForSell( - currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), - ) ?: false + return runCatching { + exchangeService?.availableForSell( + currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), + ) + } + .getOrNull() + ?: false } override suspend fun availableForBuy( @@ -47,28 +52,38 @@ internal class DefaultRampManager( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): Boolean { - return when { - onrampFeatureToggles.isFeatureEnabled -> getOnrampAvailable(userWalletId, cryptoCurrency) - exchangeService != null -> exchangeService.availableForBuy( - scanResponse = scanResponse, - currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), - ) - else -> false + return runCatching { + when { + onrampFeatureToggles.isFeatureEnabled -> getOnrampAvailable(userWalletId, cryptoCurrency) + exchangeService != null -> exchangeService.availableForBuy( + scanResponse = scanResponse, + currency = cryptoCurrencyConverter.convertBack(cryptoCurrency), + ) + else -> false + } } + .getOrNull() + ?: false } override suspend fun availableForSell(userWalletId: UserWalletId, status: CryptoCurrencyStatus): Boolean { - val sellSupportedByService = isSellSupportedByService(cryptoCurrency = status.currency) + return runCatching { + val sellSupportedByService = isSellSupportedByService(cryptoCurrency = status.currency) - if (!sellSupportedByService) return false + if (!sellSupportedByService) return false - val reason = getSendUnavailabilityReason(userWalletId, status) + val reason = getSendUnavailabilityReason(userWalletId, status) - return reason == ScenarioUnavailabilityReason.None + reason == ScenarioUnavailabilityReason.None + } + .getOrNull() + ?: false } override suspend fun availableForSwap(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { - return getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom + return runCatching { getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom } + .getOrNull() + ?: false } override fun getBuyInitializationStatus(): Flow { @@ -76,7 +91,7 @@ internal class DefaultRampManager( } override suspend fun fetchBuyServiceData() { - withContext(dispatchers.io) { + runCatching(dispatchers.io) { buyService.invoke().update() } } @@ -86,18 +101,18 @@ internal class DefaultRampManager( } override suspend fun fetchSellServiceData() { - withContext(dispatchers.io) { + runCatching(dispatchers.io) { sellService.invoke().update() } } - override fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow { - return swapServiceLoader.getInitializationStatus(userWalletId) + override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow { + return expressServiceLoader.getInitializationStatus(userWalletId) } private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { return withContext(dispatchers.io) { - val asset = swapServiceLoader.getInitializationStatus(userWalletId) + val asset = expressServiceLoader.getInitializationStatus(userWalletId) .value .getOrNull() ?.find { cryptoCurrency.findAssetPredicate(it) } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 5f2471ff8b..aa962825e1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -3,6 +3,7 @@ android:id="@+id/fragment_container" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/background_primary"> + android:background="@color/background_primary" + android:filterTouchesWhenObscured="true"> \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index 620038bf9a..9f5bf56e7d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -240,6 +240,11 @@ sealed class NotificationUM(val config: NotificationConfig) { onClick = onRefresh, ), ) + + data object SwapNoAvailablePair : Warning( + title = resourceReference(id = R.string.action_buttons_swap_no_available_pair_notification_title), + subtitle = resourceReference(id = R.string.action_buttons_swap_no_available_pair_notification_message), + ) } open class Info( diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/ExchangeServiceLoaderModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/ExchangeServiceLoaderModule.kt index 03ab7fa98a..d9ae44675f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/ExchangeServiceLoaderModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/exchangeservice/ExchangeServiceLoaderModule.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.di.exchangeservice -import com.tangem.datasource.exchangeservice.swap.DefaultSwapServiceLoader -import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader +import com.tangem.datasource.exchangeservice.swap.DefaultExpressServiceLoader +import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -14,5 +14,5 @@ internal interface ExchangeServiceLoaderModule { @Binds @Singleton - fun bindSwapServiceLoader(defaultSwapServiceLoader: DefaultSwapServiceLoader): SwapServiceLoader + fun bindExpressServiceLoader(defaultExpressServiceLoader: DefaultExpressServiceLoader): ExpressServiceLoader } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultSwapServiceLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultExpressServiceLoader.kt similarity index 79% rename from core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultSwapServiceLoader.kt rename to core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultExpressServiceLoader.kt index 90a267cc3d..c2818ecd87 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultSwapServiceLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultExpressServiceLoader.kt @@ -2,11 +2,9 @@ package com.tangem.datasource.exchangeservice.swap import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi -import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE import com.tangem.datasource.api.express.models.request.AssetsRequestBody import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.express.models.response.Asset -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.lceContent @@ -23,39 +21,32 @@ import javax.inject.Inject typealias InitializationStatusFlow = MutableStateFlow>> /** - * Default implementation of [SwapServiceLoader] + * Default implementation of [ExpressServiceLoader] * * @property tangemExpressApi express api * @property expressAssetsStore local storage * [REDACTED_AUTHOR] */ -internal class DefaultSwapServiceLoader @Inject constructor( +internal class DefaultExpressServiceLoader @Inject constructor( private val tangemExpressApi: TangemExpressApi, private val expressAssetsStore: ExpressAssetsStore, private val dispatchers: CoroutineDispatcherProvider, -) : SwapServiceLoader { +) : ExpressServiceLoader { private val initializationStatuses = MutableStateFlow>(value = emptyMap()) - override suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse) { + override suspend fun update(userWalletId: UserWalletId, userTokens: List) { withContext(dispatchers.io) { val initializationStatus = getInitializationStatusInternal(userWalletId) initializationStatus.update { lceLoading() } try { - val tokensList = userTokens.tokens.map { - LeastTokenInfo( - contractAddress = it.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE, - network = it.networkId, - ) - } - - if (tokensList.isNotEmpty()) { + if (userTokens.isNotEmpty()) { val response = tangemExpressApi.getAssets( - body = AssetsRequestBody(tokensList = tokensList), + body = AssetsRequestBody(tokensList = userTokens), ).getOrThrow() expressAssetsStore.store(userWalletId, response) diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/SwapServiceLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressServiceLoader.kt similarity index 69% rename from core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/SwapServiceLoader.kt rename to core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressServiceLoader.kt index 0bee5e6d0b..72e59dba3a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/SwapServiceLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressServiceLoader.kt @@ -1,20 +1,20 @@ package com.tangem.datasource.exchangeservice.swap +import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.express.models.response.Asset -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.domain.core.lce.Lce import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.StateFlow /** - * Swap service loader + * Express service loader * [REDACTED_AUTHOR] */ -interface SwapServiceLoader { +interface ExpressServiceLoader { /** Update service using [userWalletId] and [userTokens] */ - suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse) + suspend fun update(userWalletId: UserWalletId, userTokens: List) /** Get initialization status by [userWalletId] */ fun getInitializationStatus(userWalletId: UserWalletId): StateFlow>> diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index f2ab9fb04c..df2f985c7d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -101,8 +101,6 @@ object PreferencesKeys { val SHOULD_SHOW_RING_PROMO_KEY by lazy { booleanPreferencesKey(name = "shouldShowRingPromo") } - val ONRAMP_DEFAULT_CURRENCY by lazy { stringPreferencesKey(name = "onrampDefaultCurrency") } - val ONRAMP_DEFAULT_COUNTRY by lazy { stringPreferencesKey(name = "onrampDefaultCountry") } val ONRAMP_TRANSACTIONS_STATUSES_KEY by lazy { stringPreferencesKey(name = "onrampTransactionsStatuses") } diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 7f2d6cb4e4..1055b86c5d 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -3,10 +3,18 @@ Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum kaufen im Portfolio hinzu. Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum verkaufen im Portfolio hinzu. Verkaufen + Einige Netzwerke sind derzeit nicht erreichbar. Bitte versuche es später erneut. + Die Daten wurden noch nicht geladen. + Die Aktion ist derzeit nicht verfügbar. Bitte versuche es später erneut oder aktualisiere die Daten, indem Du auf dem Bildschirm nach unten wischst. + Schaltfläche ist nicht verfügbar Wähle den Token Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum tauschen im Portfolio hinzu. Es gibt keine verfügbaren Token, die mit dem ausgewählten Token getauscht werden können. Bitte wähle einen anderen. Kein verfügbares Paar + Um die Tauschfunktion zu nutzen, muss Dein Portfolio mindestens 2 Token enthalten. + Token hinzufügen + Du hast Deinem Portfolio nur 1 Token hinzugefügt. Um die Tauschfunktion zu verwenden, musst Du mindestens 2 Token hinzufügen. + Token hinzufügen Wähle den Token aus, den Du erhalten möchtest Wähle den Token, den Du tauschen möchtest Netzwerk wählen @@ -263,6 +271,9 @@ Der Betrag wurde in %1$s (%2$s Netzwerk) zurückerstattet. Besuche die Website des Anbieters zur Überprüfung KYC-Überprüfung durch den Anbieter erforderlich + Gekauft + Einkaufen + Einkaufen... Abgebrochen Bestätigt Bestätigen @@ -308,6 +319,7 @@ Bitte sag uns, welche Karte oder Ring du hast Hallo Support-Team, Bitte erzähle uns mehr über dein Problem. Jedes kleine Detail kann helfen. + Zuvor aktivierte Wallet Meine Vorschläge Kann eine Karte oder Ring nicht scannen Rückmeldung @@ -585,7 +597,7 @@ Residenz Bitte wähle das richtige Land aus, um korrekte Zahlungsoptionen und Dienstleistungen zu gewährleisten. Einstellungen - Du kannst den Transaktionsstatus auf der Token-Seite überprüfen. + Du kannst diesen Bildschirm schließen und den Transaktionsstatus auf dem Bildschirm mit den Token-Details überprüfen. Du kannst diesen Bildschirm schließen und den Transaktionsstatus auf dem Bildschirm mit den Token-Details überprüfen. Über Gruppe erstellen @@ -600,6 +612,7 @@ %1$s ( %2$s ) im %3$s Netzwerk Das Senden einer anderen Währung führt zu deren irreversiblem Verlust. Sende nur %s an diese Adresse. Der Versand einer anderen Währung führt zu ihrem unwiderruflichen Verlust. + Senden nur %1$s im Netzwerk %2$s Überweise Geld von einem anderen Wallet oder einer anderen Börse Teilnehmen Die Informationen zum Empfehlungsprogramm konnten nicht geladen werden. Bitte versuche es später noch einmal. @@ -651,6 +664,7 @@ Karte oder Ring scannen Scanne die Karte oder Ring, um ihre Einstellungen zu ändern. Die Änderungen wirken sich nur auf die von dir gescannte Karte oder Ring aus und haben keine Auswirkungen auf andere mit deiner Wallet verknüpften Geräte. Halte deine Karte oder Ring bereit! + Sicherheitswarnung Dein Konto verfügt nicht über genügend Guthaben, um diese Kryptowährung zu verkaufen. Bitte zahle den gewünschten Vermögenswert ein, um fortzufahren. Unzureichendes Guthaben Der Verkauf von Kryptowährung ist in Deiner Region derzeit nicht möglich. Wir arbeiten aktiv daran, Ihnen diese Option bald anzubieten – bleib dran! @@ -868,6 +882,7 @@ Du erhältst Token auswählen Nicht verfügbar + Das ist meine Wallet Guthaben versteckt Angezeigte Salden Rückgängig machen @@ -937,6 +952,7 @@ Alle mit %s freischalten Blockchain ist nicht erreichbar. Versuche es später nochmal Karte oder Ring scannen + Diese Wallet wurde bereits früher aktiviert.\nWenn es nicht von Dir durchgeführt wurde, wende Dich bitte an den Support.\nTangem verkauft niemals Wallets zusammen mit dem vorgenerierten Zugangscode. Aufforderung zum Signieren einer Nachricht. \n\n %s Dapp %1$s, mit der Bitte um\nBNB-Transaktion zu unterzeichnen.\n\n%2$s Handelsauftrag für %1$s\n Preis: %2$s\n Zu erhaltender Betrag: %3$s\n Zu zahlender Betrag: %4$s @@ -984,7 +1000,7 @@ Genehmigung läuft Es scheint, dass die Aktivierung der Karte oder des Rings nicht korrekt abgeschlossen wurde. Dies kann an einem Problem mit dem NFC-Modul deines Gerätes oder an einem falschen Tippen der Karte oder Ring auf dein Gerät liegen. Bitte wende dich an unser Support-Team, um Unterstützung zu erhalten. Aktivierungsfehler - Laut den Entwicklern des BNB-Netzes wird die Unterstützung für den BEP-2-Standard im Juni 2024 enden. Um den Verlust von Vermögenswerten mit diesem Standard zu vermeiden, konvertiere bitte in den BEP-20 Standard. Nutze gerne unseren Swap-Service, um sie auf das BNB Smart Chain Netzwerk zu übertragen. + Am 3. Dezember 2024 wurde das BEP-2-Netzwerk auf Entscheidung der Netzwerkentwickler deaktiviert und wird nicht mehr unterstützt BNB Beacon Chain wird abgeschaltet Bitte zahle ein paar %1$s ein, um die Netzwerkgebühr zu decken Unzureichende Mittel zur Deckung der Netzgebühr diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 78fee270ad..8ecdd2c7e7 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -176,7 +176,7 @@ Enviar Con éxito Soporte - Intercambiar + Swap términos y condiciones Condiciones de uso Hoy @@ -982,7 +982,7 @@ Aprobación en curso Parece que la activación de la tarjeta no ha ido correctamente. Esto puede deberse a un problema con el módulo NFC de su dispositivo o a una mala conexión de la tarjeta de su dispositivo. Comuníquese con nuestro equipo de soporte para obtener ayuda. Error de activación - Según los desarrolladores de la red BNB, el soporte para el estándar BEP-2\nfinalizará en junio de 2024. Para evitar perder activos con este estándar, por favor conviértalos al estándar BEP-20. Usa nuestro servicio de swap para transferirlos a la red BNB Smart Chain. + El 3 de diciembre de 2024, la red BEP-2 fue desactivada por decisión de los desarrolladores de la red y ya no es compatible BNB Beacon Chain se cerrará Por favor deposite %1$s para cubrir la tarifa de red Fondos insuficientes para cubrir la tarifa de red diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index f1a4e1d5a3..4e3fe4704b 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -982,7 +982,7 @@ Approbation en cours Il semble que l\'activation de la carte ne se soit pas déroulée correctement. Cela peut être dû à un problème avec le module NFC de votre appareil ou à une mauvaise connexion de la carte sur votre appareil. Veuillez contacter notre équipe de support pour obtenir de l’aide. Erreur d\'activation - Selon les développeurs du réseau BNB, le support de la norme BEP-2\nprendra fin en juin 2024. Pour éviter de perdre des actifs avec cette norme, veuillez les convertir à la norme BEP-20. Utilisez notre service de d\'échange pour les transférer sur le réseau BNB Smart Chain. + Le 3 décembre 2024, le réseau BEP-2 a été désactivé par décision des développeurs du réseau et n\'est plus pris en charge BNB Beacon Chain va s\'arrêter de fonctionner Veuillez déposer environ %1$s pour couvrir les frais de réseau Fonds insuffisants pour couvrir les frais de réseau diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 73cff6874b..7b931f0406 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -3,8 +3,8 @@ トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して買付できるようにします。 トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して売却できるようにします。 売却 - 接続には数秒かかる場合があります。しばらくお待ちください。 - 接続 + これには数秒かかる場合があります。しばらくしてからもう一度お試しください。 + データはまだ読み込まれていません。 この操作は現在利用できません。しばらくしてからもう一度お試しいただくか、画面を下にスワイプしてデータを更新してください。 ボタンは使用できません トークンを選択 @@ -316,6 +316,7 @@ お持ちのカードまたはリングについて教えてください サポートチームの皆さん、こんにちは。 問題について詳しく教えてください。どんな些細なことでも役に立ちます。 + 以前に有効化されたウォレット 私の提案 カード / リングをスキャンできません フィードバック @@ -588,7 +589,7 @@ 住居 正確なお支払い方法とサービスを確保するため、正しい国を選択してください。 設定 - トークンページから取引状況を確認できます。 + この画面を閉じて、トークンの詳細画面で取引状況を確認できます。 この画面を閉じて、トークンの詳細画面で取引状況を確認できます。 経由 グループ @@ -653,7 +654,8 @@ カードまたはリングをスキャン カードまたはリングをスキャンして設定を変更します。変更はスキャンしたカードまたはリングにのみ影響し、ウォレットに関連付けられている他のデバイスには影響しません。 Tangemを準備してください! - この暗号資産を売却するのに十分な資金が残高にありません。続行するには、希望の資産を入金してください。 + セキュリティ警告 + 暗号通貨を売却するための十分な資金が残高にありません。続行するには希望の資産を入金してください。 残高不足 現在、お住まいの地域では暗号資産の売却はご利用いただけません。このオプションをすぐにご利用いただけるよう積極的に取り組んでいますので、お楽しみに! 地域制限 @@ -870,6 +872,7 @@ 受け取る トークンを選択 利用不可 + これは私のウォレットです 残高非表示 残高表示 元に戻す @@ -939,6 +942,7 @@ %sですべてをロック解除 ブロックチェーンにアクセスできません。後でもう一度お試しください。 カードまたはリングをスキャン + このウォレットはすでに有効化されています。\nあなたが行ったのでない場合は、サポートに連絡してください。\nTangemは、事前に生成されたアクセスコードと一緒にウォレットを販売することはありません。 メッセージに署名することを要求しています。 \n\n %s Dapp%1$s 、BNB取引の署名を要求しています\n\n%2$s %1$sの取引注文\n価格: %2$s\n受取金額: %3$s\n支払金額: %4$s @@ -986,8 +990,8 @@ 承認中 カードまたはリングのアクティベーションが正しく完了しませんでした。デバイスのNFCモジュールに問題があるか、カードまたはリングをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。 アクティベーションに失敗しました - BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください。 - BNBビーコンチェーンは閉鎖されます。 + 2024年12月3日、BEP-2ネットワークはネットワーク開発者の決定により使用不能となり、サポートは終了しました。 + BNBビーコンチェーンは閉鎖されました ネットワーク手数料をカバーするために%1$sを入金してください ネットワーク手数料をカバーする資金が不足しています もっと良くなるはず diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 4ba37fba49..5424eee393 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -3,8 +3,8 @@ Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для покупки. Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для продажи. Продать - Подключение может занять несколько секунд. Пожалуйста, подождите. - Соединение + Это может занять несколько секунд. Пожалуйста, попробуйте позже. + Данные ещё не загрузились Действие в данный момент недоступно, пожалуйста попробуйте позже или обновить данные, сделав свайп экрана вниз. Кнопка недоступна Выберите токен @@ -682,7 +682,7 @@ Отсканируйте карту или кольцо, чтобы изменить ее настройки. Изменения затронут только ту карту или кольцо, которые вы отсканировали, и не повлияют на другие устройства, привязанные к вашему кошельку. Приготовьте свой Tangem! Уведомление безопасности - На вашем балансе недостаточно средств для продажи этой криптовалюты. Пожалуйста, пополните нужный актив, чтобы продолжить. + На вашем балансе недостаточно средств для продажи криптовалюты. Пожалуйста, пополните нужный актив, чтобы продолжить. Недостаточно средств Продажа криптовалюты в вашем регионе временно недоступна. Мы активно работаем над тем, чтобы добавить эту возможность. Следите за нашими новостями! Региональные ограничения @@ -1016,7 +1016,7 @@ Выдача разрешения Похоже, что процесс активации карт или кольца не был завершен корректно. Это могло быть вызвано проблемой взаимодействия с модулем NFC либо некорректным прикладыванием карты или кольца к телефону. Пожалуйста, обратитесь в нашу службу поддержки для уточнения деталей. Ошибка активации - По решению разработчиков сети BNB стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении или сторонние сервисы, чтобы перевести средства в cеть BNB Smart Chain. + С 3 декабря 2024 года сеть BEP-2 была отключена по решению разработчиков сети и более не поддерживается Отключение сети BNB Beacon Chain Внесите немного %1$s, чтобы покрыть комиссию сети Недостаточно средств для оплаты комиссии сети diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 745f163055..80f21fbd9b 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -988,7 +988,7 @@ Підтвердження в процесі Схоже, що активація карток або кільця була виконана неправильно. Це може бути пов\'язано з проблемою з модулем NFC вашого пристрою або неправильним прикладанням картки або кільця до телефону. Будь ласка, зверніться за допомогою до нашої служби підтримки для уточнення деталей. Помилка активації - За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain. + 3 грудня 2024 року мережу BEP-2 було відключено за рішенням розробників мережі, і вона більше не підтримується Відключення мережі BNB Beacon Chain Будь ласка, поповніть рахунок на %1$s для покриття комісії мережі Недостатньо коштів для покриття комісії мережі diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f17f29f971..ffccaa511b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -3,8 +3,8 @@ Can’t find your token? Go to the Market section on the main page and add it to your portfolio for purchase Can’t find your token? Go to the Market section on the main page and add it to your portfolio for selling. Sell - Connection may take a few seconds. Please wait. - Connection + This may take a few seconds. Please try again later. + The data has not loaded yet. The action is currently unavailable. Please try again later or refresh the data by swiping down on the screen. Button is unavailable Choose the Token @@ -584,6 +584,7 @@ Search by country Unavailable Search by currency + By using onramp functionality, you agree with provider’s %1$s and %2$s The purchase amount should be no more than %s The amount to buy must be at least %s No available providers for this currency @@ -665,7 +666,7 @@ Scan the card or ring to change its settings. The changes will impact only the card or ring you\'ve scanned and will not affect other devices tied to your wallet. Get your Tangem ready! Security Alert - You don’t have enough funds in your balance to sell this cryptocurrency. Please deposit the desired asset to proceed. + You don’t have enough funds in your balance to sell cryptocurrency. Please deposit the desired asset to proceed. Insufficient Balance Selling cryptocurrency is unavailable in your region at the moment. We’re actively working to bring this option to you soon—stay tuned! Regional Restriction @@ -1000,8 +1001,8 @@ Approval in Progress It seems that the card or ring activation was not completed correctly. This could be due to an issue with your device\'s NFC module or incorrect tapping of the card or ring to your device. Please contact our Support team for assistance. Activation error - According to BNB network developers, support for the BEP-2 standard will end in June 2024. To avoid losing assets with this standard, please convert them to the BEP-20 standard. Use our swap service or third-party services to transfer funds to the BNB Smart Chain network. - BNB Beacon Chain will shut down + On December 3, 2024, the BEP-2 network was disabled by decision of the network developers and is no longer supported + BNB Beacon Chain shut down Please deposit some %1$s to cover the network fee Insufficient funds to cover the network fee Could be better diff --git a/core/ui/src/main/java/com/tangem/core/ui/webview/WebViewExt.kt b/core/ui/src/main/java/com/tangem/core/ui/webview/WebViewExt.kt new file mode 100644 index 0000000000..39ec3d8e8d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/webview/WebViewExt.kt @@ -0,0 +1,30 @@ +package com.tangem.core.ui.webview + +import android.webkit.WebSettings +import android.webkit.WebView + +/** + * Applies set of settings to prevent base security issues + * +[REDACTED_AUTHOR] + */ +fun WebView.applySafeSettings() { + settings.apply { + // disable to use scripts + javaScriptEnabled = false + + // disable access to files + allowFileAccess = false + + // disable access to content by system content provider + allowContentAccess = false + + // disable to use cached data (scripts, files, etc) + cacheMode = WebSettings.LOAD_NO_CACHE + + // disable to use local storage + domStorageEnabled = false + } + + setDownloadListener(null) +} \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index 25a90ba084..d8222cc618 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -21,7 +21,6 @@ import com.tangem.datasource.api.onramp.models.common.OnrampDestinationDTO import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest import com.tangem.datasource.api.onramp.models.response.OnrampDataJson import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO -import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO import com.tangem.datasource.crypto.DataSignatureVerifier @@ -83,9 +82,7 @@ internal class DefaultOnrampRepository( private val onrampErrorAdapter = moshi.adapter(ExpressErrorResponse::class.java) private val onrampErrorConverter = OnrampErrorConverter(onrampErrorAdapter) - override suspend fun getCurrencies(): Flow> = withContext(dispatchers.io) { - currenciesStore.get(CURRENCIES_KEY) - } + override fun getCurrencies(): Flow> = currenciesStore.get(CURRENCIES_KEY) override suspend fun fetchCurrencies() = withContext(dispatchers.io) { if (!currenciesStore.getSyncOrNull(CURRENCIES_KEY).isNullOrEmpty()) return@withContext @@ -97,22 +94,22 @@ internal class DefaultOnrampRepository( currenciesStore.store(CURRENCIES_KEY, result) } - override suspend fun getCountries(): Flow> = withContext(dispatchers.io) { - countriesStore.get(COUNTRIES_KEY) - } + override fun getCountries(): Flow> = countriesStore.get(COUNTRIES_KEY) override suspend fun getCountriesSync(): List? { return countriesStore.getSyncOrNull(COUNTRIES_KEY) } - override suspend fun fetchCountries() = withContext(dispatchers.io) { - if (!countriesStore.getSyncOrNull(COUNTRIES_KEY).isNullOrEmpty()) return@withContext + override suspend fun fetchCountries(): List = withContext(dispatchers.io) { + if (!countriesStore.getSyncOrNull(COUNTRIES_KEY).isNullOrEmpty()) return@withContext emptyList() val result = onrampApi.getCountries() .getOrThrow() .map(countryConverter::convert) countriesStore.store(COUNTRIES_KEY, result) + + result } override suspend fun getCountryByIp(): OnrampCountry = withContext(dispatchers.io) { @@ -128,22 +125,23 @@ internal class DefaultOnrampRepository( } override suspend fun saveDefaultCurrency(currency: OnrampCurrency) = withContext(dispatchers.io) { - appPreferencesStore.storeObject( - key = PreferencesKeys.ONRAMP_DEFAULT_CURRENCY, - value = currencyConverter.convertBack(currency), + val country = getDefaultCountrySync() ?: return@withContext + appPreferencesStore.storeObject( + key = PreferencesKeys.ONRAMP_DEFAULT_COUNTRY, + value = countryConverter.convertBack(country.copy(defaultCurrency = currency)), ) } override suspend fun getDefaultCurrencySync(): OnrampCurrency? = withContext(dispatchers.io) { appPreferencesStore - .getObjectSyncOrNull(PreferencesKeys.ONRAMP_DEFAULT_CURRENCY) - ?.let(currencyConverter::convert) + .getObjectSyncOrNull(PreferencesKeys.ONRAMP_DEFAULT_COUNTRY) + ?.let(countryConverter::convert)?.defaultCurrency } override fun getDefaultCurrency(): Flow { return appPreferencesStore - .getObject(PreferencesKeys.ONRAMP_DEFAULT_CURRENCY) - .map { it?.let(currencyConverter::convert) } + .getObject(PreferencesKeys.ONRAMP_DEFAULT_COUNTRY) + .map { it?.let(countryConverter::convert)?.defaultCurrency } } override suspend fun saveDefaultCountry(country: OnrampCountry) = withContext(dispatchers.io) { @@ -376,7 +374,12 @@ internal class DefaultOnrampRepository( OnrampProvider( id = onrampProviderDTO.providerId, - info = OnrampProviderInfo(name = providerInfo.name, imageLarge = providerInfo.imageLargeUrl), + info = OnrampProviderInfo( + name = providerInfo.name, + imageLarge = providerInfo.imageLargeUrl, + termsOfUseLink = providerInfo.termsOfUse, + privacyPolicyLink = providerInfo.privacyPolicy, + ), paymentMethods = onrampPaymentMethods.filter { paymentMethod -> onrampProviderDTO.paymentMethods.any { paymentMethod.id == it } }, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 3a211c70b8..4250ccab0e 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -4,7 +4,7 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.repository.* import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader +import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.network.NetworksStatusesStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.quote.QuotesStore @@ -31,7 +31,7 @@ internal object TokensDataModule { walletManagersFacade: WalletManagersFacade, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, - swapServiceLoader: SwapServiceLoader, + expressServiceLoader: ExpressServiceLoader, excludedBlockchains: ExcludedBlockchains, ): CurrenciesRepository { return DefaultCurrenciesRepository( @@ -41,7 +41,7 @@ internal object TokensDataModule { cacheRegistry = cacheRegistry, appPreferencesStore = appPreferencesStore, dispatchers = dispatchers, - swapServiceLoader = swapServiceLoader, + expressServiceLoader = expressServiceLoader, excludedBlockchains = excludedBlockchains, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index b1b23ef78b..a5d7aa511a 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -14,9 +14,11 @@ import com.tangem.data.tokens.utils.CustomTokensMerger import com.tangem.data.tokens.utils.UserTokensBackwardCompatibility import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.express.models.TangemExpressValues.EMPTY_CONTRACT_ADDRESS_VALUE +import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.exchangeservice.swap.SwapServiceLoader +import com.tangem.datasource.exchangeservice.swap.ExpressServiceLoader import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObject @@ -48,7 +50,7 @@ internal class DefaultCurrenciesRepository( private val walletManagersFacade: WalletManagersFacade, private val cacheRegistry: CacheRegistry, private val appPreferencesStore: AppPreferencesStore, - private val swapServiceLoader: SwapServiceLoader, + private val expressServiceLoader: ExpressServiceLoader, private val dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, ) : CurrenciesRepository { @@ -95,7 +97,7 @@ internal class DefaultCurrenciesRepository( response = updatedResponse, ) - fetchExchangeableUserMarketCoinsByIds(userWalletId, updatedResponse) + fetchExpressAssetsByNetworkIds(userWalletId, updatedResponse) } } @@ -117,7 +119,7 @@ internal class DefaultCurrenciesRepository( userWalletId = userWalletId, response = updatedResponse, ) - fetchExchangeableUserMarketCoinsByIds(userWalletId, updatedResponse) + fetchExpressAssetsByNetworkIds(userWalletId, updatedResponse) } } @@ -214,21 +216,33 @@ internal class DefaultCurrenciesRepository( } } - override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { + override suspend fun getSingleCurrencyWalletPrimaryCurrency( + userWalletId: UserWalletId, + refresh: Boolean, + ): CryptoCurrency { return withContext(dispatchers.io) { val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) - cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + val currency = cardCurrenciesFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet.scanResponse) + fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency), refresh) + currency } } - override suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List { + override suspend fun getSingleCurrencyWalletWithCardCurrencies( + userWalletId: UserWalletId, + refresh: Boolean, + ): List { return withContext(dispatchers.io) { val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = false) - cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) + val currencies = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken( + userWallet.scanResponse, + ) + fetchExpressAssetsByNetworkIds(userWalletId, currencies, refresh) + currencies } } @@ -243,6 +257,8 @@ internal class DefaultCurrenciesRepository( val currency = cardCurrenciesFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet.scanResponse) .find { it.id == id } requireNotNull(currency) { "Unable to find currency with provided ID: $id" } + fetchExpressAssetsByNetworkIds(userWalletId, listOf(currency)) + currency } } @@ -553,7 +569,7 @@ internal class DefaultCurrenciesRepository( value = compatibleUserTokensResponse, ) - fetchExchangeableUserMarketCoinsByIds(userWalletId, compatibleUserTokensResponse) + fetchExpressAssetsByNetworkIds(userWalletId, compatibleUserTokensResponse) } private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet): Boolean { @@ -572,13 +588,37 @@ internal class DefaultCurrenciesRepository( pushTokens(userWalletId, response) } - private suspend fun fetchExchangeableUserMarketCoinsByIds( - userWalletId: UserWalletId, - userTokens: UserTokensResponse, - ) { - swapServiceLoader.update(userWalletId, userTokens) + private suspend fun fetchExpressAssetsByNetworkIds(userWalletId: UserWalletId, userTokens: UserTokensResponse) { + val tokens = userTokens.tokens.map { token -> + LeastTokenInfo( + contractAddress = token.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE, + network = token.networkId, + ) + } + expressServiceLoader.update(userWalletId, tokens) } + private suspend fun fetchExpressAssetsByNetworkIds( + userWalletId: UserWalletId, + cryptoCurrencies: List, + refresh: Boolean = false, + ) { + val tokens = cryptoCurrencies.map { currency -> + val tokenCurrency = currency as? CryptoCurrency.Token + LeastTokenInfo( + contractAddress = tokenCurrency?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE, + network = currency.network.backendId, + ) + } + cacheRegistry.invokeOnExpire( + key = getAssetsCacheKey(userWalletId), + skipCache = refresh, + block = { expressServiceLoader.update(userWalletId, tokens) }, + ) + } + + private fun getAssetsCacheKey(userWalletId: UserWalletId): String = "assets_cache_key_${userWalletId.stringValue}" + private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse { val userWalletId = userWallet.walletId val response = appPreferencesStore.getObjectSyncOrNull( diff --git a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt index 8e4928a02d..0adb568906 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/exchange/RampStateManager.kt @@ -39,5 +39,5 @@ interface RampStateManager { fun getSellInitializationStatus(): Flow> - fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow> + fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow> } \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProvider.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProvider.kt index 35c4934530..1c0f50f578 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProvider.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampProvider.kt @@ -10,4 +10,9 @@ data class OnrampProvider( ) @Serializable -data class OnrampProviderInfo(val name: String, val imageLarge: String) \ No newline at end of file +data class OnrampProviderInfo( + val name: String, + val imageLarge: String, + val termsOfUseLink: String?, + val privacyPolicyLink: String?, +) \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt index 52971ea345..60cb522991 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt @@ -26,16 +26,16 @@ class CheckOnrampAvailabilityUseCase( } private suspend fun proceedWithSavedCountry(savedCountry: OnrampCountry): OnrampAvailability { - val countries = repository.getCountriesSync().orEmpty() - val onrampAvailable = countries.find { it == savedCountry }?.onrampAvailable ?: false - return if (onrampAvailable) { + val countries = repository.fetchCountries() + val updatedCountry = countries.find { it.id == savedCountry.id } ?: savedCountry + return if (updatedCountry.onrampAvailable) { val currency = repository.getDefaultCurrencySync() ?: run { repository.saveDefaultCurrency(savedCountry.defaultCurrency) savedCountry.defaultCurrency } OnrampAvailability.Available(country = savedCountry, currency = currency) } else { - OnrampAvailability.NotSupported(savedCountry) + OnrampAvailability.NotSupported(updatedCountry) } } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/FetchOnrampCountriesUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/FetchOnrampCountriesUseCase.kt index 8f320a1247..1920178751 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/FetchOnrampCountriesUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/FetchOnrampCountriesUseCase.kt @@ -11,6 +11,6 @@ class FetchOnrampCountriesUseCase( ) { suspend operator fun invoke(): Either { - return Either.catch { repository.fetchCountries() }.mapLeft(errorResolver::resolve) + return Either.catch { repository.fetchCountries() }.mapLeft(errorResolver::resolve) } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrenciesUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrenciesUseCase.kt index 33f5ac2f22..4c028a7b02 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrenciesUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrenciesUseCase.kt @@ -13,7 +13,7 @@ class GetOnrampCurrenciesUseCase( private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(): EitherFlow { + operator fun invoke(): EitherFlow { return onrampRepository.getCurrencies().map { currenciesList -> Either.catch { val (populars, others) = currenciesList.toSet() diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrencyUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrencyUseCase.kt deleted file mode 100644 index 210a6c3051..0000000000 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCurrencyUseCase.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.domain.onramp - -import arrow.core.Either -import arrow.core.left -import arrow.core.right -import com.tangem.domain.onramp.model.OnrampCurrency -import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.onramp.repositories.OnrampErrorResolver -import com.tangem.domain.onramp.repositories.OnrampRepository -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map - -class GetOnrampCurrencyUseCase( - private val repository: OnrampRepository, - private val errorResolver: OnrampErrorResolver, -) { - - operator fun invoke(): Flow> { - return repository.getDefaultCurrency() - .map> { it.right() } - .catch { - emit(errorResolver.resolve(it).left()) - } - } -} \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveDefaultCountryUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveDefaultCountryUseCase.kt index 9451683177..dc4771d68a 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveDefaultCountryUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampSaveDefaultCountryUseCase.kt @@ -11,7 +11,6 @@ class OnrampSaveDefaultCountryUseCase( ) { suspend operator fun invoke(country: OnrampCountry) = Either.catch { - repository.saveDefaultCurrency(country.defaultCurrency) repository.saveDefaultCountry(country) }.mapLeft(errorResolver::resolve) } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt index c5d09a5da7..87e9bff955 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt @@ -10,13 +10,13 @@ import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") interface OnrampRepository { // api - suspend fun getCurrencies(): Flow> - suspend fun getCountries(): Flow> + fun getCurrencies(): Flow> + fun getCountries(): Flow> suspend fun getCountriesSync(): List? suspend fun getCountryByIp(): OnrampCountry suspend fun getStatus(txId: String): OnrampStatus suspend fun fetchCurrencies() - suspend fun fetchCountries() + suspend fun fetchCountries(): List suspend fun fetchPaymentMethodsIfAbsent() suspend fun fetchPairs(currency: OnrampCurrency, country: OnrampCountry, cryptoCurrency: CryptoCurrency) suspend fun fetchQuotes(cryptoCurrency: CryptoCurrency, amount: Amount) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt index e40ecd7e98..0fc9607d48 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCardTokenListUseCase.kt @@ -25,7 +25,7 @@ class FetchCardTokenListUseCase( suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either { return either { - val currencies = fetchCurrencies(userWalletId = userWalletId) + val currencies = fetchCurrencies(userWalletId = userWalletId, refresh = refresh) coroutineScope { val fetchStatuses = async { @@ -53,9 +53,17 @@ class FetchCardTokenListUseCase( } } - private suspend fun Raise.fetchCurrencies(userWalletId: UserWalletId): List { + private suspend fun Raise.fetchCurrencies( + userWalletId: UserWalletId, + refresh: Boolean = false, + ): List { return catch( - block = { currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(userWalletId = userWalletId) }, + block = { + currenciesRepository.getSingleCurrencyWalletWithCardCurrencies( + userWalletId = userWalletId, + refresh = refresh, + ) + }, catch = { raise(TokenListError.DataError(it)) }, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt index 7b631669e7..b5a73a5ac3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/FetchCurrencyStatusUseCase.kt @@ -65,7 +65,7 @@ class FetchCurrencyStatusUseCase( refresh: Boolean = false, ): Either { return either { - val currency = getPrimaryCurrency(userWalletId) + val currency = getPrimaryCurrency(userWalletId, refresh) fetchCurrencyStatus(userWalletId, currency, refresh) } @@ -102,8 +102,11 @@ class FetchCurrencyStatusUseCase( } } - private suspend fun Raise.getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { - return catch({ currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }) { + private suspend fun Raise.getPrimaryCurrency( + userWalletId: UserWalletId, + refresh: Boolean = false, + ): CryptoCurrency { + return catch({ currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId, refresh) }) { raise(CurrencyStatusError.DataError(it)) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index f040d02154..fe343236b9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -9,6 +9,7 @@ import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.transaction.models.AssetRequirementsCondition import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -67,7 +68,7 @@ class GetCryptoCurrencyActionsUseCase( userWallet = userWallet, coinStatus = maybeCoinStatus.getOrNull(), cryptoCurrencyStatus = cryptoCurrencyStatus, - needAssociateAsset = requirements != null, + requirements = requirements, ) } @@ -79,7 +80,7 @@ class GetCryptoCurrencyActionsUseCase( userWallet: UserWallet, coinStatus: CryptoCurrencyStatus?, cryptoCurrencyStatus: CryptoCurrencyStatus, - needAssociateAsset: Boolean, + requirements: AssetRequirementsCondition?, ): TokenActionsState { return TokenActionsState( walletId = userWallet.walletId, @@ -88,7 +89,7 @@ class GetCryptoCurrencyActionsUseCase( userWallet = userWallet, coinStatus = coinStatus, cryptoCurrencyStatus = cryptoCurrencyStatus, - needAssociateAsset = needAssociateAsset, + requirements = requirements, ), ) } @@ -102,14 +103,14 @@ class GetCryptoCurrencyActionsUseCase( userWallet: UserWallet, coinStatus: CryptoCurrencyStatus?, cryptoCurrencyStatus: CryptoCurrencyStatus, - needAssociateAsset: Boolean, + requirements: AssetRequirementsCondition?, ): List { val cryptoCurrency = cryptoCurrencyStatus.currency if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) { return listOf(TokenActionsState.ActionState.HideToken(ScenarioUnavailabilityReason.None)) } if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Unreachable) { - return getActionsForUnreachableCurrency(userWallet, cryptoCurrencyStatus, needAssociateAsset) + return getActionsForUnreachableCurrency(userWallet, cryptoCurrencyStatus, requirements) } val activeList = mutableListOf() @@ -128,11 +129,7 @@ class GetCryptoCurrencyActionsUseCase( // receive if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - val scenario = if (needAssociateAsset) { - ScenarioUnavailabilityReason.UnassociatedAsset - } else { - ScenarioUnavailabilityReason.None - } + val scenario = getReceiveScenario(requirements) activeList.add(TokenActionsState.ActionState.Receive(scenario)) } @@ -243,7 +240,7 @@ class GetCryptoCurrencyActionsUseCase( private suspend fun getActionsForUnreachableCurrency( userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus, - needAssociateAsset: Boolean, + requirements: AssetRequirementsCondition?, ): List { val actionsList = mutableListOf() @@ -265,11 +262,7 @@ class GetCryptoCurrencyActionsUseCase( actionsList.add(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.Unreachable)) actionsList.add(TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.Unreachable)) if (isAddressAvailable(cryptoCurrencyStatus.value.networkAddress)) { - val scenario = if (needAssociateAsset) { - ScenarioUnavailabilityReason.UnassociatedAsset - } else { - ScenarioUnavailabilityReason.None - } + val scenario = getReceiveScenario(requirements) actionsList.add(TokenActionsState.ActionState.Receive(scenario)) } actionsList.add(TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.Unreachable, null)) @@ -278,6 +271,16 @@ class GetCryptoCurrencyActionsUseCase( return actionsList } + private fun getReceiveScenario(requirements: AssetRequirementsCondition?): ScenarioUnavailabilityReason { + return if (requirements is AssetRequirementsCondition.PaidTransaction || + requirements is AssetRequirementsCondition.PaidTransactionWithFee + ) { + ScenarioUnavailabilityReason.UnassociatedAsset + } else { + ScenarioUnavailabilityReason.None + } + } + private fun getSendUnavailabilityReason( cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 6f2c349a7c..3e54e19951 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -50,6 +50,7 @@ class GetCurrencyWarningsUseCase( stakingRepository = stakingRepository, userWalletId = userWalletId, ) + // don't add here notifications that require async requests return combine( getCoinRelatedWarnings( userWalletId = userWalletId, @@ -62,14 +63,8 @@ class GetCurrencyWarningsUseCase( flowOf(currencyChecksRepository.getRentInfoWarning(userWalletId, currencyStatus)), flowOf(currencyChecksRepository.getExistentialDeposit(userWalletId, currency.network)), flowOf(currencyChecksRepository.getFeeResourceAmount(userWalletId, currency.network)), - getSwapPromoNotificationWarning( - operations = operations, - userWalletId = userWalletId, - currencyStatus = currencyStatus, - ).conflate(), - ) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeFeeResource, maybeSwapPromo -> + ) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeFeeResource -> setOfNotNull( - maybeSwapPromo, maybeRentWarning, maybeEdWarning?.let { getExistentialDepositWarning(currency, it) }, maybeFeeResource?.let { getFeeResourceWarning(it) }, @@ -80,19 +75,11 @@ class GetCurrencyWarningsUseCase( getAssetRequirementsWarning(userWalletId = userWalletId, currency = currency), getMigrationFromMaticToPolWarning(currency), ) - } - .onEmpty { - setOfNotNull( - getNetworkUnavailableWarning(currencyStatus), - getNetworkNoAccountWarning(currencyStatus), - getBeaconChainShutdownWarning(currency.network.id), - getAssetRequirementsWarning(userWalletId = userWalletId, currency = currency), - getMigrationFromMaticToPolWarning(currency), - ) - } - .flowOn(dispatchers.io) + }.flowOn(dispatchers.io) } + // disabled for now, don't use it directly in combine() to avoid blocking other notifications + @Suppress("UnusedPrivateMember") private suspend fun getSwapPromoNotificationWarning( operations: CurrenciesStatusesOperations, userWalletId: UserWalletId, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index dbb73abf78..16052e2974 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -18,6 +18,7 @@ import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* internal class CurrenciesStatusesLceOperations( @@ -169,6 +170,8 @@ internal class CurrenciesStatusesLceOperations( .map, Either>> { it.right() } .retryWhen { cause, _ -> emit(TokenListError.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_QUOTES_DELAY) true } .distinctUntilChanged() @@ -215,4 +218,8 @@ internal class CurrenciesStatusesLceOperations( else -> null } } + + companion object { + private const val RETRY_QUOTES_DELAY = 2000L + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 66af3545ce..b3a7846497 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -86,21 +86,29 @@ interface CurrenciesRepository { * Retrieves the primary cryptocurrency for a specific single-currency user wallet. * * @param userWalletId The unique identifier of the user wallet. + * @param refresh Indicates whether to force a refresh of the status data. * @return The primary cryptocurrency associated with the user wallet. * @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet * ID provided. */ - suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency + suspend fun getSingleCurrencyWalletPrimaryCurrency( + userWalletId: UserWalletId, + refresh: Boolean = false, + ): CryptoCurrency /** * Retrieves the cryptocurrencies for a specific single-currency user wallet with tokens on the card. * * @param userWalletId The unique identifier of the user wallet. + * @param refresh Indicates whether to force a refresh of the status data. * @return The primary cryptocurrency associated with the user wallet. * @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet * ID provided. */ - suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List + suspend fun getSingleCurrencyWalletWithCardCurrencies( + userWalletId: UserWalletId, + refresh: Boolean = false, + ): List /** * Retrieves the cryptocurrency for a specific single-currency user old wallet diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index d51f5710d8..6f9e9811af 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -70,11 +70,17 @@ internal class MockCurrenciesRepository( return tokens.first().getOrElse { e -> throw e } } - override suspend fun getSingleCurrencyWalletPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency { + override suspend fun getSingleCurrencyWalletPrimaryCurrency( + userWalletId: UserWalletId, + refresh: Boolean, + ): CryptoCurrency { return token.getOrElse { e -> throw e } } - override suspend fun getSingleCurrencyWalletWithCardCurrencies(userWalletId: UserWalletId): List { + override suspend fun getSingleCurrencyWalletWithCardCurrencies( + userWalletId: UserWalletId, + refresh: Boolean, + ): List { return tokens.first().getOrElse { e -> throw e } } diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/IncompleteTransactionError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/IncompleteTransactionError.kt index 0c80d368c5..4c707fa76d 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/IncompleteTransactionError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/IncompleteTransactionError.kt @@ -1,5 +1,6 @@ package com.tangem.domain.transaction.error sealed class IncompleteTransactionError { + data class SendError(val error: SendTransactionError) : IncompleteTransactionError() data class DataError(val message: String?) : IncompleteTransactionError() } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/DismissIncompleteTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/DismissIncompleteTransactionUseCase.kt index 1b11ad2cfd..5219bf88cc 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/DismissIncompleteTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/DismissIncompleteTransactionUseCase.kt @@ -16,7 +16,7 @@ class DismissIncompleteTransactionUseCase( suspend operator fun invoke( userWalletId: UserWalletId, currency: CryptoCurrency, - ): Either { + ): Either { return either { catch( block = { diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/RetryIncompleteTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/RetryIncompleteTransactionUseCase.kt index fa8fa53c7e..9a227b2c03 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/RetryIncompleteTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/RetryIncompleteTransactionUseCase.kt @@ -3,10 +3,12 @@ package com.tangem.domain.transaction.usecase import arrow.core.Either import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.blockchain.common.BlockchainSdkError import com.tangem.blockchain.extensions.SimpleResult import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.transaction.error.IncompleteTransactionError +import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId @@ -25,9 +27,19 @@ class RetryIncompleteTransactionUseCase( catch( block = { when (val result = walletManagersFacade.fulfillRequirements(userWalletId, currency, signer)) { - is SimpleResult.Failure -> raise( - IncompleteTransactionError.DataError(result.error.customMessage), - ) + is SimpleResult.Failure -> { + val error = result.error as? BlockchainSdkError + when (error) { + is BlockchainSdkError.WrappedTangemError -> parseWrappedError(error) + null -> SendTransactionError.UnknownError() + else -> SendTransactionError.BlockchainSdkError( + code = error.code, + message = error.customMessage, + ) + }.let { + raise(IncompleteTransactionError.SendError(it)) + } + } SimpleResult.Success -> Unit } }, diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 3d3f3063c6..12f09f23d3 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -132,33 +132,33 @@ class SendTransactionUseCase( } } } +} - private fun parseWrappedError(error: BlockchainSdkError.WrappedTangemError): SendTransactionError { - return if (error.code == USER_CANCELLED_ERROR_CODE) { - SendTransactionError.UserCancelledError - } else { - when (val tangemError = error.tangemError) { - is TangemSdkError -> { - val resource = tangemError.localizedDescriptionRes() - val resId = resource.resId ?: R.string.common_unknown_error - val resArgs = resource.args.map { it.value } - SendTransactionError.TangemSdkError(tangemError.code, resId, wrappedList(resArgs)) - } - is BlockchainSdkError.WrappedTangemError -> { - parseWrappedError(tangemError) // todo remove when sdk errors are revised - } - is BlockchainSdkError.WrappedThrowable -> { - val causeError = tangemError.cause - if (causeError is BlockchainSdkError) { - SendTransactionError.BlockchainSdkError(causeError.code, causeError.customMessage) - } else { - SendTransactionError.BlockchainSdkError(tangemError.code, tangemError.customMessage) - } - } - else -> { - SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) +fun parseWrappedError(error: BlockchainSdkError.WrappedTangemError): SendTransactionError { + return if (error.code == USER_CANCELLED_ERROR_CODE) { + SendTransactionError.UserCancelledError + } else { + when (val tangemError = error.tangemError) { + is TangemSdkError -> { + val resource = tangemError.localizedDescriptionRes() + val resId = resource.resId ?: R.string.common_unknown_error + val resArgs = resource.args.map { it.value } + SendTransactionError.TangemSdkError(tangemError.code, resId, wrappedList(resArgs)) + } + is BlockchainSdkError.WrappedTangemError -> { + parseWrappedError(tangemError) // todo remove when sdk errors are revised + } + is BlockchainSdkError.WrappedThrowable -> { + val causeError = tangemError.cause + if (causeError is BlockchainSdkError) { + SendTransactionError.BlockchainSdkError(causeError.code, causeError.customMessage) + } else { + SendTransactionError.BlockchainSdkError(tangemError.code, tangemError.customMessage) } } + else -> { + SendTransactionError.BlockchainSdkError(error.code, tangemError.customMessage) + } } } } \ No newline at end of file diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt index f3fe9913c7..1fcc978ace 100644 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt +++ b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerScreen.kt @@ -1,7 +1,7 @@ package com.tangem.features.disclaimer.impl.ui -import android.annotation.SuppressLint import android.content.res.Configuration +import android.webkit.WebView import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -14,7 +14,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource @@ -37,29 +36,22 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TestTags.DISCLAIMER_SCREEN_ACCEPT_BUTTON import com.tangem.core.ui.test.TestTags.DISCLAIMER_SCREEN_CONTAINER +import com.tangem.core.ui.webview.applySafeSettings import com.tangem.features.disclaimer.impl.R import com.tangem.features.disclaimer.impl.entity.DisclaimerUM import com.tangem.features.disclaimer.impl.entity.DummyDisclaimer import com.tangem.features.disclaimer.impl.local.localTermsOfServices import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull +import java.nio.charset.StandardCharsets @Composable internal fun DisclaimerScreen(state: DisclaimerUM) { val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - val bottomPadding = if (state.isTosAccepted) { - bottomBarHeight + TangemTheme.dimens.size16 - } else { - bottomBarHeight + TangemTheme.dimens.size64 - } - val backgroundColor = if (state.isTosAccepted) TangemTheme.colors.background.primary else TangemColorPalette.Dark6 - val (textColor, iconColor) = if (state.isTosAccepted) { - TangemTheme.colors.text.primary1 to TangemTheme.colors.icon.primary1 - } else { - TangemColorPalette.Light4 to TangemColorPalette.Light4 - } + val bottomPadding = bottomBarHeight + TangemTheme.dimens.size16 + Box( modifier = Modifier - .background(backgroundColor) + .background(TangemTheme.colors.background.primary) .statusBarsPadding() .testTag(DISCLAIMER_SCREEN_CONTAINER), ) { @@ -75,14 +67,15 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { onIconClicked = state.popBack, ).takeIf { state.isTosAccepted }, titleAlignment = Alignment.CenterHorizontally, - textColor = textColor, - iconTint = iconColor, ) - DisclaimerContent(state.url, state.isTosAccepted) + DisclaimerContent(state.url) } if (!state.isTosAccepted) { - BottomFade(Modifier.align(Alignment.BottomCenter), backgroundColor = backgroundColor) + BottomFade( + modifier = Modifier.align(Alignment.BottomCenter), + backgroundColor = TangemTheme.colors.background.primary, + ) DisclaimerButton(state.onAccept) } else { NavigationBar3ButtonsScrim() @@ -90,14 +83,14 @@ internal fun DisclaimerScreen(state: DisclaimerUM) { } } -@SuppressLint("SetJavaScriptEnabled") @Composable -private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { - val backgroundColor = if (isTosAccepted) TangemTheme.colors.background.primary else TangemColorPalette.Dark6 - +private fun DisclaimerContent(url: String) { val webViewStateUrl = rememberWebViewState(url) - val webViewStateData = - rememberWebViewStateWithHTMLData(data = localTermsOfServices, mimeType = "text/html", encoding = "UTF-8") + val webViewStateData = rememberWebViewStateWithHTMLData( + data = localTermsOfServices, + mimeType = "text/html", + encoding = StandardCharsets.UTF_8.name(), + ) val webViewState by remember { derivedStateOf { @@ -113,12 +106,10 @@ private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { WebView( state = webViewState, captureBackPresses = false, - onCreated = { - it.settings.javaScriptEnabled = !isTosAccepted - it.setBackgroundColor(backgroundColor.toArgb()) - }, - client = remember { DisclaimerWebViewClient() }, - modifier = Modifier.fillMaxSize(), + onCreated = WebView::applySafeSettings, + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), ) AnimatedVisibility( @@ -128,12 +119,12 @@ private fun DisclaimerContent(url: String, isTosAccepted: Boolean) { exit = fadeOut(), modifier = Modifier .fillMaxSize() - .background(backgroundColor), + .background(TangemTheme.colors.background.primary), ) { Box( modifier = Modifier .fillMaxSize() - .background(backgroundColor), + .background(TangemTheme.colors.background.primary), ) { CircularProgressIndicator( color = TangemTheme.colors.icon.informative, diff --git a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt b/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt deleted file mode 100644 index c3046d0855..0000000000 --- a/features/disclaimer/impl/src/main/java/com/tangem/features/disclaimer/impl/ui/DisclaimerWebViewClient.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.features.disclaimer.impl.ui - -import android.graphics.Bitmap -import android.webkit.WebView -import com.google.accompanist.web.AccompanistWebViewClient - -internal enum class ProgressState { - Loading, - Done, - Error, -} - -/** - * Workaround to display web view with ToS only in dark theme - */ -private fun WebView.injectCSS() { - val code = "javascript:(function() {" + - "var node = document.createElement('style');" + - "node.type = 'text/css';" + - " node.innerHTML = 'body, label,th,p,a, td, tr,li,ul,span,table,h1,h2,h3,h4,h5,h6,h7,div,small {" + - " color: #C9C9C9;" + - "background-color: #1E1E1E;" + - " } ';" + - " document.head.appendChild(node);})();" - - evaluateJavascript(code, null) -} - -internal class DisclaimerWebViewClient : AccompanistWebViewClient() { - - override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { - view?.injectCSS() - super.onPageStarted(view, url, favicon) - } - - override fun onPageCommitVisible(view: WebView?, url: String?) { - view?.injectCSS() - super.onPageCommitVisible(view, url) - } - - override fun onPageFinished(view: WebView?, url: String?) { - view?.injectCSS() - super.onPageFinished(view, url) - } -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/analytics/OnboardingEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/analytics/OnboardingEvent.kt index 2945b7ce27..f6ffdafe88 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/analytics/OnboardingEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/analytics/OnboardingEvent.kt @@ -64,5 +64,15 @@ sealed class OnboardingEvent( event = "Reset Card Notification", params = mapOf("Option" to "Reset"), ) + + data object ResumeInterruptedBackup : Backup( + event = "Notice - Backup Canceled", + params = mapOf("Action" to "Resume"), + ) + + data object CancelInterruptedBackup : Backup( + event = "Notice - Backup Canceled", + params = mapOf("Action" to "Cancel"), + ) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt index d9d4c128ec..5237af3c2b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/model/ConfirmResidencyModel.kt @@ -8,18 +8,14 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.onramp.GetOnrampCountryUseCase import com.tangem.domain.onramp.OnrampSaveDefaultCountryUseCase import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyBottomSheetConfig import com.tangem.features.onramp.confirmresidency.entity.ConfirmResidencyUM import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @@ -29,7 +25,6 @@ internal class ConfirmResidencyModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val router: Router, private val saveDefaultCountryUseCase: OnrampSaveDefaultCountryUseCase, - getOnrampCountryUseCase: GetOnrampCountryUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -50,17 +45,6 @@ internal class ConfirmResidencyModel @Inject constructor( init { analyticsEventHandler.send(OnrampAnalyticsEvent.ResidenceConfirmScreenOpened(params.country.name)) - getOnrampCountryUseCase.invoke() - .onEach { maybeCountry -> - maybeCountry.onLeft { - analyticsEventHandler.sendOnrampErrorEvent(it, params.cryptoCurrency.symbol) - } - val country = maybeCountry.getOrNull() - if (country != null) { - params.onDismiss(country) - } - } - .launchIn(modelScope) } private fun getPrimaryButtonConfig() = if (params.country.onrampAvailable) { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt index 96e54c7555..5654a17174 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt @@ -9,4 +9,5 @@ interface OnrampIntents { fun onBuyClick(quote: OnrampProviderWithQuote.Data) fun openProviders() fun onRefresh() + fun onLinkClick(link: String) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt index 6f62be5aed..5434116a79 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt @@ -9,7 +9,10 @@ sealed class OnrampProviderBlockUM { val providerId: String, val paymentMethod: OnrampPaymentMethod, val providerName: String, + val termsOfUseLink: String?, + val privacyPolicyLink: String?, val isBestRate: Boolean, + val onLinkClick: (String) -> Unit, val onClick: () -> Unit, ) : OnrampProviderBlockUM() } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt index 184b3a1dd3..cb2186a058 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt @@ -61,6 +61,7 @@ internal class OnrampAmountFieldChangeConverter( amountFieldModel = amountFieldModel, secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY), ), + buyButtonConfig = buyButtonConfig.copy(enabled = false), providerBlockState = OnrampProviderBlockUM.Empty, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt index f4ea0494ce..cb9d102bc6 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt @@ -122,6 +122,9 @@ internal class OnrampAmountStateFactory( providerName = providerResult.provider.info.name, isBestRate = isBestRate, onClick = onrampIntents::openProviders, + termsOfUseLink = providerResult.provider.info.termsOfUseLink, + privacyPolicyLink = providerResult.provider.info.privacyPolicyLink, + onLinkClick = onrampIntents::onLinkClick, ), buyButtonConfig = currentState.buyButtonConfig.copy( enabled = providerResult is SelectProviderResult.ProviderWithQuote, @@ -148,6 +151,9 @@ internal class OnrampAmountStateFactory( providerName = provider.info.name, isBestRate = isBestRate, onClick = onrampIntents::openProviders, + termsOfUseLink = provider.info.termsOfUseLink, + privacyPolicyLink = provider.info.privacyPolicyLink, + onLinkClick = onrampIntents::onLinkClick, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 45d142a862..c85fe513cc 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -10,6 +10,7 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.BasicDialog import com.tangem.core.ui.components.DialogButtonUM import com.tangem.core.ui.extensions.resolveReference @@ -53,13 +54,14 @@ internal class OnrampMainComponentModel @Inject constructor( private val router: Router, private val isDemoCardUseCase: IsDemoCardUseCase, private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase, - private val getOnrampCurrencyUseCase: GetOnrampCurrencyUseCase, + private val getOnrampCountryUseCase: GetOnrampCountryUseCase, private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase, private val fetchQuotesUseCase: OnrampFetchQuotesUseCase, private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase, private val fetchPairsUseCase: OnrampFetchPairsUseCase, private val amountInputManager: InputManager, private val messageSender: UiMessageSender, + private val urlOpener: UrlOpener, getWalletsUseCase: GetWalletsUseCase, paramsContainer: ParamsContainer, ) : Model(), OnrampIntents { @@ -105,7 +107,7 @@ internal class OnrampMainComponentModel @Inject constructor( fun handleOnrampAvailable(currency: OnrampCurrency) { _state.update { stateFactory.getReadyState(currency) } - subscribeToCurrencyUpdates() + subscribeToCountryAndCurrencyUpdates() subscribeToQuotesUpdate() } @@ -130,14 +132,14 @@ internal class OnrampMainComponentModel @Inject constructor( } } - private fun subscribeToCurrencyUpdates() { - getOnrampCurrencyUseCase.invoke() - .onEach { maybeCurrency -> - maybeCurrency.fold( + private fun subscribeToCountryAndCurrencyUpdates() { + getOnrampCountryUseCase.invoke() + .onEach { maybeCountry -> + maybeCountry.fold( ifLeft = ::handleOnrampError, - ifRight = { currency -> - if (currency == null) return@onEach - _state.update { amountStateFactory.getUpdatedCurrencyState(currency) } + ifRight = { country -> + if (country == null) return@onEach + _state.update { amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) } updatePairsAndQuotes() }, ) @@ -286,6 +288,8 @@ internal class OnrampMainComponentModel @Inject constructor( } } + override fun onLinkClick(link: String) = urlOpener.openUrl(link) + override fun onDestroy() { modelScope.launch { clearOnrampCacheUseCase.invoke() } quotesTaskScheduler.cancelTask() diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt new file mode 100644 index 0000000000..6c65dfd14b --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt @@ -0,0 +1,100 @@ +package com.tangem.features.onramp.main.ui + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.ClickableText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.extensions.appendColored +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM + +private const val TERMS_OF_USE_KEY = "termsOfUse" +private const val PRIVACY_POLICY_KEY = "privacyPolicy" + +@Composable +internal fun OnrampButtonComponent(state: OnrampMainComponentUM) { + val content = state as? OnrampMainComponentUM.Content + val providerState = content?.providerBlockState as? OnrampProviderBlockUM.Content + Column( + modifier = Modifier + .navigationBarsPadding() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + OnrampTosText(providerState) + PrimaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.common_buy), + onClick = state.buyButtonConfig.onClick, + enabled = state.buyButtonConfig.enabled, + ) + } +} + +@Composable +private fun OnrampTosText(provider: OnrampProviderBlockUM.Content?) { + val termsOfUse = stringResource(R.string.common_terms_of_use) + val privacyPolicy = stringResource(R.string.common_privacy_policy) + val tosText = stringResource(R.string.onramp_legal, termsOfUse, privacyPolicy) + + val clickableAnnotation = buildAnnotatedString { + append(tosText.substringBefore(termsOfUse)) + + pushStringAnnotation(TERMS_OF_USE_KEY, "") + appendColored(termsOfUse, TangemTheme.colors.text.accent) + pop() + + append(tosText.substringAfter(termsOfUse).substringBefore(privacyPolicy)) + + pushStringAnnotation(PRIVACY_POLICY_KEY, "") + appendColored(privacyPolicy, TangemTheme.colors.text.accent) + pop() + } + + AnimatedContent( + targetState = provider, + transitionSpec = { fadeIn().togetherWith(fadeOut()) }, + label = "Onramp Legal Info Animation", + ) { state -> + val termsOfUseLink = provider?.termsOfUseLink + val privacyPolicyLink = provider?.privacyPolicyLink + + if (state != null && termsOfUseLink != null && privacyPolicyLink != null) { + ClickableText( + text = clickableAnnotation, + style = TangemTheme.typography.caption2.copy( + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ), + onClick = { offset -> + clickableAnnotation.getStringAnnotations( + tag = TERMS_OF_USE_KEY, + start = offset, + end = offset, + ).firstOrNull()?.let { + state.onLinkClick(termsOfUseLink) + } + + clickableAnnotation.getStringAnnotations( + tag = PRIVACY_POLICY_KEY, + start = offset, + end = offset, + ).firstOrNull()?.let { + state.onLinkClick(privacyPolicyLink) + } + }, + ) + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt index ee9194f0a3..fa0797f476 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt @@ -11,16 +11,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.stringResource import com.tangem.core.ui.components.CircleShimmer -import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero -import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.entity.OnrampMainComponentUM @Composable @@ -49,15 +46,7 @@ internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: } }, floatingActionButton = { - PrimaryButton( - modifier = Modifier - .navigationBarsPadding() - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - text = stringResource(id = R.string.common_buy), - onClick = state.buyButtonConfig.onClick, - enabled = state.buyButtonConfig.enabled, - ) + OnrampButtonComponent(state) }, floatingActionButtonPosition = FabPosition.Center, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsErrorTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsErrorTransformer.kt new file mode 100644 index 0000000000..f6346df096 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/entity/transformer/UpdateCountryItemsErrorTransformer.kt @@ -0,0 +1,15 @@ +package com.tangem.features.onramp.selectcountry.entity.transformer + +import com.tangem.features.onramp.selectcountry.entity.CountryListUM +import com.tangem.utils.transformer.Transformer + +internal class UpdateCountryItemsErrorTransformer( + private val onRetry: () -> Unit, +) : Transformer { + override fun transform(prevState: CountryListUM): CountryListUM { + return CountryListUM.Error( + searchBarUM = prevState.searchBarUM, + onRetry = onRetry, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt index 60c6819e3b..d7e2a1c6ad 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt @@ -17,6 +17,7 @@ import com.tangem.features.onramp.selectcountry.SelectCountryComponent import com.tangem.features.onramp.selectcountry.entity.CountryItemState import com.tangem.features.onramp.selectcountry.entity.CountryListUM import com.tangem.features.onramp.selectcountry.entity.CountryListUMController +import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsErrorTransformer import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsLoadingTransformer import com.tangem.features.onramp.selectcountry.entity.transformer.UpdateCountryItemsTransformer import com.tangem.features.onramp.utils.InputManager @@ -26,8 +27,10 @@ import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @@ -44,13 +47,12 @@ internal class OnrampSelectCountryModel @Inject constructor( paramsContainer: ParamsContainer, ) : Model() { - val state: StateFlow get() = countryListUMController.state + val state: StateFlow get() = controller.state private val params: SelectCountryComponent.Params = paramsContainer.require() - private val countryListUMController = CountryListUMController( + private val controller = CountryListUMController( searchBarUM = createSearchBarUM(), loadingItems = loadingItems, ) - private val refreshTrigger = MutableSharedFlow() init { analyticsEventHandler.send(OnrampAnalyticsEvent.SelectResidenceOpened) @@ -58,20 +60,13 @@ internal class OnrampSelectCountryModel @Inject constructor( modelScope.launch { subscribeOnUpdateState() } } - private fun updateCountriesList() { - modelScope.launch { - fetchOnrampCountriesUseCase() - } - } - fun dismiss() { params.onDismiss() } - @OptIn(ExperimentalCoroutinesApi::class) private suspend fun subscribeOnUpdateState() { combine( - flow = refreshTrigger.onStart { emit(Unit) }.flatMapLatest { getOnrampCountriesUseCase() }, + flow = getOnrampCountriesUseCase(), flow2 = getOnrampCountryUseCase(), flow3 = searchManager.query, ) { maybeCountries, maybeCountry, query -> @@ -89,7 +84,7 @@ internal class OnrampSelectCountryModel @Inject constructor( onCountryClick = ::saveCountry, ) } - .onEach(countryListUMController::update) + .onEach(controller::update) .launchIn(modelScope) } @@ -102,23 +97,31 @@ internal class OnrampSelectCountryModel @Inject constructor( } private fun onRetry() { - modelScope.launch { refreshTrigger.emit(Unit) } - countryListUMController.update(UpdateCountryItemsLoadingTransformer(loadingItems)) + controller.update(UpdateCountryItemsLoadingTransformer(loadingItems)) + updateCountriesList() + } + + private fun updateCountriesList() { + modelScope.launch { + fetchOnrampCountriesUseCase().onLeft { + controller.update(UpdateCountryItemsErrorTransformer(onRetry = ::onRetry)) + } + } } private fun onSearchQueryChange(newQuery: String) { - val searchBarUM = countryListUMController.state.value.searchBarUM + val searchBarUM = controller.state.value.searchBarUM if (searchBarUM.query == newQuery) return modelScope.launch { - countryListUMController.update(transformer = UpdateSearchQueryTransformer(newQuery)) + controller.update(transformer = UpdateSearchQueryTransformer(newQuery)) searchManager.update(newQuery) } } private fun onSearchBarActiveChange(isActive: Boolean) { - countryListUMController.update( + controller.update( transformer = UpdateSearchBarActiveStateTransformer( isActive = isActive, placeHolder = resourceReference(id = R.string.common_search), @@ -137,7 +140,7 @@ internal class OnrampSelectCountryModel @Inject constructor( } private companion object { - private const val LOADING_ITEMS_COUNT = 5 + const val LOADING_ITEMS_COUNT = 5 val loadingItems: ImmutableList = MutableList(LOADING_ITEMS_COUNT) { CountryItemState.Loading("Loading #$it") }.toImmutableList() diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsErrorTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsErrorTransformer.kt new file mode 100644 index 0000000000..6b89b621fa --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/entity/transformer/UpdateCurrencyItemsErrorTransformer.kt @@ -0,0 +1,15 @@ +package com.tangem.features.onramp.selectcurrency.entity.transformer + +import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM +import com.tangem.utils.transformer.Transformer + +internal class UpdateCurrencyItemsErrorTransformer( + private val onRetry: () -> Unit, +) : Transformer { + override fun transform(prevState: CurrenciesListUM): CurrenciesListUM { + return CurrenciesListUM.Error( + searchBarUM = prevState.searchBarUM, + onRetry = onRetry, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt index 3ff2d6fc80..127d38cb94 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt @@ -18,6 +18,7 @@ import com.tangem.features.onramp.selectcurrency.entity.CurrenciesListUM import com.tangem.features.onramp.selectcurrency.entity.CurrenciesSection import com.tangem.features.onramp.selectcurrency.entity.CurrencyItemState import com.tangem.features.onramp.selectcurrency.entity.CurrencyListController +import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsErrorTransformer import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsLoadingTransformer import com.tangem.features.onramp.selectcurrency.entity.transformer.UpdateCurrencyItemsTransformer import com.tangem.features.onramp.utils.InputManager @@ -27,8 +28,10 @@ import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @@ -51,23 +54,15 @@ internal class OnrampSelectCurrencyModel @Inject constructor( currencySearchBarUM = createSearchBarUM(), loadingSections = loadingSections, ) - private val refreshTrigger = MutableSharedFlow() init { updateCurrenciesList() subscribeOnUpdateState() } - private fun updateCurrenciesList() { - modelScope.launch { - fetchOnrampCurrenciesUseCase() - } - } - - @OptIn(ExperimentalCoroutinesApi::class) private fun subscribeOnUpdateState() { combine( - flow = refreshTrigger.onStart { emit(Unit) }.flatMapLatest { getOnrampCurrenciesUseCase() }, + flow = getOnrampCurrenciesUseCase(), flow2 = searchManager.query, ) { maybeCurrencies, query -> maybeCurrencies.onLeft { @@ -97,8 +92,16 @@ internal class OnrampSelectCurrencyModel @Inject constructor( } private fun onRetry() { - modelScope.launch { refreshTrigger.emit(Unit) } controller.update(UpdateCurrencyItemsLoadingTransformer(loadingSections)) + updateCurrenciesList() + } + + private fun updateCurrenciesList() { + modelScope.launch { + fetchOnrampCurrenciesUseCase().onLeft { + controller.update(UpdateCurrencyItemsErrorTransformer(onRetry = ::onRetry)) + } + } } private fun onSearchQueryChange(newQuery: String) { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetErrorWarningTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetErrorWarningTransformer.kt new file mode 100644 index 0000000000..d3410bd2dc --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetErrorWarningTransformer.kt @@ -0,0 +1,27 @@ +package com.tangem.features.onramp.swap.availablepairs.entity.transformers + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.feature.swap.domain.models.ExpressException +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer +import kotlinx.collections.immutable.persistentListOf + +/** +[REDACTED_AUTHOR] + */ +internal class SetErrorWarningTransformer( + private val cause: Throwable, + private val onRefresh: () -> Unit, +) : TokenListUMTransformer { + + override fun transform(prevState: TokenListUM): TokenListUM { + return prevState.copy( + availableItems = persistentListOf(), + unavailableItems = persistentListOf(), + warning = NotificationUM.Warning.OnrampErrorNotification( + errorCode = (cause as? ExpressException)?.expressDataError?.code?.toString(), + onRefresh = onRefresh, + ), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt index b14b3a023e..b1a335134f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetLoadingTokenItemsTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList /** @@ -18,6 +19,8 @@ internal class SetLoadingTokenItemsTransformer( override fun transform(prevState: TokenListUM): TokenListUM { return prevState.copy( availableItems = LoadingTokenListItemConverter.convertList(input = statuses).toImmutableList(), + unavailableItems = persistentListOf(), + warning = null, ) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt new file mode 100644 index 0000000000..7e9c537c9e --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt @@ -0,0 +1,36 @@ +package com.tangem.features.onramp.swap.availablepairs.entity.transformers + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer +import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory +import com.tangem.features.onramp.tokenlist.entity.utils.addHeader +import kotlinx.collections.immutable.persistentListOf + +/** +[REDACTED_AUTHOR] + */ +internal class SetNoAvailablePairsTransformer( + private val appCurrency: AppCurrency, + private val unavailableStatuses: List, + private val isBalanceHidden: Boolean, + private val unavailableTokensHeaderReference: TextReference, +) : TokenListUMTransformer { + + override fun transform(prevState: TokenListUM): TokenListUM { + val unavailableItems = OnrampTokenItemStateConverterFactory.createUnavailableItemConverter(appCurrency) + .convertList(unavailableStatuses) + .map(TokensListItemUM::Token) + + return prevState.copy( + availableItems = persistentListOf(), + unavailableItems = unavailableItems.addHeader(textReference = unavailableTokensHeaderReference), + isBalanceHidden = isBalanceHidden, + warning = NotificationUM.Warning.SwapNoAvailablePair, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index cf2d9797ed..d31bb7513d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -9,7 +9,11 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.getOrElse +import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceError +import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -19,9 +23,12 @@ import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.SwapPairLeast import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent +import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer +import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMController +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer import com.tangem.features.onramp.utils.InputManager @@ -33,11 +40,13 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +private typealias AvailablePairsState = Lce> + @Suppress("LongParameterList") internal class AvailableSwapPairsModel @Inject constructor( paramsContainer: ParamsContainer, - getTokenListUseCase: GetTokenListUseCase, override val dispatchers: CoroutineDispatcherProvider, + private val getTokenListUseCase: GetTokenListUseCase, private val tokenListUMController: TokenListUMController, private val searchManager: InputManager, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, @@ -49,29 +58,37 @@ internal class AvailableSwapPairsModel @Inject constructor( private var params: AvailableSwapPairsComponent.Params = paramsContainer.require() - private val tokenListFlow = getTokenListUseCase.launch(userWalletId = params.userWalletId) - .distinctUntilChanged() - .map { maybeTokenList -> - maybeTokenList.getOrElse( - ifLoading = { it ?: TokenList.Empty }, - ifError = { TokenList.Empty }, - ) - .flattenCurrencies() - } - .shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1) + private val tokenListFlow = getTokenListUseCaseFlow() - private val availablePairsByNetworkFlow = MutableStateFlow>>(emptyMap()) + private val availablePairsByNetworkFlow = MutableStateFlow>(emptyMap()) init { + initializeSearchBardCallbacks() + + subscribeOnUpdateState() + subscribeOnAvailablePairsUpdates() + } + + private fun getTokenListUseCaseFlow(): SharedFlow> { + return getTokenListUseCase.launch(userWalletId = params.userWalletId) + .distinctUntilChanged() + .map { maybeTokenList -> + maybeTokenList.getOrElse( + ifLoading = { it ?: TokenList.Empty }, + ifError = { TokenList.Empty }, + ) + .flattenCurrencies() + } + .shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1) + } + + private fun initializeSearchBardCallbacks() { tokenListUMController.update( transformer = UpdateSearchBarCallbacksTransformer( onQueryChange = ::onSearchQueryChange, onActiveChange = ::onSearchBarActiveChange, ), ) - - subscribeOnUpdateState() - subscribeOnAvailablePairsUpdates() } private fun subscribeOnUpdateState() { @@ -81,69 +98,143 @@ internal class AvailableSwapPairsModel @Inject constructor( flow3 = params.selectedStatus, flow4 = searchManager.query, flow5 = availablePairsByNetworkFlow - .map { it[params.selectedStatus.value?.toLeastTokenInfo()].orEmpty() } + .map { it[params.selectedStatus.value?.toLeastTokenInfo()] } .distinctUntilChanged(), - ) { currencies, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairs -> - if (availablePairs.isEmpty()) { - SetLoadingTokenItemsTransformer(currencies) - } else { - val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding - - val filterByQueryTokenList = currencies - .filter { it.currency != selectedStatus?.currency } - .filterByQuery(query = query) - - if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) { - SetNothingToFoundStateTransformer( - isBalanceHidden = isBalanceHidden, - emptySearchMessageReference = resourceReference( - id = R.string.action_buttons_swap_empty_search_message, - ), + ) { currencies, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairsState -> + availablePairsState?.fold( + ifLoading = { SetLoadingTokenItemsTransformer(currencies) }, + ifContent = { pairs -> + handleContentState( + appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding, + currencies = currencies, + selectedStatus = selectedStatus, + query = query, + availablePairs = pairs, ) - } else { - UpdateTokenItemsTransformer( - appCurrency = appCurrency, - onItemClick = params.onTokenClick, - statuses = filterByQueryTokenList.filterByAvailability(availablePairs = availablePairs), - isBalanceHidden = isBalanceHidden, - unavailableTokensHeaderReference = resourceReference( - id = R.string.tokens_list_unavailable_to_swap_header, - wrappedList(selectedStatus?.currency?.name?.capitalize() ?: ""), - ), + }, + ifError = { + handleErrorState( + cause = it, + networkInfo = params.selectedStatus.value?.toLeastTokenInfo(), + currencies = currencies, ) - } - } + }, + ) + ?: SetLoadingTokenItemsTransformer(currencies) } .onEach(tokenListUMController::update) .flowOn(dispatchers.main) .launchIn(modelScope) } + private fun handleContentState( + appCurrencyAndBalanceHiding: Pair, + currencies: List, + selectedStatus: CryptoCurrencyStatus?, + query: String, + availablePairs: List, + ): TokenListUMTransformer { + val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding + + if (availablePairs.isEmpty()) { + return SetNoAvailablePairsTransformer( + appCurrency = appCurrency, + unavailableStatuses = currencies, + isBalanceHidden = isBalanceHidden, + unavailableTokensHeaderReference = resourceReference( + id = R.string.tokens_list_unavailable_to_swap_header, + wrappedList(selectedStatus?.currency?.name?.capitalize() ?: ""), + ), + ) + } + + val filterByQueryTokenList = currencies + .filter { it.currency != selectedStatus?.currency } + .filterByQuery(query = query) + + return if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) { + SetNothingToFoundStateTransformer( + isBalanceHidden = isBalanceHidden, + emptySearchMessageReference = resourceReference( + id = R.string.action_buttons_swap_empty_search_message, + ), + ) + } else { + UpdateTokenItemsTransformer( + appCurrency = appCurrency, + onItemClick = params.onTokenClick, + statuses = filterByQueryTokenList.filterByAvailability(availablePairs = availablePairs), + isBalanceHidden = isBalanceHidden, + unavailableTokensHeaderReference = resourceReference( + id = R.string.tokens_list_unavailable_to_swap_header, + wrappedList(selectedStatus?.currency?.name?.capitalize() ?: ""), + ), + ) + } + } + + private fun handleErrorState( + cause: Throwable, + networkInfo: LeastTokenInfo?, + currencies: List, + ): SetErrorWarningTransformer { + return SetErrorWarningTransformer( + cause = cause, + onRefresh = { + modelScope.launch { + if (networkInfo != null) { + updateAvailablePairs(networkInfo, currencies) + } + } + }, + ) + } + private fun subscribeOnAvailablePairsUpdates() { modelScope.launch { params.selectedStatus .filterNotNull() .collectLatest { selectedStatus -> - val initialCurrency = selectedStatus.toLeastTokenInfo() - val tokenList = tokenListFlow.firstOrNull() ?: return@collectLatest + val networkInfo = selectedStatus.toLeastTokenInfo() - val availablePairs = availablePairsByNetworkFlow.value[initialCurrency] - if (!availablePairs.isNullOrEmpty()) return@collectLatest + val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() ?: false + if (isAlreadyLoaded) return@collectLatest - val pairs = getAvailablePairsUseCase( - initialCurrency = initialCurrency, - currencies = tokenList.map(CryptoCurrencyStatus::currency), - ) + val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest - availablePairsByNetworkFlow.update { - it.toMutableMap().apply { - put(initialCurrency, pairs) - } - } + updateAvailablePairs(networkInfo = networkInfo, statuses = statuses) } } } + private suspend fun updateAvailablePairs(networkInfo: LeastTokenInfo, statuses: List) { + runCatching { + availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = lceLoading()) + + getAvailablePairsUseCase( + initialCurrency = networkInfo, + currencies = statuses.map(CryptoCurrencyStatus::currency), + ) + } + .onSuccess { pairs -> + availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = pairs.lceContent()) + } + .onFailure { cause -> + availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = cause.lceError()) + } + } + + private fun MutableStateFlow>.update( + networkInfo: LeastTokenInfo, + state: AvailablePairsState, + ) { + update { + it.toMutableMap().apply { + put(networkInfo, state) + } + } + } + private fun getAppCurrencyAndBalanceHidingFlow(): Flow> { return combine( flow = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(), @@ -186,7 +277,8 @@ internal class AvailableSwapPairsModel @Inject constructor( isAvailable && status.value !is CryptoCurrencyStatus.MissedDerivation && - status.value !is CryptoCurrencyStatus.Unreachable + status.value !is CryptoCurrencyStatus.Unreachable && + !status.currency.isCustom } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt index d941f4f61d..615823f4e4 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt @@ -1,5 +1,6 @@ package com.tangem.features.onramp.tokenlist.entity +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import kotlinx.collections.immutable.ImmutableList @@ -19,4 +20,5 @@ internal data class TokenListUM( val availableItems: ImmutableList, val unavailableItems: ImmutableList, val isBalanceHidden: Boolean, + val warning: NotificationUM? = null, ) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt index 66f2119526..2913ce4223 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateTokenItemsTransformer.kt @@ -1,22 +1,18 @@ package com.tangem.features.onramp.tokenlist.entity.transformer import com.tangem.common.ui.tokens.TokenItemStateConverter -import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedCryptoAmount -import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount -import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer -import kotlinx.collections.immutable.toImmutableList +import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory +import com.tangem.features.onramp.tokenlist.entity.utils.addHeader -@Suppress("LongParameterList") internal class UpdateTokenItemsTransformer( private val appCurrency: AppCurrency, private val onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit, @@ -27,36 +23,22 @@ internal class UpdateTokenItemsTransformer( override fun transform(prevState: TokenListUM): TokenListUM { val availableItems = convertStatuses( - converter = createAvailableTokenItemStateConverter(), + converter = OnrampTokenItemStateConverterFactory.createAvailableItemConverter(appCurrency, onItemClick), statuses = statuses[true].orEmpty(), ) val unavailableItems = convertStatuses( - converter = createUnavailableTokenItemStateConverter(), + converter = OnrampTokenItemStateConverterFactory.createUnavailableItemConverter(appCurrency), statuses = statuses[false].orEmpty(), ) return prevState.copy( - availableItems = buildList { - if (availableItems.isNotEmpty()) { - createGroupTitle( - textReference = resourceReference(id = R.string.exchange_tokens_available_tokens_header), - ) - .let(::add) - } - - addAll(availableItems) - } - .toImmutableList(), - unavailableItems = buildList { - if (unavailableItems.isNotEmpty()) { - createGroupTitle(textReference = unavailableTokensHeaderReference).let(::add) - } - - addAll(unavailableItems) - } - .toImmutableList(), + availableItems = availableItems.addHeader( + textReference = resourceReference(id = R.string.exchange_tokens_available_tokens_header), + ), + unavailableItems = unavailableItems.addHeader(textReference = unavailableTokensHeaderReference), isBalanceHidden = isBalanceHidden, + warning = null, ) } @@ -67,88 +49,4 @@ internal class UpdateTokenItemsTransformer( return converter.convertList(statuses) .map(TokensListItemUM::Token) } - - private fun createAvailableTokenItemStateConverter(): TokenItemStateConverter { - return TokenItemStateConverter( - appCurrency = appCurrency, - subtitleStateProvider = { createSubtitleState(status = it, isAvailable = true) }, - subtitle2StateProvider = ::createSubtitle2State, - fiatAmountStateProvider = { createFiatAmountStateProvider(status = it, isAvailable = true) }, - onItemClick = onItemClick, - ) - } - - private fun createUnavailableTokenItemStateConverter(): TokenItemStateConverter { - return TokenItemStateConverter( - appCurrency = appCurrency, - iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) }, - titleStateProvider = { - TokenItemState.TitleState.Content( - text = stringReference(value = it.currency.name), - isAvailable = false, - ) - }, - subtitleStateProvider = { createSubtitleState(status = it, isAvailable = false) }, - subtitle2StateProvider = ::createSubtitle2State, - fiatAmountStateProvider = { createFiatAmountStateProvider(status = it, isAvailable = false) }, - ) - } - - private fun createSubtitleState(status: CryptoCurrencyStatus, isAvailable: Boolean): TokenItemState.SubtitleState { - return when (status.value) { - CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading - else -> { - TokenItemState.SubtitleState.TextContent( - value = stringReference(value = status.currency.symbol), - isAvailable = isAvailable, - ) - } - } - } - - private fun createSubtitle2State(status: CryptoCurrencyStatus): TokenItemState.Subtitle2State? { - return when (status.value) { - is CryptoCurrencyStatus.Loaded, - is CryptoCurrencyStatus.Custom, - is CryptoCurrencyStatus.NoQuote, - is CryptoCurrencyStatus.NoAccount, - -> { - TokenItemState.Subtitle2State.TextContent( - text = status.getFormattedCryptoAmount(includeStaking = false), - ) - } - is CryptoCurrencyStatus.Loading, - is CryptoCurrencyStatus.MissedDerivation, - is CryptoCurrencyStatus.Unreachable, - is CryptoCurrencyStatus.NoAmount, - -> null - } - } - - private fun createFiatAmountStateProvider( - status: CryptoCurrencyStatus, - isAvailable: Boolean, - ): TokenItemState.FiatAmountState? { - return when (status.value) { - is CryptoCurrencyStatus.Loaded, - is CryptoCurrencyStatus.Custom, - is CryptoCurrencyStatus.NoQuote, - is CryptoCurrencyStatus.NoAccount, - -> { - TokenItemState.FiatAmountState.TextContent( - text = status.getFormattedFiatAmount(appCurrency = appCurrency, includeStaking = false), - isAvailable = isAvailable, - ) - } - is CryptoCurrencyStatus.Unreachable, - is CryptoCurrencyStatus.NoAmount, - is CryptoCurrencyStatus.MissedDerivation, - is CryptoCurrencyStatus.Loading, - -> null - } - } - - private fun createGroupTitle(textReference: TextReference): TokensListItemUM.GroupTitle { - return TokensListItemUM.GroupTitle(id = textReference.hashCode(), text = textReference) - } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt new file mode 100644 index 0000000000..e48ddc2674 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt @@ -0,0 +1,104 @@ +package com.tangem.features.onramp.tokenlist.entity.utils + +import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedCryptoAmount +import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormattedFiatAmount +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus + +/** +[REDACTED_AUTHOR] + */ +internal object OnrampTokenItemStateConverterFactory { + + fun createAvailableItemConverter( + appCurrency: AppCurrency, + onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit, + ): TokenItemStateConverter { + return TokenItemStateConverter( + appCurrency = appCurrency, + subtitleStateProvider = { createSubtitleState(status = it, isAvailable = true) }, + subtitle2StateProvider = ::createSubtitle2State, + fiatAmountStateProvider = { + createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true) + }, + onItemClick = onItemClick, + ) + } + + fun createUnavailableItemConverter(appCurrency: AppCurrency): TokenItemStateConverter { + return TokenItemStateConverter( + appCurrency = appCurrency, + iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) }, + titleStateProvider = { + TokenItemState.TitleState.Content( + text = stringReference(value = it.currency.name), + isAvailable = false, + ) + }, + subtitleStateProvider = { createSubtitleState(status = it, isAvailable = false) }, + subtitle2StateProvider = ::createSubtitle2State, + fiatAmountStateProvider = { + createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false) + }, + ) + } + + private fun createSubtitleState(status: CryptoCurrencyStatus, isAvailable: Boolean): TokenItemState.SubtitleState { + return when (status.value) { + CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading + else -> { + TokenItemState.SubtitleState.TextContent( + value = stringReference(value = status.currency.symbol), + isAvailable = isAvailable, + ) + } + } + } + + private fun createSubtitle2State(status: CryptoCurrencyStatus): TokenItemState.Subtitle2State? { + return when (status.value) { + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> { + TokenItemState.Subtitle2State.TextContent( + text = status.getFormattedCryptoAmount(includeStaking = false), + ) + } + is CryptoCurrencyStatus.Loading, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + -> null + } + } + + private fun createFiatAmountStateProvider( + status: CryptoCurrencyStatus, + appCurrency: AppCurrency, + isAvailable: Boolean, + ): TokenItemState.FiatAmountState? { + return when (status.value) { + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> { + TokenItemState.FiatAmountState.TextContent( + text = status.getFormattedFiatAmount(appCurrency = appCurrency, includeStaking = false), + isAvailable = isAvailable, + ) + } + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Loading, + -> null + } + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenListUMExt.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenListUMExt.kt new file mode 100644 index 0000000000..27c3500039 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenListUMExt.kt @@ -0,0 +1,23 @@ +package com.tangem.features.onramp.tokenlist.entity.utils + +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +fun List.addHeader(textReference: TextReference): ImmutableList { + val items = this@addHeader + + return buildList { + if (items.isNotEmpty()) { + createGroupTitle(textReference = textReference).let(::add) + } + + addAll(items) + } + .toImmutableList() +} + +private fun createGroupTitle(textReference: TextReference): TokensListItemUM.GroupTitle { + return TokensListItemUM.GroupTitle(id = textReference.hashCode(), text = textReference) +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index e39e10eff6..069b27e874 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -14,9 +14,11 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.SpacerH12 import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.tokenlist.TokenListItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration @@ -37,15 +39,29 @@ import kotlinx.collections.immutable.ImmutableList @Composable internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) { Column(modifier) { - SearchBar(searchBarUM = state.searchBarUM) + if (state.warning == null) { + SearchBar(searchBarUM = state.searchBarUM) + } else { + when (state.warning) { + is NotificationUM.Warning.OnrampErrorNotification -> { + Notification(config = state.warning.config, containerColor = TangemTheme.colors.background.primary) + } + is NotificationUM.Warning.SwapNoAvailablePair -> { + Notification(config = state.warning.config, containerColor = TangemTheme.colors.button.disabled) + } + else -> Unit + } + } - SpacerH12() + if (state.availableItems.isNotEmpty()) { + SpacerH12() + ItemsBlock(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) + } - ItemsBlock(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) - - SpacerH12() - - ItemsBlock(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) + if (state.unavailableItems.isNotEmpty()) { + SpacerH12() + ItemsBlock(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) + } } } diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt index 66be1782f1..b1245ee718 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/AgreementBottomSheetContent.kt @@ -20,6 +20,7 @@ import com.google.accompanist.web.rememberWebViewState import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.webview.applySafeSettings import com.tangem.feature.referral.presentation.R /** @@ -50,12 +51,7 @@ private fun AgreementHtmlView(url: String) { modifier = Modifier.background(TangemTheme.colors.background.primary), captureBackPresses = false, onCreated = { - if (!isInPreviewMode) { - it.settings.apply { - javaScriptEnabled = false - allowFileAccess = false - } - } + if (!isInPreviewMode) it.applySafeSettings() }, ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt index bf4385fd78..709bd1f9bc 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/FeeStateFactory.kt @@ -166,6 +166,31 @@ internal class FeeStateFactory( ) } + fun tryAutoFixCustomFeeValue(): SendUiState { + val state = currentStateProvider() + val isEditState = stateRouterProvider().isEditState + val feeState = state.getFeeState(isEditState) ?: return state + val feeSelectorState = feeState.feeSelectorState as? FeeSelectorState.Content ?: return state + return when (feeSelectorState.selectedFee) { + FeeType.Slow, + FeeType.Market, + FeeType.Fast, + -> state + FeeType.Custom -> { + val updatedFeeSelectorState = customFeeFieldConverter.tryAutoFixValue(feeSelectorState) + + val fee = feeConverter.convert(updatedFeeSelectorState) + return state.copyWrapped( + isEditState = isEditState, + feeState = feeState.copy( + feeSelectorState = updatedFeeSelectorState, + fee = fee, + ), + ) + } + } + } + private fun isPrimaryButtonEnabled( feeState: SendStates.FeeState, notifications: ImmutableList, diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt index 16a8666b36..23571ac345 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/SendFeeCustomFieldConverter.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.state.fee import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.features.send.impl.presentation.state.StateRouter @@ -78,4 +79,19 @@ internal class SendFeeCustomFieldConverter( else -> feeSelectorState.customValues }, ) + + fun tryAutoFixValue(feeSelectorState: FeeSelectorState.Content) = feeSelectorState.copy( + customValues = when (feeSelectorState.fees) { + is TransactionFee.Choosable -> feeSelectorState.fees.minimum + is TransactionFee.Single -> feeSelectorState.fees.normal + }.let { + when (it) { + is Fee.Kaspa -> kaspaCustomFeeConverter.tryAutoFixValue( + minimumFee = it, + customValues = feeSelectorState.customValues, + ) + else -> feeSelectorState.customValues + } + }, + ) } \ No newline at end of file diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/KaspaCustomFeeConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/KaspaCustomFeeConverter.kt index a85d1bb97e..769bd3ee75 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/KaspaCustomFeeConverter.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fee/custom/KaspaCustomFeeConverter.kt @@ -95,6 +95,40 @@ internal class KaspaCustomFeeConverter( }.toImmutableList() } + fun tryAutoFixValue( + minimumFee: Fee.Kaspa, + customValues: ImmutableList, + ): ImmutableList { + val mutableCustomValues = customValues.toMutableList() + val minimumFeeAmountValue = minimumFee.amount.value + + return mutableCustomValues.apply { + // check that there is reveal transaction info (= krc-20 token transfer) + // return without changes otherwise + if (minimumFee.revealTransactionFee != null && minimumFeeAmountValue != null) { + getOrNull(FEE_AMOUNT_INDEX)?.let { + val valueDecimal = it.value.parseToBigDecimal(it.decimals) + // krc-20 transaction will be failed if custom fee value is less than minimum, + // so we set value to minimum in this case + if (valueDecimal < minimumFee.amount.value) { + val fixedValue = minimumFeeAmountValue.parseBigDecimal(it.decimals) + set( + FEE_AMOUNT_INDEX, + it.copy( + value = fixedValue, + label = getFiatReference( + rate = feeCryptoCurrencyStatusProvider()?.value?.fiatRate, + value = valueDecimal, + appCurrency = appCurrencyProvider(), + ), + ), + ) + } + } + } + }.toImmutableList() + } + private companion object { private const val FEE_AMOUNT_INDEX = 0 } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt index 14b94f1d08..cdd155d645 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/viewmodel/SendViewModel.kt @@ -513,13 +513,12 @@ internal class SendViewModel @Inject constructor( when (currentState.type) { SendUiStateType.Fee, SendUiStateType.EditFee, - -> if (onFeeNext()) return + -> if (onFeeNextIntercept(isFromEdit)) return SendUiStateType.Amount, SendUiStateType.EditAmount, -> loadFee() else -> Unit } - stateRouter.onNextClick() } @@ -580,9 +579,23 @@ internal class SendViewModel @Inject constructor( override fun onTokenDetailsClick(currency: CryptoCurrency) = innerRouter.openTokenDetails(userWalletId, currency) - private fun onFeeNext(): Boolean { + private fun onFeeNextIntercept(isFromEdit: Boolean): Boolean { val feeState = uiState.value.getFeeState(stateRouter.isEditState) val feeSelectorState = feeState?.feeSelectorState as? FeeSelectorState.Content ?: return false + + if (isFromEdit) { + // in some cases, if it's possible to fix incorrect fee automatically, + // do it, update current state and go let continue the flow + val fixedState = feeStateFactory.tryAutoFixCustomFeeValue() + if (fixedState.editFeeState != feeState) { + uiState.value = fixedState + val currentState = stateRouter.currentState.value + uiState.value = stateFactory.syncEditStates(isFromEdit = isFromEdit) + sendScreenAnalyticSender.send(currentState.type, uiState.value) + return false + } + } + if (checkIfFeeTooLow(feeSelectorState)) { uiState.value = eventStateFactory.getFeeTooLowAlert( onConsume = { uiState.value = eventStateFactory.onConsumeEventState() }, diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 15e9420092..58596e7a24 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -78,6 +78,7 @@ dependencies { implementation(projects.domain.balanceHiding) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.transaction) + implementation(projects.domain.transaction.models) implementation(projects.domain.staking) implementation(projects.domain.markets.models) implementation(projects.domain.onramp) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index f41620317c..480de2e7f2 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.* import androidx.paging.cachedIn import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType +import com.tangem.common.core.TangemSdkError import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.routing.bundle.unbundle @@ -51,6 +52,7 @@ import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent.Compan import com.tangem.domain.tokens.model.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.transaction.error.AssociateAssetError import com.tangem.domain.transaction.error.IncompleteTransactionError +import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.AssociateAssetUseCase import com.tangem.domain.transaction.usecase.DismissIncompleteTransactionUseCase import com.tangem.domain.transaction.usecase.RetryIncompleteTransactionUseCase @@ -240,6 +242,7 @@ internal class TokenDetailsViewModel @Inject constructor( currentCryptoCurrencyStatus?.let { cryptoCurrencyStatus = it updateButtons(it) + updateWarnings(it) } } } @@ -873,14 +876,26 @@ internal class TokenDetailsViewModel @Inject constructor( currency = cryptoCurrency, ).fold( ifLeft = { e -> - when (e) { - is IncompleteTransactionError.DataError -> { - internalUiState.value = stateFactory.getStateWithErrorDialog( - stringReference(e.message.orEmpty()), - ) - Timber.e(e.message) + val message = when (e) { + is IncompleteTransactionError.DataError -> e.message.orEmpty() + is IncompleteTransactionError.SendError -> { + when (val error = e.error) { + is SendTransactionError.UserCancelledError, + is SendTransactionError.CreateAccountUnderfunded, + is SendTransactionError.TangemSdkError, + is SendTransactionError.DemoCardError, + -> null + is SendTransactionError.DataError -> error.message + is SendTransactionError.BlockchainSdkError -> error.message + is SendTransactionError.NetworkError -> error.message + is SendTransactionError.UnknownError -> error.ex?.localizedMessage + } } } + message?.let { + internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(it)) + Timber.e(it) + } }, ifRight = { internalUiState.value = stateFactory.getStateWithRemovedKaspaIncompleteTransactionNotification() @@ -902,14 +917,10 @@ internal class TokenDetailsViewModel @Inject constructor( currency = cryptoCurrency, ).fold( ifLeft = { e -> - when (e) { - is IncompleteTransactionError.DataError -> { - internalUiState.value = stateFactory.getStateWithErrorDialog( - stringReference(e.message.orEmpty()), - ) - Timber.e(e.message) - } - } + internalUiState.value = stateFactory.getStateWithErrorDialog( + stringReference(e.message.orEmpty()), + ) + Timber.e(e.message) }, ifRight = { internalUiState.value = stateFactory.getStateWithRemovedKaspaIncompleteTransactionNotification() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionsSubscriber.kt deleted file mode 100644 index 266bb7b67c..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionsSubscriber.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.exchange.RampStateManager -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.UpdateMultiWalletActionsTransformer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.onEach - -/** - * Multi-currency wallet subscriber for actions state updating - * - * @property userWallet user wallet - * @property rampStateManager ramp state manager - * @property stateController state controller - * -[REDACTED_AUTHOR] - */ -internal class MultiWalletActionsSubscriber( - private val userWallet: UserWallet, - private val rampStateManager: RampStateManager, - private val stateController: WalletStateController, -) : WalletSubscriber() { - - override fun create(coroutineScope: CoroutineScope): Flow<*> { - return combine( - flow = rampStateManager.getBuyInitializationStatus(), - flow2 = rampStateManager.getSellInitializationStatus(), - flow3 = rampStateManager.getSwapInitializationStatus(userWalletId = userWallet.walletId), - transform = ::RampStatuses, - ) - .onEach { statuses -> - stateController.update( - UpdateMultiWalletActionsTransformer( - userWalletId = userWallet.walletId, - buyStatus = statuses.buy, - sellStatus = statuses.sell, - swapStatus = statuses.swap, - ), - ) - } - } - - private data class RampStatuses( - val buy: Lce, - val sell: Lce, - val swap: Lce, - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt index f40ea9b059..786be57b6c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletClickIntents.kt @@ -18,6 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetRefreshStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer +import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.coroutines.CoroutineScope @@ -46,6 +47,7 @@ internal class WalletClickIntents @Inject constructor( private val neverToShowWalletsScrollPreview: NeverToShowWalletsScrollPreview, private val rampStateManager: RampStateManager, private val dispatchers: CoroutineDispatcherProvider, + private val onrampFeatureToggles: OnrampFeatureToggles, ) : BaseWalletClickIntents(), WalletCardClickIntents by walletCardClickIntentsImplementor, WalletWarningsClickIntents by warningsClickIntentsImplementer, @@ -120,10 +122,13 @@ internal class WalletClickIntents @Inject constructor( fetchTokenListUseCase(userWalletId = userWallet.walletId, mode = RefreshMode.FULL) } - listOf( - async { rampStateManager.fetchBuyServiceData() }, - async { rampStateManager.fetchSellServiceData() }, - ) + buildList { + if (!onrampFeatureToggles.isFeatureEnabled) { + async { rampStateManager.fetchBuyServiceData() }.let(::add) + } + + async { rampStateManager.fetchSellServiceData() }.let(::add) + } .awaitAll() maybeFetchResult.onLeft { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt index 85cd2a7a03..3b15d7ee6c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletCurrencyActionsClickIntents.kt @@ -48,6 +48,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender +import com.tangem.features.onramp.OnrampFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.scopes.ViewModelScoped import kotlinx.collections.immutable.toImmutableList @@ -119,6 +120,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val appRouter: AppRouter, private val rampStateManager: RampStateManager, private val getUserCountryUseCase: GetUserCountryUseCase, + private val onrampFeatureToggles: OnrampFeatureToggles, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( @@ -493,7 +495,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } onMultiWalletActionClick( - statusFlow = rampStateManager.getSwapInitializationStatus(userWalletId), + statusFlow = rampStateManager.getExpressInitializationStatus(userWalletId), route = AppRoute.SwapCrypto(userWalletId = userWalletId), eventCreator = MainScreenAnalyticsEvent::ButtonSwap, ) @@ -501,7 +503,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onMultiWalletBuyClick(userWalletId: UserWalletId) { onMultiWalletActionClick( - statusFlow = rampStateManager.getBuyInitializationStatus(), + statusFlow = if (onrampFeatureToggles.isFeatureEnabled) { + rampStateManager.getExpressInitializationStatus(userWalletId) + } else { + rampStateManager.getBuyInitializationStatus() + }, route = AppRoute.BuyCrypto(userWalletId = userWalletId), eventCreator = MainScreenAnalyticsEvent::ButtonBuy, )