Updated on 2026-08-14
This commit is contained in:
commit
0e11bf0ffb
77 changed files with 1099 additions and 629 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -82,15 +82,6 @@ internal object OnrampDomainModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetOnrampCurrencyUseCase(
|
||||
onrampRepository: OnrampRepository,
|
||||
onrampErrorResolver: OnrampErrorResolver,
|
||||
): GetOnrampCurrencyUseCase {
|
||||
return GetOnrampCurrencyUseCase(onrampRepository, onrampErrorResolver)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideGetOnrampTransactionsUseCase(
|
||||
|
|
|
|||
|
|
@ -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)))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -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>>>
|
||||
|
|
@ -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") }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 l’aide.</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
<string name="action_buttons_buy_empty_search_message">Can’t 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">Can’t 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 provider’s %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 don’t 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 don’t 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. We’re 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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 }
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>>
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ class OnrampSaveDefaultCountryUseCase(
|
|||
) {
|
||||
|
||||
suspend operator fun invoke(country: OnrampCountry) = Either.catch {
|
||||
repository.saveDefaultCurrency(country.defaultCurrency)
|
||||
repository.saveDefaultCountry(country)
|
||||
}.mapLeft(errorResolver::resolve)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)) },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@ interface OnrampIntents {
|
|||
fun onBuyClick(quote: OnrampProviderWithQuote.Data)
|
||||
fun openProviders()
|
||||
fun onRefresh()
|
||||
fun onLinkClick(link: String)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -61,6 +61,7 @@ internal class OnrampAmountFieldChangeConverter(
|
|||
amountFieldModel = amountFieldModel,
|
||||
secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY),
|
||||
),
|
||||
buyButtonConfig = buyButtonConfig.copy(enabled = false),
|
||||
providerBlockState = OnrampProviderBlockUM.Empty,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue