Updated on 2026-08-14

This commit is contained in:
Tangem 2024-12-13 12:57:35 +03:00
commit 0e11bf0ffb
77 changed files with 1099 additions and 629 deletions

View file

@ -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)

View file

@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.HIDE_OVERLAY_WINDOWS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.INTERNET" />

View file

@ -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

View file

@ -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,

View file

@ -82,15 +82,6 @@ internal object OnrampDomainModule {
)
}
@Provides
@Singleton
fun provideGetOnrampCurrencyUseCase(
onrampRepository: OnrampRepository,
onrampErrorResolver: OnrampErrorResolver,
): GetOnrampCurrencyUseCase {
return GetOnrampCurrencyUseCase(onrampRepository, onrampErrorResolver)
}
@Provides
@Singleton
fun provideGetOnrampTransactionsUseCase(

View file

@ -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)))
}

View file

@ -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<ExchangeService>,
private val sellService: Provider<ExchangeService>,
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<ExchangeServiceInitializationStatus> {
@ -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<ExchangeServiceInitializationStatus> {
return swapServiceLoader.getInitializationStatus(userWalletId)
override fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow<ExchangeServiceInitializationStatus> {
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) }

View file

@ -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">
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -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(

View file

@ -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
}

View file

@ -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<Lce<Throwable, List<Asset>>>
/**
* 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<Map<UserWalletId, InitializationStatusFlow>>(value = emptyMap())
override suspend fun update(userWalletId: UserWalletId, userTokens: UserTokensResponse) {
override suspend fun update(userWalletId: UserWalletId, userTokens: List<LeastTokenInfo>) {
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)

View file

@ -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<LeastTokenInfo>)
/** Get initialization status by [userWalletId] */
fun getInitializationStatus(userWalletId: UserWalletId): StateFlow<Lce<Throwable, List<Asset>>>

View file

@ -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") }

View file

@ -3,10 +3,18 @@
<string name="action_buttons_buy_empty_search_message">Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum kaufen im Portfolio hinzu.</string>
<string name="action_buttons_sell_empty_search_message">Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum verkaufen im Portfolio hinzu.</string>
<string name="action_buttons_sell_navigation_bar_title">Verkaufen</string>
<string name="action_buttons_service_loading_alert_message">Einige Netzwerke sind derzeit nicht erreichbar. Bitte versuche es später erneut.</string>
<string name="action_buttons_service_loading_alert_title">Die Daten wurden noch nicht geladen.</string>
<string name="action_buttons_something_wrong_alert_message">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.</string>
<string name="action_buttons_something_wrong_alert_title">Schaltfläche ist nicht verfügbar</string>
<string name="action_buttons_swap_choose_token">Wähle den Token</string>
<string name="action_buttons_swap_empty_search_message">Du kannst Deinen Token nicht finden? Gehe zum Bereich „Markt“ auf der Hauptseite und fügen diesen zum tauschen im Portfolio hinzu.</string>
<string name="action_buttons_swap_no_available_pair_notification_message">Es gibt keine verfügbaren Token, die mit dem ausgewählten Token getauscht werden können. Bitte wähle einen anderen.</string>
<string name="action_buttons_swap_no_available_pair_notification_title">Kein verfügbares Paar</string>
<string name="action_buttons_swap_no_tokens_added_alert_message">Um die Tauschfunktion zu nutzen, muss Dein Portfolio mindestens 2 Token enthalten.</string>
<string name="action_buttons_swap_no_tokens_added_alert_title">Token hinzufügen</string>
<string name="action_buttons_swap_not_enough_tokens_alert_message">Du hast Deinem Portfolio nur 1 Token hinzugefügt. Um die Tauschfunktion zu verwenden, musst Du mindestens 2 Token hinzufügen.</string>
<string name="action_buttons_swap_not_enough_tokens_alert_title">Token hinzufügen</string>
<string name="action_buttons_you_want_to_receive">Wähle den Token aus, den Du erhalten möchtest</string>
<string name="action_buttons_you_want_to_swap">Wähle den Token, den Du tauschen möchtest</string>
<string name="add_custom_token_choose_network">Netzwerk wählen</string>
@ -263,6 +271,9 @@
<string name="express_exchange_notification_refund_title">Der Betrag wurde in %1$s (%2$s Netzwerk) zurückerstattet.</string>
<string name="express_exchange_notification_verification_text">Besuche die Website des Anbieters zur Überprüfung</string>
<string name="express_exchange_notification_verification_title">KYC-Überprüfung durch den Anbieter erforderlich</string>
<string name="express_exchange_status_bought">Gekauft</string>
<string name="express_exchange_status_buying">Einkaufen</string>
<string name="express_exchange_status_buying_active">Einkaufen...</string>
<string name="express_exchange_status_canceled">Abgebrochen</string>
<string name="express_exchange_status_confirmed">Bestätigt</string>
<string name="express_exchange_status_confirming">Bestätigen</string>
@ -308,6 +319,7 @@
<string name="feedback_preface_scan_failed">Bitte sag uns, welche Karte oder Ring du hast</string>
<string name="feedback_preface_support">Hallo Support-Team,</string>
<string name="feedback_preface_tx_failed">Bitte erzähle uns mehr über dein Problem. Jedes kleine Detail kann helfen.</string>
<string name="feedback_subject_pre_activated_wallet">Zuvor aktivierte Wallet</string>
<string name="feedback_subject_rate_negative">Meine Vorschläge</string>
<string name="feedback_subject_scan_failed">Kann eine Karte oder Ring nicht scannen</string>
<string name="feedback_subject_support">Rückmeldung</string>
@ -585,7 +597,7 @@
<string name="onramp_settings_residence">Residenz</string>
<string name="onramp_settings_residence_description">Bitte wähle das richtige Land aus, um korrekte Zahlungsoptionen und Dienstleistungen zu gewährleisten.</string>
<string name="onramp_settings_title">Einstellungen</string>
<string name="onramp_statuses_view_footer">Du kannst den Transaktionsstatus auf der Token-Seite überprüfen.</string>
<string name="onramp_statuses_view_footer">Du kannst diesen Bildschirm schließen und den Transaktionsstatus auf dem Bildschirm mit den Token-Details überprüfen.</string>
<string name="onramp_transaction_status_footer_text">Du kannst diesen Bildschirm schließen und den Transaktionsstatus auf dem Bildschirm mit den Token-Details überprüfen.</string>
<string name="onramp_via">Über</string>
<string name="organize_tokens_group">Gruppe erstellen</string>
@ -600,6 +612,7 @@
<string name="receive_bottom_sheet_warning_message">%1$s ( %2$s ) im %3$s Netzwerk</string>
<string name="receive_bottom_sheet_warning_message_description">Das Senden einer anderen Währung führt zu deren irreversiblem Verlust.</string>
<string name="receive_bottom_sheet_warning_message_full">Sende nur %s an diese Adresse. Der Versand einer anderen Währung führt zu ihrem unwiderruflichen Verlust.</string>
<string name="receive_bottom_sheet_warning_title">Senden nur %1$s im Netzwerk %2$s</string>
<string name="receive_token_description">Überweise Geld von einem anderen Wallet oder einer anderen Börse</string>
<string name="referral_button_participate">Teilnehmen</string>
<string name="referral_error_failed_to_load_info">Die Informationen zum Empfehlungsprogramm konnten nicht geladen werden. Bitte versuche es später noch einmal.</string>
@ -651,6 +664,7 @@
<string name="scan_card_settings_button">Karte oder Ring scannen</string>
<string name="scan_card_settings_message">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.</string>
<string name="scan_card_settings_title">Halte deine Karte oder Ring bereit!</string>
<string name="security_alert_title">Sicherheitswarnung</string>
<string name="selling_insufficient_balance_alert_message">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.</string>
<string name="selling_insufficient_balance_alert_title">Unzureichendes Guthaben</string>
<string name="selling_regional_restriction_alert_message">Der Verkauf von Kryptowährung ist in Deiner Region derzeit nicht möglich. Wir arbeiten aktiv daran, Ihnen diese Option bald anzubieten bleib dran!</string>
@ -868,6 +882,7 @@
<string name="swapping_to_title">Du erhältst</string>
<string name="swapping_token_list_title">Token auswählen</string>
<string name="swapping_token_not_available">Nicht verfügbar</string>
<string name="this_is_my_wallet_title">Das ist meine Wallet</string>
<string name="toast_balances_hidden">Guthaben versteckt</string>
<string name="toast_balances_shown">Angezeigte Salden</string>
<string name="toast_undo">Rückgängig machen</string>
@ -937,6 +952,7 @@
<string name="user_wallet_list_unlock_all_with">Alle mit %s freischalten</string>
<string name="wallet_balance_blockchain_unreachable_try_later">Blockchain ist nicht erreichbar. Versuche es später nochmal</string>
<string name="wallet_balance_missing_derivation">Karte oder Ring scannen</string>
<string name="wallet_been_activated_message">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.</string>
<string name="wallet_connect_alert_sign_message">Aufforderung zum Signieren einer Nachricht. \n\n %s</string>
<string name="wallet_connect_bnb_sign_message">Dapp %1$s, mit der Bitte um\nBNB-Transaktion zu unterzeichnen.\n\n%2$s</string>
<string name="wallet_connect_bnb_trade_order_message">Handelsauftrag für %1$s\n Preis: %2$s\n Zu erhaltender Betrag: %3$s\n Zu zahlender Betrag: %4$s</string>
@ -984,7 +1000,7 @@
<string name="warning_approval_in_progress_title">Genehmigung läuft</string>
<string name="warning_backup_errors_message">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.</string>
<string name="warning_backup_errors_title">Aktivierungsfehler</string>
<string name="warning_beacon_chain_retirement_content">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.</string>
<string name="warning_beacon_chain_retirement_content">Am 3. Dezember 2024 wurde das BEP-2-Netzwerk auf Entscheidung der Netzwerkentwickler deaktiviert und wird nicht mehr unterstützt</string>
<string name="warning_beacon_chain_retirement_title">BNB Beacon Chain wird abgeschaltet</string>
<string name="warning_blocked_funds_for_fee_message">Bitte zahle ein paar %1$s ein, um die Netzwerkgebühr zu decken</string>
<string name="warning_blocked_funds_for_fee_title">Unzureichende Mittel zur Deckung der Netzgebühr</string>

View file

@ -176,7 +176,7 @@
<string name="common_submit">Enviar</string>
<string name="common_success">Con éxito</string>
<string name="common_support">Soporte</string>
<string name="common_swap">Intercambiar</string>
<string name="common_swap">Swap</string>
<string name="common_terms_and_conditions">términos y condiciones</string>
<string name="common_terms_of_use">Condiciones de uso</string>
<string name="common_today">Hoy</string>
@ -982,7 +982,7 @@
<string name="warning_approval_in_progress_title">Aprobación en curso</string>
<string name="warning_backup_errors_message">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.</string>
<string name="warning_backup_errors_title">Error de activación</string>
<string name="warning_beacon_chain_retirement_content">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.</string>
<string name="warning_beacon_chain_retirement_content">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</string>
<string name="warning_beacon_chain_retirement_title">BNB Beacon Chain se cerrará</string>
<string name="warning_blocked_funds_for_fee_message">Por favor deposite %1$s para cubrir la tarifa de red</string>
<string name="warning_blocked_funds_for_fee_title">Fondos insuficientes para cubrir la tarifa de red</string>

View file

@ -982,7 +982,7 @@
<string name="warning_approval_in_progress_title">Approbation en cours</string>
<string name="warning_backup_errors_message">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 laide.</string>
<string name="warning_backup_errors_title">Erreur d\'activation</string>
<string name="warning_beacon_chain_retirement_content">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.</string>
<string name="warning_beacon_chain_retirement_content">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</string>
<string name="warning_beacon_chain_retirement_title">BNB Beacon Chain va s\'arrêter de fonctionner</string>
<string name="warning_blocked_funds_for_fee_message">Veuillez déposer environ %1$s pour couvrir les frais de réseau</string>
<string name="warning_blocked_funds_for_fee_title">Fonds insuffisants pour couvrir les frais de réseau</string>

View file

@ -3,8 +3,8 @@
<string name="action_buttons_buy_empty_search_message">トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して買付できるようにします。</string>
<string name="action_buttons_sell_empty_search_message">トークンが見つかりませんか?メインページのマーケットセクションに移動し、ポートフォリオに追加して売却できるようにします。</string>
<string name="action_buttons_sell_navigation_bar_title">売却</string>
<string name="action_buttons_service_loading_alert_message">接続には数秒かかる場合があります。しばらくお待ちください。</string>
<string name="action_buttons_service_loading_alert_title">接続</string>
<string name="action_buttons_service_loading_alert_message">これには数秒かかる場合があります。しばらくしてからもう一度お試しください。</string>
<string name="action_buttons_service_loading_alert_title">データはまだ読み込まれていません。</string>
<string name="action_buttons_something_wrong_alert_message">この操作は現在利用できません。しばらくしてからもう一度お試しいただくか、画面を下にスワイプしてデータを更新してください。</string>
<string name="action_buttons_something_wrong_alert_title">ボタンは使用できません</string>
<string name="action_buttons_swap_choose_token">トークンを選択</string>
@ -316,6 +316,7 @@
<string name="feedback_preface_scan_failed">お持ちのカードまたはリングについて教えてください</string>
<string name="feedback_preface_support">サポートチームの皆さん、こんにちは。</string>
<string name="feedback_preface_tx_failed">問題について詳しく教えてください。どんな些細なことでも役に立ちます。</string>
<string name="feedback_subject_pre_activated_wallet">以前に有効化されたウォレット</string>
<string name="feedback_subject_rate_negative">私の提案</string>
<string name="feedback_subject_scan_failed">カード / リングをスキャンできません</string>
<string name="feedback_subject_support">フィードバック</string>
@ -588,7 +589,7 @@
<string name="onramp_settings_residence">住居</string>
<string name="onramp_settings_residence_description">正確なお支払い方法とサービスを確保するため、正しい国を選択してください。</string>
<string name="onramp_settings_title">設定</string>
<string name="onramp_statuses_view_footer">トークンページから取引状況を確認できます。</string>
<string name="onramp_statuses_view_footer">この画面を閉じて、トークンの詳細画面で取引状況を確認できます。</string>
<string name="onramp_transaction_status_footer_text">この画面を閉じて、トークンの詳細画面で取引状況を確認できます。</string>
<string name="onramp_via">経由</string>
<string name="organize_tokens_group">グループ</string>
@ -653,7 +654,8 @@
<string name="scan_card_settings_button">カードまたはリングをスキャン</string>
<string name="scan_card_settings_message">カードまたはリングをスキャンして設定を変更します。変更はスキャンしたカードまたはリングにのみ影響し、ウォレットに関連付けられている他のデバイスには影響しません。</string>
<string name="scan_card_settings_title">Tangemを準備してください</string>
<string name="selling_insufficient_balance_alert_message">この暗号資産を売却するのに十分な資金が残高にありません。続行するには、希望の資産を入金してください。</string>
<string name="security_alert_title">セキュリティ警告</string>
<string name="selling_insufficient_balance_alert_message">暗号通貨を売却するための十分な資金が残高にありません。続行するには希望の資産を入金してください。</string>
<string name="selling_insufficient_balance_alert_title">残高不足</string>
<string name="selling_regional_restriction_alert_message">現在、お住まいの地域では暗号資産の売却はご利用いただけません。このオプションをすぐにご利用いただけるよう積極的に取り組んでいますので、お楽しみに!</string>
<string name="selling_regional_restriction_alert_title">地域制限</string>
@ -870,6 +872,7 @@
<string name="swapping_to_title">受け取る</string>
<string name="swapping_token_list_title">トークンを選択</string>
<string name="swapping_token_not_available">利用不可</string>
<string name="this_is_my_wallet_title">これは私のウォレットです</string>
<string name="toast_balances_hidden">残高非表示</string>
<string name="toast_balances_shown">残高表示</string>
<string name="toast_undo">元に戻す</string>
@ -939,6 +942,7 @@
<string name="user_wallet_list_unlock_all_with">%sですべてをロック解除</string>
<string name="wallet_balance_blockchain_unreachable_try_later">ブロックチェーンにアクセスできません。後でもう一度お試しください。</string>
<string name="wallet_balance_missing_derivation">カードまたはリングをスキャン</string>
<string name="wallet_been_activated_message">このウォレットはすでに有効化されています。\nあなたが行ったのでない場合は、サポートに連絡してください。\nTangemは、事前に生成されたアクセスコードと一緒にウォレットを販売することはありません。</string>
<string name="wallet_connect_alert_sign_message">メッセージに署名することを要求しています。 \n\n %s</string>
<string name="wallet_connect_bnb_sign_message">Dapp%1$s 、BNB取引の署名を要求しています\n\n%2$s</string>
<string name="wallet_connect_bnb_trade_order_message">%1$sの取引注文\n価格: %2$s\n受取金額: %3$s\n支払金額: %4$s</string>
@ -986,8 +990,8 @@
<string name="warning_approval_in_progress_title">承認中</string>
<string name="warning_backup_errors_message">カードまたはリングのアクティベーションが正しく完了しませんでした。デバイスのNFCモジュールに問題があるか、カードまたはリングをデバイスに正しくタップしていないことが原因かもしれません。サポートチームにお問い合わせください。</string>
<string name="warning_backup_errors_title">アクティベーションに失敗しました</string>
<string name="warning_beacon_chain_retirement_content">BNBネットワーク開発者によると、BEP-2規格のサポートは2024年6月に終了します。この規格の資産を失わないために、BEP-20規格に変換してください。BNBスマートチェーンネットワークへ移行するには、Tangemのスワップサービスをご利用ください</string>
<string name="warning_beacon_chain_retirement_title">BNBビーコンチェーンは閉鎖されます。</string>
<string name="warning_beacon_chain_retirement_content">2024年12月3日、BEP-2ネットワークはネットワーク開発者の決定により使用不能となり、サポートは終了しました</string>
<string name="warning_beacon_chain_retirement_title">BNBビーコンチェーンは閉鎖されました</string>
<string name="warning_blocked_funds_for_fee_message">ネットワーク手数料をカバーするために%1$sを入金してください</string>
<string name="warning_blocked_funds_for_fee_title">ネットワーク手数料をカバーする資金が不足しています</string>
<string name="warning_button_could_be_better">もっと良くなるはず</string>

View file

@ -3,8 +3,8 @@
<string name="action_buttons_buy_empty_search_message">Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для покупки.</string>
<string name="action_buttons_sell_empty_search_message">Не нашли свой токен? Перейдите в раздел «Рынок» на главной странице и добавьте его в свой портфель для продажи.</string>
<string name="action_buttons_sell_navigation_bar_title">Продать</string>
<string name="action_buttons_service_loading_alert_message">Подключение может занять несколько секунд. Пожалуйста, подождите.</string>
<string name="action_buttons_service_loading_alert_title">Соединение</string>
<string name="action_buttons_service_loading_alert_message">Это может занять несколько секунд. Пожалуйста, попробуйте позже.</string>
<string name="action_buttons_service_loading_alert_title">Данные ещё не загрузились</string>
<string name="action_buttons_something_wrong_alert_message">Действие в данный момент недоступно, пожалуйста попробуйте позже или обновить данные, сделав свайп экрана вниз.</string>
<string name="action_buttons_something_wrong_alert_title">Кнопка недоступна</string>
<string name="action_buttons_swap_choose_token">Выберите токен</string>
@ -682,7 +682,7 @@
<string name="scan_card_settings_message">Отсканируйте карту или кольцо, чтобы изменить ее настройки. Изменения затронут только ту карту или кольцо, которые вы отсканировали, и не повлияют на другие устройства, привязанные к вашему кошельку.</string>
<string name="scan_card_settings_title">Приготовьте свой Tangem!</string>
<string name="security_alert_title">Уведомление безопасности</string>
<string name="selling_insufficient_balance_alert_message">На вашем балансе недостаточно средств для продажи этой криптовалюты. Пожалуйста, пополните нужный актив, чтобы продолжить.</string>
<string name="selling_insufficient_balance_alert_message">На вашем балансе недостаточно средств для продажи криптовалюты. Пожалуйста, пополните нужный актив, чтобы продолжить.</string>
<string name="selling_insufficient_balance_alert_title">Недостаточно средств</string>
<string name="selling_regional_restriction_alert_message">Продажа криптовалюты в вашем регионе временно недоступна. Мы активно работаем над тем, чтобы добавить эту возможность. Следите за нашими новостями!</string>
<string name="selling_regional_restriction_alert_title">Региональные ограничения</string>
@ -1016,7 +1016,7 @@
<string name="warning_approval_in_progress_title">Выдача разрешения</string>
<string name="warning_backup_errors_message">Похоже, что процесс активации карт или кольца не был завершен корректно. Это могло быть вызвано проблемой взаимодействия с модулем NFC либо некорректным прикладыванием карты или кольца к телефону. Пожалуйста, обратитесь в нашу службу поддержки для уточнения деталей.</string>
<string name="warning_backup_errors_title">Ошибка активации</string>
<string name="warning_beacon_chain_retirement_content">По решению разработчиков сети BNB стандарт BEP-2 перестанет поддерживаться в июне 2024 года. Чтобы не потерять активы, их необходимо преобразовать в стандарт BEP-20. Используйте функцию обмена в приложении или сторонние сервисы, чтобы перевести средства в cеть BNB Smart Chain.</string>
<string name="warning_beacon_chain_retirement_content">С 3 декабря 2024 года сеть BEP-2 была отключена по решению разработчиков сети и более не поддерживается</string>
<string name="warning_beacon_chain_retirement_title">Отключение сети BNB Beacon Chain</string>
<string name="warning_blocked_funds_for_fee_message">Внесите немного %1$s, чтобы покрыть комиссию сети</string>
<string name="warning_blocked_funds_for_fee_title">Недостаточно средств для оплаты комиссии сети</string>

View file

@ -988,7 +988,7 @@
<string name="warning_approval_in_progress_title">Підтвердження в процесі</string>
<string name="warning_backup_errors_message">Схоже, що активація карток або кільця була виконана неправильно. Це може бути пов\'язано з проблемою з модулем NFC вашого пристрою або неправильним прикладанням картки або кільця до телефону. Будь ласка, зверніться за допомогою до нашої служби підтримки для уточнення деталей.</string>
<string name="warning_backup_errors_title">Помилка активації</string>
<string name="warning_beacon_chain_retirement_content">За рішенням розробників мережі BNB стандарт BEP-2 перестане підтримуватись у червні 2024 року. Щоб не втратити свої активи, їх необхідно конвертувати у стандарт BEP-20. Використовуйте функцію обміну, щоб перевести їх у мережу BNB Smart Chain.</string>
<string name="warning_beacon_chain_retirement_content">3 грудня 2024 року мережу BEP-2 було відключено за рішенням розробників мережі, і вона більше не підтримується</string>
<string name="warning_beacon_chain_retirement_title">Відключення мережі BNB Beacon Chain</string>
<string name="warning_blocked_funds_for_fee_message">Будь ласка, поповніть рахунок на %1$s для покриття комісії мережі</string>
<string name="warning_blocked_funds_for_fee_title">Недостатньо коштів для покриття комісії мережі</string>

View file

@ -3,8 +3,8 @@
<string name="action_buttons_buy_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for purchase</string>
<string name="action_buttons_sell_empty_search_message">Cant find your token? Go to the Market section on the main page and add it to your portfolio for selling.</string>
<string name="action_buttons_sell_navigation_bar_title">Sell</string>
<string name="action_buttons_service_loading_alert_message">Connection may take a few seconds. Please wait.</string>
<string name="action_buttons_service_loading_alert_title">Connection</string>
<string name="action_buttons_service_loading_alert_message">This may take a few seconds. Please try again later.</string>
<string name="action_buttons_service_loading_alert_title">The data has not loaded yet.</string>
<string name="action_buttons_something_wrong_alert_message">The action is currently unavailable. Please try again later or refresh the data by swiping down on the screen.</string>
<string name="action_buttons_something_wrong_alert_title">Button is unavailable</string>
<string name="action_buttons_swap_choose_token">Choose the Token</string>
@ -584,6 +584,7 @@
<string name="onramp_country_search">Search by country</string>
<string name="onramp_country_unavailable">Unavailable</string>
<string name="onramp_currency_search">Search by currency</string>
<string name="onramp_legal">By using onramp functionality, you agree with providers %1$s and %2$s</string>
<string name="onramp_max_amount_restriction">The purchase amount should be no more than %s</string>
<string name="onramp_min_amount_restriction">The amount to buy must be at least %s</string>
<string name="onramp_no_available_providers">No available providers for this currency</string>
@ -665,7 +666,7 @@
<string name="scan_card_settings_message">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.</string>
<string name="scan_card_settings_title">Get your Tangem ready!</string>
<string name="security_alert_title">Security Alert</string>
<string name="selling_insufficient_balance_alert_message">You dont have enough funds in your balance to sell this cryptocurrency. Please deposit the desired asset to proceed.</string>
<string name="selling_insufficient_balance_alert_message">You dont have enough funds in your balance to sell cryptocurrency. Please deposit the desired asset to proceed.</string>
<string name="selling_insufficient_balance_alert_title">Insufficient Balance</string>
<string name="selling_regional_restriction_alert_message">Selling cryptocurrency is unavailable in your region at the moment. Were actively working to bring this option to you soon—stay tuned!</string>
<string name="selling_regional_restriction_alert_title">Regional Restriction</string>
@ -1000,8 +1001,8 @@
<string name="warning_approval_in_progress_title">Approval in Progress</string>
<string name="warning_backup_errors_message">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.</string>
<string name="warning_backup_errors_title">Activation error</string>
<string name="warning_beacon_chain_retirement_content">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.</string>
<string name="warning_beacon_chain_retirement_title">BNB Beacon Chain will shut down</string>
<string name="warning_beacon_chain_retirement_content">On December 3, 2024, the BEP-2 network was disabled by decision of the network developers and is no longer supported</string>
<string name="warning_beacon_chain_retirement_title">BNB Beacon Chain shut down</string>
<string name="warning_blocked_funds_for_fee_message">Please deposit some %1$s to cover the network fee</string>
<string name="warning_blocked_funds_for_fee_title">Insufficient funds to cover the network fee</string>
<string name="warning_button_could_be_better">Could be better</string>

View file

@ -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)
}

View file

@ -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<List<OnrampCurrency>> = withContext(dispatchers.io) {
currenciesStore.get(CURRENCIES_KEY)
}
override fun getCurrencies(): Flow<List<OnrampCurrency>> = 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<List<OnrampCountry>> = withContext(dispatchers.io) {
countriesStore.get(COUNTRIES_KEY)
}
override fun getCountries(): Flow<List<OnrampCountry>> = countriesStore.get(COUNTRIES_KEY)
override suspend fun getCountriesSync(): List<OnrampCountry>? {
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<OnrampCountry> = 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<OnrampCurrencyDTO>(
key = PreferencesKeys.ONRAMP_DEFAULT_CURRENCY,
value = currencyConverter.convertBack(currency),
val country = getDefaultCountrySync() ?: return@withContext
appPreferencesStore.storeObject<OnrampCountryDTO>(
key = PreferencesKeys.ONRAMP_DEFAULT_COUNTRY,
value = countryConverter.convertBack(country.copy(defaultCurrency = currency)),
)
}
override suspend fun getDefaultCurrencySync(): OnrampCurrency? = withContext(dispatchers.io) {
appPreferencesStore
.getObjectSyncOrNull<OnrampCurrencyDTO>(PreferencesKeys.ONRAMP_DEFAULT_CURRENCY)
?.let(currencyConverter::convert)
.getObjectSyncOrNull<OnrampCountryDTO>(PreferencesKeys.ONRAMP_DEFAULT_COUNTRY)
?.let(countryConverter::convert)?.defaultCurrency
}
override fun getDefaultCurrency(): Flow<OnrampCurrency?> {
return appPreferencesStore
.getObject<OnrampCurrencyDTO>(PreferencesKeys.ONRAMP_DEFAULT_CURRENCY)
.map { it?.let(currencyConverter::convert) }
.getObject<OnrampCountryDTO>(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 }
},

View file

@ -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,
)
}

View file

@ -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<CryptoCurrency> {
override suspend fun getSingleCurrencyWalletWithCardCurrencies(
userWalletId: UserWalletId,
refresh: Boolean,
): List<CryptoCurrency> {
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<CryptoCurrency>,
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(

View file

@ -39,5 +39,5 @@ interface RampStateManager {
fun getSellInitializationStatus(): Flow<Lce<Throwable, Any>>
fun getSwapInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, Any>>
fun getExpressInitializationStatus(userWalletId: UserWalletId): Flow<Lce<Throwable, Any>>
}

View file

@ -10,4 +10,9 @@ data class OnrampProvider(
)
@Serializable
data class OnrampProviderInfo(val name: String, val imageLarge: String)
data class OnrampProviderInfo(
val name: String,
val imageLarge: String,
val termsOfUseLink: String?,
val privacyPolicyLink: String?,
)

View file

@ -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)
}
}
}

View file

@ -11,6 +11,6 @@ class FetchOnrampCountriesUseCase(
) {
suspend operator fun invoke(): Either<OnrampError, Unit> {
return Either.catch { repository.fetchCountries() }.mapLeft(errorResolver::resolve)
return Either.catch<Unit> { repository.fetchCountries() }.mapLeft(errorResolver::resolve)
}
}

View file

@ -13,7 +13,7 @@ class GetOnrampCurrenciesUseCase(
private val errorResolver: OnrampErrorResolver,
) {
suspend operator fun invoke(): EitherFlow<OnrampError, OnrampCurrencies> {
operator fun invoke(): EitherFlow<OnrampError, OnrampCurrencies> {
return onrampRepository.getCurrencies().map { currenciesList ->
Either.catch {
val (populars, others) = currenciesList.toSet()

View file

@ -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<Either<OnrampError, OnrampCurrency?>> {
return repository.getDefaultCurrency()
.map<OnrampCurrency?, Either<OnrampError, OnrampCurrency?>> { it.right() }
.catch {
emit(errorResolver.resolve(it).left())
}
}
}

View file

@ -11,7 +11,6 @@ class OnrampSaveDefaultCountryUseCase(
) {
suspend operator fun invoke(country: OnrampCountry) = Either.catch {
repository.saveDefaultCurrency(country.defaultCurrency)
repository.saveDefaultCountry(country)
}.mapLeft(errorResolver::resolve)
}

View file

@ -10,13 +10,13 @@ import kotlinx.coroutines.flow.Flow
@Suppress("TooManyFunctions")
interface OnrampRepository {
// api
suspend fun getCurrencies(): Flow<List<OnrampCurrency>>
suspend fun getCountries(): Flow<List<OnrampCountry>>
fun getCurrencies(): Flow<List<OnrampCurrency>>
fun getCountries(): Flow<List<OnrampCountry>>
suspend fun getCountriesSync(): List<OnrampCountry>?
suspend fun getCountryByIp(): OnrampCountry
suspend fun getStatus(txId: String): OnrampStatus
suspend fun fetchCurrencies()
suspend fun fetchCountries()
suspend fun fetchCountries(): List<OnrampCountry>
suspend fun fetchPaymentMethodsIfAbsent()
suspend fun fetchPairs(currency: OnrampCurrency, country: OnrampCountry, cryptoCurrency: CryptoCurrency)
suspend fun fetchQuotes(cryptoCurrency: CryptoCurrency, amount: Amount)

View file

@ -25,7 +25,7 @@ class FetchCardTokenListUseCase(
suspend operator fun invoke(userWalletId: UserWalletId, refresh: Boolean = false): Either<TokenListError, Unit> {
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<TokenListError>.fetchCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
private suspend fun Raise<TokenListError>.fetchCurrencies(
userWalletId: UserWalletId,
refresh: Boolean = false,
): List<CryptoCurrency> {
return catch(
block = { currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(userWalletId = userWalletId) },
block = {
currenciesRepository.getSingleCurrencyWalletWithCardCurrencies(
userWalletId = userWalletId,
refresh = refresh,
)
},
catch = { raise(TokenListError.DataError(it)) },
)
}

View file

@ -65,7 +65,7 @@ class FetchCurrencyStatusUseCase(
refresh: Boolean = false,
): Either<CurrencyStatusError, Unit> {
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<CurrencyStatusError>.getPrimaryCurrency(userWalletId: UserWalletId): CryptoCurrency {
return catch({ currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId) }) {
private suspend fun Raise<CurrencyStatusError>.getPrimaryCurrency(
userWalletId: UserWalletId,
refresh: Boolean = false,
): CryptoCurrency {
return catch({ currenciesRepository.getSingleCurrencyWalletPrimaryCurrency(userWalletId, refresh) }) {
raise(CurrencyStatusError.DataError(it))
}
}

View file

@ -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<TokenActionsState.ActionState> {
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<TokenActionsState.ActionState>()
@ -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<TokenActionsState.ActionState> {
val actionsList = mutableListOf<TokenActionsState.ActionState>()
@ -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?,

View file

@ -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,

View file

@ -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<Set<Quote>, Either<TokenListError, Set<Quote>>> { 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
}
}

View file

@ -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<CryptoCurrency>
suspend fun getSingleCurrencyWalletWithCardCurrencies(
userWalletId: UserWalletId,
refresh: Boolean = false,
): List<CryptoCurrency>
/**
* Retrieves the cryptocurrency for a specific single-currency user old wallet

View file

@ -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<CryptoCurrency> {
override suspend fun getSingleCurrencyWalletWithCardCurrencies(
userWalletId: UserWalletId,
refresh: Boolean,
): List<CryptoCurrency> {
return tokens.first().getOrElse { e -> throw e }
}

View file

@ -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()
}

View file

@ -16,7 +16,7 @@ class DismissIncompleteTransactionUseCase(
suspend operator fun invoke(
userWalletId: UserWalletId,
currency: CryptoCurrency,
): Either<IncompleteTransactionError, Unit> {
): Either<IncompleteTransactionError.DataError, Unit> {
return either {
catch(
block = {

View file

@ -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
}
},

View file

@ -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)
}
}
}
}

View file

@ -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,

View file

@ -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)
}
}

View file

@ -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"),
)
}
}

View file

@ -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) {

View file

@ -9,4 +9,5 @@ interface OnrampIntents {
fun onBuyClick(quote: OnrampProviderWithQuote.Data)
fun openProviders()
fun onRefresh()
fun onLinkClick(link: String)
}

View file

@ -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()
}

View file

@ -61,6 +61,7 @@ internal class OnrampAmountFieldChangeConverter(
amountFieldModel = amountFieldModel,
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY),
),
buyButtonConfig = buyButtonConfig.copy(enabled = false),
providerBlockState = OnrampProviderBlockUM.Empty,
)
}

View file

@ -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,
)
}

View file

@ -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()

View file

@ -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)
}
},
)
}
}
}

View file

@ -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,
)

View file

@ -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<CountryListUM> {
override fun transform(prevState: CountryListUM): CountryListUM {
return CountryListUM.Error(
searchBarUM = prevState.searchBarUM,
onRetry = onRetry,
)
}
}

View file

@ -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<CountryListUM> get() = countryListUMController.state
val state: StateFlow<CountryListUM> 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<Unit>()
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<CountryItemState.Loading> = MutableList(LOADING_ITEMS_COUNT) {
CountryItemState.Loading("Loading #$it")
}.toImmutableList()

View file

@ -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<CurrenciesListUM> {
override fun transform(prevState: CurrenciesListUM): CurrenciesListUM {
return CurrenciesListUM.Error(
searchBarUM = prevState.searchBarUM,
onRetry = onRetry,
)
}
}

View file

@ -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<Unit>()
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) {

View file

@ -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,
),
)
}
}

View file

@ -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,
)
}
}

View file

@ -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<CryptoCurrencyStatus>,
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,
)
}
}

View file

@ -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<Throwable, List<SwapPairLeast>>
@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<Map<LeastTokenInfo, List<SwapPairLeast>>>(emptyMap())
private val availablePairsByNetworkFlow = MutableStateFlow<Map<LeastTokenInfo, AvailablePairsState>>(emptyMap())
init {
initializeSearchBardCallbacks()
subscribeOnUpdateState()
subscribeOnAvailablePairsUpdates()
}
private fun getTokenListUseCaseFlow(): SharedFlow<List<CryptoCurrencyStatus>> {
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<AppCurrency, Boolean>,
currencies: List<CryptoCurrencyStatus>,
selectedStatus: CryptoCurrencyStatus?,
query: String,
availablePairs: List<SwapPairLeast>,
): 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<CryptoCurrencyStatus>,
): 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<CryptoCurrencyStatus>) {
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<Map<LeastTokenInfo, AvailablePairsState>>.update(
networkInfo: LeastTokenInfo,
state: AvailablePairsState,
) {
update {
it.toMutableMap().apply {
put(networkInfo, state)
}
}
}
private fun getAppCurrencyAndBalanceHidingFlow(): Flow<Pair<AppCurrency, Boolean>> {
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
}
}

View file

@ -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<TokensListItemUM>,
val unavailableItems: ImmutableList<TokensListItemUM>,
val isBalanceHidden: Boolean,
val warning: NotificationUM? = null,
)

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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<TokensListItemUM>.addHeader(textReference: TextReference): ImmutableList<TokensListItemUM> {
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)
}

View file

@ -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)
}
}
}

View file

@ -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()
},
)
}

View file

@ -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<NotificationUM>,

View file

@ -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
}
},
)
}

View file

@ -95,6 +95,40 @@ internal class KaspaCustomFeeConverter(
}.toImmutableList()
}
fun tryAutoFixValue(
minimumFee: Fee.Kaspa,
customValues: ImmutableList<SendTextField.CustomFee>,
): ImmutableList<SendTextField.CustomFee> {
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
}

View file

@ -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() },

View file

@ -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)

View file

@ -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()

View file

@ -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<Throwable, Any>,
val sell: Lce<Throwable, Any>,
val swap: Lce<Throwable, Any>,
)
}

View file

@ -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 {

View file

@ -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,
)