From 1c45da6145c0c55b15c823f6c3f09809d68b1c53 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 May 2026 16:45:25 +0300 Subject: [PATCH 01/29] Updated on 2026-08-14 --- .../kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt | 1 + gradle/tangem_dependencies.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt index 2ed1be037d..ef0f7ffdf6 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/common/visa/VisaUtilities.kt @@ -46,6 +46,7 @@ object VisaUtilities { fun generateAddressFromExtendedKey(extendedPublicKey: ExtendedPublicKey): String { val derivationData = visaBlockchain.makeAddressesFromExtendedPublicKey( extendedPublicKey = extendedPublicKey, + rawPath = null, // if null, the function will use default derivation path for the blockchain cachedIndex = null, ) return derivationData.address diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 94aa5668ec..cbb84649f4 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.37-1494" +tangemBlockchainSdk = "releases-5.37-1508" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.37-603" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 2a8e79ed67581b7d98c1225dc25bb95e6827e771 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 May 2026 13:25:39 +0300 Subject: [PATCH 02/29] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 4 ++-- core/res/src/main/res/values-pt-rBR/strings.xml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 41c4f24cdb..34d53f84b5 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -180,8 +180,8 @@ %d Geräte - Token - Tokens + %d Token + %d Tokens Bitte setze das nächste Gerät zurück, um fortzufahren. Wallet zurückgesetzt diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml index 45dbb43d03..9cc146f0bc 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -176,12 +176,12 @@ Iniciar processo de backup Use um cartão bancário ou outros métodos de pagamento. - dispositivo - dispositivos + %d dispositivo + %d dispositivos - token - tokens + %d token + %d tokens Reinicie o próximo dispositivo para continuar. Reiniciar Carteira From 012792e5869d883fddcabaf67302fbed8fac858f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 May 2026 19:00:03 +0300 Subject: [PATCH 03/29] Updated on 2026-08-14 --- .../DefaultPaymentAccountStatusFetcher.kt | 17 ++- .../DefaultPaymentAccountStatusProducer.kt | 14 ++- .../pay/store/PaymentAccountStatusesStore.kt | 35 +++++- .../DefaultSingleAccountStatusListProducer.kt | 66 +++++++++++- .../usecase/IsAccountsModeEnabledUseCase.kt | 34 +++++- .../TangemPayMainScreenCustomerInfoUseCase.kt | 31 +++++- .../subscribers/AccountListSubscriber.kt | 100 ++++++++++++------ 7 files changed, 248 insertions(+), 49 deletions(-) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index c55ac9cb0c..947cf63839 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -46,7 +46,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( override suspend fun invoke(params: PaymentAccountStatusFetcher.Params): Either = Either.catchOn(dispatchers.default) { val account = Account.Payment(userWalletId = params.userWalletId) - logger.i("fetch: ${params.userWalletId.stringValue}") + logger.i("invoke() start: ${params.userWalletId.stringValue}") if (deviceSecurity.isSecurityExposed()) { logger.i("fetch security info: rooted: ${deviceSecurity.isRooted}") @@ -80,11 +80,14 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( userWalletId = params.userWalletId, status = AccountStatus.Payment(account = account, value = status), ) - }.onLeft { + }.onLeft { throwable -> + logger.e("invoke() ${params.userWalletId} threw, falling back to ONLY_CACHE", throwable) paymentAccountStatusesStore.updateStatusSource( userWalletId = params.userWalletId, source = StatusSource.ONLY_CACHE, ) + }.also { result -> + logger.i("invoke() end ${params.userWalletId}: isRight=${result.isRight()}") } private suspend fun proceedHasTangemPayResult( @@ -101,7 +104,12 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private suspend fun fetchTangemPayAccountStatus(account: Account.Payment): PaymentAccountStatusValue { val prevResult = paymentAccountStatusesStore.getSyncOrNull(account.userWalletId) + logger.i( + "fetchTangemPayAccountStatus ${account.userWalletId}: " + + "prevResultType=${prevResult?.value?.let { it::class.simpleName } ?: "null"}", + ) if (prevResult == null || prevResult.value is PaymentAccountStatusValue.Error.Unavailable) { + logger.i("fetchTangemPayAccountStatus ${account.userWalletId}: writing Loading placeholder to store") paymentAccountStatusesStore.store( userWalletId = account.userWalletId, status = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Loading), @@ -112,10 +120,13 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( } private suspend fun proceedWithOrderId(account: Account.Payment): PaymentAccountStatusValue { - return if (!onboardingRepository.isTangemPayInitialDataProduced(account.userWalletId)) { + val isInitial = onboardingRepository.isTangemPayInitialDataProduced(account.userWalletId) + logger.i("proceedWithOrderId ${account.userWalletId}: isTangemPayInitialDataProduced=$isInitial") + return if (!isInitial) { PaymentAccountStatusValue.Error.NotSynced } else { val orderId = onboardingRepository.getOrderId(account.userWalletId) + logger.i("proceedWithOrderId ${account.userWalletId}: orderIdPresent=${orderId != null}") if (orderId != null) { proceedWithOrderId(account = account, orderId = orderId) } else { diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt index c2c53ae7cd..153f0d327d 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt @@ -9,12 +9,17 @@ import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEmpty +import kotlinx.coroutines.flow.onStart + +private const val TAG = "PaymentAccountStatusProducer" internal class DefaultPaymentAccountStatusProducer @AssistedInject constructor( @Assisted private val params: PaymentAccountStatusProducer.Params, @@ -24,13 +29,20 @@ internal class DefaultPaymentAccountStatusProducer @AssistedInject constructor( ) : PaymentAccountStatusProducer { private val account = Account.Payment(userWalletId = params.userWalletId) + private val logger = TangemLogger.withTag(TAG) override val fallback: Option get() = AccountStatus.Payment(account = account, value = PaymentAccountStatusValue.Error.Unavailable).some() override fun produce(): Flow { + logger.i("[${params.userWalletId}] produce() called") return paymentAccountStatusesStore.get(userWalletId = params.userWalletId) - .onEmpty { emit(value = AccountStatus.Payment(account, PaymentAccountStatusValue.NotCreated)) } + .onStart { logger.i("[${params.userWalletId}] flow subscribed to store") } + .onEach { logger.i("[${params.userWalletId}] flow emits statusType=${it.value::class.simpleName}") } + .onEmpty { + logger.i("[${params.userWalletId}] onEmpty triggered: emitting NotCreated fallback") + emit(value = AccountStatus.Payment(account, PaymentAccountStatusValue.NotCreated)) + } .flowOn(dispatchers.default) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt index ec8a48d880..cb96162ce4 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt @@ -15,11 +15,15 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.launch internal typealias WalletIdWithPaymentStatus = Map internal typealias WalletIdWithPaymentStatusDM = Map +private const val TAG = "PaymentAccountStatusesStore" + /** * Store for payment account statuses with dual storage (runtime + persistence). * @@ -32,10 +36,18 @@ internal class PaymentAccountStatusesStore( scope: AppCoroutineScope, ) { + private val logger = TangemLogger.withTag(TAG) + init { scope.launch { + logger.i("init: loading cached payment statuses from persistence") try { - val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch + val cachedStatuses = persistenceDataStore.data.firstOrNull() + if (cachedStatuses == null) { + logger.i("init: persistence empty (firstOrNull == null), runtimeStore stays empty") + return@launch + } + logger.i("init: loaded ${cachedStatuses.size} cached entries; populating runtimeStore") runtimeStore.store( value = cachedStatuses.mapValues { (rawUserWalletId, statusDM) -> val account = Account.Payment(userWalletId = UserWalletId(rawUserWalletId)) @@ -43,18 +55,32 @@ internal class PaymentAccountStatusesStore( AccountStatus.Payment(account = account, value = statusValue) }, ) + logger.i("init: runtimeStore populated with ${cachedStatuses.size} entries") } catch (e: Exception) { - TangemLogger.e("Error while loading cached payment account statuses", e) + logger.e("Error while loading cached payment account statuses", e) } } } fun get(userWalletId: UserWalletId): Flow { - return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } + return runtimeStore.get() + .onStart { logger.i("get($userWalletId): subscribed to runtimeStore") } + .onEach { map -> + logger.i( + "get($userWalletId): runtimeStore emitted map size=${map.size}, " + + "hasEntry=${map.containsKey(userWalletId.stringValue)}", + ) + } + .mapNotNull { it[userWalletId.stringValue] } + .onEach { status -> + logger.i("get($userWalletId): emitting statusType=${status.value::class.simpleName}") + } } suspend fun getSyncOrNull(userWalletId: UserWalletId): AccountStatus.Payment? { - return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue) + val result = runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue) + logger.i("getSyncOrNull($userWalletId) valueType=${result?.value?.let { it::class.simpleName } ?: "null"}") + return result } suspend fun updateStatusSource(userWalletId: UserWalletId, source: StatusSource) { @@ -68,6 +94,7 @@ internal class PaymentAccountStatusesStore( } suspend fun store(userWalletId: UserWalletId, status: AccountStatus.Payment) { + logger.i("store($userWalletId): valueType=${status.value::class.simpleName}") coroutineScope { launch { storeInRuntime(userWalletId = userWalletId, status = status) } launch { storeInPersistence(userWalletId = userWalletId, status = status.value) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt index a2bf732b26..1da0c170d6 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt @@ -46,6 +46,7 @@ import com.tangem.domain.tokens.operations.TokenListFactory import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -73,7 +74,7 @@ import java.math.BigDecimal [REDACTED_AUTHOR] */ // TODO: Move to :data:account:status [REDACTED_JIRA] -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @OptIn(ExperimentalCoroutinesApi::class) internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor( @Assisted private val params: SingleAccountStatusListProducer.Params, @@ -90,17 +91,29 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : SingleAccountStatusListProducer { + private val logger = TangemLogger.withTag(TAG) + override val fallback: Option = none() override fun produce(): Flow { + logger.i("produce() called for ${params.userWalletId}") return flattenFlow() + .onEach { list -> + logger.i( + "produce()[${params.userWalletId}] emit: accounts=${list.accountStatuses.size}, " + + "currencies=${list.flattenCurrencies().size}, " + + "totalFiatType=${list.totalFiatBalance::class.simpleName}", + ) + } .flowOn(dispatchers.default) } @Suppress("LongMethod") private fun flattenFlow(): Flow = channelFlow { val walletId = params.userWalletId + logger.i("flattenFlow[$walletId]: start") val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) + logger.i("flattenFlow[$walletId]: userWallet resolved (type=${userWallet::class.simpleName})") val flattenCurrency: MutableSharedFlow> = MutableSharedFlow( replay = 1, @@ -108,11 +121,20 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo ) val accountListFlow: StateFlow = singleAccountListSupplier(walletId) - .onEach { accountList -> flattenCurrency.tryEmit(accountList.flattenMapCurrencies()) } + .onEach { accountList -> + logger.i( + "flattenFlow[$walletId]: accountList emitted accounts=${accountList.accounts.size}, " + + "currencies=${accountList.flattenMapCurrencies().size}", + ) + flattenCurrency.tryEmit(accountList.flattenMapCurrencies()) + } .stateIn(this) val hasCachedNetworks = networksRepository.hasCachedStatuses(walletId) + logger.i("flattenFlow[$walletId]: hasCachedNetworks=$hasCachedNetworks") if (!hasCachedNetworks) { + val initialAccounts = accountListFlow.value.accounts.size + logger.i("flattenFlow[$walletId]: sending Loading placeholder (accounts=$initialAccounts)") send(createLoadingAccountStatusList(accountListFlow.value)) } @@ -121,11 +143,19 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo flattenCurrency = flattenCurrency, ) - if (userWallet.isPaymentAccountSupported()) { + val isPaymentSupported = userWallet.isPaymentAccountSupported() + logger.i("flattenFlow[$walletId]: isPaymentAccountSupported=$isPaymentSupported") + if (isPaymentSupported) { combineWithPaymentAccount( accountListFlow = accountListFlow, cryptoCurrencyStatusFlow = cryptoCurrencyStatusFlow, - paymentAccountStatusFlow = paymentAccountStatusSupplier.invoke(userWalletId = params.userWalletId), + paymentAccountStatusFlow = paymentAccountStatusSupplier.invoke(userWalletId = params.userWalletId) + .onEach { paymentStatus -> + logger.i( + "flattenFlow[$walletId]: paymentAccountStatus emitted " + + "valueType=${paymentStatus.value::class.simpleName}", + ) + }, ) } else { combineWithoutPaymentAccount( @@ -146,6 +176,12 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo flow2 = cryptoCurrencyStatusFlow, flow3 = paymentAccountStatusFlow, transform = { accountList, currencyStatusMap, paymentAccountStatus -> + logger.i( + "combineWithPayment[${params.userWalletId}] transform: " + + "accounts=${accountList.accounts.size}, " + + "currencyStatusMap=${currencyStatusMap.size}, " + + "paymentType=${paymentAccountStatus.value::class.simpleName}", + ) val accountStatuses = accountList.accounts.map { account -> when (account) { is Account.Payment -> paymentAccountStatus @@ -192,6 +228,11 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo flow = accountListFlow, flow2 = cryptoCurrencyStatusFlow, transform = { accountList, currencyStatusMap -> + logger.i( + "combineWithoutPayment[${params.userWalletId}] transform: " + + "accounts=${accountList.accounts.size}, " + + "currencyStatusMap=${currencyStatusMap.size}", + ) val accountStatuses = accountList.accounts .filterIsInstance() .map { account -> @@ -242,14 +283,18 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo ): Flow> { val walletId = userWallet.walletId val networkStatusFlow: SharedFlow> = networkStatusFlow(walletId) + .onEach { logger.i("flattenCurrencyStatusFlow[$walletId]: networkStatuses emitted size=${it.size}") } .shareIn(this, started = SharingStarted.Eagerly, replay = 1) val stakingBalanceFlow: SharedFlow>> = stakingFlow(userWallet) + .onEach { logger.i("flattenCurrencyStatusFlow[$walletId]: stakingBalances emitted stakingIds=${it.size}") } .shareIn(this, started = SharingStarted.Eagerly, replay = 1) val quoteStatusFlow: SharedFlow> = quoteStatusFlow() + .onEach { logger.i("flattenCurrencyStatusFlow[$walletId]: quoteStatuses emitted size=${it.size}") } .shareIn(this, started = SharingStarted.Eagerly, replay = 1) return flattenCurrency .distinctUntilChanged() + .onEach { logger.i("flattenCurrencyStatusFlow[$walletId]: flattenCurrency emitted size=${it.size}") } .flatMapLatest { a -> combine( flow = networkStatusFlow, @@ -264,6 +309,15 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo ) } .distinctUntilChanged() + .onEach { box -> + logger.i( + "flattenCurrencyStatusFlow[$walletId]: box emitted " + + "currencies=${box.flattenCurrencyMap.size}, " + + "networks=${box.networkStatusMap.size}, " + + "stakings=${box.stakingBalanceMap.size}, " + + "quotes=${box.quoteStatusMap.size}", + ) + } .map { box -> val flattenCurrencyMap: Map = box.flattenCurrencyMap val networkStatusMap: Map = box.networkStatusMap @@ -402,4 +456,8 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo interface Factory : SingleAccountStatusListProducer.Factory { override fun create(params: SingleAccountStatusListProducer.Params): DefaultSingleAccountStatusListProducer } + + private companion object { + const val TAG = "SingleAccountStatusListProducer" + } } \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCase.kt index 6d858dc872..a335565ea1 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/IsAccountsModeEnabledUseCase.kt @@ -6,6 +6,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* @@ -27,27 +28,51 @@ class IsAccountsModeEnabledUseCase( @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke(): Flow { + TangemLogger.i("$TAG: invoke() started") + val cryptoMode = multiAccountListSupplier.invoke() - .map { lists -> lists.any { it.hasMultipleCryptoPortfolios() } } + .onEach { lists -> + TangemLogger.i("$TAG: multiAccountListSupplier emitted ${lists.size} lists (cryptoMode branch)") + } + .map { lists -> + val isCryptoMode = lists.any { it.hasMultipleCryptoPortfolios() } + TangemLogger.i("$TAG: cryptoMode=$isCryptoMode") + isCryptoMode + } val paymentMode = multiAccountListSupplier.invoke().flatMapLatest { lists -> val walletIdsWithPayment = lists.mapNotNull { list -> if (list.accounts.any { it is Account.Payment }) list.userWalletId else null } + TangemLogger.i("$TAG: walletIdsWithPayment=${walletIdsWithPayment.size}") if (walletIdsWithPayment.isEmpty()) { flowOf(false) } else { val flows = walletIdsWithPayment.map { walletId -> paymentAccountStatusSupplier.invoke(walletId) + .onEach { status -> + TangemLogger.i("$TAG: paymentStatus for $walletId = ${status.value::class.simpleName}") + } .map { it.value.isActivePayment() } - .onStart { emit(false) } + .onStart { + TangemLogger.i("$TAG: paymentAccountStatusSupplier onStart for $walletId") + emit(false) + } + } + combine(flows) { results -> + val isPaymentMode = results.any { it } + TangemLogger.i("$TAG: paymentMode combine result=$isPaymentMode (${results.toList()})") + isPaymentMode } - combine(flows) { results -> results.any { it } } } } - return combine(cryptoMode, paymentMode) { crypto, payment -> crypto || payment } + return combine(cryptoMode, paymentMode) { crypto, payment -> + val isEnabled = crypto || payment + TangemLogger.i("$TAG: final combine crypto=$crypto, payment=$payment, result=$isEnabled") + isEnabled + } .distinctUntilChanged() } @@ -90,5 +115,6 @@ class IsAccountsModeEnabledUseCase( private companion object { const val PAYMENT_STATUS_SYNC_TIMEOUT_MS = 1_000L + const val TAG = "IsAccountsModeEnabledUseCase" } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 2e62f7190e..370eb2d543 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -97,13 +97,29 @@ class TangemPayMainScreenCustomerInfoUseCase( operator fun invoke( userWalletId: UserWalletId, ): Flow> { + logger.i("invoke($userWalletId): flow requested") return state.mapNotNull { map -> map[userWalletId] } + .onStart { logger.i("invoke($userWalletId): flow subscribed (current state size=${state.value.size})") } + .onEach { value -> + logger.i( + "invoke($userWalletId): emit ${value.fold( + { "Left(${it.javaClass.simpleName})" }, + { "Right(${it.javaClass.simpleName})" }, + )}", + ) + } } private fun updateState( userWalletId: UserWalletId, either: Either, ) { + logger.i( + "updateState($userWalletId) -> ${either.fold( + { "Left(${it.javaClass.simpleName})" }, + { "Right(${it.javaClass.simpleName})" }, + )}", + ) state.update { currentMap -> currentMap.toMutableMap().apply { this[userWalletId] = either } } @@ -112,10 +128,14 @@ class TangemPayMainScreenCustomerInfoUseCase( private suspend fun proceedWithPaeraCustomerResult( userWalletId: UserWalletId, ): Either { - if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { + logger.i("proceedWithPaeraCustomerResult($userWalletId) entry") + val isInitial = onboardingRepository.isTangemPayInitialDataProduced(userWalletId) + logger.i("proceedWithPaeraCustomerResult($userWalletId): isTangemPayInitialDataProduced=$isInitial") + if (!isInitial) { return TangemPayCustomerInfoError.RefreshNeededError.left() } val orderId = onboardingRepository.getOrderId(userWalletId) + logger.i("proceedWithPaeraCustomerResult($userWalletId): orderId=$orderId") return if (orderId != null) { proceedWithOrderId(userWalletId = userWalletId, orderId = orderId) } else { @@ -146,18 +166,25 @@ class TangemPayMainScreenCustomerInfoUseCase( userWalletId: UserWalletId, orderId: String, ): Either { + logger.i("proceedWithOrderId($userWalletId, orderId=$orderId) entry") return customerOrderRepository.getOrderData(userWalletId, orderId = orderId) .fold( ifLeft = { error -> + logger.e("proceedWithOrderId($userWalletId): getOrderData failed: ${error.javaClass.simpleName}") error.mapErrorForCustomer().left() }, ifRight = { orderData -> + logger.i("proceedWithOrderId($userWalletId): orderData.status=${orderData.status}") if (orderData.status in setOf(OrderStatus.COMPLETED, OrderStatus.UNKNOWN)) { onboardingRepository.clearOrderId(userWalletId) } onboardingRepository.getCustomerInfo(userWalletId = userWalletId) - .mapLeft { it.mapErrorForCustomer() } + .mapLeft { error -> + logger.e("proceedWithOrderId($userWalletId): getCustomerInfo failed: $error") + error.mapErrorForCustomer() + } .map { customerInfo -> + logger.i("proceedWithOrderId($userWalletId): got customerInfo") MainScreenCustomerInfo(info = customerInfo, orderStatus = orderData.status) } }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt index 2d7ba38899..4eff9de5fa 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt @@ -13,6 +13,7 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.utils.coroutines.combine7 +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -21,6 +22,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart import java.math.BigDecimal /** @@ -41,38 +44,69 @@ internal class AccountListSubscriber @AssistedInject constructor( private val designFeatureToggles: DesignFeatureToggles, ) : BasicAccountListSubscriber() { - override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7( - flow1 = getAccountStatusListFlow(), - flow2 = getAppCurrencyFlow(), - flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet), - flow4 = accountDependencies.isAccountsModeEnabledUseCase(), - flow5 = yieldSupplyApyFlow(), - flow6 = yieldSupplyGetShouldShowMainPromoFlow(), - flow7 = stakingAvailabilityFlow(), - ) { - accountList, appCurrency, expandedAccounts, isAccountMode, - yieldSupplyApyMap, shouldShowMainPromo, stakingAvailabilityMap, - -> - if (designFeatureToggles.isRedesignEnabled) { - updateState2( - accountList = accountList, - appCurrency = appCurrency, - expandedAccounts = expandedAccounts, - isAccountMode = isAccountMode, - yieldSupplyApyMap = yieldSupplyApyMap, - stakingAvailabilityMap = stakingAvailabilityMap, - shouldShowMainPromo = shouldShowMainPromo, - ) - } else { - updateState( - accountList = accountList, - appCurrency = appCurrency, - expandedAccounts = expandedAccounts, - isAccountMode = isAccountMode, - yieldSupplyApyMap = yieldSupplyApyMap, - stakingAvailabilityMap = stakingAvailabilityMap, - shouldShowMainPromo = shouldShowMainPromo, + override fun create(coroutineScope: CoroutineScope): Flow<*> { + val walletId = userWallet.walletId.stringValue + TangemLogger.i("$TAG[$walletId]: create() called, building combine7") + return combine7( + flow1 = getAccountStatusListFlow() + .onStart { TangemLogger.i("$TAG[$walletId]: flow1 accountStatusList subscribed") } + .onEach { list -> + val count = list.flattenCurrencies().size + TangemLogger.i("$TAG[$walletId]: flow1 accountStatusList emitted (currencies=$count)") + }, + flow2 = getAppCurrencyFlow() + .onStart { TangemLogger.i("$TAG[$walletId]: flow2 appCurrency subscribed") } + .onEach { TangemLogger.i("$TAG[$walletId]: flow2 appCurrency emitted=${it.code}") }, + flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet) + .onStart { TangemLogger.i("$TAG[$walletId]: flow3 expandedAccounts subscribed") } + .onEach { TangemLogger.i("$TAG[$walletId]: flow3 expandedAccounts emitted (size=${it.size})") }, + flow4 = accountDependencies.isAccountsModeEnabledUseCase() + .onStart { TangemLogger.i("$TAG[$walletId]: flow4 isAccountsModeEnabled subscribed") } + .onEach { TangemLogger.i("$TAG[$walletId]: flow4 isAccountsModeEnabled emitted=$it") }, + flow5 = yieldSupplyApyFlow() + .onStart { TangemLogger.i("$TAG[$walletId]: flow5 yieldSupplyApy subscribed") } + .onEach { TangemLogger.i("$TAG[$walletId]: flow5 yieldSupplyApy emitted (size=${it.size})") }, + flow6 = yieldSupplyGetShouldShowMainPromoFlow() + .onStart { TangemLogger.i("$TAG[$walletId]: flow6 shouldShowMainPromo subscribed") } + .onEach { TangemLogger.i("$TAG[$walletId]: flow6 shouldShowMainPromo emitted=$it") }, + flow7 = stakingAvailabilityFlow() + .onStart { TangemLogger.i("$TAG[$walletId]: flow7 stakingAvailability subscribed") } + .onEach { TangemLogger.i("$TAG[$walletId]: flow7 stakingAvailability emitted (size=${it.size})") }, + ) { + accountList, appCurrency, expandedAccounts, isAccountMode, + yieldSupplyApyMap, shouldShowMainPromo, stakingAvailabilityMap, + -> + TangemLogger.i( + "$TAG[$walletId]: combine7 transform fired — " + + "currencies=${accountList.flattenCurrencies().size}, " + + "appCurrency=${appCurrency.code}, " + + "expanded=${expandedAccounts.size}, " + + "isAccountMode=$isAccountMode, " + + "apyMap=${yieldSupplyApyMap.size}, " + + "promo=$shouldShowMainPromo, " + + "stakingMap=${stakingAvailabilityMap.size}", ) + if (designFeatureToggles.isRedesignEnabled) { + updateState2( + accountList = accountList, + appCurrency = appCurrency, + expandedAccounts = expandedAccounts, + isAccountMode = isAccountMode, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + ) + } else { + updateState( + accountList = accountList, + appCurrency = appCurrency, + expandedAccounts = expandedAccounts, + isAccountMode = isAccountMode, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + ) + } } } @@ -99,4 +133,8 @@ internal class AccountListSubscriber @AssistedInject constructor( interface Factory { fun create(userWallet: UserWallet): AccountListSubscriber } + + private companion object { + const val TAG = "AccountListSubscriber" + } } \ No newline at end of file From 34fed24784e8ed20decbc038afea122639b23aa6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 May 2026 16:52:05 +0000 Subject: [PATCH 04/29] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index cbb84649f4..76d48569f9 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.37-1508" +tangemBlockchainSdk = "releases-5.38-1512" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.37-603" +tangemCardSdk = "releases-5.38-615" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 3a5acd5aa0a60052b13028c13696e7cc7c374d43 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 May 2026 10:18:07 +0300 Subject: [PATCH 05/29] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index cbb84649f4..a0cb218ffd 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.37-1508" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.37-603" +tangemCardSdk = "releases-5.37-608" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From a2480ff02102de1073a49cb0778687ad797bf933 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 May 2026 13:45:16 +0500 Subject: [PATCH 06/29] Updated on 2026-08-14 --- .../tangem/core/analytics/models/AnalyticsParam.kt | 1 + .../domain/card/analytics/IntroductionProcess.kt | 11 +++++++++-- .../createwalletstart/CreateWalletStartModel.kt | 5 +++++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index d6b77eb6ba..9a0e69c86b 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -297,6 +297,7 @@ sealed class AnalyticsParam { const val REFERRAL_ID = "Referral_ID" const val SEARCHED = "Searched" const val RATE_TYPE = "Rate Type" + const val SCREEN_TYPE = "Screen Type" } } diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt index 98114357c7..cf48b96aea 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt @@ -15,20 +15,27 @@ sealed class IntroductionProcess( class ButtonScanCardLegacy : IntroductionProcess("Button - Scan Card") class CreateWalletIntroScreenOpened( + screenType: ScreenType, referralId: String?, ) : IntroductionProcess( event = "Create Wallet Intro Screen Opened", params = buildMap { + put(AnalyticsParam.SCREEN_TYPE, screenType.value) putAll(getReferralParams(referralId)) }, - ) + ) { + enum class ScreenType(val value: String) { + Cold("Cold Wallet"), + Hot("Mobile Wallet"), + } + } class ButtonScanCard( val source: AnalyticsParam.ScreensSources, ) : IntroductionProcess( event = "Button - Scan Card", params = mapOf( - AnalyticsParam.Key.SOURCE to source.value, + AnalyticsParam.SOURCE to source.value, ), ) } \ No newline at end of file diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt index 71b570076c..22f08deb02 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt @@ -23,6 +23,7 @@ import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDi import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.analytics.IntroductionProcess +import com.tangem.domain.card.analytics.IntroductionProcess.CreateWalletIntroScreenOpened.ScreenType import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.error.SaveWalletError @@ -137,6 +138,10 @@ internal class CreateWalletStartModel @Inject constructor( modelScope.launch { analyticsEventHandler.send( event = IntroductionProcess.CreateWalletIntroScreenOpened( + screenType = when (params.mode) { + CreateWalletStartComponent.Mode.ColdWallet -> ScreenType.Cold + CreateWalletStartComponent.Mode.HotWallet -> ScreenType.Hot + }, referralId = appsFlyerStore.get()?.refcode, ), ) From ba8797ec978260454ccdc55ca91d1b62e90d0377 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 May 2026 12:46:01 +0400 Subject: [PATCH 07/29] Updated on 2026-08-14 --- .../approval/impl/model/GiveApprovalModel.kt | 10 +- .../impl/model/GiveApprovalModelTest.kt | 193 ++++++++++++++++-- 2 files changed, 183 insertions(+), 20 deletions(-) diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt index 108efdb33c..dbe9d28ed2 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -18,6 +18,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.utils.parseBigDecimalOrNull import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError @@ -286,10 +287,9 @@ internal class GiveApprovalModel @Inject constructor( } private fun getApprovalAmount(): BigDecimal? { - return if (uiState.value.approveType == ApproveType.LIMITED) { - params.amount.toBigDecimalOrNull() - } else { - null + return when (uiState.value.approveType) { + ApproveType.LIMITED -> params.amount.parseBigDecimalOrNull() + ApproveType.UNLIMITED -> null } } @@ -302,7 +302,7 @@ internal class GiveApprovalModel @Inject constructor( val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return GetFeeError.DataError(IllegalStateException("Currency is not a token")).left() - val amount = params.amount.toBigDecimalOrNull() + val amount = params.amount.parseBigDecimalOrNull() ?: return GetFeeError.DataError(IllegalArgumentException("Invalid amount format")).left() val allowance = getAllowanceInfoUseCase( diff --git a/features/approval/impl/src/test/kotlin/com/tangem/features/approval/impl/model/GiveApprovalModelTest.kt b/features/approval/impl/src/test/kotlin/com/tangem/features/approval/impl/model/GiveApprovalModelTest.kt index cf09525c4c..acf4eaa4da 100644 --- a/features/approval/impl/src/test/kotlin/com/tangem/features/approval/impl/model/GiveApprovalModelTest.kt +++ b/features/approval/impl/src/test/kotlin/com/tangem/features/approval/impl/model/GiveApprovalModelTest.kt @@ -2,16 +2,21 @@ package com.tangem.features.approval.impl.model import arrow.core.right import com.google.common.truth.Truth.assertThat +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.MutableParamsContainer -import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.models.AllowanceInfo +import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetAllowanceInfoUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase @@ -23,6 +28,7 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk @@ -30,6 +36,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import java.math.BigDecimal @OptIn(ExperimentalCoroutinesApi::class) class GiveApprovalModelTest { @@ -48,31 +55,73 @@ class GiveApprovalModelTest { private val userWalletId = UserWalletId(stringValue = "0123456789ABCDEF") private val userWallet: UserWallet.Hot = mockk(relaxed = true) - private val cryptoCurrencyStatus: CryptoCurrencyStatus = mockk { - every { currency } returns mockk(relaxed = true) + private val tokenCurrency: CryptoCurrency.Token = mockk(relaxed = true) { + every { contractAddress } returns "0xContract" + every { network } returns mockk(relaxed = true) } - private val params = GiveApprovalComponent.Params( - userWalletId = userWalletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - feeCryptoCurrencyStatus = cryptoCurrencyStatus, - amount = "10", - spenderAddress = "0xSpender", - amountFooter = TextReference.EMPTY, - feeFooter = TextReference.EMPTY, - callback = mockk(relaxed = true), - ) + private val cryptoCurrencyStatus: CryptoCurrencyStatus = mockk { + every { currency } returns tokenCurrency + } + + private val approvalTx: TransactionData.Uncompiled = mockk(relaxed = true) + private val transactionFee: TransactionFee = mockk(relaxed = true) + private val transactionFeeExtended: TransactionFeeExtended = mockk(relaxed = true) { + every { transactionFee } returns this@GiveApprovalModelTest.transactionFee + } private val getUserWalletUseCase: GetUserWalletUseCase = mockk(relaxed = true) @BeforeEach fun setUp() { every { getUserWalletUseCase.invoke(userWalletId) } returns userWallet.right() + + coEvery { + createApprovalTransactionUseCase( + cryptoCurrencyStatus = any(), + userWalletId = any(), + amount = any(), + contractAddress = any(), + spenderAddress = any(), + ) + } returns approvalTx.right() + + coEvery { + getAllowanceInfoUseCase( + userWalletId = any(), + cryptoCurrency = any(), + spenderAddress = any(), + requiredAmount = any(), + ) + } returns AllowanceInfo.Enough(allowance = BigDecimal.ZERO).right() + + coEvery { + getFeeUseCase(transactionData = any(), userWallet = any(), network = any()) + } returns transactionFee.right() + + coEvery { + getFeeForGaslessUseCase(transactionData = any(), userWallet = any(), network = any()) + } returns transactionFeeExtended.right() + + coEvery { + getFeeForTokenUseCase(transactionData = any(), userWallet = any(), token = any()) + } returns transactionFeeExtended.right() } - private fun createModel(): GiveApprovalModel = GiveApprovalModel( + private fun createParams(amount: String): GiveApprovalComponent.Params = GiveApprovalComponent.Params( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCryptoCurrencyStatus = cryptoCurrencyStatus, + amount = amount, + spenderAddress = "0xSpender", + amountFooter = TextReference.EMPTY, + feeFooter = TextReference.EMPTY, + callback = mockk(relaxed = true), + ) + + private fun createModel(amount: String = "10"): GiveApprovalModel = GiveApprovalModel( dispatchers = TestingCoroutineDispatcherProvider(), - paramsContainer = MutableParamsContainer(params), + paramsContainer = MutableParamsContainer(createParams(amount)), createApprovalTransactionUseCase = createApprovalTransactionUseCase, getAllowanceInfoUseCase = getAllowanceInfoUseCase, sendTransactionUseCase = sendTransactionUseCase, @@ -109,4 +158,118 @@ class GiveApprovalModelTest { coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerLoadingState() } coVerify(exactly = 0) { feeSelectorReloadTrigger.triggerUpdate() } } + + @Test + fun `GIVEN comma decimal amount and LIMITED approveType WHEN loadFeeExtended THEN creates approval tx with parsed amount`() = + runTest { + val model = createModel(amount = "1,1") + + val result = model.loadFeeExtended(maybeToken = null) + + assertThat(result.isRight()).isTrue() + coVerify { + createApprovalTransactionUseCase( + cryptoCurrencyStatus = any(), + userWalletId = any(), + amount = match { it != null && it.compareTo(BigDecimal("1.1")) == 0 }, + contractAddress = any(), + spenderAddress = any(), + ) + } + } + + @Test + fun `GIVEN point decimal amount and LIMITED approveType WHEN loadFeeExtended THEN creates approval tx with parsed amount`() = + runTest { + val model = createModel(amount = "1.1") + + val result = model.loadFeeExtended(maybeToken = null) + + assertThat(result.isRight()).isTrue() + coVerify { + createApprovalTransactionUseCase( + cryptoCurrencyStatus = any(), + userWalletId = any(), + amount = match { it != null && it.compareTo(BigDecimal("1.1")) == 0 }, + contractAddress = any(), + spenderAddress = any(), + ) + } + } + + @Test + fun `GIVEN comma decimal amount and UNLIMITED approveType WHEN loadFeeExtended THEN creates approval tx with null amount`() = + runTest { + val model = createModel(amount = "1,1") + model.onChangeApproveType(ApproveType.UNLIMITED) + + val result = model.loadFeeExtended(maybeToken = null) + + assertThat(result.isRight()).isTrue() + coVerify { + createApprovalTransactionUseCase( + cryptoCurrencyStatus = any(), + userWalletId = any(), + amount = isNull(), + contractAddress = any(), + spenderAddress = any(), + ) + } + } + + @Test + fun `GIVEN unparseable amount and LIMITED approveType WHEN loadFeeExtended THEN creates approval tx with null amount`() = + runTest { + val model = createModel(amount = "abc") + + val result = model.loadFeeExtended(maybeToken = null) + + assertThat(result.isRight()).isTrue() + coVerify { + createApprovalTransactionUseCase( + cryptoCurrencyStatus = any(), + userWalletId = any(), + amount = isNull(), + contractAddress = any(), + spenderAddress = any(), + ) + } + } + + @Test + fun `GIVEN comma decimal amount and LIMITED approveType WHEN loadFee THEN creates approval tx with parsed amount`() = + runTest { + val model = createModel(amount = "2,5") + + val result = model.loadFee() + + assertThat(result.isRight()).isTrue() + coVerify { + createApprovalTransactionUseCase( + cryptoCurrencyStatus = any(), + userWalletId = any(), + amount = match { it != null && it.compareTo(BigDecimal("2.5")) == 0 }, + contractAddress = any(), + spenderAddress = any(), + ) + } + } + + @Test + fun `GIVEN unparseable amount WHEN loadFee THEN returns DataError and does not check allowance`() = runTest { + val model = createModel(amount = "abc") + + val result = model.loadFee() + + assertThat(result.isLeft()).isTrue() + assertThat(result.leftOrNull()).isInstanceOf(GetFeeError.DataError::class.java) + coVerify(exactly = 0) { + getAllowanceInfoUseCase( + userWalletId = any(), + cryptoCurrency = any(), + spenderAddress = any(), + requiredAmount = any(), + ) + } + } } \ No newline at end of file From 152df94941dacd45d532e06f74162cb2b46831cc Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 May 2026 01:52:45 -0700 Subject: [PATCH 08/29] Updated on 2026-08-14 --- .../tangempay/ui/TangemPayCardPageScreen.kt | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt index 5217c1be53..8ea4e2139d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt @@ -76,23 +76,25 @@ internal fun TangemPayCardPageScreen( state = cardDetailsState, ) } - if (state.addToWalletBlockState != null) { - cardPageItem(key = "GooglePay") { - TangemPayAddToWalletBlock(state = state.addToWalletBlockState) - } - } - cardPageItem(key = "Limit") { - TangemPayDailyLimitBlock(state = state.dailyLimitState) - } - if (state.dailyLimitState == TangemPayDailyLimitBlockState.Error) { - cardPageItem(key = "LimitError") { - TangemPayDailyLimitErrorBlock() - } - } - cardPageItem(key = "Settings") { - if (state.isReissueInProgress) { + if (state.isReissueInProgress) { + cardPageItem(key = "Reissue") { TangemPayReplacingCardBlock() - } else { + } + } else { + if (state.addToWalletBlockState != null) { + cardPageItem(key = "GooglePay") { + TangemPayAddToWalletBlock(state = state.addToWalletBlockState) + } + } + cardPageItem(key = "Limit") { + TangemPayDailyLimitBlock(state = state.dailyLimitState) + } + if (state.dailyLimitState == TangemPayDailyLimitBlockState.Error) { + cardPageItem(key = "LimitError") { + TangemPayDailyLimitErrorBlock() + } + } + cardPageItem(key = "Settings") { TangemPayCardPageSettingsBlock(settings = state.settings) } } From 1bf8751230406de61c14930a1447dd601cc20573 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 May 2026 15:51:28 +0500 Subject: [PATCH 09/29] Updated on 2026-08-14 --- .../local/visa/entity/PaymentAccountStatusValueDM.kt | 1 + .../pay/converter/PaymentAccountStatusValueDMConverter.kt | 2 ++ .../data/pay/flow/DefaultPaymentAccountStatusFetcher.kt | 1 + .../tangem/data/pay/repository/DefaultOnboardingRepository.kt | 3 +++ .../tangem/domain/models/account/PaymentAccountStatusValue.kt | 4 +++- .../main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt | 1 + 6 files changed, 11 insertions(+), 1 deletion(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt index d51eed7b2f..1f74827d91 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt @@ -45,6 +45,7 @@ sealed interface PaymentAccountStatusValueDM { @Json(name = "deposit_address") val depositAddress: String?, @Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM, @Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM, + @Json(name = "available_for_withdrawal") val availableForWithdrawal: BigDecimal, @Json(name = "cards") val cards: List, ) : PaymentAccountStatusValueDM diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt index 23c8ce5ea3..0c2fd37b83 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt @@ -41,6 +41,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( depositAddress = value.depositAddress, fiatBalance = value.fiatBalance.toDM(), cryptoBalance = value.cryptoBalance.toDM(), + availableForWithdrawal = value.availableForWithdrawal, cards = value.cards.map { card -> PaymentAccountStatusValueDM.TangemPayCard( id = card.id, @@ -86,6 +87,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( depositAddress = value.depositAddress, fiatBalance = value.fiatBalance.toDomain(), cryptoBalance = value.cryptoBalance.toDomain(), + availableForWithdrawal = value.availableForWithdrawal, cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), cards = value.cards.map { card -> TangemPayCard( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index 2f13c10d8d..bc789c6728 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -289,6 +289,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( depositAddress = cardInfo.depositAddress, fiatBalance = cardInfo.fiatBalance, cryptoBalance = cardInfo.cryptoBalance, + availableForWithdrawal = cardInfo.availableForWithdrawal, cryptoCurrency = cryptoCurrency, cards = listOf( TangemPayCard( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index a05f3e7b14..d6dd2b330a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -35,6 +35,7 @@ import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.orZero import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -174,6 +175,7 @@ internal class DefaultOnboardingRepository @Inject constructor( val card = response?.card val fiatBalance = response?.balance?.fiat val cryptoBalance = response?.balance?.crypto + val availableForWithdrawal = response?.balance?.availableForWithdrawal val paymentAccount = response?.paymentAccount val cardInfo = if (paymentAccount != null && card != null && fiatBalance != null && cryptoBalance != null) { CardInfo( @@ -190,6 +192,7 @@ internal class DefaultOnboardingRepository @Inject constructor( tokenContractAddress = cryptoBalance.tokenContractAddress, balance = cryptoBalance.balance, ), + availableForWithdrawal = availableForWithdrawal?.amount.orZero(), ) } else { null diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt index 97af48e470..520c4c4328 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt @@ -115,6 +115,7 @@ sealed class PaymentAccountStatusValue { * @property depositAddress The address for deposits, if available. * @property fiatBalance The fiat balance details. * @property cryptoBalance The crypto balance details. + * @property availableForWithdrawal The crypto amount currently available for withdrawal/swap (excludes pending/locked funds). * @property cards The list of user's cards. */ @Serializable @@ -125,13 +126,14 @@ sealed class PaymentAccountStatusValue { val depositAddress: String?, val fiatBalance: FiatBalance, val cryptoBalance: CryptoBalance, + val availableForWithdrawal: SerializedBigDecimal, val cryptoCurrency: CryptoCurrency.Token, val cards: List, ) : PaymentAccountStatusValue() { val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( currency = cryptoCurrency, value = CryptoCurrencyStatus.Loaded( - amount = cryptoBalance.balance, + amount = availableForWithdrawal, fiatAmount = fiatBalance.availableBalance, fiatRate = BigDecimal.ONE, priceChange = BigDecimal.ZERO, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index 67942f98f2..ed28273924 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -82,5 +82,6 @@ data class CustomerInfo( val isPinSet: Boolean, val fiatBalance: PaymentAccountStatusValue.FiatBalance, val cryptoBalance: PaymentAccountStatusValue.CryptoBalance, + val availableForWithdrawal: BigDecimal, ) } \ No newline at end of file From b206bf28c56aecb52e1784493086996e990b80bf Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 May 2026 11:08:33 +0000 Subject: [PATCH 10/29] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index a0cb218ffd..76d48569f9 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.37-1508" +tangemBlockchainSdk = "releases-5.38-1512" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.37-608" +tangemCardSdk = "releases-5.38-615" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 9f6d8cb6b5dd1125b85a1f452c15c90627028ce1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 May 2026 16:50:03 +0500 Subject: [PATCH 11/29] Updated on 2026-08-14 --- .../TangemPayEditDisplayNameComponent.kt | 1 + .../tangempay/entity/TangemPayDetailsUM.kt | 1 + .../entity/TangemPayEditDisplayNameUM.kt | 1 + .../model/TangemPayEditDisplayNameModel.kt | 7 +++-- .../tangempay/ui/TangemPayCardDetailsBlock.kt | 17 ++++++----- .../tangempay/ui/TangemPayDetailsScreen.kt | 1 + .../ui/TangemPayEditDisplayNameScreen.kt | 28 ++++++------------- 7 files changed, 26 insertions(+), 30 deletions(-) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayEditDisplayNameComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayEditDisplayNameComponent.kt index 571c294824..162a780e99 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayEditDisplayNameComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayEditDisplayNameComponent.kt @@ -35,6 +35,7 @@ internal class TangemPayEditDisplayNameComponent( displayNameState = DisplayNameState.Editing( displayName = state.editingValue.text, editingValue = state.editingValue, + isSubmitEnabled = state.isDoneEnabled, onValueChanged = state.onValueChanged, onSubmit = state.onDoneClick, onDismiss = state.onDismiss, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt index 50fb7a2683..2a192392ec 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt @@ -47,6 +47,7 @@ internal sealed interface DisplayNameState { data class Editing( override val displayName: String, val editingValue: TextFieldValue, + val isSubmitEnabled: Boolean, val onValueChanged: (TextFieldValue) -> Unit, val onSubmit: () -> Unit, val onDismiss: () -> Unit, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEditDisplayNameUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEditDisplayNameUM.kt index 8d4c09a816..9885d4f337 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEditDisplayNameUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEditDisplayNameUM.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.text.input.TextFieldValue internal data class TangemPayEditDisplayNameUM( val editingValue: TextFieldValue, val isLoading: Boolean, + val isDoneEnabled: Boolean, val onValueChanged: (TextFieldValue) -> Unit, val onDoneClick: () -> Unit, val onDismiss: () -> Unit, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt index 352d5d1373..b9f9fe24d8 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt @@ -49,6 +49,7 @@ internal class TangemPayEditDisplayNameModel @Inject constructor( selection = TextRange(originalDisplayName.length), ), isLoading = false, + isDoneEnabled = true, onValueChanged = ::onValueChanged, onDoneClick = ::onDoneClick, onDismiss = ::onDismiss, @@ -84,9 +85,9 @@ internal class TangemPayEditDisplayNameModel @Inject constructor( } private fun onValueChanged(value: TextFieldValue) { - if (value.text.length <= CardDisplayName.MAX_LENGTH) { - uiState.update { it.copy(editingValue = value) } - } + val displayName = CardDisplayName(value.text) + val isAvailableForConfirm = displayName.isRight() + uiState.update { it.copy(editingValue = value, isDoneEnabled = isAvailableForConfirm) } } private fun onDoneClick() { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt index 0a58c2672a..18bf622b32 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt @@ -50,7 +50,6 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TangemPayTestTags -import com.tangem.domain.models.account.CardDisplayName import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.entity.DisplayNameState @@ -273,11 +272,7 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo BasicTextField( value = state.editingValue, - onValueChange = { newValue -> - if (newValue.text.length <= CardDisplayName.MAX_LENGTH) { - state.onValueChanged(newValue) - } - }, + onValueChange = state.onValueChanged, modifier = modifier .width(textWidthDp.coerceAtLeast(1.dp)) .focusRequester(focusRequester), @@ -285,7 +280,13 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo singleLine = true, cursorBrush = SolidColor(TangemTheme.colors.text.constantWhite), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { state.onSubmit() }), + keyboardActions = KeyboardActions( + onDone = if (state.isSubmitEnabled) { + { state.onSubmit() } + } else { + null + }, + ), decorationBox = { innerTextField -> Box { if (state.editingValue.text.isEmpty()) { @@ -481,6 +482,7 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide displayName = "Tangem", editingValue = TextFieldValue(text = "movet", selection = TextRange("movet".length)), onValueChanged = {}, + isSubmitEnabled = true, onSubmit = {}, onDismiss = {}, ), @@ -500,6 +502,7 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide displayName = "Tangem", editingValue = TextFieldValue(text = ""), onValueChanged = {}, + isSubmitEnabled = true, onSubmit = {}, onDismiss = {}, ), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index c6f426ec27..fabad9ef58 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -106,6 +106,7 @@ internal fun TangemPayDetailsScreen( TangemPayDetailsBalanceBlock( modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) + .padding(top = 12.dp) .fillMaxWidth(), state = state.balanceBlockState, isBalanceHidden = state.isBalanceHidden, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayEditDisplayNameScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayEditDisplayNameScreen.kt index 6acfea29a1..18008536a7 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayEditDisplayNameScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayEditDisplayNameScreen.kt @@ -1,25 +1,15 @@ package com.tangem.features.tangempay.ui import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp -import com.tangem.common.ui.navigationButtons.NavigationButton -import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton -import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent import com.tangem.features.tangempay.details.impl.R @@ -66,17 +56,15 @@ internal fun TangemPayEditDisplayNameScreen( Spacer(modifier = Modifier.weight(1f)) - NavigationPrimaryButton( + PrimaryButton( modifier = Modifier .imePadding() .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .fillMaxWidth(), - primaryButton = NavigationButton( - textReference = resourceReference(R.string.common_done), - onClick = state.onDoneClick, - shouldShowProgress = state.isLoading, - isEnabled = !state.isLoading && state.editingValue.text.isNotBlank(), - ), + text = stringResourceSafe(R.string.common_done), + onClick = state.onDoneClick, + showProgress = state.isLoading, + enabled = !state.isLoading && state.isDoneEnabled, ) } } \ No newline at end of file From a35305794a88083e6989a65eff0d7f066d4f691f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 May 2026 01:00:29 +0500 Subject: [PATCH 12/29] Updated on 2026-08-14 --- .../src/main/java/com/tangem/feature/swap/model/SwapModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 6755e4209a..839bc9a964 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -739,7 +739,7 @@ internal class SwapModel @Inject constructor( shouldSanitize = false, ) } - }.getOrNull() + }.getOrNull() ?: fromSwapCurrencyStatus.status } dataState = dataState.copy(feePaidCryptoCurrency = feePaidCryptoCurrency) From 42fb506a0177184863e7e5c256b06426920f3435 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 May 2026 18:27:43 +0100 Subject: [PATCH 13/29] Updated on 2026-08-14 --- .../navigation/email/AndroidEmailSender.kt | 10 +- .../navigation/email/EmailMessageTruncator.kt | 35 +++++++ .../navigation/email/EmailSenderModule.kt | 5 +- .../email/EmailMessageTruncatorTest.kt | 98 +++++++++++++++++++ .../features/details/model/DetailsModel.kt | 38 ++++--- 5 files changed, 169 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/core/navigation/email/EmailMessageTruncator.kt create mode 100644 app/src/test/kotlin/com/tangem/tap/core/navigation/email/EmailMessageTruncatorTest.kt diff --git a/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt b/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt index e0aea6d0e0..a8b0bd68d8 100644 --- a/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt +++ b/app/src/main/java/com/tangem/tap/core/navigation/email/AndroidEmailSender.kt @@ -1,11 +1,11 @@ package com.tangem.tap.core.navigation.email import android.content.Intent -import android.net.Uri import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ShareCompat import androidx.core.content.ContextCompat import androidx.core.content.FileProvider +import androidx.core.net.toUri import com.tangem.core.navigation.email.EmailSender import com.tangem.tap.foregroundActivityObserver import com.tangem.utils.logging.TangemLogger @@ -15,7 +15,9 @@ import com.tangem.utils.logging.TangemLogger * [REDACTED_AUTHOR] */ -internal class AndroidEmailSender : EmailSender { +internal class AndroidEmailSender( + private val messageTruncator: EmailMessageTruncator, +) : EmailSender { override fun send(email: EmailSender.Email, onFail: ((Exception) -> Unit)?) { val activity = foregroundActivityObserver.foregroundActivity @@ -26,7 +28,7 @@ internal class AndroidEmailSender : EmailSender { } val originalIntent = createEmailShareIntent(activity, email) - val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")) + val emailFilterIntent = Intent(Intent.ACTION_SENDTO, "mailto:".toUri()) val packageManager = activity.packageManager val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0) @@ -59,7 +61,7 @@ internal class AndroidEmailSender : EmailSender { .setType("message/rfc822") .setEmailTo(arrayOf(email.address)) .setSubject(email.subject) - .setText(email.message) + .setText(messageTruncator.truncate(email.message)) email.attachment?.let { file -> builder.setStream( diff --git a/app/src/main/java/com/tangem/tap/core/navigation/email/EmailMessageTruncator.kt b/app/src/main/java/com/tangem/tap/core/navigation/email/EmailMessageTruncator.kt new file mode 100644 index 0000000000..f7a4bb281d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/core/navigation/email/EmailMessageTruncator.kt @@ -0,0 +1,35 @@ +package com.tangem.tap.core.navigation.email + +import java.nio.ByteBuffer +import java.nio.charset.CodingErrorAction + +/** + * Truncates an email body so the resulting Intent fits inside the per-process Binder buffer (1 MB). + * + * The chooser fans the Intent out to every installed email client (with extras duplicated per target), + * so the body must be kept well below the raw 1 MB ceiling. + */ +internal class EmailMessageTruncator { + + fun truncate(message: String): String { + val bytes = message.toByteArray(Charsets.UTF_8) + if (bytes.size <= MAX_MESSAGE_BYTES) return message + + val suffix = TRUNCATION_SUFFIX_TEMPLATE.format(bytes.size) + val suffixBytes = suffix.toByteArray(Charsets.UTF_8).size + val cutSize = MAX_MESSAGE_BYTES - suffixBytes + + // Drop a partial UTF-8 sequence at the cut boundary rather than replacing it with U+FFFD + // (which is 3 bytes in UTF-8 and would push the result over the cap). + val decoder = Charsets.UTF_8.newDecoder().onMalformedInput(CodingErrorAction.IGNORE) + val head = decoder.decode(ByteBuffer.wrap(bytes, 0, cutSize)).toString() + return head + suffix + } + + private companion object { + // Chooser duplicates EXTRA_TEXT once per target email app (EXTRA_INITIAL_INTENTS), + // so parcel ≈ N × body. 20 KB clears the 1 MB Binder limit for up to ~30 mail clients. + const val MAX_MESSAGE_BYTES = 20_000 + const val TRUNCATION_SUFFIX_TEMPLATE = "\n\n…[truncated, original %d bytes]" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/core/navigation/email/EmailSenderModule.kt b/app/src/main/java/com/tangem/tap/di/core/navigation/email/EmailSenderModule.kt index b986cfe46d..0aacfd2742 100644 --- a/app/src/main/java/com/tangem/tap/di/core/navigation/email/EmailSenderModule.kt +++ b/app/src/main/java/com/tangem/tap/di/core/navigation/email/EmailSenderModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.core.navigation.email import com.tangem.core.navigation.email.EmailSender import com.tangem.tap.core.navigation.email.AndroidEmailSender +import com.tangem.tap.core.navigation.email.EmailMessageTruncator import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -14,5 +15,7 @@ internal object EmailSenderModule { @Provides @Singleton - fun provideEmailSender(): EmailSender = AndroidEmailSender() + fun provideEmailSender(): EmailSender = AndroidEmailSender( + messageTruncator = EmailMessageTruncator(), + ) } \ No newline at end of file diff --git a/app/src/test/kotlin/com/tangem/tap/core/navigation/email/EmailMessageTruncatorTest.kt b/app/src/test/kotlin/com/tangem/tap/core/navigation/email/EmailMessageTruncatorTest.kt new file mode 100644 index 0000000000..07cd8a2d33 --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/core/navigation/email/EmailMessageTruncatorTest.kt @@ -0,0 +1,98 @@ +package com.tangem.tap.core.navigation.email + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class EmailMessageTruncatorTest { + + private val truncator = EmailMessageTruncator() + + @Test + fun `empty message returned as-is`() { + val result = truncator.truncate("") + + assertThat(result).isEqualTo("") + } + + @Test + fun `message under cap returned unchanged`() { + val message = "small message" + + val result = truncator.truncate(message) + + assertThat(result).isEqualTo(message) + } + + @Test + fun `message exactly at cap returned unchanged`() { + val message = "a".repeat(MAX_MESSAGE_BYTES) + + val result = truncator.truncate(message) + + assertThat(result).isEqualTo(message) + } + + @Test + fun `message over cap is truncated to fit within cap in bytes`() { + val message = "a".repeat(MAX_MESSAGE_BYTES + 1_000) + + val result = truncator.truncate(message) + + assertThat(result.toByteArray(Charsets.UTF_8).size).isAtMost(MAX_MESSAGE_BYTES) + } + + @Test + fun `truncated message preserves the head of the original`() { + val head = "HEAD_MARKER_" + "x".repeat(100) + val tail = "y".repeat(MAX_MESSAGE_BYTES) + val message = head + tail + + val result = truncator.truncate(message) + + assertThat(result).startsWith(head) + } + + @Test + fun `truncated message ends with the truncation suffix`() { + val message = "a".repeat(MAX_MESSAGE_BYTES + 1_000) + + val result = truncator.truncate(message) + + assertThat(result).contains("[truncated, original ${message.length} bytes]") + } + + @Test + fun `truncation suffix reports original byte length not character length`() { + // Each emoji is 4 bytes in UTF-8. + val emoji = "😀" // 😀 + val message = emoji.repeat(MAX_MESSAGE_BYTES / 4 + 10) + val originalBytes = message.toByteArray(Charsets.UTF_8).size + + val result = truncator.truncate(message) + + assertThat(result).contains("[truncated, original $originalBytes bytes]") + } + + @Test + fun `multi-byte UTF-8 boundary stays within cap and produces valid output`() { + // Build a message where the cap falls inside a multi-byte char. + val emoji = "😀" // 😀, 4 bytes in UTF-8 + val message = emoji.repeat(MAX_MESSAGE_BYTES) // Way over cap. + + val result = truncator.truncate(message) + + // Partial trailing char is dropped (not replaced with U+FFFD which is 3 bytes and would + // push the result over the cap), so the result must stay within the cap and survive a + // UTF-8 round-trip. + val roundTripped = String(result.toByteArray(Charsets.UTF_8), Charsets.UTF_8) + assertThat(roundTripped).isEqualTo(result) + assertThat(result.toByteArray(Charsets.UTF_8).size).isAtMost(MAX_MESSAGE_BYTES) + } + + private companion object { + // Mirror the constant inside EmailMessageTruncator. Keep in sync if it changes there. + const val MAX_MESSAGE_BYTES = 20_000 + } +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index 840d7776a1..7127fd8a3b 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -124,18 +124,29 @@ internal class DetailsModel @Inject constructor( val metaInfo = getWalletMetaInfoUseCase(selectedUserWallet.walletId).getOrNull() ?: return@launch val visaCustomerId = getTangemPayCustomerIdUseCase(selectedUserWallet.walletId).getOrNull() + val coldVisaPredicate = { userWallet: UserWallet -> + userWallet is UserWallet.Cold && userWallet.scanResponse.card.isVisa && !visaCustomerId.isNullOrEmpty() + } + val hotWalletOrNotVisaPredicate = { userWallet: UserWallet -> + userWallet !is UserWallet.Cold || userWallet.scanResponse.card.isVisa.not() + } val feedbackType = when { - userWallets.all { - it is UserWallet.Cold && it.scanResponse.card.isVisa && !visaCustomerId.isNullOrEmpty() - } -> + userWallets.all(coldVisaPredicate) -> { FeedbackEmailType.Visa.DirectUserRequest( walletMetaInfo = metaInfo, customerId = requireNotNull(visaCustomerId), ) - userWallets.all { it !is UserWallet.Cold || it.scanResponse.card.isVisa.not() } -> - FeedbackEmailType.DirectUserRequest(metaInfo) + } + userWallets.all(hotWalletOrNotVisaPredicate) -> { + FeedbackEmailType.DirectUserRequest( + walletMetaInfo = metaInfo, + ) + } else -> { - showFeedbackEmailTypeOptionBS(selectedWalletMetaInfo = metaInfo, visaCustomerId = visaCustomerId) + showFeedbackEmailTypeOptionBS( + selectedWalletMetaInfo = metaInfo, + visaCustomerId = visaCustomerId, + ) return@launch } } @@ -161,8 +172,9 @@ internal class DetailsModel @Inject constructor( onDismissRequest = { state.update { current.copy( - selectFeedbackEmailTypeBSConfig = - current.selectFeedbackEmailTypeBSConfig.copy(isShown = false), + selectFeedbackEmailTypeBSConfig = current.selectFeedbackEmailTypeBSConfig.copy( + isShown = false, + ), ) } }, @@ -174,10 +186,12 @@ internal class DetailsModel @Inject constructor( visaCustomerId = visaCustomerId, ) - state.update { - it.copy( - selectFeedbackEmailTypeBSConfig = - it.selectFeedbackEmailTypeBSConfig.copy(isShown = false), + state.update { details -> + val hiddenConfig = details.selectFeedbackEmailTypeBSConfig.copy( + isShown = false, + ) + details.copy( + selectFeedbackEmailTypeBSConfig = hiddenConfig, ) } }, From 8098476fde0942dbed330fea71a2f36ac17ff5d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 May 2026 05:44:29 -0700 Subject: [PATCH 14/29] Updated on 2026-08-14 --- .../tangem/data/pay/di/TangemPayDataModule.kt | 24 +++ .../DefaultReissueCardRepository.kt | 28 +-- .../DefaultTangemPayCardDetailsRepository.kt | 179 ++++++------------ ...MockAwareTangemPayCardDetailsRepository.kt | 13 +- domain/visa/build.gradle.kts | 12 ++ .../tangem/domain/pay/model/OrderStatus.kt | 5 +- ...ssueOrderInfo.kt => TangemPayOrderInfo.kt} | 2 +- .../TangemPayCardDetailsRepository.kt | 11 +- .../TangemPayReissueCardRepository.kt | 6 +- .../usecase/ChangeCardFrozenStateUseCase.kt | 45 +++++ .../StartTangemPayOrderPollingUseCase.kt | 35 ++++ .../ChangeCardFrozenStateUseCaseTest.kt | 107 +++++++++++ .../StartTangemPayOrderPollingUseCaseTest.kt | 119 ++++++++++++ .../TangemPayReissueCardComponent.kt | 4 +- .../tangempay/model/TangemPayCardPageModel.kt | 49 ++--- 15 files changed, 459 insertions(+), 180 deletions(-) rename domain/visa/src/main/kotlin/com/tangem/domain/pay/model/{TangemPayReissueOrderInfo.kt => TangemPayOrderInfo.kt} (71%) create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ChangeCardFrozenStateUseCase.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCase.kt create mode 100644 domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/ChangeCardFrozenStateUseCaseTest.kt create mode 100644 domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCaseTest.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index ade825296a..aea6884b96 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -26,9 +26,11 @@ import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.flow.PaymentAccountStatusProducer import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.repository.* +import com.tangem.domain.pay.usecase.ChangeCardFrozenStateUseCase import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase +import com.tangem.domain.pay.usecase.StartTangemPayOrderPollingUseCase import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase @@ -172,5 +174,27 @@ internal interface TangemPayDataModule { ): ProduceTangemPayInitialDataUseCase { return ProduceTangemPayInitialDataUseCase(repository = repository) } + + @Provides + fun provideChangeCardFrozenStateUseCase( + cardDetailsRepository: TangemPayCardDetailsRepository, + startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase, + appCoroutineScope: AppCoroutineScope, + ): ChangeCardFrozenStateUseCase { + return ChangeCardFrozenStateUseCase( + cardDetailsRepository = cardDetailsRepository, + startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase, + appCoroutineScope = appCoroutineScope, + ) + } + + @Provides + @Singleton + fun provideStartTangemPayPollingUseCase( + cardDetailsRepository: TangemPayCardDetailsRepository, + paymentAccountStatusFetcher: PaymentAccountStatusFetcher, + ): StartTangemPayOrderPollingUseCase { + return StartTangemPayOrderPollingUseCase(cardDetailsRepository, paymentAccountStatusFetcher) + } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultReissueCardRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultReissueCardRepository.kt index 0e85fa2f63..a2295443b3 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultReissueCardRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultReissueCardRepository.kt @@ -7,12 +7,11 @@ import com.tangem.core.error.UniversalError import com.tangem.data.pay.util.OrderStatusConverter import com.tangem.datasource.api.pay.TangemPayApi import com.tangem.datasource.api.pay.models.request.ReissueCardRequest -import com.tangem.datasource.api.pay.models.response.OrderResponse import com.tangem.datasource.local.visa.TangemPayReissueCardStore import com.tangem.domain.models.pay.TangemPayReissueCardFee import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.model.OrderStatus -import com.tangem.domain.pay.model.TangemPayReissueOrderInfo +import com.tangem.domain.pay.model.TangemPayOrderInfo +import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayReissueCardRepository import com.tangem.domain.visa.error.VisaApiError import com.tangem.utils.coroutines.runSuspendCatching @@ -22,6 +21,7 @@ internal class DefaultReissueCardRepository @Inject constructor( private val tangemPayApi: TangemPayApi, private val requestHelper: TangemPayRequestPerformer, private val tangemPayReissueCardStore: TangemPayReissueCardStore, + private val cardDetailsRepository: TangemPayCardDetailsRepository, ) : TangemPayReissueCardRepository { override suspend fun getReissueCardFee(userWalletId: UserWalletId): Either = @@ -53,14 +53,14 @@ internal class DefaultReissueCardRepository @Inject constructor( override suspend fun reissueCard( userWalletId: UserWalletId, cardId: String, - ): Either = either { + ): Either = either { val response = requestHelper.performRequest(userWalletId) { authHeader -> tangemPayApi.reissueCard( authHeader = authHeader, body = ReissueCardRequest(cardId = cardId), ) }.bind() - TangemPayReissueOrderInfo( + TangemPayOrderInfo( orderId = response.result.orderId, orderStatus = OrderStatusConverter.convert(response.result.status), ) @@ -77,28 +77,14 @@ internal class DefaultReissueCardRepository @Inject constructor( override suspend fun getReissueOrderInfo( userWalletId: UserWalletId, cardId: String, - ): Either = either { + ): Either = either { val orderId = runSuspendCatching { tangemPayReissueCardStore.getOrderId(cardId) }.getOrNull() if (orderId == null) { return null.right() } - val order = requestHelper.performRequest(userWalletId) { authHeader -> - tangemPayApi.getOrder(authHeader, orderId) - }.bind() - - val result = order.result ?: raise(VisaApiError.Unspecified) - - TangemPayReissueOrderInfo( - orderId = result.id, - orderStatus = when (result.status) { - OrderResponse.Result.Status.NEW -> OrderStatus.NEW - OrderResponse.Result.Status.PROCESSING -> OrderStatus.PROCESSING - OrderResponse.Result.Status.COMPLETED -> OrderStatus.COMPLETED - OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED - }, - ) + cardDetailsRepository.getOrderInfo(userWalletId, orderId).bind() } private companion object { diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 97657f4c0d..cb621a3e3d 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -3,6 +3,7 @@ package com.tangem.data.pay.repository import arrow.core.Either import arrow.core.left import arrow.core.raise.catch +import arrow.core.raise.either import arrow.core.right import com.tangem.core.error.UniversalError import com.tangem.data.pay.util.RainCryptoUtil @@ -22,22 +23,17 @@ import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.models.account.CardDisplayName import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardDetails +import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayCardFrozenState -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.logging.TangemLogger -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds private const val TAG = "TangemPay: CardDetailsRepository" @@ -51,12 +47,8 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( private val storage: TangemPayStorage, private val cardFrozenStateStore: TangemPayCardFrozenStateStore, private val errorConverter: TangemPayErrorConverter, - private val pollingScope: AppCoroutineScope, ) : TangemPayCardDetailsRepository { - private val pollingJobs = mutableMapOf() - private val storePollingMutex = Mutex() - override suspend fun getCardBalance(userWalletId: UserWalletId): Either { return catch( block = { @@ -201,123 +193,43 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( override suspend fun freezeCard( userWalletId: UserWalletId, cardId: String, - ): Either { - cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Pending) - return requestHelper.performRequest(userWalletId) { + ): Either = either { + val response = requestHelper.performRequest(userWalletId) { tangemPayApi.freezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId)) - }.onLeft { - cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Unfrozen) - }.map { response -> - val state = when (response.result?.status) { - FreezeUnfreezeCardResponse.Status.COMPLETED -> TangemPayCardFrozenState.Frozen - FreezeUnfreezeCardResponse.Status.NEW, - FreezeUnfreezeCardResponse.Status.PROCESSING, - -> TangemPayCardFrozenState.Pending - FreezeUnfreezeCardResponse.Status.CANCELED, - null, - -> TangemPayCardFrozenState.Unfrozen - } - if (state == TangemPayCardFrozenState.Pending) { - startOrderIdPolling( - userWalletId = userWalletId, - cardId = cardId, - orderId = response.result?.orderId, - isFreeze = true, - ) - } - cardFrozenStateStore.store(cardId, state) + }.bind() - state - } + val result = response.result ?: raise(VisaApiError.Unspecified) + + TangemPayOrderInfo( + orderId = result.orderId, + orderStatus = when (result.status) { + FreezeUnfreezeCardResponse.Status.NEW -> OrderStatus.NEW + FreezeUnfreezeCardResponse.Status.PROCESSING -> OrderStatus.PROCESSING + FreezeUnfreezeCardResponse.Status.COMPLETED -> OrderStatus.COMPLETED + FreezeUnfreezeCardResponse.Status.CANCELED -> OrderStatus.CANCELED + }, + ) } override suspend fun unfreezeCard( userWalletId: UserWalletId, cardId: String, - ): Either { - cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Pending) - return requestHelper.performRequest(userWalletId) { + ): Either = either { + val response = requestHelper.performRequest(userWalletId) { tangemPayApi.unfreezeCard(authHeader = it, body = FreezeUnfreezeCardRequest(cardId = cardId)) - }.onLeft { - cardFrozenStateStore.store(cardId, TangemPayCardFrozenState.Frozen) - }.map { response -> - val state = when (response.result?.status) { - FreezeUnfreezeCardResponse.Status.COMPLETED -> TangemPayCardFrozenState.Unfrozen - FreezeUnfreezeCardResponse.Status.NEW, - FreezeUnfreezeCardResponse.Status.PROCESSING, - -> TangemPayCardFrozenState.Pending - FreezeUnfreezeCardResponse.Status.CANCELED, - null, - -> TangemPayCardFrozenState.Frozen - } - if (state == TangemPayCardFrozenState.Pending) { - startOrderIdPolling( - userWalletId = userWalletId, - cardId = cardId, - orderId = response.result?.orderId, - isFreeze = false, - ) - } - cardFrozenStateStore.store(cardId, state) + }.bind() - state - } - } + val result = response.result ?: raise(VisaApiError.Unspecified) - private suspend fun startOrderIdPolling( - userWalletId: UserWalletId, - cardId: String, - orderId: String?, - isFreeze: Boolean, - ) { - if (orderId.isNullOrEmpty()) return - storePollingMutex.withLock { - if (pollingJobs.containsKey(orderId)) return - val pollingJob = pollingScope.launch { - try { - var retryCount = 0 - while (isActive && pollingJobs.containsKey(orderId)) { - delay(duration = 5.seconds) - - val orderStatus = requestHelper.performRequest(userWalletId) { authHeader -> - tangemPayApi.getOrder(authHeader, orderId) - } - - orderStatus.onRight { response -> - val status = response.result?.status - if (status == Status.COMPLETED || status == Status.CANCELED) { - // Remove from jobs - pollingJobs.remove(key = orderId) - - // Final card state - val finalState = when { - status == Status.COMPLETED && isFreeze - -> TangemPayCardFrozenState.Frozen - status == Status.COMPLETED && !isFreeze - -> TangemPayCardFrozenState.Unfrozen - else -> return@launch - } - - cardFrozenStateStore.store(cardId, finalState) - } - }.onLeft { error -> - TangemLogger.e("error ${error.errorCode}") - // stop retrying after 3 errors - if (retryCount > MAX_POLLING_RETRIES) { - pollingJobs.remove(key = orderId) - } - } - retryCount++ - } - } catch (e: Exception) { - TangemLogger.e("Error", e) - storePollingMutex.withLock { - pollingJobs.remove(orderId) - } - } - } - pollingJobs[orderId] = pollingJob - } + TangemPayOrderInfo( + orderId = result.orderId, + orderStatus = when (result.status) { + FreezeUnfreezeCardResponse.Status.NEW -> OrderStatus.NEW + FreezeUnfreezeCardResponse.Status.PROCESSING -> OrderStatus.PROCESSING + FreezeUnfreezeCardResponse.Status.COMPLETED -> OrderStatus.COMPLETED + FreezeUnfreezeCardResponse.Status.CANCELED -> OrderStatus.CANCELED + }, + ) } override suspend fun updateCardDisplayName( @@ -376,6 +288,31 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( return cardFrozenStateStore.getSyncOrNull(cardId) } + override suspend fun setCardFrozenState(cardId: String, state: TangemPayCardFrozenState) { + cardFrozenStateStore.store(cardId, state) + } + + override suspend fun getOrderInfo( + userWalletId: UserWalletId, + orderId: String, + ): Either = either { + val order = requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.getOrder(authHeader, orderId) + }.bind() + + val result = order.result ?: raise(VisaApiError.Unspecified) + + TangemPayOrderInfo( + orderId = result.id, + orderStatus = when (result.status) { + Status.NEW -> OrderStatus.NEW + Status.PROCESSING -> OrderStatus.PROCESSING + Status.COMPLETED -> OrderStatus.COMPLETED + Status.CANCELED -> OrderStatus.CANCELED + }, + ) + } + private suspend fun getPublicKeyBase64(): String { val env = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemPay).environment return when (env) { @@ -395,8 +332,4 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( TangemLogger.withTag(TAG).e("Error", throwable) return errorConverter.convert(throwable).left() } - - private companion object { - const val MAX_POLLING_RETRIES = 3 - } } \ No newline at end of file diff --git a/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareTangemPayCardDetailsRepository.kt b/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareTangemPayCardDetailsRepository.kt index fadd88988c..17a322804b 100644 --- a/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareTangemPayCardDetailsRepository.kt +++ b/data/visa/src/mocked/kotlin/com/tangem/data/pay/repository/MockAwareTangemPayCardDetailsRepository.kt @@ -11,6 +11,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardDetails +import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.visa.model.TangemPayCardFrozenState import kotlinx.coroutines.flow.Flow @@ -65,12 +66,12 @@ internal class MockAwareTangemPayCardDetailsRepository @Inject constructor( override suspend fun freezeCard( userWalletId: UserWalletId, cardId: String, - ): Either = real.freezeCard(userWalletId, cardId) + ): Either = real.freezeCard(userWalletId, cardId) override suspend fun unfreezeCard( userWalletId: UserWalletId, cardId: String, - ): Either = real.unfreezeCard(userWalletId, cardId) + ): Either = real.unfreezeCard(userWalletId, cardId) override fun cardFrozenState(cardId: String): Flow = real.cardFrozenState(cardId) @@ -78,6 +79,14 @@ internal class MockAwareTangemPayCardDetailsRepository @Inject constructor( override suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState? = real.cardFrozenStateSync(cardId) + override suspend fun setCardFrozenState(cardId: String, state: TangemPayCardFrozenState) = + real.setCardFrozenState(cardId, state) + + override suspend fun getOrderInfo( + userWalletId: UserWalletId, + orderId: String, + ): Either = real.getOrderInfo(userWalletId, orderId) + override suspend fun updateCardDisplayName( cardId: String, userWalletId: UserWalletId, diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index ec2d1affc7..47c4e35082 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -9,6 +9,10 @@ android { namespace = "com.tangem.domain.visa" } +tasks.withType().configureEach { + useJUnitPlatform() +} + dependencies { /** Project - Core */ api(projects.core.pagination) @@ -34,4 +38,12 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) ksp(deps.moshi.kotlin.codegen) + + /** Tests */ + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(projects.common.test) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt index fd3d717107..035572167a 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt @@ -5,4 +5,7 @@ enum class OrderStatus { PROCESSING, COMPLETED, CANCELED, -} \ No newline at end of file +} + +val OrderStatus.isFinalStatus + get() = this == OrderStatus.COMPLETED || this == OrderStatus.CANCELED \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayReissueOrderInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayOrderInfo.kt similarity index 71% rename from domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayReissueOrderInfo.kt rename to domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayOrderInfo.kt index 45945f7204..eebc510bd5 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayReissueOrderInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/TangemPayOrderInfo.kt @@ -1,6 +1,6 @@ package com.tangem.domain.pay.model -data class TangemPayReissueOrderInfo( +data class TangemPayOrderInfo( val orderId: String, val orderStatus: OrderStatus, ) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt index 6778223272..b3eed00dba 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt @@ -7,6 +7,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardDetails +import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.domain.visa.model.TangemPayCardFrozenState import kotlinx.coroutines.flow.Flow @@ -24,14 +25,12 @@ interface TangemPayCardDetailsRepository { suspend fun setAddToWalletAsDone(userWalletId: UserWalletId): Either - suspend fun freezeCard(userWalletId: UserWalletId, cardId: String): Either - suspend fun unfreezeCard( - userWalletId: UserWalletId, - cardId: String, - ): Either + suspend fun freezeCard(userWalletId: UserWalletId, cardId: String): Either + suspend fun unfreezeCard(userWalletId: UserWalletId, cardId: String): Either fun cardFrozenState(cardId: String): Flow suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState? + suspend fun setCardFrozenState(cardId: String, state: TangemPayCardFrozenState) suspend fun updateCardDisplayName( cardId: String, @@ -44,4 +43,6 @@ interface TangemPayCardDetailsRepository { userWalletId: UserWalletId, limit: String, ): Either + + suspend fun getOrderInfo(userWalletId: UserWalletId, orderId: String): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayReissueCardRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayReissueCardRepository.kt index 3f756df4ee..1487f57359 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayReissueCardRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayReissueCardRepository.kt @@ -4,19 +4,19 @@ import arrow.core.Either import com.tangem.core.error.UniversalError import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.pay.TangemPayReissueCardFee -import com.tangem.domain.pay.model.TangemPayReissueOrderInfo +import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.domain.visa.error.VisaApiError interface TangemPayReissueCardRepository { suspend fun getReissueCardFee(userWalletId: UserWalletId): Either - suspend fun reissueCard(userWalletId: UserWalletId, cardId: String): Either + suspend fun reissueCard(userWalletId: UserWalletId, cardId: String): Either suspend fun storeReissueOrderId(cardId: String, orderId: String): Either suspend fun getReissueOrderInfo( userWalletId: UserWalletId, cardId: String, - ): Either + ): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ChangeCardFrozenStateUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ChangeCardFrozenStateUseCase.kt new file mode 100644 index 0000000000..3aeb3e865f --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ChangeCardFrozenStateUseCase.kt @@ -0,0 +1,45 @@ +package com.tangem.domain.pay.usecase + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.core.error.UniversalError +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.utils.coroutines.AppCoroutineScope +import kotlinx.coroutines.async + +class ChangeCardFrozenStateUseCase( + private val cardDetailsRepository: TangemPayCardDetailsRepository, + private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase, + private val appCoroutineScope: AppCoroutineScope, +) { + suspend operator fun invoke( + userWalletId: UserWalletId, + cardId: String, + isFreezing: Boolean, + ): Either { + val successState = if (isFreezing) TangemPayCardFrozenState.Frozen else TangemPayCardFrozenState.Unfrozen + val failState = if (isFreezing) TangemPayCardFrozenState.Unfrozen else TangemPayCardFrozenState.Frozen + return either { + cardDetailsRepository.setCardFrozenState(cardId, TangemPayCardFrozenState.Pending) + + val order = if (isFreezing) { + cardDetailsRepository.freezeCard(userWalletId, cardId).bind() + } else { + cardDetailsRepository.unfreezeCard(userWalletId, cardId).bind() + } + + val isCompleted = appCoroutineScope.async { + val isCompleted = startTangemPayOrderPollingUseCase(order, userWalletId) + cardDetailsRepository.setCardFrozenState(cardId, if (isCompleted) successState else failState) + isCompleted + }.await() + + if (!isCompleted) raise(VisaApiError.Unspecified) + }.onLeft { + cardDetailsRepository.setCardFrozenState(cardId, failState) + } + } +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCase.kt new file mode 100644 index 0000000000..50ea3fdfd9 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCase.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.pay.usecase + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.model.TangemPayOrderInfo +import com.tangem.domain.pay.model.isFinalStatus +import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import kotlinx.coroutines.delay + +class StartTangemPayOrderPollingUseCase( + private val cardDetailsRepository: TangemPayCardDetailsRepository, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, +) { + suspend operator fun invoke(order: TangemPayOrderInfo, userWalletId: UserWalletId): Boolean { + while (true) { + val newOrder = if (order.orderStatus.isFinalStatus) { + order + } else { + cardDetailsRepository.getOrderInfo(userWalletId, order.orderId).getOrNull() + } + + if (newOrder != null && newOrder.orderStatus.isFinalStatus) { + paymentAccountStatusFetcher.invoke(userWalletId) + return newOrder.orderStatus == OrderStatus.COMPLETED + } + + delay(POLLING_DELAY) + } + } + + companion object { + private const val POLLING_DELAY = 3000L + } +} \ No newline at end of file diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/ChangeCardFrozenStateUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/ChangeCardFrozenStateUseCaseTest.kt new file mode 100644 index 0000000000..67fd67b1a5 --- /dev/null +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/ChangeCardFrozenStateUseCaseTest.kt @@ -0,0 +1,107 @@ +package com.tangem.domain.pay.usecase + +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth.assertThat +import com.tangem.common.test.TestAppCoroutineScope +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.model.TangemPayOrderInfo +import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.domain.visa.model.TangemPayCardFrozenState +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.mockk +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +internal class ChangeCardFrozenStateUseCaseTest { + + private val cardDetailsRepository: TangemPayCardDetailsRepository = mockk(relaxUnitFun = true) + private val startPollingUseCase: StartTangemPayOrderPollingUseCase = mockk() + + @Test + fun `GIVEN freezeCard fails WHEN invoke with isFreezing=true THEN sets Pending then Unfrozen and returns Left`() = + runTest { + val useCase = createUseCase() + coEvery { + cardDetailsRepository.freezeCard(USER_WALLET_ID, CARD_ID) + } returns VisaApiError.Unspecified.left() + + val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = true) + + assertThat(result.isLeft()).isTrue() + coVerifyOrder { + cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending) + cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Unfrozen) + } + coVerify(exactly = 0) { startPollingUseCase(any(), any()) } + } + + @Test + fun `GIVEN unfreezeCard fails WHEN invoke with isFreezing=false THEN sets Pending then Frozen and returns Left`() = + runTest { + val useCase = createUseCase() + coEvery { + cardDetailsRepository.unfreezeCard(USER_WALLET_ID, CARD_ID) + } returns VisaApiError.Unspecified.left() + + val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = false) + + assertThat(result.isLeft()).isTrue() + coVerifyOrder { + cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending) + cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Frozen) + } + coVerify(exactly = 0) { startPollingUseCase(any(), any()) } + } + + @Test + fun `GIVEN freeze succeeds and order COMPLETED WHEN invoke with isFreezing=true THEN sets Frozen and returns Right`() = + runTest { + val useCase = createUseCase() + val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED) + coEvery { cardDetailsRepository.freezeCard(USER_WALLET_ID, CARD_ID) } returns order.right() + coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true + + val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = true) + + assertThat(result.isRight()).isTrue() + coVerifyOrder { + cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending) + cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Frozen) + } + } + + @Test + fun `GIVEN unfreeze succeeds and order COMPLETED WHEN invoke with isFreezing=false THEN sets Unfrozen and returns Right`() = + runTest { + val useCase = createUseCase() + val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED) + coEvery { cardDetailsRepository.unfreezeCard(USER_WALLET_ID, CARD_ID) } returns order.right() + coEvery { startPollingUseCase(order, USER_WALLET_ID) } returns true + + val result = useCase(USER_WALLET_ID, CARD_ID, isFreezing = false) + + assertThat(result.isRight()).isTrue() + coVerifyOrder { + cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Pending) + cardDetailsRepository.setCardFrozenState(CARD_ID, TangemPayCardFrozenState.Unfrozen) + } + } + + private fun TestScope.createUseCase() = ChangeCardFrozenStateUseCase( + cardDetailsRepository = cardDetailsRepository, + startTangemPayOrderPollingUseCase = startPollingUseCase, + appCoroutineScope = TestAppCoroutineScope(this), + ) + + private companion object { + val USER_WALLET_ID = UserWalletId("aabbcc112233") + const val CARD_ID = "card-test-id" + const val ORDER_ID = "order-test-1" + } +} \ No newline at end of file diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCaseTest.kt new file mode 100644 index 0000000000..86ab6a5bf2 --- /dev/null +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/StartTangemPayOrderPollingUseCaseTest.kt @@ -0,0 +1,119 @@ +package com.tangem.domain.pay.usecase + +import arrow.core.left +import arrow.core.right +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.model.TangemPayOrderInfo +import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.visa.error.VisaApiError +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +internal class StartTangemPayOrderPollingUseCaseTest { + + private val cardDetailsRepository: TangemPayCardDetailsRepository = mockk() + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk() + + private val useCase = StartTangemPayOrderPollingUseCase( + cardDetailsRepository = cardDetailsRepository, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, + ) + + @Test + fun `GIVEN order already COMPLETED WHEN invoke THEN returns true without polling`() = runTest { + val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED) + coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right() + + val result = useCase(order, USER_WALLET_ID) + + assertThat(result).isTrue() + coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } + coVerify(exactly = 0) { cardDetailsRepository.getOrderInfo(any(), any()) } + } + + @Test + fun `GIVEN order already CANCELED WHEN invoke THEN returns false without polling`() = runTest { + val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.CANCELED) + coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right() + + val result = useCase(order, USER_WALLET_ID) + + assertThat(result).isFalse() + coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } + coVerify(exactly = 0) { cardDetailsRepository.getOrderInfo(any(), any()) } + } + + @Test + fun `GIVEN processing order WHEN poll returns COMPLETED THEN returns true and fetches status`() = runTest { + val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING) + coEvery { + cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) + } returns TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED).right() + coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right() + + val result = useCase(order, USER_WALLET_ID) + + assertThat(result).isTrue() + coVerify(exactly = 1) { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) } + coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } + } + + @Test + fun `GIVEN processing order WHEN poll returns CANCELED THEN returns false and fetches status`() = runTest { + val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING) + coEvery { + cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) + } returns TangemPayOrderInfo(ORDER_ID, OrderStatus.CANCELED).right() + coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right() + + val result = useCase(order, USER_WALLET_ID) + + assertThat(result).isFalse() + coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } + } + + @Test + fun `GIVEN new order WHEN getOrderInfo fails once then returns COMPLETED THEN returns true after two polls`() = runTest { + val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.NEW) + coEvery { + cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) + } returnsMany listOf( + VisaApiError.Unspecified.left(), + TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED).right(), + ) + coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right() + + val result = useCase(order, USER_WALLET_ID) + + assertThat(result).isTrue() + coVerify(exactly = 2) { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) } + } + + @Test + fun `GIVEN processing order WHEN multiple non-final polls then COMPLETED THEN returns true after all polls`() = runTest { + val order = TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING) + coEvery { + cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) + } returnsMany listOf( + TangemPayOrderInfo(ORDER_ID, OrderStatus.PROCESSING).right(), + TangemPayOrderInfo(ORDER_ID, OrderStatus.NEW).right(), + TangemPayOrderInfo(ORDER_ID, OrderStatus.COMPLETED).right(), + ) + coEvery { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } returns Unit.right() + + val result = useCase(order, USER_WALLET_ID) + + assertThat(result).isTrue() + coVerify(exactly = 3) { cardDetailsRepository.getOrderInfo(USER_WALLET_ID, ORDER_ID) } + coVerify(exactly = 1) { paymentAccountStatusFetcher.invoke(USER_WALLET_ID) } + } + + private companion object { + val USER_WALLET_ID = UserWalletId("aabbcc112233") + const val ORDER_ID = "order-test-1" + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayReissueCardComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayReissueCardComponent.kt index 13f1c7c061..918ad5d9af 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayReissueCardComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayReissueCardComponent.kt @@ -7,7 +7,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.model.TangemPayReissueOrderInfo +import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.features.tangempay.model.TangemPayReissueCardModel import com.tangem.features.tangempay.ui.TangemPayReissueCardContent @@ -34,7 +34,7 @@ internal class TangemPayReissueCardComponent( } internal interface ReissueCardListener { - fun onReissueOrderCreate(order: TangemPayReissueOrderInfo) + fun onReissueOrderCreate(order: TangemPayOrderInfo) fun onDismissReissueCard() fun onClickAddFunds() } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt index f5c7e0a5e5..c0f9d07ad2 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt @@ -19,6 +19,7 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode import com.tangem.core.ui.format.bigdecimal.optionalDecimals import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.core.ui.test.TangemPayTestTags import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.account.PaymentAccountStatusValue @@ -28,12 +29,12 @@ import com.tangem.domain.models.pay.TangemPayCard import com.tangem.domain.models.pay.TangemPayCardLimitPeriod import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.model.OrderStatus -import com.tangem.domain.pay.model.TangemPayReissueOrderInfo +import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayReissueCardRepository +import com.tangem.domain.pay.usecase.ChangeCardFrozenStateUseCase import com.tangem.domain.tangempay.TangemPayAnalyticsEvents -import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.features.tangempay.components.AddFundsListener import com.tangem.features.tangempay.components.ReissueCardListener import com.tangem.features.tangempay.components.TangemPayCardPageComponent @@ -63,12 +64,14 @@ internal class TangemPayCardPageModel @Inject constructor( private val cardDetailsRepository: TangemPayCardDetailsRepository, private val uiMessageSender: UiMessageSender, private val reissueCardRepository: TangemPayReissueCardRepository, + private val changeCardFrozenStateUseCase: ChangeCardFrozenStateUseCase, ) : Model(), ViewPinListener, ReissueCardListener, AddFundsListener { private val params: TangemPayCardPageComponent.Params = paramsContainer.require() private val addToWalletBannerJobHolder = JobHolder() private val addFundsJobHolder = JobHolder() + private val frozenStateJobHolder = JobHolder() val uiState: StateFlow field = MutableStateFlow( @@ -119,12 +122,18 @@ internal class TangemPayCardPageModel @Inject constructor( TangemPayCardPageSetting( title = TextReference.Res(R.string.tangempay_card_details_change_pin), onSettingClick = { onClickChangePIN(card.hasPinCode) }, - testTag = com.tangem.core.ui.test.TangemPayTestTags.CHANGE_PIN_ROW, + testTag = TangemPayTestTags.CHANGE_PIN_ROW, ), TangemPayCardPageSetting( - title = TextReference.Res(R.string.tangempay_card_details_freeze_card), + title = TextReference.Res( + if (card.isFrozen) { + R.string.tangempay_card_details_unfreeze_card + } else { + R.string.tangempay_card_details_freeze_card + }, + ), onSettingClick = { onClickFreezeOrUnfreezeCard(card.isFrozen) }, - testTag = com.tangem.core.ui.test.TangemPayTestTags.FREEZE_CARD_ROW, + testTag = TangemPayTestTags.FREEZE_CARD_ROW, ), TangemPayCardPageSetting( title = TextReference.Res(R.string.tangempay_card_details_reissue_card), @@ -152,6 +161,8 @@ internal class TangemPayCardPageModel @Inject constructor( } private fun onClickFreezeOrUnfreezeCard(isFrozen: Boolean) { + if (frozenStateJobHolder.isActive) return + val message = if (isFrozen) { TangemPayMessagesFactory.createUnfreezeCardMessage(onUnfreezeClicked = ::unfreezeCard) } else { @@ -165,7 +176,7 @@ internal class TangemPayCardPageModel @Inject constructor( bottomSheetNavigation.activate(TangemPayCardNavigation.ReissueCard) } - override fun onReissueOrderCreate(order: TangemPayReissueOrderInfo) { + override fun onReissueOrderCreate(order: TangemPayOrderInfo) { bottomSheetNavigation.dismiss() onReissueOrderStatusReceived(order.orderStatus) if (order.orderStatus != OrderStatus.CANCELED) { @@ -238,40 +249,34 @@ internal class TangemPayCardPageModel @Inject constructor( private fun freezeCard() { modelScope.launch { - cardDetailsRepository.freezeCard( + changeCardFrozenStateUseCase( userWalletId = params.userWalletId, cardId = params.config.cardId, + isFreezing = true, ).onLeft { val message = SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_failed)) uiMessageSender.send(message) - }.onRight { state -> - val message = if (state == TangemPayCardFrozenState.Frozen) { - SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_success)) - } else { - SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_failed)) - } + }.onRight { + val message = SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_success)) uiMessageSender.send(message) } - } + }.saveIn(frozenStateJobHolder) } private fun unfreezeCard() { modelScope.launch { - cardDetailsRepository.unfreezeCard( + changeCardFrozenStateUseCase( userWalletId = params.userWalletId, cardId = params.config.cardId, + isFreezing = false, ).onLeft { val message = SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_failed)) uiMessageSender.send(message) - }.onRight { state -> - val message = if (state == TangemPayCardFrozenState.Unfrozen) { - SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_success)) - } else { - SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_failed)) - } + }.onRight { + val message = SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_success)) uiMessageSender.send(message) } - } + }.saveIn(frozenStateJobHolder) } private fun fetchAddToWalletBanner() { From e257706f0f57a53287562c2510f004f9bed66eb8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 May 2026 14:03:33 +0500 Subject: [PATCH 15/29] Updated on 2026-08-14 --- .../tangem/feature/swap/model/SwapModel.kt | 11 +---- .../swap/model/SwapNotificationsFactory.kt | 45 ++++++++++++++----- .../tangem/feature/swap/models/UiActions.kt | 1 - .../swap/models/states/SwapNotificationUM.kt | 11 +++-- .../tangem/feature/swap/ui/StateBuilder.kt | 3 ++ .../swap/StateBuilderInitialStateTest.kt | 3 ++ .../feature/swap/StateBuilderPairsTest.kt | 3 ++ .../feature/swap/StateBuilderQuotesTest.kt | 6 +-- .../feature/swap/StateBuilderSwapDataTest.kt | 5 ++- 9 files changed, 58 insertions(+), 30 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 6755e4209a..687fd60f96 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -185,6 +185,7 @@ internal class SwapModel @Inject constructor( appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), isAccountsModeProvider = Provider { isAccountsMode }, iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork, + appRouter = appRouter, ) private val inputNumberFormatter = InputNumberFormatter( @@ -1499,16 +1500,6 @@ internal class SwapModel @Inject constructor( ) } }, - onBuyClick = { - val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus ?: return@UiActions - val feePaidCryptoCurrency = dataState.feePaidCryptoCurrency ?: return@UiActions - val route = AppRoute.CurrencyDetails( - userWalletId = fromSwapCurrencyStatus.userWalletId, - currency = feePaidCryptoCurrency.currency, - ) - - appRouter.push(route) - }, onRetryClick = { startLoadingQuotesFromLastState() }, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 6f203e2e72..f4199313f4 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -1,5 +1,7 @@ package com.tangem.feature.swap.model +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification @@ -106,6 +108,7 @@ internal class SwapNotificationsFactory( selectedFeeType: FeeType, providerName: String, hideFee: Boolean, + appRouter: AppRouter, ): ImmutableList { val warnings = buildList { maybeAddRentExemptionError(quoteModel) @@ -113,7 +116,12 @@ internal class SwapNotificationsFactory( maybeAddNeedReserveToCreateAccountWarning(quoteModel) maybeAddPermissionNeededWarning(quoteModel, providerName) maybeAddNetworkFeeCoverageWarning(quoteModel, selectedFeeType) - maybeAddUnableCoverFeeWarning(quoteModel, feeCryptoCurrencyStatus, hideFee) + maybeAddUnableCoverFeeWarning( + quoteModel = quoteModel, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + hideFee = hideFee, + appRouter = appRouter, + ) maybeAddTransactionInProgressWarning(quoteModel) maybeAddPriceImpactNotification(quoteModel.priceImpact) } @@ -299,17 +307,20 @@ internal class SwapNotificationsFactory( } } + @Suppress("CyclomaticComplexMethod") private fun MutableList.maybeAddUnableCoverFeeWarning( quoteModel: SwapState.QuotesLoadedState, feeCryptoCurrencyStatus: CryptoCurrencyStatus?, hideFee: Boolean, + appRouter: AppRouter, ) { - if (hideFee) return - val fromCurrency = quoteModel.fromTokenInfo.swapCurrencyStatus.currency + if (hideFee || feeCryptoCurrencyStatus == null) return + val fromSwapCurrency = quoteModel.fromTokenInfo.swapCurrencyStatus + val fromCurrency = fromSwapCurrency.currency val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough val shouldShowCoverWarning = !quoteModel.preparedSwapConfigState.isBalanceEnough && quoteModel.permissionState !is PermissionDataState.PermissionLoading && - feeCryptoCurrencyStatus?.currency != fromCurrency + feeCryptoCurrencyStatus.currency != fromCurrency val isCEXProvider = quoteModel.swapProvider.type == ExchangeProviderType.CEX @@ -320,13 +331,25 @@ internal class SwapNotificationsFactory( if (shouldShowCoverWarning && !isGaslessAvailable || isNotEnoughFee) { add( - SwapNotificationUM.Error.UnableToCoverFeeWarning( - fromToken = fromCurrency, - feeCurrency = feeCryptoCurrencyStatus?.currency, - currencyName = feeEnoughState?.currencyName ?: fromCurrency.network.name, - currencySymbol = feeEnoughState?.currencySymbol ?: fromCurrency.network.currencySymbol, - onConfirmClick = actions.onBuyClick, - ), + if (fromCurrency.id == feeCryptoCurrencyStatus.currency.id) { + SwapNotificationUM.Error.InsufficientFunds + } else { + val route = AppRoute.CurrencyDetails( + userWalletId = fromSwapCurrency.userWalletId, + currency = feeCryptoCurrencyStatus.currency, + ) + SwapNotificationUM.Error.UnableToCoverFeeWarning( + fromToken = fromCurrency, + feeCurrency = feeCryptoCurrencyStatus.currency, + currencyName = feeEnoughState?.currencyName ?: fromCurrency.network.name, + currencySymbol = feeEnoughState?.currencySymbol ?: fromCurrency.network.currencySymbol, + onConfirmClick = if (!appRouter.stack.contains(route)) { + { appRouter.push(route) } + } else { + null + }, + ) + }, ) } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt index ac922cd6f9..73cb3aa224 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -20,7 +20,6 @@ internal data class UiActions( val onSelectFeeType: (TxFee.Legacy) -> Unit, val onProviderClick: (String) -> Unit, val onProviderSelect: (String) -> Unit, - val onBuyClick: () -> Unit, val onSelectTokenClick: (TokenSelectionDirection) -> Unit, val onSuccess: () -> Unit, val onLinkClick: (String) -> Unit, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt index 1cb665edc9..476edcbc27 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt @@ -55,12 +55,17 @@ internal object SwapNotificationUM { ), ) + data object InsufficientFunds : Error( + title = resourceReference(R.string.swapping_insufficient_funds), + subtitle = resourceReference(R.string.swapping_insufficient_funds_description), + ) + data class UnableToCoverFeeWarning( val fromToken: CryptoCurrency, val currencyName: String, val currencySymbol: String, - val feeCurrency: CryptoCurrency?, - val onConfirmClick: () -> Unit, + val feeCurrency: CryptoCurrency, + val onConfirmClick: (() -> Unit)?, ) : Error( title = resourceReference( R.string.warning_express_not_enough_fee_for_token_tx_title, @@ -71,7 +76,7 @@ internal object SwapNotificationUM { wrappedList(currencyName, currencySymbol), ), iconResId = fromToken.networkIconResId, - buttonState = feeCurrency?.let { + buttonState = onConfirmClick?.let { NotificationConfig.ButtonsState.SecondaryButtonConfig( text = resourceReference(R.string.common_buy_currency, wrappedList(currencySymbol)), onClick = onConfirmClick, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index deb2acdde2..e1b77b1fc2 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.ui import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.common.routing.AppRouter import com.tangem.common.ui.account.AccountIconUM import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter @@ -58,6 +59,7 @@ internal class StateBuilder( private val appCurrencyProvider: Provider, private val isAccountsModeProvider: Provider, private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork, + private val appRouter: AppRouter, ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) @@ -463,6 +465,7 @@ internal class StateBuilder( selectedFeeType = selectedFeeType, providerName = swapProvider.name, hideFee = hideFee, + appRouter = appRouter, ) val fromAccountTitleUM = when { diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderInitialStateTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderInitialStateTest.kt index e8724ad601..c59fbb8255 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderInitialStateTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderInitialStateTest.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap import com.google.common.truth.Truth.assertThat +import com.tangem.common.routing.AppRouter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.account.Account @@ -31,6 +32,7 @@ internal class StateBuilderInitialStateTest { private val appCurrencyProvider: Provider = mockk() private val isAccountsModeProvider: Provider = mockk() private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk() + private val appRouter: AppRouter = mockk() private lateinit var sut: StateBuilder @@ -48,6 +50,7 @@ internal class StateBuilderInitialStateTest { appCurrencyProvider = appCurrencyProvider, isAccountsModeProvider = isAccountsModeProvider, iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork, + appRouter = appRouter ) } diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderPairsTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderPairsTest.kt index c1f52f0e5d..6a4bd0b2e2 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderPairsTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderPairsTest.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap import com.google.common.truth.Truth.assertThat +import com.tangem.common.routing.AppRouter import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.wallet.UserWallet @@ -26,6 +27,7 @@ internal class StateBuilderPairsTest { private val appCurrencyProvider: Provider = mockk() private val isAccountsModeProvider: Provider = mockk() private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk() + private val appRouter: AppRouter = mockk() private lateinit var sut: StateBuilder @@ -53,6 +55,7 @@ internal class StateBuilderPairsTest { appCurrencyProvider = appCurrencyProvider, isAccountsModeProvider = isAccountsModeProvider, iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork, + appRouter = appRouter, ) } diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderQuotesTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderQuotesTest.kt index e335298820..fcc7fa39b6 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderQuotesTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderQuotesTest.kt @@ -1,8 +1,8 @@ package com.tangem.feature.swap import com.google.common.truth.Truth.assertThat +import com.tangem.common.routing.AppRouter import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.swap.models.SwapCurrencyStatus @@ -14,12 +14,10 @@ import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState -import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.ui.StateBuilder import com.tangem.utils.Provider import io.mockk.every import io.mockk.mockk -import kotlinx.collections.immutable.persistentListOf import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test @@ -32,6 +30,7 @@ internal class StateBuilderQuotesTest { private val appCurrencyProvider: Provider = mockk() private val isAccountsModeProvider: Provider = mockk() private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk() + private val appRouter: AppRouter = mockk() private lateinit var sut: StateBuilder @@ -60,6 +59,7 @@ internal class StateBuilderQuotesTest { appCurrencyProvider = appCurrencyProvider, isAccountsModeProvider = isAccountsModeProvider, iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork, + appRouter = appRouter, ) } diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt index ed55977ec8..fb7636352b 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt @@ -1,16 +1,15 @@ package com.tangem.feature.swap import com.google.common.truth.Truth.assertThat +import com.tangem.common.routing.AppRouter import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.swap.models.SwapCurrencyStatus import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.model.SwapProcessDataState import com.tangem.feature.swap.models.* -import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.ui.StateBuilder @@ -31,6 +30,7 @@ internal class StateBuilderSwapDataTest { private val appCurrencyProvider: Provider = mockk() private val isAccountsModeProvider: Provider = mockk() private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk() + private val appRouter: AppRouter = mockk() private lateinit var sut: StateBuilder @@ -59,6 +59,7 @@ internal class StateBuilderSwapDataTest { appCurrencyProvider = appCurrencyProvider, isAccountsModeProvider = isAccountsModeProvider, iGaslessFeeSupportedForNetwork = iGaslessFeeSupportedForNetwork, + appRouter = appRouter, ) } From 6d7227689881a2d4f9c84db6a238be9df82eb708 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 May 2026 06:07:20 -0700 Subject: [PATCH 16/29] Updated on 2026-08-14 --- .../datasource/di/TangemPayStoresModule.kt | 4 +- .../visa/DefaultTangemPayReissueCardStore.kt | 15 +++++-- .../entity/PaymentAccountStatusValueDM.kt | 1 + core/res/src/main/res/values/strings.xml | 1 + .../PaymentAccountStatusValueDMConverter.kt | 2 + .../tangem/data/pay/di/TangemPayDataModule.kt | 16 ++++++++ .../DefaultPaymentAccountStatusFetcher.kt | 16 +++++++- .../tangem/domain/models/pay/TangemPayCard.kt | 1 + .../usecase/ReissueTangemPayCardUseCase.kt | 33 ++++++++++++++++ .../TangemPayReissueCardComponent.kt | 2 - .../tangempay/model/TangemPayCardPageModel.kt | 39 ++++--------------- .../model/TangemPayReissueCardModel.kt | 11 +++--- .../tangempay/ui/TangemPayCardPageScreen.kt | 16 -------- .../ui/TangemPayReplacingCardBlock.kt | 35 +++++++++++++++++ .../setup/TangemPayCardLimitSetupModelTest.kt | 1 + .../converter/TangemPayMainBlockConverter.kt | 7 +++- 16 files changed, 138 insertions(+), 62 deletions(-) create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ReissueTangemPayCardUseCase.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayReplacingCardBlock.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/TangemPayStoresModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/TangemPayStoresModule.kt index 135c8bfaa8..f0f88d8c24 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/TangemPayStoresModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/TangemPayStoresModule.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.di import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.visa.DefaultTangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.DefaultTangemPayReissueCardStore import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore @@ -25,9 +26,10 @@ internal object TangemPayStoresModule { @Provides @Singleton - fun provideTangemPayReissueCardStore(): TangemPayReissueCardStore { + fun provideTangemPayReissueCardStore(prefs: AppPreferencesStore): TangemPayReissueCardStore { return DefaultTangemPayReissueCardStore( feeStore = RuntimeDataStore(), + prefs = prefs, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStore.kt index 666b4e790c..bb25810fba 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/DefaultTangemPayReissueCardStore.kt @@ -1,11 +1,16 @@ package com.tangem.datasource.local.visa +import androidx.datastore.preferences.core.stringPreferencesKey import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.utils.getSyncOrNull +import com.tangem.datasource.local.preferences.utils.store import com.tangem.domain.models.pay.TangemPayReissueCardFee import com.tangem.domain.models.wallet.UserWalletId internal class DefaultTangemPayReissueCardStore( private val feeStore: RuntimeDataStore, + private val prefs: AppPreferencesStore, ) : TangemPayReissueCardStore { override suspend fun storeReissueFee( @@ -20,11 +25,15 @@ internal class DefaultTangemPayReissueCardStore( } override suspend fun storeReissueOrderId(cardId: String, orderId: String) { - // TODO v_rodionov: #[REDACTED_TASK_KEY] store orderId in app prefs + prefs.store( + key = getReissueKey(cardId), + value = orderId, + ) } override suspend fun getOrderId(cardId: String): String? { - // TODO v_rodionov: #[REDACTED_TASK_KEY] store orderId in app prefs - return null + return prefs.getSyncOrNull(key = getReissueKey(cardId)) } + + private fun getReissueKey(cardId: String) = stringPreferencesKey("tangem_pay_reissue_card_$cardId") } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt index 1f74827d91..a3937b260b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt @@ -85,5 +85,6 @@ sealed interface PaymentAccountStatusValueDM { @Json(name = "admin_daily_limit") val adminDailyLimit: SerializedBigDecimal?, @Json(name = "is_frozen") val isFrozen: Boolean, @Json(name = "last_digits") val lastDigits: String, + @Json(name = "is_reissuing") val isReissuing: Boolean, ) } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index f8c7c79e89..73aedfe1a8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1766,6 +1766,7 @@ Unable to display details. However, card payments are still working. Set \nPIN code Card deactivated + Replacing your card Session expired Renew session Use USDC for everyday payments diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt index 0c2fd37b83..a8df3bf885 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt @@ -51,6 +51,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( adminDailyLimit = card.limit?.adminCardLimit?.amount, isFrozen = card.isFrozen, lastDigits = card.lastDigits, + isReissuing = card.isReissuing, ) }, ) @@ -104,6 +105,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( ), isFrozen = card.isFrozen, lastDigits = card.lastDigits, + isReissuing = card.isReissuing, ) }, ) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index aea6884b96..599536adc0 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -29,6 +29,7 @@ import com.tangem.domain.pay.repository.* import com.tangem.domain.pay.usecase.ChangeCardFrozenStateUseCase import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase +import com.tangem.domain.pay.usecase.ReissueTangemPayCardUseCase import com.tangem.domain.pay.usecase.SetTangemPayCardLimitUseCase import com.tangem.domain.pay.usecase.StartTangemPayOrderPollingUseCase import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase @@ -196,5 +197,20 @@ internal interface TangemPayDataModule { ): StartTangemPayOrderPollingUseCase { return StartTangemPayOrderPollingUseCase(cardDetailsRepository, paymentAccountStatusFetcher) } + + @Provides + fun provideReissueTangemPayCardUseCase( + reissueCardRepository: TangemPayReissueCardRepository, + startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase, + appCoroutineScope: AppCoroutineScope, + paymentAccountStatusFetcher: PaymentAccountStatusFetcher, + ): ReissueTangemPayCardUseCase { + return ReissueTangemPayCardUseCase( + reissueCardRepository = reissueCardRepository, + startTangemPayOrderPollingUseCase = startTangemPayOrderPollingUseCase, + appCoroutineScope = appCoroutineScope, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, + ) + } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index ca4dad4ed3..951f04e9a1 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -20,6 +20,7 @@ import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.model.TangemPayEntryPoint import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.pay.repository.TangemPayReissueCardRepository import com.tangem.domain.visa.error.VisaApiError import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.security.DeviceSecurityInfoProvider @@ -43,6 +44,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val tangemPayCurrencyFactory: TangemPayCurrencyFactory, private val eligibilityManager: TangemPayEligibilityManager, + private val reissueCardRepository: TangemPayReissueCardRepository, ) : PaymentAccountStatusFetcher { private val logger = TangemLogger.withTag(TAG) @@ -254,7 +256,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( ) } - private fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue { + private suspend fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue { val cardInfo = this.cardInfo val productInstance = this.productInstance @@ -286,12 +288,21 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( } } - private fun convertToContentState( + private suspend fun convertToContentState( userWalletId: UserWalletId, productInstance: CustomerInfo.ProductInstance, cardInfo: CustomerInfo.CardInfo, customerId: String, ): PaymentAccountStatusValue { + val reissueOrder = reissueCardRepository.getReissueOrderInfo( + userWalletId = userWalletId, + cardId = productInstance.cardId, + ).getOrNull() + + val isReissuing = reissueOrder != null && + reissueOrder.orderStatus != OrderStatus.CANCELED && + reissueOrder.orderStatus != OrderStatus.COMPLETED + val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId) return PaymentAccountStatusValue.Loaded( source = StatusSource.ACTUAL, @@ -313,6 +324,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( ), isFrozen = productInstance.frozenState is TangemPayCardFrozenState.Frozen, lastDigits = cardInfo.lastFourDigits, + isReissuing = isReissuing, ), ), ) diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/pay/TangemPayCard.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/pay/TangemPayCard.kt index 115e387985..621ef7e2d0 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/pay/TangemPayCard.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/pay/TangemPayCard.kt @@ -22,4 +22,5 @@ data class TangemPayCard( @SerialName("limit") val limit: TangemPayCardLimitData?, @SerialName("is_frozen") val isFrozen: Boolean, @SerialName("last_digits") val lastDigits: String, + @SerialName("is_reissuing") val isReissuing: Boolean, ) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ReissueTangemPayCardUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ReissueTangemPayCardUseCase.kt new file mode 100644 index 0000000000..7420980320 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/ReissueTangemPayCardUseCase.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.pay.usecase + +import arrow.core.Either +import arrow.core.raise.either +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.repository.TangemPayReissueCardRepository +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.utils.coroutines.AppCoroutineScope +import kotlinx.coroutines.launch + +class ReissueTangemPayCardUseCase( + private val reissueCardRepository: TangemPayReissueCardRepository, + private val startTangemPayOrderPollingUseCase: StartTangemPayOrderPollingUseCase, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, + private val appCoroutineScope: AppCoroutineScope, +) { + suspend operator fun invoke(userWalletId: UserWalletId, cardId: String): Either = either { + val order = reissueCardRepository.reissueCard(userWalletId, cardId).bind() + + if (order.orderStatus == OrderStatus.CANCELED) { + raise(VisaApiError.Unspecified) + } + + reissueCardRepository.storeReissueOrderId(cardId, order.orderId) + paymentAccountStatusFetcher.invoke(userWalletId) + + appCoroutineScope.launch { + startTangemPayOrderPollingUseCase(order, userWalletId) + } + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayReissueCardComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayReissueCardComponent.kt index 918ad5d9af..dcb213790d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayReissueCardComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayReissueCardComponent.kt @@ -7,7 +7,6 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.features.tangempay.model.TangemPayReissueCardModel import com.tangem.features.tangempay.ui.TangemPayReissueCardContent @@ -34,7 +33,6 @@ internal class TangemPayReissueCardComponent( } internal interface ReissueCardListener { - fun onReissueOrderCreate(order: TangemPayOrderInfo) fun onDismissReissueCard() fun onClickAddFunds() } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt index c0f9d07ad2..54dd034211 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt @@ -28,11 +28,8 @@ import com.tangem.domain.models.account.requireCardWithId import com.tangem.domain.models.pay.TangemPayCard import com.tangem.domain.models.pay.TangemPayCardLimitPeriod import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier -import com.tangem.domain.pay.model.OrderStatus -import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository -import com.tangem.domain.pay.repository.TangemPayReissueCardRepository import com.tangem.domain.pay.usecase.ChangeCardFrozenStateUseCase import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.features.tangempay.components.AddFundsListener @@ -63,7 +60,6 @@ internal class TangemPayCardPageModel @Inject constructor( private val analytics: AnalyticsEventHandler, private val cardDetailsRepository: TangemPayCardDetailsRepository, private val uiMessageSender: UiMessageSender, - private val reissueCardRepository: TangemPayReissueCardRepository, private val changeCardFrozenStateUseCase: ChangeCardFrozenStateUseCase, ) : Model(), ViewPinListener, ReissueCardListener, AddFundsListener { @@ -84,7 +80,6 @@ internal class TangemPayCardPageModel @Inject constructor( val bottomSheetNavigation: SlotNavigation = SlotNavigation() - // TODO v_rodionov: #[REDACTED_TASK_KEY] check reissue order state before card details are showed init { analytics.send(TangemPayAnalyticsEvents.CardManagementScreenOpened()) fetchAddToWalletBanner() @@ -109,7 +104,13 @@ internal class TangemPayCardPageModel @Inject constructor( } else { TangemPayDailyLimitBlockState.Error } - uiState.update { it.copy(dailyLimitState = dailyLimitState, settings = buildSettings(card)) } + uiState.update { uiState -> + uiState.copy( + dailyLimitState = dailyLimitState, + settings = buildSettings(card), + isReissueInProgress = card.isReissuing, + ) + } } else { uiState.update { it.copy(dailyLimitState = TangemPayDailyLimitBlockState.Error) } } @@ -176,18 +177,6 @@ internal class TangemPayCardPageModel @Inject constructor( bottomSheetNavigation.activate(TangemPayCardNavigation.ReissueCard) } - override fun onReissueOrderCreate(order: TangemPayOrderInfo) { - bottomSheetNavigation.dismiss() - onReissueOrderStatusReceived(order.orderStatus) - if (order.orderStatus != OrderStatus.CANCELED) { - modelScope.launch { - reissueCardRepository.storeReissueOrderId(params.config.cardId, order.orderId) - } - } else { - uiMessageSender.send(SnackbarMessage(resourceReference(R.string.common_something_went_wrong))) - } - } - override fun onDismissReissueCard() { bottomSheetNavigation.dismiss() } @@ -314,18 +303,4 @@ internal class TangemPayCardPageModel @Inject constructor( override fun onDismissViewPin() { bottomSheetNavigation.dismiss() } - - private fun onReissueOrderStatusReceived(orderStatus: OrderStatus) { - when (orderStatus) { - OrderStatus.NEW, OrderStatus.PROCESSING, OrderStatus.COMPLETED -> { - uiState.update { state -> - state.copy( - addToWalletBlockState = null, - isReissueInProgress = true, - ) - } - } - OrderStatus.CANCELED -> Unit - } - } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayReissueCardModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayReissueCardModel.kt index 6c454671a1..ff2a256c1f 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayReissueCardModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayReissueCardModel.kt @@ -13,6 +13,7 @@ import com.tangem.core.ui.format.bigdecimal.getJavaCurrencyByCode import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.pay.repository.TangemPayReissueCardRepository +import com.tangem.domain.pay.usecase.ReissueTangemPayCardUseCase import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.features.tangempay.components.TangemPayReissueCardComponent import com.tangem.features.tangempay.details.impl.R @@ -29,6 +30,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject +@Suppress("LongParameterList") @Stable @ModelScoped internal class TangemPayReissueCardModel @Inject constructor( @@ -36,6 +38,7 @@ internal class TangemPayReissueCardModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val cardDetailsRepository: TangemPayCardDetailsRepository, private val reissueCardRepository: TangemPayReissueCardRepository, + private val reissueTangemPayCardUseCase: ReissueTangemPayCardUseCase, private val uiMessageSender: UiMessageSender, private val analytics: AnalyticsEventHandler, ) : Model() { @@ -72,15 +75,13 @@ internal class TangemPayReissueCardModel @Inject constructor( analytics.send(TangemPayAnalyticsEvents.ReplaceCardConfirmed()) state.update { it.copy(isReissuingInProgress = true) } modelScope.launch { - reissueCardRepository.reissueCard( + reissueTangemPayCardUseCase( userWalletId = params.userWalletId, cardId = params.cardId, ).onLeft { uiMessageSender.send(SnackbarMessage(resourceReference(R.string.common_something_went_wrong))) - onDismiss() - }.onRight { order -> - params.listener.onReissueOrderCreate(order) } + onDismiss() }.saveIn(reissueJobHolder) } @@ -96,7 +97,7 @@ internal class TangemPayReissueCardModel @Inject constructor( val error = if (fee == null || cardBalance == null) { TangemPayReissueCardError.InitialDataLoading - } else if (cardBalance.availableForWithdrawal < fee.amount) { + } else if (cardBalance.fiatBalance < fee.amount) { TangemPayReissueCardError.InsufficientFunds } else { null diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt index 8ea4e2139d..f225204b8b 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt @@ -24,10 +24,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.components.appbar.AppBarWithBackButton -import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -102,19 +99,6 @@ internal fun TangemPayCardPageScreen( } } -@Composable -private fun TangemPayReplacingCardBlock(modifier: Modifier = Modifier) { - Notification( - modifier = modifier, - config = NotificationConfig( - iconResId = com.tangem.core.ui.R.drawable.ic_update_32, - iconTint = NotificationConfig.IconTint.Accent, - title = resourceReference(R.string.tangempay_reissue_card_in_progress), - subtitle = resourceReference(R.string.tangempay_reissue_card_in_progress_description), - ), - ) -} - @Composable private fun TangemPayCardPageSettingsBlock( settings: ImmutableList, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayReplacingCardBlock.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayReplacingCardBlock.kt new file mode 100644 index 0000000000..ac1af73851 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayReplacingCardBlock.kt @@ -0,0 +1,35 @@ +package com.tangem.features.tangempay.ui + +import android.content.res.Configuration +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.tangempay.details.impl.R + +@Composable +internal fun TangemPayReplacingCardBlock(modifier: Modifier = Modifier) { + Notification( + modifier = modifier, + config = NotificationConfig( + iconResId = R.drawable.ic_update_32, + iconTint = NotificationConfig.IconTint.Accent, + title = resourceReference(R.string.tangempay_reissue_card_in_progress), + subtitle = resourceReference(R.string.tangempay_reissue_card_in_progress_description), + ), + containerColor = TangemTheme.colors.background.primary, + ) +} + +@Preview(showBackground = true) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview() { + TangemThemePreview { + TangemPayReplacingCardBlock() + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt index 6de5f4d546..822ebcf651 100644 --- a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt @@ -74,6 +74,7 @@ internal class TangemPayCardLimitSetupModelTest { ) } ), + isReissuing = false, ) val statusWithLimit: PaymentAccountStatusValue.Loaded = mockk(relaxed = true) { every { source } returns StatusSource.ACTUAL diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt index 9a7844cdeb..f5b9d0c634 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import androidx.compose.ui.text.SpanStyle import com.tangem.common.ui.R import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format @@ -85,7 +86,11 @@ internal class TangemPayMainBlockConverter( is PaymentAccountStatusValue.Loaded -> { val card = statusValue.cards.firstOrNull() ?: return TangemPayMainUM.TemporaryUnavailable TangemPayMainUM.Content( - subtitle = stringReference("*${card.lastDigits}"), + subtitle = if (card.isReissuing) { + resourceReference(R.string.tangempay_status_replacing) + } else { + stringReference("*${card.lastDigits}") + }, isBalanceFlickering = statusValue.source == StatusSource.CACHE, balance = getBalanceText( currencyCode = statusValue.currencyCode, From 097e9c1bce3888074dd565f9beddd4dd162d2688 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 May 2026 16:12:33 +0300 Subject: [PATCH 17/29] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 498b0bcd0d..158fbd8808 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 498b0bcd0d871ed60c43b5d44f646548a4f11d37 +Subproject commit 158fbd8808d2db92ef82d3f9ed92c81340c707c5 From 2437fa39d8c837c8e48fda7bc39d27dec026366f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 May 2026 18:18:29 +0400 Subject: [PATCH 18/29] Updated on 2026-08-14 --- .../DefaultSingleAccountListProducer.kt | 45 +++++++++++++++---- .../producer/WalletAccountListFlowFactory.kt | 11 ++++- .../DefaultCardCryptoCurrencyFactory.kt | 11 ++--- .../fetcher/CommonNetworkStatusFetcher.kt | 8 ++++ .../fetcher/CommonNetworkStatusFetcherTest.kt | 26 +++++++++++ .../model/WalletsUpdateActionResolver.kt | 23 ++++++++-- .../wallet/state/WalletStateController.kt | 12 ++++- .../transformers/UnlockWalletTransformer.kt | 4 +- .../transformers/WalletStateTransformer.kt | 2 +- 9 files changed, 120 insertions(+), 22 deletions(-) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt index 28fd5fd6be..76b5728dc8 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt @@ -11,9 +11,11 @@ import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.core.flow.FlowProducerTools import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.hot.sdk.model.HotWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -21,6 +23,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach /** * Default implementation of [SingleAccountListProducer]. @@ -43,23 +46,45 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor( override val fallback: Option = none() + private val logger = TangemLogger.withTag(TAG) + @OptIn(ExperimentalCoroutinesApi::class) override fun produce(): Flow { + val walletId = params.userWalletId + logger.i("produce() called for $walletId") val accountListFlow: Flow = if (tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled) { - combineWithPaymentAccount() + combineWithPaymentAccount(walletId) } else { - walletAccountListFlowFactory.create(userWalletId = params.userWalletId) + walletAccountListFlowFactory.create(userWalletId = walletId) } - return accountListFlow.flowOn(dispatchers.default) + return accountListFlow + .onEach { accountList -> + logger.i( + "produce()[$walletId] emit: accounts=${accountList.accounts.size}, " + + "currencies=${accountList.flattenMapCurrencies().size}", + ) + } + .flowOn(dispatchers.default) } - private fun combineWithPaymentAccount(): Flow { - return walletAccountListFlowFactory.create(params.userWalletId) + private fun combineWithPaymentAccount(walletId: UserWalletId): Flow { + return walletAccountListFlowFactory.create(walletId) + .onEach { accountList -> + logger.i( + "produce()[$walletId]: accountList received accounts=${accountList.accounts.size}, " + + "currencies=${accountList.flattenMapCurrencies().size}", + ) + } .map { accountList -> - val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) - if (userWallet.isPaymentAccountSupported()) { - accountList.plus(Account.Payment(params.userWalletId)).getOrElse { throwable -> + val userWallet = userWalletsListRepository.getSyncStrict(id = walletId) + val isPaymentSupported = userWallet.isPaymentAccountSupported() + logger.i( + "produce()[$walletId]: userWallet resolved (type=${userWallet::class.simpleName}), " + + "isPaymentAccountSupported=$isPaymentSupported", + ) + if (isPaymentSupported) { + accountList.plus(Account.Payment(walletId)).getOrElse { throwable -> error("Can not combine account list and payment account status: $throwable") } } else { @@ -77,4 +102,8 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor( interface Factory : SingleAccountListProducer.Factory { override fun create(params: SingleAccountListProducer.Params): DefaultSingleAccountListProducer } + + private companion object { + const val TAG = "SingleAccountListProducer" + } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt index 49d1b90eec..33749350a7 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt @@ -12,6 +12,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.* import javax.inject.Inject @@ -47,7 +48,15 @@ internal class WalletAccountListFlowFactory @Inject constructor( return accountsResponseStoreFactory.create(userWallet.walletId).data .filterNotNull() - .filter { it.accounts.isNotEmpty() } + .filter { response -> + val hasAccounts = response.accounts.isNotEmpty() + + if (!hasAccounts) { + TangemLogger.e("Account list is empty for ${userWallet.walletId}") + } + + hasAccounts + } .distinctUntilChanged() .map { converter.convert(it) } } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt index f56f43a761..addc692394 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -135,10 +135,9 @@ internal class DefaultCardCryptoCurrencyFactory( userWallet: UserWallet, networks: Set, ): Map> { - val response = walletAccountsFetcher.getSaved(userWallet.walletId) - ?: return emptyMap() + val response = walletAccountsFetcher.getSaved(userWallet.walletId) ?: return emptyMap() - val existingNetworkWithCurrencies = response.accounts.flatMapTo(hashSetOf()) { accountDTO -> + val currenciesByNetworkId = response.accounts.flatMapTo(hashSetOf()) { accountDTO -> val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull() ?: return@flatMapTo emptySet() @@ -152,9 +151,11 @@ internal class DefaultCardCryptoCurrencyFactory( accountIndex = accountIndex, ) } - .groupBy(CryptoCurrency::network) + .groupBy { it.network.id } - return networks.associateWith { emptyList() } + existingNetworkWithCurrencies + return networks.associateWith { network -> + currenciesByNetworkId[network.id].orEmpty() + } } private suspend fun getMultiWalletCurrenciesByRawId( diff --git a/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt index 38625e0f4a..8e40ea13d2 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt @@ -1,6 +1,7 @@ package com.tangem.data.networks.fetcher import arrow.core.Either +import arrow.core.right import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.data.networks.store.setSourceAsOnlyCache import com.tangem.data.networks.store.storeStatus @@ -44,6 +45,13 @@ internal class CommonNetworkStatusFetcher @Inject constructor( network: Network, networkCurrencies: Set, ): Either { + // Guard: empty networkCurrencies would result in NetworkStatus.Verified(amounts=emptyMap()), + // which overwrites any valid cached status and leaves all currencies in this network as Loading. + if (networkCurrencies.isEmpty()) { + TangemLogger.w("Skipping fetch for $userWalletId [${network.rawId}]: networkCurrencies is empty") + return Unit.right() + } + return Either.catchOn(dispatchers.default) { val result = withContext(dispatchers.io) { walletManagersFacade.update( diff --git a/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt index e6f9bf012d..e9dcb7f749 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt @@ -73,6 +73,32 @@ internal class CommonNetworkStatusFetcherTest { } } + @Test + fun `fetch skips wallet manager update when networkCurrencies is empty`() = runTest { + // Arrange + val userWalletId = UserWalletId("011") + val network = cryptoCurrencyFactory.ethereum.network + + // Act + val actual = fetcher.fetch( + userWalletId = userWalletId, + network = network, + networkCurrencies = emptySet(), + ) + + // Assert: returns success without touching the wallet manager facade — guard prevents storing + // a NetworkStatus.Verified with empty `amounts` over an existing valid cached status. + Truth.assertThat(actual).isEqualTo(Either.Right(Unit)) + + coVerify(inverse = true) { + walletManagersFacade.update( + userWalletId = any(), + network = any(), + extraTokens = any(), + ) + } + } + @ParameterizedTest @ProvideTestModels fun `fetch successfully for any result of walletManagersFacade`(model: SuccessTestModel) = runTest { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 501510ac6a..c632281590 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -68,7 +68,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( isAnyWalletUnlocked(state, wallets) -> { Action.UnlockWallet( selectedWallet = selectedWallet, - unlockedWallets = wallets.filterNot(UserWallet::isLocked), + unlockedWallets = getJustUnlockedWallets(state = state, wallets = wallets), ) } isAnyWalletNameChanged(state, wallets) -> { @@ -232,11 +232,28 @@ internal class WalletsUpdateActionResolver @Inject constructor( private fun isAnyWalletUnlocked(state: WalletScreenState, wallets: List): Boolean { return state.wallets.any { walletState -> val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } ?: return@any false - !wallet.isLocked && - (walletState is WalletState.MultiCurrency.Locked || walletState is WalletState.SingleCurrency.Locked) + wallet.isJustUnlockedFrom(walletState) } } + /** + * Wallets that transitioned from a locked UI state to unlocked data state in this update cycle. + * Does not include wallets that were already unlocked before — otherwise downstream handlers + * (e.g. [com.tangem.feature.wallet.child.wallet.model.WalletModel.unlockWallet]) would re-fetch + * already-loaded wallets and emit redundant transformer state changes. + */ + private fun getJustUnlockedWallets(state: WalletScreenState, wallets: List): List { + return state.wallets.mapNotNull { walletState -> + val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } + wallet?.takeIf { it.isJustUnlockedFrom(walletState) } + } + } + + private fun UserWallet.isJustUnlockedFrom(walletState: WalletState): Boolean { + return !isLocked && + (walletState is WalletState.MultiCurrency.Locked || walletState is WalletState.SingleCurrency.Locked) + } + private fun isSelectedWalletCardsCountChanged(state: WalletScreenState, selectedWallet: UserWallet): Boolean { if (selectedWallet !is UserWallet.Cold) return false val prevSelectedWallet = state.getPrevSelectedWallet() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index d56549b7fd..ad7df36052 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -8,12 +8,13 @@ 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.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletScreenStateTransformer +import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletStateTransformer import com.tangem.utils.extensions.indexOfFirstOrNull +import com.tangem.utils.logging.TangemLogger import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update -import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import javax.inject.Singleton @@ -41,7 +42,14 @@ internal class WalletStateController @Inject constructor( } fun update(transformer: WalletScreenStateTransformer) { - TangemLogger.d("Applying: ${transformer::class.simpleName}") + val walletStateTransformer = transformer as? WalletStateTransformer + val maybeWalletId = if (walletStateTransformer != null) { + " for ${walletStateTransformer.userWalletId}" + } else { + "" + } + + TangemLogger.d("Applying: ${transformer::class.simpleName}$maybeWalletId") mutableUiState.update(function = transformer::transform) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 6e44868ca7..a8e39cbf17 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -67,7 +67,7 @@ internal class UnlockWalletTransformer( is WalletState.MultiCurrency.Content, is WalletState.SingleCurrency.Content, -> { - TangemLogger.e("Impossible to unlock wallet with not locked state") + TangemLogger.e("createLoadingState: Impossible to unlock wallet with not locked state") prevState } } @@ -79,7 +79,7 @@ internal class UnlockWalletTransformer( userWallet = unlockedWallet, ) is WalletUM.Content -> { - TangemLogger.e("Impossible to unlock wallet with not locked state") + TangemLogger.e("createLoadingState2: Impossible to unlock wallet with not locked state") walletUM } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt index 2790e44084..437b7f9493 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt @@ -7,7 +7,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList internal abstract class WalletStateTransformer( - protected val userWalletId: UserWalletId, + internal val userWalletId: UserWalletId, ) : WalletScreenStateTransformer { abstract fun transform(prevState: WalletState): WalletState From 75af6a807e4edd2d96f952dda5734cd5add37199 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 May 2026 17:23:25 +0300 Subject: [PATCH 19/29] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 498b0bcd0d..158fbd8808 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 498b0bcd0d871ed60c43b5d44f646548a4f11d37 +Subproject commit 158fbd8808d2db92ef82d3f9ed92c81340c707c5 From 0cceaf31107a9e786192190b5e0c425b54a75041 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 May 2026 10:06:50 +0300 Subject: [PATCH 20/29] Updated on 2026-08-14 --- .../src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt index 06e365e4ec..1331b995b3 100644 --- a/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt +++ b/libs/visa/src/main/kotlin/com/tangem/lib/visa/utils/Constants.kt @@ -10,9 +10,6 @@ internal object Constants { const val GAS_LIMIT = 500_000_000L const val PRIVATE_KEY_LENGTH = 32 - const val VISA_API_PROD_URL = "https://payapi.tangem-tech.com/api/v1/" - const val VISA_API_DEV_URL = "[REDACTED_ENV_URL]" - const val NETWORK_TIMEOUT_SECONDS = 65L const val NETWORK_LOGS_TAG = "VisaNetworkLogs" } \ No newline at end of file From d99a6bf31efc411cfa331c72202d80cb17f2555b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 May 2026 01:39:48 -0700 Subject: [PATCH 21/29] Updated on 2026-08-14 --- .../domain/pay/TangemPayDetailsConfig.kt | 1 + ...faultTangemPayDetailsContainerComponent.kt | 4 +- .../entity/TangemPayDetailsStateFactory.kt | 9 +- .../tangempay/entity/TangemPayDetailsUM.kt | 6 +- .../tangempay/model/TangemPayDetailsModel.kt | 30 +++++-- .../TangemPayCardDataTransformer.kt | 30 +++++++ .../TangemPayAccountDetailsInnerRoute.kt | 3 +- .../tangempay/ui/TangemPayDetailsScreen.kt | 88 +++++++++++++------ .../tangempay/utils/TangemPayDetailIntents.kt | 3 +- .../setup/TangemPayCardLimitSetupModelTest.kt | 1 + .../converter/TangemPayMainBlockConverter.kt | 2 + 11 files changed, 137 insertions(+), 40 deletions(-) create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayCardDataTransformer.kt diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt index f94eb30195..8bbd95fb92 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt @@ -11,6 +11,7 @@ data class TangemPayDetailsConfig( val isPinSet: Boolean, val cardFrozenState: TangemPayCardFrozenState, val cardNumberEnd: String, + val isReissuing: Boolean, val chainId: Int, val isTangemPayDeactivated: Boolean, val displayName: CardDisplayName?, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt index 94de672f75..885bf7e9bc 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt @@ -65,9 +65,9 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru tokenReceiveComponentFactory = tokenReceiveComponentFactory, expressTransactionsComponentProvider = expressTransactionsComponentProvider, ) - TangemPayAccountDetailsInnerRoute.CardDetails -> tangemPayCardPageFactory.create( + is TangemPayAccountDetailsInnerRoute.CardDetails -> tangemPayCardPageFactory.create( context = childByContext(componentContext = componentContext, router = innerRouter), - params = TangemPayCardPageComponent.Params(userWalletId = params.userWalletId, config = params.config), + params = TangemPayCardPageComponent.Params(userWalletId = params.userWalletId, config = config.config), ) TangemPayAccountDetailsInnerRoute.AddToWallet -> TangemPayAddToWalletComponent( appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt index 2f630968ac..f0cddecb45 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt @@ -21,7 +21,11 @@ internal class TangemPayDetailsStateFactory( private val cardFrozenState: TangemPayCardFrozenState, ) { @Suppress("LongMethod") - fun getInitialState(isTangemPayDeactivated: Boolean, cardNumberEnd: String): TangemPayDetailsUM { + fun getInitialState( + isTangemPayDeactivated: Boolean, + cardNumberEnd: String, + isReissuing: Boolean, + ): TangemPayDetailsUM { return TangemPayDetailsUM( topBarConfig = TangemPayDetailsTopBarConfig( onBackClick = onBack, @@ -51,7 +55,8 @@ internal class TangemPayDetailsStateFactory( cards = persistentListOf( TangemPayDetailsBalanceBlockState.Card( lastDigits = cardNumberEnd, - onClick = intents::onCardClick, + onClick = {}, + isReissuing = isReissuing, ), ), onAddCardClick = intents::onAddCardClick, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt index 2a192392ec..a86c20d4d4 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt @@ -84,7 +84,11 @@ internal sealed class TangemPayDetailsBalanceBlockState { ) : TangemPayDetailsBalanceBlockState() data class CardsBlockState(val cards: ImmutableList, val onAddCardClick: () -> Unit) - data class Card(val lastDigits: String, val onClick: () -> Unit) + data class Card( + val lastDigits: String, + val onClick: () -> Unit, + val isReissuing: Boolean, + ) } internal data class AddToWalletBlockState( diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index dded4437b7..9f2a9aa630 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -19,10 +19,14 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.feedback.models.WalletMetaInfo +import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory +import com.tangem.domain.pay.TangemPayDetailsConfig +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository @@ -53,10 +57,7 @@ import com.tangem.utils.coroutines.saveIn import com.tangem.utils.logging.TangemLogger import com.tangem.utils.transformer.update import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -65,6 +66,7 @@ import javax.inject.Inject @ModelScoped internal class TangemPayDetailsModel @Inject constructor( paramsContainer: ParamsContainer, + paymentAccountStatusSupplier: PaymentAccountStatusSupplier, override val dispatchers: CoroutineDispatcherProvider, private val analytics: AnalyticsEventHandler, private val router: Router, @@ -95,6 +97,7 @@ internal class TangemPayDetailsModel @Inject constructor( stateFactory.getInitialState( isTangemPayDeactivated = params.config.isTangemPayDeactivated, cardNumberEnd = params.config.cardNumberEnd, + isReissuing = params.config.isReissuing, ), ) @@ -118,6 +121,21 @@ internal class TangemPayDetailsModel @Inject constructor( if (!params.config.isTangemPayDeactivated) { subscribeToCardFrozenState() fetchAddToWalletBanner() + + paymentAccountStatusSupplier.invoke(params.userWalletId) + .map { it.value } + .filterIsInstance() + .filter { it.source == StatusSource.ACTUAL } + .onEach { state -> + val card = state.cards.firstOrNull() ?: return@onEach + uiState.update( + TangemPayCardDataTransformer( + card = card, + onCardClick = { onCardClick(params.config.copy(cardId = card.id)) }, + ), + ) + } + .launchIn(modelScope) } } @@ -363,9 +381,9 @@ internal class TangemPayDetailsModel @Inject constructor( urlOpener.openUrl(TangemPayConstants.TERMS_AND_LIMITS_LINK) } - override fun onCardClick() { + override fun onCardClick(config: TangemPayDetailsConfig) { analytics.send(TangemPayAnalyticsEvents.CardIconClicked()) - router.push(TangemPayAccountDetailsInnerRoute.CardDetails) + router.push(TangemPayAccountDetailsInnerRoute.CardDetails(config)) } override fun onAddCardClick() { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayCardDataTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayCardDataTransformer.kt new file mode 100644 index 0000000000..cd15656cc3 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayCardDataTransformer.kt @@ -0,0 +1,30 @@ +package com.tangem.features.tangempay.model.transformers + +import com.tangem.domain.models.pay.TangemPayCard +import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState +import com.tangem.features.tangempay.entity.TangemPayDetailsUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf + +internal class TangemPayCardDataTransformer( + private val card: TangemPayCard, + private val onCardClick: () -> Unit, +) : Transformer { + + override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM { + val updatedCard = TangemPayDetailsBalanceBlockState.Card( + lastDigits = card.lastDigits, + onClick = onCardClick, + isReissuing = card.isReissuing, + ) + val cardsBlockState = prevState.balanceBlockState.cardsBlockState.copy( + cards = persistentListOf(updatedCard), + ) + val newBalanceBlockState = when (val bs = prevState.balanceBlockState) { + is TangemPayDetailsBalanceBlockState.Loading -> bs.copy(cardsBlockState = cardsBlockState) + is TangemPayDetailsBalanceBlockState.Content -> bs.copy(cardsBlockState = cardsBlockState) + is TangemPayDetailsBalanceBlockState.Error -> bs.copy(cardsBlockState = cardsBlockState) + } + return prevState.copy(balanceBlockState = newBalanceBlockState) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt index 8ef5c5e9a3..b49fa0a643 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt @@ -1,6 +1,7 @@ package com.tangem.features.tangempay.navigation import com.tangem.core.decompose.navigation.Route +import com.tangem.domain.pay.TangemPayDetailsConfig import kotlinx.serialization.Serializable @Serializable @@ -9,7 +10,7 @@ internal sealed class TangemPayAccountDetailsInnerRoute : Route { data object AccountDetails : TangemPayAccountDetailsInnerRoute() @Serializable - data object CardDetails : TangemPayAccountDetailsInnerRoute() + data class CardDetails(val config: TangemPayDetailsConfig) : TangemPayAccountDetailsInnerRoute() @Serializable data object AddToWallet : TangemPayAccountDetailsInnerRoute() diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index fabad9ef58..7044edc626 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.RectangleShimmer @@ -114,30 +115,45 @@ internal fun TangemPayDetailsScreen( SpacerH12() }, ) - if (state.addToWalletBlockState != null) { + + if (state.balanceBlockState.cardsBlockState.cards.fastAny { it.isReissuing }) { item( - key = AddToWalletBlockState::class.java, + key = "REISSUE_MESSAGE", content = { - TangemPayAddToWalletBlock( - state = state.addToWalletBlockState, - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), - ) - }, - ) - } - if (state.accountDeactivatedNotificationConfig != null) { - item( - key = "DEACTIVATION_MESSAGE", - content = { - Notification( - modifier = modifier + TangemPayReplacingCardBlock( + modifier = Modifier .padding(horizontal = TangemTheme.dimens.spacing16) .fillMaxWidth(), - config = state.accountDeactivatedNotificationConfig, ) SpacerH12() }, ) + } else { + if (state.addToWalletBlockState != null) { + item( + key = AddToWalletBlockState::class.java, + content = { + TangemPayAddToWalletBlock( + state = state.addToWalletBlockState, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) + }, + ) + } + if (state.accountDeactivatedNotificationConfig != null) { + item( + key = "DEACTIVATION_MESSAGE", + content = { + Notification( + modifier = modifier + .padding(horizontal = TangemTheme.dimens.spacing16) + .fillMaxWidth(), + config = state.accountDeactivatedNotificationConfig, + ) + SpacerH12() + }, + ) + } } if (state.accountDeactivatedNotificationConfig == null) { with(expressTransactionsComponent) { @@ -289,14 +305,26 @@ private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modi painter = painterResource(R.drawable.img_visa_card_48x32), contentDescription = null, ) - Text( - modifier = Modifier - .align(Alignment.BottomStart) - .padding(4.dp, bottom = 2.dp), - text = card.lastDigits, - style = TangemTheme.typography.overline.copy(letterSpacing = 0.sp), - color = TangemTheme.colors.text.constantWhite, - ) + if (card.isReissuing) { + Icon( + modifier = Modifier + .size(16.dp) + .align(Alignment.BottomStart) + .padding(2.dp), + painter = painterResource(R.drawable.ic_update_32), + contentDescription = null, + tint = TangemTheme.colors.text.constantWhite, + ) + } else { + Text( + modifier = Modifier + .align(Alignment.BottomStart) + .padding(4.dp, bottom = 2.dp), + text = card.lastDigits, + style = TangemTheme.typography.overline.copy(letterSpacing = 0.sp), + color = TangemTheme.colors.text.constantWhite, + ) + } } } @@ -421,8 +449,8 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider Date: Tue, 19 May 2026 08:41:29 +0000 Subject: [PATCH 22/29] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index a0cb218ffd..76d48569f9 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.37-1508" +tangemBlockchainSdk = "releases-5.38-1512" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.37-608" +tangemCardSdk = "releases-5.38-615" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 9678c49b374ab090c64df5ff09501e3286aea04a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 May 2026 12:30:46 +0400 Subject: [PATCH 23/29] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 15 +++ .../com/tangem/core/analytics/models/Basic.kt | 1 + .../ethereum/WcEthSendTransactionUseCase.kt | 2 + .../analytics/StakingAnalyticsEvent.kt | 2 + .../domain/walletconnect/WcAnalyticEvents.kt | 2 + .../approval/impl/model/GiveApprovalModel.kt | 9 +- .../v2/send/analytics/SendAnalyticEvents.kt | 2 + .../v2/send/analytics/SendAnalyticHelper.kt | 7 ++ .../analytics/NFTSendAnalyticHelper.kt | 1 + .../analytics/utils/StakingAnalyticSender.kt | 5 +- .../analytics/SendWithSwapAnalyticEvents.kt | 2 + .../confirm/model/SendWithSwapConfirmModel.kt | 10 +- .../feature/swap/analytics/SwapEvents.kt | 3 + .../tangem/feature/swap/model/SwapModel.kt | 22 +++- .../approve/model/YieldSupplyApproveModel.kt | 109 ++++++++++-------- .../model/YieldSupplyStartEarningModel.kt | 10 +- .../model/YieldSupplyStopEarningModel.kt | 6 + 17 files changed, 151 insertions(+), 57 deletions(-) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 9a0e69c86b..00e4585c49 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -108,6 +108,7 @@ sealed class AnalyticsParam { override val token: String, override val feeType: FeeType, override val feeToken: String, + override val feeAssetType: FeeAssetType, ) : TxSentFrom("Send"), TxData data class Swap( @@ -115,6 +116,7 @@ sealed class AnalyticsParam { override val token: String, override val feeType: FeeType, override val feeToken: String, + override val feeAssetType: FeeAssetType, ) : TxSentFrom("Swap"), TxData data class Staking( @@ -122,6 +124,7 @@ sealed class AnalyticsParam { override val token: String, override val feeType: FeeType, override val feeToken: String, + override val feeAssetType: FeeAssetType, ) : TxSentFrom("Staking"), TxData data class Approve( @@ -129,6 +132,7 @@ sealed class AnalyticsParam { override val token: String, override val feeType: FeeType, override val feeToken: String, + override val feeAssetType: FeeAssetType, val permissionType: String, ) : TxSentFrom("Approve"), TxData @@ -137,6 +141,7 @@ sealed class AnalyticsParam { override val token: String, override val feeType: FeeType?, override val feeToken: String, + override val feeAssetType: FeeAssetType, ) : TxSentFrom("WalletConnect"), TxData data object Sell : TxSentFrom("Sell") @@ -146,6 +151,7 @@ sealed class AnalyticsParam { override val token: String, override val feeType: FeeType, override val feeToken: String, + override val feeAssetType: FeeAssetType, ) : TxSentFrom("NFT"), TxData data class SendWithSwap( @@ -153,6 +159,7 @@ sealed class AnalyticsParam { override val token: String, override val feeType: FeeType, override val feeToken: String, + override val feeAssetType: FeeAssetType, ) : TxSentFrom("Send&Swap"), TxData data class Earning( @@ -160,6 +167,7 @@ sealed class AnalyticsParam { override val token: String, override val feeType: FeeType, override val feeToken: String, + override val feeAssetType: FeeAssetType, ) : TxSentFrom("Earning"), TxData } @@ -168,6 +176,12 @@ sealed class AnalyticsParam { val token: String val feeToken: String val feeType: FeeType? + val feeAssetType: FeeAssetType + } + + sealed class FeeAssetType(val value: String) { + data object Coin : FeeAssetType("Coin") + data object Token : FeeAssetType("Token") } sealed class FeeType(val value: String) { @@ -298,6 +312,7 @@ sealed class AnalyticsParam { const val SEARCHED = "Searched" const val RATE_TYPE = "Rate Type" const val SCREEN_TYPE = "Screen Type" + const val FEE_ASSET_TYPE = "Fee Asset Type" } } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt index 0e916a0e2d..1486944a36 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/Basic.kt @@ -94,6 +94,7 @@ sealed class Basic( this[AnalyticsParam.Key.FEE_TYPE] = it } this[AnalyticsParam.Key.FEE_TOKEN] = sentFrom.feeToken + this[AnalyticsParam.Key.FEE_ASSET_TYPE] = sentFrom.feeAssetType.value } if (sentFrom is AnalyticsParam.TxSentFrom.Approve) { this[AnalyticsParam.Key.PERMISSION_TYPE] = sentFrom.permissionType diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt index 0bee94b52c..3f24678ad7 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthSendTransactionUseCase.kt @@ -7,6 +7,7 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.TxSentFrom import com.tangem.core.analytics.models.Basic import com.tangem.core.analytics.models.Basic.TransactionSent.MemoType @@ -91,6 +92,7 @@ internal class WcEthSendTransactionUseCase @AssistedInject constructor( token = network.currencySymbol, feeType = null, feeToken = network.currencySymbol, + feeAssetType = AnalyticsParam.FeeAssetType.Coin, ), memoType = MemoType.Null, ), diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt index 8b4e35aa0d..65407e56ca 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt @@ -46,11 +46,13 @@ sealed class StakingAnalyticsEvent( data class StakeInProgressScreenOpened( val validator: String, val action: StakingActionType, + val feeAssetType: AnalyticsParam.FeeAssetType, ) : StakingAnalyticsEvent( event = "Stake In Progress Screen Opened", params = mapOf( "Validator" to validator, "Action" to action.asAnalyticName, + AnalyticsParam.Key.FEE_ASSET_TYPE to feeAssetType.value, ), ), AppsFlyerIncludedEvent diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt index 7097a4b00b..d7a799f3d5 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -157,6 +157,7 @@ sealed class WcAnalyticEvents( network: Network, securityStatus: CheckDAppResult, accountDerivation: Int?, + feeAssetType: AnalyticsParam.FeeAssetType = AnalyticsParam.FeeAssetType.Coin, ) : WcAnalyticEvents( event = "Signature Request Handled", params = buildMap { @@ -168,6 +169,7 @@ sealed class WcAnalyticEvents( accountDerivation?.let { put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) } + put(AnalyticsParam.Key.FEE_ASSET_TYPE, feeAssetType.value) }, ), AppsFlyerIncludedEvent diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt index dbe9d28ed2..2a82f3cbdb 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -266,7 +266,13 @@ internal class GiveApprovalModel @Inject constructor( private fun sendApproveSuccessAnalytics(feeContent: FeeSelectorUM.Content) { val currency = params.cryptoCurrencyStatus.currency - val feeToken = feeContent.feeExtraInfo.feeCryptoCurrencyStatus.currency.symbol + val feeCurrency = feeContent.feeExtraInfo.feeCryptoCurrencyStatus.currency + val feeToken = feeCurrency.symbol + val feeAssetType = if (feeCurrency is CryptoCurrency.Coin) { + AnalyticsParam.FeeAssetType.Coin + } else { + AnalyticsParam.FeeAssetType.Token + } val permissionType = when (uiState.value.approveType) { ApproveType.LIMITED -> "Current transaction" ApproveType.UNLIMITED -> "Unlimited" @@ -276,6 +282,7 @@ internal class GiveApprovalModel @Inject constructor( token = currency.symbol, feeType = feeContent.toAnalyticType(), feeToken = feeToken, + feeAssetType = feeAssetType, permissionType = permissionType, ) analyticsEventHandler.send( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index e288c415ef..73d13815e7 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -30,6 +30,7 @@ internal sealed class SendAnalyticEvents( val isNonceNotEmpty: Boolean, private val ensStatus: AnalyticsParam.EmptyFull, private val feeToken: String, + private val feeAssetType: AnalyticsParam.FeeAssetType, private val fromDerivationIndex: Int?, private val toDerivationIndex: Int?, ) : SendAnalyticEvents( @@ -47,6 +48,7 @@ internal sealed class SendAnalyticEvents( } put(ENS_ADDRESS, ensAddress) put(FEE_TOKEN, feeToken) + put(AnalyticsParam.Key.FEE_ASSET_TYPE, feeAssetType.value) }, ), AppsFlyerIncludedEvent diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt index c3d536b383..8ab32e31a1 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticHelper.kt @@ -31,6 +31,11 @@ internal class SendAnalyticHelper @Inject constructor( val feeSelectorUM = sendUM.feeSelectorUM as? FeeSelectorUM.Content ?: return val feeType = feeSelectorUM.toAnalyticType() val feeTokenSymbol = feeToken.symbol + val feeAssetType = if (feeToken is CryptoCurrency.Coin) { + AnalyticsParam.FeeAssetType.Coin + } else { + AnalyticsParam.FeeAssetType.Token + } val fromDerivationIndex = account?.derivationIndex?.value val destination = destinationUM?.addressTextField?.actualAddress ?: return val destinationAccount = getAccountCurrencyByAddressUseCase(destination) @@ -46,6 +51,7 @@ internal class SendAnalyticHelper @Inject constructor( fromDerivationIndex = fromDerivationIndex, toDerivationIndex = toDerivationIndex, feeToken = feeTokenSymbol, + feeAssetType = feeAssetType, ), ) analyticsEventHandler.send( @@ -55,6 +61,7 @@ internal class SendAnalyticHelper @Inject constructor( token = cryptoCurrency.symbol, feeType = feeType, feeToken = feeTokenSymbol, + feeAssetType = feeAssetType, ), memoType = getSendTransactionMemoType(destinationUM.memoTextField), ), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt index b8c6f2e66c..5172f13211 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt @@ -37,6 +37,7 @@ internal class NFTSendAnalyticHelper @Inject constructor( token = NFT_SEND_CATEGORY, // should send "NFT" in token param feeType = feeType, feeToken = cryptoCurrency.symbol, + feeAssetType = AnalyticsParam.FeeAssetType.Coin, ), memoType = getSendTransactionMemoType(destinationUM?.memoTextField), ), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt index d02f06b8ba..7d1a8d7044 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -73,6 +73,7 @@ internal class StakingAnalyticSender( feeType = AnalyticsParam.FeeType.Normal, permissionType = ApproveType.LIMITED.name, feeToken = tokenCryptoCurrency.symbol, + feeAssetType = AnalyticsParam.FeeAssetType.Coin, ), memoType = Basic.TransactionSent.MemoType.Null, ), @@ -82,7 +83,7 @@ internal class StakingAnalyticSender( fun sendTransactionStakingAnalytics(value: StakingUiState, cryptoCurrencyStatus: CryptoCurrencyStatus) { val validatorState = value.validatorState as? StakingStates.ValidatorState.Data val validatorName = validatorState?.chosenTarget?.name ?: return - + val feeAssetType = AnalyticsParam.FeeAssetType.Coin // support only coin as fee asset for staking transactions analyticsEventHandler.send( Basic.TransactionSent( sentFrom = AnalyticsParam.TxSentFrom.Staking( @@ -90,6 +91,7 @@ internal class StakingAnalyticSender( token = value.cryptoCurrencySymbol, feeType = AnalyticsParam.FeeType.Normal, feeToken = cryptoCurrencyStatus.currency.symbol, + feeAssetType = feeAssetType, ), memoType = Basic.TransactionSent.MemoType.Null, ), @@ -98,6 +100,7 @@ internal class StakingAnalyticSender( StakingAnalyticsEvent.StakeInProgressScreenOpened( validator = validatorName, action = getStakingActionType(value), + feeAssetType = feeAssetType, ), ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt index 5a68284603..e51dca3a75 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -61,6 +61,7 @@ internal sealed class SendWithSwapAnalyticEvents( val feeType: AnalyticsParam.FeeType, val fromToken: CryptoCurrency, val toToken: CryptoCurrency, + val feeAssetType: AnalyticsParam.FeeAssetType, val fromDerivationIndex: Int?, val toDerivationIndex: Int?, ) : SendWithSwapAnalyticEvents( @@ -74,6 +75,7 @@ internal sealed class SendWithSwapAnalyticEvents( put(RECEIVE_BLOCKCHAIN, toToken.network.name) if (fromDerivationIndex != null) put(ACCOUNT_DERIVATION_FROM, fromDerivationIndex.toString()) if (toDerivationIndex != null) put(ACCOUNT_DERIVATION_TO, toDerivationIndex.toString()) + put(AnalyticsParam.Key.FEE_ASSET_TYPE, feeAssetType.value) }, ), AppsFlyerIncludedEvent diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 7e59abe87e..ad6eac10b6 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -503,12 +503,19 @@ internal class SendWithSwapConfirmModel @Inject constructor( .getOrNull()?.account val toDerivationIndex = destinationAccount?.derivationIndex?.value + val feeToken = getSelectedFeeToken() + val feeAssetType = if (feeToken is CryptoCurrency.Coin) { + AnalyticsParam.FeeAssetType.Coin + } else { + AnalyticsParam.FeeAssetType.Token + } analyticsEventHandler.send( SendWithSwapAnalyticEvents.TransactionScreenOpened( providerName = selectedProvider.name, feeType = feeType, fromToken = fromCurrency, toToken = toCurrency, + feeAssetType = feeAssetType, fromDerivationIndex = fromDerivationIndex, toDerivationIndex = toDerivationIndex, ), @@ -519,7 +526,8 @@ internal class SendWithSwapConfirmModel @Inject constructor( blockchain = fromCurrency.network.name, token = fromCurrency.symbol, feeType = feeType, - feeToken = getSelectedFeeToken().symbol, + feeToken = feeToken.symbol, + feeAssetType = feeAssetType, ), memoType = Basic.TransactionSent.MemoType.Null, ), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 04428126e9..54c93824d2 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -5,6 +5,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FR import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_TO import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TOKEN import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN @@ -103,6 +104,7 @@ sealed class SwapEvents( val sendToken: String, val receiveToken: String, val feeToken: String, + val feeAssetType: AnalyticsParam.FeeAssetType, val fromDerivationIndex: Int?, val toDerivationIndex: Int?, val referralId: String?, @@ -118,6 +120,7 @@ sealed class SwapEvents( if (fromDerivationIndex != null) put(ACCOUNT_DERIVATION_FROM, fromDerivationIndex.toString()) if (toDerivationIndex != null) put(ACCOUNT_DERIVATION_TO, toDerivationIndex.toString()) put(FEE_TOKEN, feeToken) + put(AnalyticsParam.Key.FEE_ASSET_TYPE, feeAssetType.value) putAll(getReferralParams(referralId)) }, ), AppsFlyerIncludedEvent diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index f3e444ddbb..0303bff707 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -1093,7 +1093,7 @@ internal class SwapModel @Inject constructor( } }, ) - sendSuccessEvent() + sendSwapInProgressEvent() router.replaceAll(SwapRoute.Success) } @@ -1169,7 +1169,7 @@ internal class SwapModel @Inject constructor( } } - private suspend fun sendSuccessEvent() { + private suspend fun sendSwapInProgressEvent() { val provider = dataState.selectedProvider ?: return val fee = (getSelectedFee() as? TxFee.Legacy)?.feeType ?: FeeType.NORMAL val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus ?: return @@ -1177,6 +1177,12 @@ internal class SwapModel @Inject constructor( val fromDerivationIndex = fromSwapCurrencyStatus.account.derivationIndex?.value val toDerivationIndex = toSwapCurrencyStatus.account.derivationIndex?.value + val feeToken = getFeeToken() + val feeAssetType = if (feeToken is CryptoCurrency.Coin) { + AnalyticsParam.FeeAssetType.Coin + } else { + AnalyticsParam.FeeAssetType.Token + } analyticsEventHandler.send( SwapEvents.SwapInProgressScreen( provider = provider, @@ -1185,7 +1191,8 @@ internal class SwapModel @Inject constructor( receiveBlockchain = toSwapCurrencyStatus.currency.network.name, sendToken = fromSwapCurrencyStatus.currency.symbol, receiveToken = toSwapCurrencyStatus.currency.symbol, - feeToken = getFeeToken().symbol, + feeToken = feeToken.symbol, + feeAssetType = feeAssetType, fromDerivationIndex = fromDerivationIndex, toDerivationIndex = toDerivationIndex, referralId = appsFlyerStore.get()?.refcode, @@ -1558,11 +1565,18 @@ internal class SwapModel @Inject constructor( } private fun sendSuccessSwapEvent(fromToken: CryptoCurrency, feeType: FeeType) { + val feeToken = getFeeToken() + val feeAssetType = if (feeToken is CryptoCurrency.Coin) { + AnalyticsParam.FeeAssetType.Coin + } else { + AnalyticsParam.FeeAssetType.Token + } val event = AnalyticsParam.TxSentFrom.Swap( blockchain = fromToken.network.name, token = fromToken.symbol, feeType = AnalyticsParam.FeeType.fromString(feeType.getNameForAnalytics()), - feeToken = getFeeToken().symbol, + feeToken = feeToken.symbol, + feeAssetType = feeAssetType, ) analyticsEventHandler.send( Basic.TransactionSent( diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt index 1cbf1b303f..8cc09abf5b 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/approve/model/YieldSupplyApproveModel.kt @@ -25,6 +25,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase +import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetContractAddressUseCase @@ -141,59 +142,69 @@ internal class YieldSupplyApproveModel @Inject constructor( userWallet = userWallet, network = cryptoCurrency.network, ).fold( - ifLeft = { error -> - TangemLogger.e(error.toString()) - uiState.update(YieldSupplyTransactionReadyTransformer) - analyticsEventHandler.send( - YieldSupplyAnalytics.EarnErrors( - action = YieldSupplyAnalytics.Action.Approve, - errorDescription = error.getAnalyticsDescription(), - ), - ) - yieldSupplyAlertFactory.getSendTransactionErrorState( - error = error, - popBack = params.callback::onDismissClick, - onFailedTxEmailClick = { errorMessage -> - modelScope.launch(dispatchers.default) { - yieldSupplyAlertFactory.onFailedTxEmailClick( - userWallet = userWallet, - cryptoCurrency = cryptoCurrency, - errorMessage = errorMessage, - ) - } - }, - ) - params.callback.onTransactionProgress(false) - }, - ifRight = { txHash -> - yieldSupplyPendingTracker.addPending( - userWalletId = userWallet.walletId, - cryptoCurrency = cryptoCurrency, - txIds = listOf(txHash), - ) - val event = AnalyticsParam.TxSentFrom.Earning( - blockchain = cryptoCurrency.network.name, - token = cryptoCurrency.symbol, - feeType = AnalyticsParam.FeeType.Normal, - feeToken = feeCryptoCurrencyStatus.currency.symbol, - ) - analyticsEventHandler.send( - Basic.TransactionSent( - sentFrom = event, - memoType = Basic.TransactionSent.MemoType.Null, - ), - ) - analyticsEventHandler.send(YieldSupplyAnalytics.ApprovalAction( - token = cryptoCurrency.symbol, - blockchain = cryptoCurrency.network.name, - action = YieldSupplyAnalytics.Action.Approve, - )) - params.callback.onTransactionSent() - }, + ifLeft = ::onTransactionError, + ifRight = { onTransactionSuccess(it) }, ) } } + private fun onTransactionError(error: SendTransactionError) { + TangemLogger.e(error.toString()) + uiState.update(YieldSupplyTransactionReadyTransformer) + analyticsEventHandler.send( + YieldSupplyAnalytics.EarnErrors( + action = YieldSupplyAnalytics.Action.Approve, + errorDescription = error.getAnalyticsDescription(), + ), + ) + yieldSupplyAlertFactory.getSendTransactionErrorState( + error = error, + popBack = params.callback::onDismissClick, + onFailedTxEmailClick = { errorMessage -> + modelScope.launch(dispatchers.default) { + yieldSupplyAlertFactory.onFailedTxEmailClick( + userWallet = userWallet, + cryptoCurrency = cryptoCurrency, + errorMessage = errorMessage, + ) + } + }, + ) + params.callback.onTransactionProgress(false) + } + + private suspend fun onTransactionSuccess(txHash: String) { + yieldSupplyPendingTracker.addPending( + userWalletId = userWallet.walletId, + cryptoCurrency = cryptoCurrency, + txIds = listOf(txHash), + ) + val feeAssetType = if (feeCryptoCurrencyStatus.currency is CryptoCurrency.Coin) { + AnalyticsParam.FeeAssetType.Coin + } else { + AnalyticsParam.FeeAssetType.Token + } + val event = AnalyticsParam.TxSentFrom.Earning( + blockchain = cryptoCurrency.network.name, + token = cryptoCurrency.symbol, + feeType = AnalyticsParam.FeeType.Normal, + feeToken = feeCryptoCurrencyStatus.currency.symbol, + feeAssetType = feeAssetType, + ) + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = event, + memoType = Basic.TransactionSent.MemoType.Null, + ), + ) + analyticsEventHandler.send(YieldSupplyAnalytics.ApprovalAction( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + action = YieldSupplyAnalytics.Action.Approve, + )) + params.callback.onTransactionSent() + } + private fun subscribeOnCurrencyStatusUpdates() { modelScope.launch { feeCryptoCurrencyStatusFlow.update { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index c850cb0581..3a08b50d1e 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -17,6 +17,7 @@ import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSy import com.tangem.domain.account.status.utils.CryptoCurrencyStatusOperations.getCryptoCurrencyStatus import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isHotWallet @@ -267,11 +268,18 @@ internal class YieldSupplyStartEarningModel @Inject constructor( cryptoCurrency = cryptoCurrency, yieldSupplyPendingStatus = YieldSupplyPendingStatus.Enter(txsData), ) + val feeCurrency = feeCryptoCurrencyStatusFlow.value.currency + val feeAssetType = if (feeCurrency is CryptoCurrency.Coin) { + AnalyticsParam.FeeAssetType.Coin + } else { + AnalyticsParam.FeeAssetType.Token + } val event = AnalyticsParam.TxSentFrom.Earning( blockchain = cryptoCurrency.network.name, token = cryptoCurrency.symbol, feeType = AnalyticsParam.FeeType.Normal, - feeToken = feeCryptoCurrencyStatusFlow.value.currency.symbol, + feeToken = feeCurrency.symbol, + feeAssetType = feeAssetType, ) analytics.send( YieldSupplyAnalytics.FundsEarned( diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt index 0a339baa9b..3123618250 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt @@ -195,11 +195,17 @@ internal class YieldSupplyStopEarningModel @Inject constructor( referralId = appsFlyerStore.get()?.refcode, ), ) + val feeAssetType = if (feeCryptoCurrencyStatus.currency is CryptoCurrency.Coin) { + AnalyticsParam.FeeAssetType.Coin + } else { + AnalyticsParam.FeeAssetType.Token + } val event = AnalyticsParam.TxSentFrom.Earning( blockchain = cryptoCurrency.network.name, token = cryptoCurrency.symbol, feeType = AnalyticsParam.FeeType.Normal, feeToken = feeCryptoCurrencyStatus.currency.symbol, + feeAssetType = feeAssetType, ) analytics.send( Basic.TransactionSent( From c7269fbadb126ffb1c09e3354dd70142d8993c67 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 May 2026 12:37:35 +0400 Subject: [PATCH 24/29] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 09b5c11731..e3cbf8b44f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1950,6 +1950,7 @@ internal class SwapInteractorImpl @Inject constructor( private fun Fee.increaseGasLimitBy(percentage: Int): Fee { if (this !is Fee.Ethereum) return this val gasLimit = this.gasLimit + if (gasLimit == BigInteger.ZERO) return this val increasedGasPrice = this.amount.value?.movePointRight(this.amount.decimals) ?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP) val increasedGasLimit = gasLimit.multiply(percentage.toBigInteger()).divide(hundredPercent) From 305758f4be1e3aa747ef05007f059c8f0cb928d5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 May 2026 14:02:58 +0200 Subject: [PATCH 25/29] Updated on 2026-08-14 --- .../model/AddToPortfolioModel.kt | 6 +- .../components/earn/DefaultEarnComponent.kt | 18 ++-- .../components/feed/DefaultFeedComponent.kt | 7 +- .../features/feed/model/earn/EarnModel.kt | 97 +++++++++---------- .../feed/model/feed/FeedComponentModel.kt | 71 ++++++++------ 5 files changed, 102 insertions(+), 97 deletions(-) diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt index 0aaee52e02..d6fc2e1ff3 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt @@ -136,12 +136,12 @@ internal class AddToPortfolioModel @Inject constructor( // but its call AddToPortfolioManager.onAddedTokenClick callback !isAvailableToAdd -> { val singleNetwork = portfolio.account.addedMarketNetworks.first() - callbackDelegate.onNetworkSelected(singleNetwork) + callbackDelegate.onNetworkSelected.send(singleNetwork) } // force select a network, triggers [selectedNetwork] isSingleAvailableNetwork -> { val singleNetwork = portfolio.account.availableToAddNetworks.first() - callbackDelegate.onNetworkSelected(singleNetwork) + callbackDelegate.onNetworkSelected.send(singleNetwork) } // it's important to control root screen, UI depends on it(close/arrow icon) isSinglePortfolio -> navigation.replaceAll(routeToNetworkSelector(portfolio)) @@ -352,7 +352,7 @@ internal class AddToPortfolioModel @Inject constructor( val isSingleAvailableNetwork = portfolio.account.isSingleNetwork if (isSingleAvailableNetwork) { val singleNetwork = portfolio.account.availableToAddNetworks.first() - callbackDelegate.onNetworkSelected(singleNetwork) + callbackDelegate.onNetworkSelected.send(singleNetwork) } else { navigation.pushNew(routeToNetworkSelector(portfolio)) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt index 9e4094404f..17381adf81 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt @@ -33,15 +33,14 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent import com.tangem.domain.models.earn.PreselectedEarnType +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent import com.tangem.features.feed.components.feed.FeedBottomSheetRoute -import kotlinx.serialization.Serializable import com.tangem.features.feed.model.earn.EarnModel -import com.tangem.features.feed.model.earn.analytics.EarnSource import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.earn.EarnContent import dev.chrisbanes.haze.HazeProgressive +import kotlinx.serialization.Serializable internal class DefaultEarnComponent( appComponentContext: AppComponentContext, @@ -130,17 +129,12 @@ internal class DefaultEarnComponent( componentContext: ComponentContext, ): ComposableBottomSheetComponent = when (config) { is FeedBottomSheetRoute.AddToPortfolio -> { + val manager = checkNotNull(earnModel.currentAddToPortfolioManager) { + "currentAddToPortfolioManager must be set before activating AddToPortfolio slot" + } addToPortfolioComponentFactory.create( context = childByContext(componentContext), - params = AddToPortfolioComponent.Params( - addToPortfolioManager = when { - config.source == EarnSource.BEST_OPPORTUNITIES_SOURCE.value -> - earnModel.addBestOpportunitiesPortfolioManager - config.source == EarnSource.MOSTLY_USED_SOURCE.value -> - earnModel.addMostlyUsedPortfolioManager - else -> error("Unknown source: ${config.source}") - }, - ), + params = AddToPortfolioComponent.Params(addToPortfolioManager = manager), ) } is FeedBottomSheetRoute.NetworkFilter -> EarnNetworkFilterComponent( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index 21ebee8140..f83d390045 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -98,11 +98,12 @@ internal class DefaultFeedComponent( componentContext: ComponentContext, ): ComposableBottomSheetComponent = when (config) { is FeedBottomSheetRoute.AddToPortfolio -> { + val manager = checkNotNull(feedComponentModel.currentAddToPortfolioManager) { + "currentAddToPortfolioManager must be set before activating AddToPortfolio slot" + } addToPortfolioComponentFactory.create( context = childByContext(componentContext), - params = AddToPortfolioComponent.Params( - addToPortfolioManager = feedComponentModel.addToPortfolioManager, - ), + params = AddToPortfolioComponent.Params(addToPortfolioManager = manager), ) } is FeedBottomSheetRoute.NetworkFilter -> EmptyComposableBottomSheetComponent diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index ae1280b2cf..bd7e22b2e4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -22,8 +22,9 @@ import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.earn.EarnNetworks import com.tangem.domain.models.earn.EarnTokenWithCurrency -import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager import com.tangem.domain.models.earn.PreselectedEarnType +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager.AnalyticsParams.Companion.CategoryEarn import com.tangem.features.feed.components.earn.DefaultEarnComponent import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent import com.tangem.features.feed.components.earn.EarnTypeFilterComponent @@ -44,8 +45,8 @@ import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM import com.tangem.features.feed.ui.earn.state.EarnUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import javax.inject.Inject @Stable @@ -85,24 +86,13 @@ internal class EarnModel @Inject constructor( dispatchers = dispatchers, ) - val addBestOpportunitiesPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create( - scope = modelScope, - settings = AddToPortfolioManager.Settings.Earn, - analyticsParams = AddToPortfolioManager.AnalyticsParams(source = EarnSource.BEST_OPPORTUNITIES_SOURCE.value), - ).apply { - updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected) - } - - val addMostlyUsedPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create( - scope = modelScope, - settings = AddToPortfolioManager.Settings.Earn, - analyticsParams = AddToPortfolioManager.AnalyticsParams(source = EarnSource.MOSTLY_USED_SOURCE.value), - ).apply { - updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected) - } - val bottomSheetNavigation: SlotNavigation = SlotNavigation() + var currentAddToPortfolioManager: AddToPortfolioManager? = null + private set + + private var currentAddToPortfolioManagerScope: CoroutineScope? = null + val state: StateFlow get() = stateController.uiState @@ -114,30 +104,6 @@ internal class EarnModel @Inject constructor( subscribeOnNetworks() subscribeOnBatchFlow() subscribeToMostlyUsed() - - addBestOpportunitiesPortfolioManager.onDismiss.receiveAsFlow() - .onEach { bottomSheetNavigation.dismiss() } - .launchIn(modelScope) - addBestOpportunitiesPortfolioManager.onSuccessAdded.receiveAsFlow() - .onEach { bottomSheetNavigation.dismiss() } - .onEach(::openCurrencyDetails) - .launchIn(modelScope) - addBestOpportunitiesPortfolioManager.onAddedTokenClick.receiveAsFlow() - .onEach { bottomSheetNavigation.dismiss() } - .onEach(::openCurrencyDetails) - .launchIn(modelScope) - - addMostlyUsedPortfolioManager.onDismiss.receiveAsFlow() - .onEach { bottomSheetNavigation.dismiss() } - .launchIn(modelScope) - addMostlyUsedPortfolioManager.onSuccessAdded.receiveAsFlow() - .onEach { bottomSheetNavigation.dismiss() } - .onEach(::openCurrencyDetails) - .launchIn(modelScope) - addMostlyUsedPortfolioManager.onAddedTokenClick.receiveAsFlow() - .onEach { bottomSheetNavigation.dismiss() } - .onEach(::openCurrencyDetails) - .launchIn(modelScope) } private fun openCurrencyDetails(result: AddToPortfolioManager.Result) { @@ -354,17 +320,44 @@ internal class EarnModel @Inject constructor( contractAddress = earnTokenWithCurrency.earnToken.tokenAddress, decimalCount = earnTokenWithCurrency.earnToken.decimalCount, ) - when (source) { - EarnSource.BEST_OPPORTUNITIES_SOURCE -> addBestOpportunitiesPortfolioManager.apply { - setTokenParams(token) - setTokenNetworks(listOf(network)) - } - EarnSource.MOSTLY_USED_SOURCE -> addMostlyUsedPortfolioManager.apply { - setTokenParams(token) - setTokenNetworks(listOf(network)) - } + val manager = createAddToPortfolioManager(source = source).apply { + setTokenParams(token) + setTokenNetworks(listOf(network)) } - bottomSheetNavigation.activate(FeedBottomSheetRoute.AddToPortfolio(source.value)) + currentAddToPortfolioManager = manager + // Drop the slot through null so the same-source repeat click still recreates the child. + bottomSheetNavigation.dismiss() + bottomSheetNavigation.activate(FeedBottomSheetRoute.AddToPortfolio(source = source.value)) + } + + private fun createAddToPortfolioManager(source: EarnSource): AddToPortfolioManager { + currentAddToPortfolioManagerScope?.cancel() + val managerScope = CoroutineScope( + modelScope.coroutineContext + SupervisorJob(modelScope.coroutineContext.job), + ) + currentAddToPortfolioManagerScope = managerScope + + val manager = addToPortfolioManagerFactory.create( + scope = managerScope, + settings = AddToPortfolioManager.Settings.Earn, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = source.value, category = CategoryEarn), + ).apply { + updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected) + } + + manager.onDismiss.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .launchIn(managerScope) + manager.onSuccessAdded.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .onEach(::openCurrencyDetails) + .launchIn(managerScope) + manager.onAddedTokenClick.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .onEach(::openCurrencyDetails) + .launchIn(managerScope) + + return manager } private fun onTypeFilterOptionSelected(type: EarnFilterType) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index 7a7c71ccc5..a0cb8e55db 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -41,10 +41,8 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentHashMap -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import org.joda.time.DateTime import org.joda.time.DateTimeZone import javax.inject.Inject @@ -62,7 +60,7 @@ internal class FeedComponentModel @Inject constructor( private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase, private val appRouter: AppRouter, private val designFeatureToggles: DesignFeatureToggles, - addToPortfolioManagerFactory: AddToPortfolioManager.Factory, + private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, paramsContainer: ParamsContainer, @@ -87,16 +85,13 @@ internal class FeedComponentModel @Inject constructor( dispatchers = dispatchers, ) - val addToPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create( - scope = modelScope, - settings = AddToPortfolioManager.Settings.Earn, - analyticsParams = AddToPortfolioManager.AnalyticsParams(source = AnalyticsParam.ScreensSources.Markets.value), - ).apply { - updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected) - } - val bottomSheetNavigation: SlotNavigation = SlotNavigation() + var currentAddToPortfolioManager: AddToPortfolioManager? = null + private set + + private var currentAddToPortfolioManagerScope: CoroutineScope? = null + val state: StateFlow get() = stateController.uiState @@ -110,18 +105,6 @@ internal class FeedComponentModel @Inject constructor( fetchCharts() subscribeOnCurrencyUpdate() subscribeOnDataState() - - addToPortfolioManager.onDismiss.receiveAsFlow() - .onEach { bottomSheetNavigation.dismiss() } - .launchIn(modelScope) - addToPortfolioManager.onSuccessAdded.receiveAsFlow() - .onEach { bottomSheetNavigation.dismiss() } - .onEach(::openCurrencyDetails) - .launchIn(modelScope) - addToPortfolioManager.onAddedTokenClick.receiveAsFlow() - .onEach { bottomSheetNavigation.dismiss() } - .onEach(::openCurrencyDetails) - .launchIn(modelScope) } private fun openCurrencyDetails(result: AddToPortfolioManager.Result) { @@ -431,12 +414,46 @@ internal class FeedComponentModel @Inject constructor( contractAddress = earnTokenWithCurrency.earnToken.tokenAddress, decimalCount = earnTokenWithCurrency.earnToken.decimalCount, ) - val route = FeedBottomSheetRoute.AddToPortfolio(AnalyticsParam.ScreensSources.Markets.value) - addToPortfolioManager.apply { + val manager = createAddToPortfolioManager().apply { setTokenParams(token) setTokenNetworks(listOf(network)) } - bottomSheetNavigation.activate(route) + currentAddToPortfolioManager = manager + // Drop the slot through null so the same-source repeat click still recreates the child. + bottomSheetNavigation.dismiss() + bottomSheetNavigation.activate(FeedBottomSheetRoute.AddToPortfolio(AnalyticsParam.ScreensSources.Markets.value)) + } + + private fun createAddToPortfolioManager(): AddToPortfolioManager { + currentAddToPortfolioManagerScope?.cancel() + val managerScope = CoroutineScope( + modelScope.coroutineContext + SupervisorJob(modelScope.coroutineContext.job), + ) + currentAddToPortfolioManagerScope = managerScope + + val manager = addToPortfolioManagerFactory.create( + scope = managerScope, + settings = AddToPortfolioManager.Settings.Earn, + analyticsParams = AddToPortfolioManager.AnalyticsParams( + source = AnalyticsParam.ScreensSources.Markets.value, + ), + ).apply { + updateLaunchMode(AddToPortfolioManager.LaunchMode.Preselected) + } + + manager.onDismiss.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .launchIn(managerScope) + manager.onSuccessAdded.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .onEach(::openCurrencyDetails) + .launchIn(managerScope) + manager.onAddedTokenClick.receiveAsFlow() + .onEach { bottomSheetNavigation.dismiss() } + .onEach(::openCurrencyDetails) + .launchIn(managerScope) + + return manager } private fun handleEarnPageOpenClicked() { From fa7926ebf9a23656efed52af629d487c199000f3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 May 2026 17:07:34 +0300 Subject: [PATCH 26/29] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 169 +++++++++++++++--- core/res/src/main/res/values-es/strings.xml | 24 ++- core/res/src/main/res/values-fr/strings.xml | 16 ++ core/res/src/main/res/values-it/strings.xml | 2 + core/res/src/main/res/values-ja/strings.xml | 89 ++++++++- .../src/main/res/values-pt-rBR/strings.xml | 82 ++++++++- core/res/src/main/res/values-ru/strings.xml | 45 ++++- .../src/main/res/values-uk-rUA/strings.xml | 29 +++ .../src/main/res/values-zh-rCN/strings.xml | 90 ++++++++++ .../src/main/res/values-zh-rTW/strings.xml | 2 + core/res/src/main/res/values/strings.xml | 100 ++++++++++- 11 files changed, 608 insertions(+), 40 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 34d53f84b5..786150a81b 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -80,6 +80,9 @@ Token hinzufügen Wähle den Token aus, den Du erhalten möchtest Wähle den Token, den Du tauschen möchtest + Guthaben hinzufügen + Tauschen + Übertragung Zum Portfolio hinzufügen Token hinzufügen Sortieren und Gruppieren @@ -87,6 +90,10 @@ Netzwerk wählen Token anlegen Token verwalten + Kreditkarte oder Bankkonto + Teile deine Adresse oder dein QR-Code + Zwische deinen Portfolios + Empfangen Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen. Standard Altbestand @@ -136,7 +143,7 @@ Du hast Deine Wallet erfolgreich gesichert. Diese Wörter können bei Verlust nicht wiederhergestellt werden. Bewahre diese an einem sicheren Ort auf. Sicherung abgeschlossen - Deine geheime Wiederherstellungsphrase ist eine feste Folge von %s zufälligen Wörtern, mit denen Du auf Deiner Wallet zugreifefst und sie wiederherstellen kannst. + Deine geheime Wiederherstellungsphrase ist eine feste Folge von %s zufälligen Wörtern, mit denen Du auf Deiner Wallet zugreifst und sie wiederherstellen kannst. Diese Worte sind unwiederbringlich verloren. Bewahre diese gut auf. Sicher aufbewahren Speicher diese %s Wörter an einem sicheren Ort und gebe diese niemals an andere weiter. @@ -217,8 +224,10 @@ Zugang verweigert Konto Konten + %s fehlgeschlagen Aktivieren Hinzufügen + Guthaben hinzufügen Zum Portfolio hinzufügen Token hinzufügen Token hinzufügen @@ -232,6 +241,8 @@ Anwenden Genehmigung Genehmigen + Genehmigt + Genehmigen Achtung Verfügbare Netzwerke Sicherungskopie @@ -274,14 +285,20 @@ Tag Tage + + %d Tag zuvor + %d Tage zuvor + Entfernen Deaktivieren Deaktiviert + Deaktivieren Trennen Erledigt Bearbeiten Aktivieren Aktiviert + Aktivieren Fehler Aufladegebühr Umtausch @@ -308,11 +325,16 @@ Ausblenden Halten bis %s Stunde + + Stunde + Stunde + %dStunde her %dStunden her Importieren + in In Arbeit Unzureichende Mittel Später @@ -327,6 +349,7 @@ %dMinuten her Monat + Mehr Netzgebühr Der überwiesene Betrag wird um %1$s (%2$s) gekürzt, um die gewählte Gebührenhöhe zu decken. @@ -356,6 +379,8 @@ %1$s — %2$s Weiterlesen Empfangen + Erhalten + Empfang Empfohlen Ablehnen Neu laden @@ -372,7 +397,10 @@ Aktion auswählen Verkaufen Senden + Senden: Absenden der Transaktion fehlgeschlagen + Senden + Gesendet Der Server ist nicht verfügbar. Bitte versuche es später erneut. Teilen Link teilen @@ -383,6 +411,7 @@ Überspringen Etwas ist schiefgelaufen. Staken + Einsatz Staking Start Einreichen @@ -390,6 +419,8 @@ Unterstützung Unterstützte Netzwerke Tauschen + Tauschen + Tauschen Tangem Tangem Wallet Tippen und halten @@ -398,6 +429,7 @@ An Zu %s Heute + Token Zu sendendes Token %d Token @@ -407,6 +439,8 @@ Transaktionsstatus Transaktionen Überweisung + Übertragen + Bitte versuche es später noch einmal. Die Daten konnten nicht geladen werden… Ich verstehe Ich verstehe, fahre bitte fort. @@ -416,10 +450,12 @@ Staking beenden Aufgrund der Beschränkungen von %1$s können nur %2$d UTXOs in eine einzige Transaktion passen. Das bedeutet, dass du nur %3$s oder weniger senden kannst. Du musst den Betrag reduzieren. Wert kopiert + Abstimmen Meine Wallet Warnung Woche mit + Überweisen Ja Ertragsmodus Vertragsadresse kopiert! @@ -505,6 +541,10 @@ Nicht verfügbar Wir können im Moment keine Verbindung zum Provider herstellen. Bitte versuchen Sie es später noch einmal. Der Dienst ist nicht verfügbar. Bitte versuchen Sie es erneut. + Es wurden Gelder an zusätzlichen Adressen gefunden. Aktivieren deine Dynamische Adressen, um auf diese zuzugreifen. + Auf weiteren Adressen gefundene Gelder + Dynamische Adresse + Die Verwaltung dynamischer Adressen wird verfügbar sein, sobald die ausstehenden Transaktionen im Netzwerk eingegangen sind. %@ ist abgeschlossen Beste Gelegenheiten Filter löschen Die Liste ist vorübergehend leer, da sie gerade aktualisiert wird. Schauen Sie in Kürze wieder rein. @@ -515,7 +555,7 @@ Netzwerke Meist verwendet Keine Ergebnisse - Verdienen + Verdiene Hallo Support-Team, ich habe einen Fehler mit dem Code %s festgestellt. WalletConnect-Fehler Du hast eine Karte oder Ring aus einer anderen Wallet verwendet. Tippe auf die Karte oder Ring, die dieser Wallet zugeordnet ist. @@ -577,12 +617,14 @@ Bester Preis Warnliste der FCA Der Festzins ist nicht verfügbar + Anbieter für Tausch Beste Wahl Anbieter in FCA-Warnliste Verfügbar bis zu %s Erhältlich bei %s Für dieses Paar nicht verfügbar Erlaubnis erforderlich + Genehmigung erforderlich Empfohlen Gekauft %s Kauf %s @@ -629,6 +671,7 @@ Die Genehmigungsfunktion ist erforderlich, um einer anderen Adresse die Berechtigung zur Verwendung einer bestimmten Menge Ihrer Token zu erteilen. Standardmäßig können Smart Contracts nicht auf deine Token zugreifen, es sei denn, du stimmen zu. Indem du deine Token \"freischaltest\", autorisierst du den StakeKit Smart Contract, sie zu verwenden. Die Miner des Netzwerks erhalten eine Gasgebühr (von dir bezahlt), um diese Aktion in der Blockchain aufzuzeichnen. Du kannst deine Token einsetzen, nachdem du die Genehmigung erteilt hast. Um fortzufahren, musst du Polygon Smart Contract erlauben, deine %s zu verwenden Um fortzufahren, erteile %1s Smart Contracts die Berechtigung, dein zu %2s verwenden. + Dezentrale Börsen benötigen eine Berechtigung, um mit Ihrer Wallet zu interagieren. %1s Erlaubnis erteilen Unbegrenzt Die Adressen werden direkt auf Deiner Tangem-Hardware-Wallet generiert – sofort einsatzbereit und vollständig geschützt. @@ -656,6 +699,8 @@ Hält Deine Kryptowährungen sicher und offline. Schlank wie eine Kreditkarte, sicherer als ein Banktresor. Wenn dies der Fall ist, musst Du von vorne beginnen. Vorhandene Wallet über Google Drive-Backup wiederherstellen + Wir arbeiten an einer Google Drive-Datensicherung, um die Wiederherstellung der Wallet noch einfacher zu gestalten. + Google Drive-Sicherung kommt bald Google Drive-Backup Erstelle eine neue, sichere Wallet und übertrage Deine Gelder, um zusätzlichen Schutz zu gewährleisten. Neue Wallet erstellen @@ -713,7 +758,7 @@ Schlüsselmigration Gerät scannen Upgrade starten - Du stehst kurz vor dem Upgrade auf unsere Hardware-Wallet. Damit werden Deine Vermögenswerte sicher in Offline-Speichern aufbewahrt. + Du stehst kurz vor dem Upgrade auf unsere Hardware-Wallet. Deine Vermögenswerte werden darin sicher im Offline-Speicher aufbewahrt. Tangem Wallet Upgrade auf Hardware Wallet Schütze Deine Kryptowährungen mit Tangems erstklassiger Hardware-Wallet. @@ -775,6 +820,7 @@ Dieses Asset ist für dieses Wallet nicht verfügbar Hinzufügen APY %s + Marktpreis Mein Portfolio Markt Verdiene Geld mit Tangem @@ -787,10 +833,11 @@ Keine Daten **Hinzufügen zu Ihrem Portfolio**, um mit dem Kauf, Tausch oder Erhalt dieses Vermögenswerts zu beginnen Zum Portfolio hinzufügen + Dein Portfolio Marktimpuls Schnelle Aktionen Alles löschen - Markt durchsuchen + Token suchen Neueste In Ihrem Portfolio Ergebnis @@ -816,6 +863,7 @@ Ertragsmodus Staking ist der einfachste Weg, um Belohnungen für Deine Kryptowährung zu erhalten. %s Verdiene bis zu %s APY + in einem anderen Netzwerk oder Konto Token hinzugefügt Über %s @@ -980,7 +1028,7 @@ Trage dich in die Warteliste ein und erhalte eine Zahlungskarte, die es so noch nie gab. Tangem Visa Card Bedingungen - Zahlen Sie mindestens $100 ein, halten Sie den Betrag 30 Tage und erhalten Sie $10. + Zahle mindestens $100 ein, halten den Betrag 30 Tage und erhalte $10. Yield-Mode-Kampagne Du musst einen einzigen Zugangscode einrichten, um alle deine Geräte zu schützen Schützen @@ -997,6 +1045,11 @@ Bitte wiederhole den Vorgang. Die Karte oder Ring wird auf Werkseinstellungen zurückgesetzt. Aktivierungsfehler Token hinzufügen + + Adressen synchronisieren, um eine Adresse für das Netzwerk %d zu erhalten + Adressen synchronisieren, um eine Adresse für das Netzwerk %d zu erhalten + + Synchronisiere dein Wallet Du hast eine Backup-Karte oder einen Backup-Ring hinzugefügt. Nach Abschluss des Backup-Vorgangs kannst Du keine weiteren Backup-Geräte hinzufügen. Wenn Du eine weitere Karte oder einen weiteren Ring hast, fügen diesen dem Backup jetzt hinzu. Möchtest Du den Backup-Vorgang fortsetzen? Der Sicherungsvorgang ist teilweise abgeschlossen. Du kannst ihn jetzt nicht beenden. Eine Passphrase ist eine optionale Sicherheitsfunktion, die Deiner Wiederherstellungsphrase ein Wort oder eine Phrase hinzufügt und so einen neuen Satz von Wallet-Adressen für zusätzlichen Schutz erstellt. @@ -1018,6 +1071,7 @@ Andere Optionen Deine Schlüssel(private-keys) werden sicher im Inneren der Karte oder Ring generiert. Es gibt keine Seed-Phrase, d. h. niemand kann sie exportieren oder stehlen. Schlüssel anonym generieren + Indem Sie fortfahren, stimmen Sie den folgenden Bedingungen zu: Deine Karte oder Ring ist aktiviert und einsatzbereit Erfolgreich! Deine Wallet ist eingerichtet und einsatzbereit! @@ -1026,7 +1080,9 @@ Erste Schritte Für die Karte oder Ring, die du hinzufügen möchtest, wurde bereits eine andere Wallets erstellt. Wenn du Guthaben auf dieser Wallets hast, hebe es bitte ab, setze diese Karte oder Ring zurück und füge sie als Backup hinzu. Sicher deine Wallet + Biometrische Daten nutzen Backups anlegen + Letzter Schritt Biometrische Daten Lese mehr über die Seed-Phrase @@ -1093,12 +1149,20 @@ Diese Transaktion wurde bereits verarbeitet. Es sind keine weiteren Maßnahmen erforderlich. Die besten Preise erzielen... Sofort + Die Verifizierung ist kostenlos und dauert in der Regel 1-2 Minuten + Tangem hat keinen Zugriff auf Ihre Identitätsinformationen; Sie teilen Daten direkt mit dem regulierten Anbieter. + Die Verifizierung schaltet den vollen Zugang zu zukünftigen Transaktionen mit diesem Anbieter frei + Wählen Sie eine andere Methode + Zur Einhaltung der örtlichen Vorschriften verlangt %@ eine Identitätsprüfung. + Identitätsprüfung durch den Zahlungsanbieter erforderlich + Verifizieren + Was ist wichtig? Durch die Nutzung der Onramp-Funktionalität stimmst Du den %1$s und %2$s des Anbieters zu. Der Service wird von einem externen Anbieter bereitgestellt. \n Tangem übernimmt keine Verantwortung. Der Kaufbetrag sollte nicht höher sein als %s Der zu kaufende Betrag muss mindestens %s betragen Keine verfügbaren Anbieter für diese Währung - Schnellste + Schnellste Bearbeitung Bezahlen mit Zahlungsmethode Verfügbar bis zu %s @@ -1133,6 +1197,8 @@ über %s Du zahlst Gruppe erstellen + Netzwerke auswählen + Nach Kontostand sortieren Nach Guthaben Token organisieren Gruppe löschen @@ -1156,6 +1222,13 @@ Keine unterstützten Token gefunden Dieser QR-Code enthält Parameter, die nicht erkannt werden: %s. Einige Zahlungsdetails können verloren gehen, wenn Sie fortfahren. Unbekannte Parameter + Kreditkarte oder Bankkonto + Teilen deine Adresse oder dein QR-Code + Sicherer Verkauf von Kryptowährungen + An eine andere Wallet senden + Zwischen deinen Portfolios + Andere + Schnell aufladen Kein Memo erforderlich %1$s ( %2$s ) im %3$s Netzwerk %1$s im %2$s Netzwerk @@ -1402,6 +1475,7 @@ Der Zeitraum, den du nach der Beantragung der Abhebung von Geldern aus dem Staking warten musst, bevor die Token verfügbar werden. Aufwärmphase Die zugewiesene Zeit für die Aktivierung der Teilnahme am Staking. + Staking aktiviert Zurzeit sind keine Validierer verfügbar. Bitte versuche es später noch einmal. Staking nicht verfügbar Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Du die Verwendung Deines Tokens für das Staking genehmigst. @@ -1512,23 +1586,34 @@ Zum Fortfahren ist eine eingehende Transaktion von mindestens %1$s erforderlich Unzureichende Mittel Durch die Genehmigung erlaubst Du dem Smart Contract, Deine Token in zukünftigen Transaktionen zu verwenden. + Detaillierter Modus Fester Zinssatz Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. + Tausch läuft Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche. Neuer Swap-Anbieter verfügbar! Suchen Sie etwas anderes?\n Versuchen Sie es mit der Suche oder erkunden Sie eine andere Kryptowährung! - Suchen Sie nach einem beliebigen Token, auch wenn es noch nicht in Ihrer Liste ist. + Suche nach einem beliebigen Token, auch wenn es noch nicht in deiner Liste ist. Nutzen Sie die Suche, um zu finden, was Sie benötigen. + Einfacher Modus Vertraue auf den rund um die Uhr verfügbaren Support bei allen Problemen Immer für Dich da Mehrere vertrauenswürdige Anbieter an einem Ort – tausche mühelos alle Vermögenswerte in Deiner Wallet + Tauschen Sie Kryptowährungen direkt in Tangem\nkeine zusätzlichen Überweisungen\nkeine Verschiebung von Geldern zu Börsen Tausche mit uns + Tausche innerhalb deiner Wallet Keine Fummeleien, keine Umsätze, keine blinden Flecken – Deine Transaktion ist immer geschützt + Tauschvorgänge werden über vertrauenswürdige Anbieter abgewickelt. Deine Schlüssel verbleiben jederzeit in deiner Tangem-Wallet. Klar. Transparent. Selbstverwahrung. Undurchdringliche Verteidigung + Du behältst die Kontrolle Maximiere Deine Wert mit Tarifen aus einem breiten Netzwerk vertrauenswürdiger Anbieter und wähle immer den Besten aus + Tangem vergleicht mehrere Anbieter, sowohl DEX als auch CEX. Der beste Kurs wird automatisch ausgewählt. Bevorzugst Du einen anderen Anbieter? Dann kannst du ihn manuell auswählen. Unschlagbare Preise + Bester verfügbarer Preis Problemlos und intuitiv, sodass Deine Token mit nur wenigen Handgriffen getauscht werden können + Tauschen Token über viele Netzwerke und Tausende von Token hinweg 0% Gebühr für Stablecoin-zu-Stablecoin-swaps Einfach bequem + 90+ Blockchains\n16.000+ Vermögenswerte Tausch über Anbieter Dein Vermögen Der Betrag umfasst:\n- Gebühr des Dienstanbieters\n- Netzgebühr für die Rücksendung von %s von der Vermittlungsstelle an die Adresse des Nutzers. @@ -1539,7 +1624,9 @@ Alle dezentralen Börsen benötigen Genehmigungen, um zu verhindern, dass intelligente Verträge ohne Ihre Erlaubnis auf Ihre Geldbörse zugreifen. Smart Contracts können nicht auf Ihre Token zugreifen, wenn Sie nicht zustimmen. Indem Sie Ihre Token \"freischalten\", ermächtigen Sie den 1-Zoll-Smart-Contract, sie auszugeben. Die Miner des Netzwerks erhalten eine (von Ihnen bezahlte) Gasgebühr, um diese Aktion in der Blockchain aufzuzeichnen. Sie können Ihre Token tauschen, nachdem Sie Ihre Zustimmung gegeben haben. Genehmigen Fehler bei der Gebührenschätzung. Bitte sende dein Feedback an den Support. + Du sendest vom Du wechselst + De sendest Der Tausch dieser Menge ausgewählter Token hat erhebliche Auswirkungen auf den Preis und verringert dein Ergebnis. Aufgrund geringer Liquidität erhalten Sie möglicherweise deutlich weniger. Versuchen Sie es mit einem kleineren Betrag oder einem anderen Anbieter. Hoher Einfluss auf den Preis @@ -1548,11 +1635,14 @@ Erlaubnis erteilen Tauschen Tauschen... + Zu erhaltender Betrag Du erhältst Token auswählen Nicht verfügbar Nicht genug Liquidität für diesen Handel. Reduzieren Sie den Betrag oder wählen Sie einen anderen Anbieter. Handel zu groß + Übertragung + Übertragung Wir freuen uns über Ihr Feedback Tangem Pay jetzt in der Beta Karte kann nicht umbenannt werden @@ -1603,7 +1693,7 @@ PIN-Code erstellt CVC Daten konnten nicht geladen werden. Versuche es später noch einmal. - Ablaufdatum + Ablauf Karte einfrieren Details ausblenden Verstecken @@ -1629,11 +1719,12 @@ Karte neu ausstellen Es sind nur Buchstaben und Zahlen erlaubt Ungültige Zeichen + Kartenname Aufdecken Details anzeigen Tausche beliebige Vermögenswerte in Deinem Portfolio für deine Karte. Kartendetails - Bitte versuchen Sie es später noch einmal. + Bitte versuche es später noch einmal. Karte entsperren Komm zurück zur App, falls du es vergisst. Dein PIN-Code @@ -1641,6 +1732,7 @@ Auszahlung derzeit nicht möglich Sie können keinen Tausch oder eine neue Auszahlung starten, bis die aktuelle abgeschlossen ist. Auszahlung läuft + Kartenname Limit festlegen ab %s Das Limit konnte nicht festgelegt werden. Bitte versuchen Sie es erneut. Ändern @@ -1665,8 +1757,15 @@ Holen Sie sich Ihre kostenlose virtuelle Tangem Visa-Karte Tangem Pay erhalten Zum Support + Es generiert einen neuen Satz Kartendaten. + Ausgabegebühr + Einzahlung von USDC auf das Zahlungskonto zur Deckung der Ausstellungsgebühr + Gebühr kann nicht gedeckt werden + Eine zusätzliche Karte ausstellen? + Karte ausstellen Es dauert in der Regel bis zu 15 Minuten. Einrichtung Ihrer Tangem-Karte + Ausstellung einer neuen digitalen Karte Ausstellung Deiner Karte Die Karte wird in der Regel innerhalb von 5 Minuten automatisch ausgestellt. In seltenen Fällen, wenn eine manuelle Überprüfung erforderlich ist, kann die Ausstellung bis zu 48 Stunden dauern. Tangem Pay @@ -1683,6 +1782,8 @@ KYC-Sperre ausblenden Leider konnten wir Ihre Identität nicht verifizieren. + Du kannst bis zu 3 Karten haben. Lösche eine, um eine neue Karte hinzuzufügen. + Maximale Anzahl ausgegebener Karten Holen Sie sich Ihre kostenlose virtuelle Tangem Visa-Karte Nutzen Sie USDC für alltägliche Zahlungen Karte erhalten @@ -1692,6 +1793,8 @@ Zahlen Sie genau das, was Sie sehen Ein separates Zahlungskonto wird erstellt, ohne Ihre Adressen und Vermögenswerte offenzulegen Unerreichte Privatsphäre + Verknüpfen Sie eine Zahlungskarte + Wir richten eine Wallet ein. Erhalten Sie Ihre kostenlose Tangem Pay Card in wenigen Minuten Bezahlen mit Zahlungskonto @@ -1712,6 +1815,7 @@ Daten können derzeit nicht angezeigt werden, Kartenzahlungen funktionieren jedoch weiterhin. Satz \nPIN-Code Karte deaktiviert + Ersetzen deine Karte Sitzung abgelaufen Zugang wiederherstellen Nutzen Sie USDC für alltägliche Zahlungen @@ -1741,6 +1845,7 @@ Die Genehmigung wurde widerrufen. Dein Guthaben befindet sich weiterhin im Ertragsmodus. Um Aktionen durchzuführen, wechsel bitte in den Ertragsmodus und erteilen die Berechtigung erneut. Verfügbares Guthaben Gesamtsaldo + Bis zu %s effektiver Jahreszins Generiere XPUB Ausblenden Du bist dabei, dieses Token vom Hauptbildschirm auszublenden. Du kannst es jederzeit über die Seite „Token verwalten“ wieder hinzufügen. @@ -1761,23 +1866,33 @@ Tausche diesen Token gegen einen anderen zu %1$s Servicegebühren von Februar %2$s-%3$s. Tausche mit Changelly, %s Gebühren Jetzt tauschen - Hot Krypto 🔥 + Markttrend 🔥 Nicht verfügbar zum Kauf Nicht zum Verkauf verfügbar Nicht verfügbar für Tausch von %s Nicht zum Tausch verfügbar + Belohnung einfordern Vertrag: %s + Deaktivierung des Ertragsmodus + Verdient aus dem Einsatz Du hast noch keine Transaktionen Der Transaktionsverlauf konnte nicht geladen werden.\nKlicke auf die Schaltfläche Neu laden, um die Informationen zu aktualisieren. + aus: %%image%% %s Mehrere Adressen Die Transaktionshistorie wird für diese Blockchain derzeit nicht verfügbar. Aber keine Sorge, wir arbeiten daran! In der Zwischenzeit kannst du es im Explorer überprüfen. Operation + Ausstehend + Belohnungen neu stecken + Belohnungen + Staking-Belohnungen + zu: %%image%% %s für: %s von: %s zu: %s Validierer: %s Benachrichtigungen sind aktiviert, funktionieren aber erst, wenn Du Benachrichtigungen in Deinen Geräteeinstellungen zulässt. Transaktionsbenachrichtigungen + Transfer läuft Minimum %s Der Mindesttransaktionsbetrag beträgt %1$s. Die Netzwerkgebühren für beliebte Token im Tron-Netzwerk können höher sein. Das Staking von TRX kann helfen, die Transaktionskosten zu senken. @@ -1809,7 +1924,9 @@ Genehmigung für Transaktionen anfordern Sei der Erste, der von neuen Aktionen erfährt Frühzeitiger Zugriff auf neue Funktionen und exklusive Angebote. + Preisbenachrichtigungen, Produktneuheiten und exklusive Angebote Updates zu Funktionen und Neuigkeiten + Angebote & Updates Möchtest du Push-Benachrichtigungen verwenden? Aktiviere Push-Benachrichtigungen und wir benachrichtigen Dich sofort, wenn Gelder eintreffen Verpasse keine Transaktion @@ -2051,6 +2168,10 @@ Verwende deine Karte oder Ring, um eine Adresse für das %d-Netz zu erhalten Verwende deine Karte oder Ring, um mehrere Adressen für die %d-Netzwerke zu erhalten + + Adressen synchronisieren, um eine Adresse für das Netzwerk %d zu erhalten + Adressen synchronisieren, um eine Adresse für das Netzwerk %d zu erhalten + Einige Adressen fehlen Das Netzwerk ist derzeit nicht erreichbar. Bitte versuchen Sie es später erneut. Netzwerk ist nicht erreichbar @@ -2217,6 +2338,7 @@ Die Gebühr wird abgezogen und Dein Vermögen wird erneut verliehen. Um weiterhin Geld verdienen zu können, ist eine Genehmigung erforderlich. Genehmigung bestätigen + Durchschnittlicher Jahreszins %1$s%% Deine Gelder werden derzeit dem Aave-Protokoll bereitgestellt, Du kannst sie jedoch jederzeit verwalten. Deine%s ist in Aave hinterlegt Chart konnte nicht geladen werden... @@ -2230,18 +2352,18 @@ Meine Mittel Deine %1$s sind nun bei Aave angelegt und erwirtschaften Rendite. Du besitzt %2$s -Token, die Dein Guthaben repräsentieren und automatisch Rendite generieren. Bei jeder Aufladung wird Dein Aave-Konto zusätzliches Guthaben gutgeschrieben, um weitere Rendite zu erzielen (abzüglich Gebühren). Ertragsmodus - Gesamtverdienst + Gesamtertrag Übertragungen zu Aave Entdecke Aave Dies ist die aktuelle Liefergebühr auf %s. Die tatsächlichen Kosten werden auf der Registerkarte \"Aktivierung\" angezeigt. Aktuelle Gebühr - Alle zukünftigen %s-Einzahlungen werden automatisch an Aave geliefert, wobei die Transaktionsgebühr abgezogen wird. + Alle zukünftigen %s Das Guthaben wird Aave automatisch gutgeschrieben, nachdem die Transaktionsgebühr abgezogen wurde. Von jeder zukünftigen Aufladung wird eine ungefähre Netzwerkgebühr von %1$s ( %2$s ) abgezogen, die Dein Limit von %3$s ( %4$s ) nicht überschreiten wird. Wenn die Netzwerkgebühren über die maximale Gebühr steigen, wird die Transaktion erst durchgeführt, wenn diese sinken. Du kannst dieses Limit später ändern. Maximale Gebühr Der Mindestbetrag wird auf Grundlage der aktuellen Netzwerkgebühr berechnet, sodass er 4%% des Aufladebetrags nicht überschreitet, was einen Mindestbetrag von %1$s (%2$s) ergibt. Mindestaufladung - Gebührenpolitik + Gebührenregelung für Aufladungen Tangem erhebt außerdem eine Servicegebühr von 15% auf den erzielten Ertrag. Deine Gelder werden automatisch an Aave überwiesen, sobald die Netzwerkgebühren niedriger sind oder Dein Guthaben den erforderlichen Mindestbetrag erreicht. Die Gebühren sind aufgrund der hohen Marktaktivität derzeit höher als üblich. Du kannst jetzt fortfahren oder später noch einmal vorbeischauen, wenn die Gebühren niedriger sind. @@ -2252,13 +2374,13 @@ Token-Genehmigung erforderlich Prüfe Deine Netzwerkverbindung Informationen zu den Netzwerkgebühren nicht erreichbar - Jede Einzahlung, die Du tätigst, wird automatisch an Aave weitergeleitet. + Jede Aufladung wird automatisch an Aave übermittelt. Alle %1$s auf Ihrem Konto werden automatisch an Aave bereitgestellt. Automatische Übertragung zu Aave Senden, tauschen oder verkaufen Deine Gelder sofort, wann immer Du willst. Sofort verfügbar Wie funktioniert das? - Aave ist ein On-Chain-Protokoll zur Erstellung von nicht-kustodialen Liquiditätsmärkten, um Zinsen mit variablem Satz zu verdienen. + Aave ist ein On-Chain-Protokoll, das Non-Custodial-Liquiditätsmärkte bietet und es Nutzern ermöglicht, Renditen zu variablen Zinssätzen zu erzielen. Dezentral und selbstverwahrend Durch die Nutzung dieses Dienstes erklärst Du Dich mit provider\n%1$s und %2$s einverstanden Mit Aave verbinden @@ -2268,18 +2390,18 @@ Aave Durchschnitt %s Renditen des letzten Jahres - Der aktuelle Zinssatz ist immer variabel und wird automatisch vom Aave On-Chain-Smart-Contract auf der Grundlage von Angebot und Nachfrage in Echtzeit berechnet. + Der aktuelle Zinssatz ist stets variabel und wird automatisch vom On-Chain-Smart-Contract von Aave auf Basis von Angebot und Nachfrage in Echtzeit berechnet. Unterstützt durch Der Zinssatz ist variabel - Wenn Du dein Guthaben auflädst, wird es automatisch an Aave weitergeleitet, um Zinsen zu verdienen. zur Deckung der Transaktionsgebühr wird ein Betrag von %s abgezogen. + Wenn du dein Guthaben auflädst, wird es automatisch an Aave weitergeleitet, um Zinsen zu verdienen. zur Deckung der Transaktionsgebühr wird ein Betrag von %s abgezogen. Vermögenswerte liefern - Dein %s wird an Aave übermittelt, bleibt aber verwaltbar. - Siehe Gebührenrichtlinie - Deine nächste Aufladung wird automatisch an Aave weitergeleitet. + Dein %s wird Aave ohne Abschließmöglichkeiten zur Verfügung gestellt und bleibt uneingeschränkt zugänglich. + Siehe die Gebührenrichtlinien für Aufladungen. + Deine nächste Aufladungen werden automatisch an Aave übermittelt. Alle Ihre zukünftigen eingehenden %1$s-Einlagen werden automatisch an Aave bereitgestellt. Aktiv Pausiert - Deaktiviere den Yield-Modus + Deaktiviere den Ertragsmodus Wenn Du diese Option deaktivierst, werden Deine Vermögenswerte von Aave abgezogen, in Deiner Wallet wieder in %s umgewandelt und die Zinsgutschrift gestoppt. Eine Netzwerkgebühr wird von der Blockchain erhoben, wenn Sie den Yield-Modus verlassen. Deaktiviere den Yield-Modus @@ -2289,7 +2411,8 @@ Zinsen fallen automatisch an. Zinsen fallen automatisch an Ertragsmodus - Bearbeitung Deiner Einzahlung + Aktivierung des Ertragsmodus + Renditemodus - %1$s%% APY Ertragsmodus Yield-Mode-Vertragsbereitstellung Ertragsmodus aktivieren @@ -2305,6 +2428,6 @@ Einzahlung von %1$s %2$s zur Deckung der Netzwerkgebühr für Transaktionen Die Gebühr %s kann nicht gedeckt werden Der Yield-Modus ist momentan nicht verfügbar. Bitte versuche es später erneut. - Ausweichmodus nicht verfügbar + Yield Mode nicht verfügbar Chart konnte nicht geladen werden... diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index f7bc36a8d3..eaea4ea109 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -309,6 +309,7 @@ Hace %dh Importe + en En progreso Fondos insuficientes Más tarde @@ -394,6 +395,7 @@ A A %s Hoy + Token Token para enviar %d token @@ -644,6 +646,8 @@ Mantiene sus criptomonedas seguras y sin conexión. Tan delgadas como una tarjeta de crédito, más seguras que una bóveda bancaria. Si lo hace, tendrá que empezar de nuevo. Recuperar la billetera existente a través de una copia de seguridad de Google Drive + Estamos trabajando en la copia de seguridad de Google Drive para facilitar aún más la recuperación de la billetera. + Próximamente, copia de seguridad de Google Drive Copia de seguridad de Google Drive Cree una billetera segura y transfiera sus fondos para mayor protección. Crear nueva billetera @@ -983,6 +987,11 @@ Por favor repita la operación. La tarjeta/anillo se restablecerá a la configuración de fábrica. Error de activación Agregar tokens + + Su wallet contiene %d token. Para continuar, por favor sincronice sus direcciones. + Su wallet contiene %d tokens. Para continuar, por favor sincronice sus direcciones. + + Sincroniza tu wallet Ha añadido una tarjeta o anillo como copia de seguridad. Una vez finalizada la copia de seguridad, no puedes añadir más dispositivos. Si tiene una tarjeta o anillo más, añádalo ahora. ¿Quiere continuar? La copia de seguridad está parcialmente completa y no se puede salir ahora. Una frase de contraseña es una función de seguridad opcional que añade una palabra o frase a su frase de recuperación, creando un nuevo conjunto de direcciones de billetera para una mayor protección. @@ -1004,6 +1013,7 @@ Otras opciones Sus claves se generarán de forma segura dentro del chip. No hay seed phrase, lo que significa que nadie puede exportarla ni robarla. Generar claves de forma privada + Al continuar, acepta las \n%s Su tarjeta está activada y lista para usar ¡Éxito! ¡Su billetera está configurada y lista para usar! @@ -1508,13 +1518,21 @@ Siéntase seguro con una asistencia permanente que le ayudará con cualquier problema Siempre aquí Múltiples proveedores de confianza en un solo lugar: intercambie cualquier activo sin esfuerzo en su billetera + Intercambie criptos directamente en Tangem\nSin transferencias adicionales\nSin mover fondos a exchanges Intercambie con nosotros + Intercambia dentro de tu billetera Sin cuelgues, sin pérdidas, sin puntos ciegos - su transacción siempre está protegida + Los intercambios se realizan a través de proveedores de confianza. Sus llaves permanecen en su billetera Tangem en todo momento. Claro. Transparente. Autocustodiado. Defensa impenetrable + Ud mantiene el control Maximice su valor con tarifas de una amplia red de proveedores de confianza, eligiendo siempre la mejor + Tangem compara múltiples proveedores, tanto DEX como CEX. La mejor tarifa se selecciona automáticamente. ¿Prefiere otro proveedor? Puede elegirlo manualmente. Tarifas inmejorables + Mejor tarifa disponible Sencillo e intuitivo, permite cambiar tokens con solo unos cuantos toques + Intercambie entre las principales redes y miles de tokens 0% Comisión por intercambios entre stablecoins Simplemente cómodo + 90+ Blockchains\n16.000+ Activos Intercambio a través del proveedor Sus activos La cantidad incluye:\n• Tarifas del proveedor de servicios\n• Tarifas de red por enviar %s desde el intercambio a la dirección del usuario. @@ -1587,7 +1605,7 @@ Código PIN creado CVC Error al cargar datos. Inténtalo de nuevo más tarde. - Caducidad + Expira Congelar tarjeta Ocultar detalles Ocultar @@ -2028,6 +2046,10 @@ Use su tarjeta o anillo para obtener una dirección para la red %d Use su tarjeta o anillo para obtener direcciónes para las redes %d + + Sincronice direcciones para obtener una dirección de la red %d + Sincronice direcciones para obtener direcciones de las redes %d + Faltan algunas direcciones La red no está disponible actualmente. Por favor, inténtalo de nuevo más tarde. La red no está disponible diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 8faa21a926..e7e7f8b950 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -309,6 +309,7 @@ Il y a %dh Importez + dans En cours Plus tard En savoir plus @@ -392,6 +393,7 @@ À À %s Aujourd\'hui + Token %d token %d tokens @@ -624,6 +626,8 @@ Gardez vos cryptomonnaies en sécurité et hors ligne. Aussi fin qu\'une carte de crédit, plus sûr qu\'un coffre-fort bancaire. Si vous le faites, vous devrez recommencer depuis le début. Récupérer un portefeuille existant via la sauvegarde Google Drive + Nous travaillons actuellement sur la sauvegarde de votre portefeuille via Google Drive afin de faciliter encore davantage la restauration de celui-ci. + La sauvegarde via Google Drive sera bientôt disponible Sauvegarde Google Drive Créez un portefeuille sécurisé et transférez vos fonds pour bénéficier d\'une protection supplémentaire. Créer un nouveau portefeuille @@ -695,6 +699,9 @@ Posez pour scanner Tapez pour signer Posez la carte + Votre wallet est synchronisé et prêt.\nDes tokens sont manquants ? + Wallet importé avec succès + Restauration %d%% Vous avez mis à jour vos données biométriques, scannez votre carte ou bague pour entrer Votre solde doit être supérieur à la valeur des frais pour effectuer un transfert Solde insuffisant @@ -943,6 +950,7 @@ Veuillez répéter l\'opération. La carte sera réinitialisée aux paramètres d\'usine. Erreur d\'activation Ajouter des jetons + Synchronisez votre wallet Vous avez ajouté une carte de sauvegarde. Une fois le processus de sauvegarde terminé, vous ne pourrez plus ajouter de cartes de sauvegarde. Si vous avez une autre carte, ajoutez-la à la sauvegarde. Souhaitez-vous poursuivre le processus de sauvegarde ? Le processus de sauvegarde est partiellement terminé. Vous ne pouvez pas le quitter maintenant. La phrase secrète est une fonctionnalité de sécurité avancée utilisée par les portefeuilles cryptographiques. Il ajoute un mot ou une phrase supplémentaire de votre choix à votre phrase de récupération déjà existante pour débloquer un tout nouvel ensemble d\'adresses. @@ -1457,13 +1465,21 @@ Ayez confiance en notre assistance 24 heures sur 24 pour vous aider à résoudre tous vos problèmes Assistance 24 heures sur 24 Plusieurs fournisseurs de confiance en un seul endroit : échangez n\'importe quel actif facilement + Échangez des cryptos directement dans Tangem\nSans transferts supplémentaires\nSans envoyer vos fonds vers un échange Échangez Avec Nous + Échangez Dans Votre Portefeuille Une sécurité de haut niveau et des fournisseurs vérifiés garantissant que vos actifs sont protégés à chaque échange + Les échanges sont effectués par des prestataires de confiance. Vos clefs ne quittent jamais votre portefeuille Tangem. Clair. Transparent. Gestion autonome Plus Sûr Que Jamais + Vous Gardez le Contrôle Maximisez votre valeur avec des tarifs d\'un large réseau de fournisseurs de confiance + Tangem compare plusieurs fournisseurs, DEX et CEX. Le meilleur tarif est sélectionné automatiquement. Vous préférez un autre fournisseur? Vous pouvez le choisir manuellement. Meilleurs Tarifs + Meilleur Tarif Disponible Simple et intuitif, vous permettant d\'échanger des jetons en quelques clics + Échangez des milliers de jetons sur les principaux réseaux 0% de frais sur les échanges stablecoin à stablecoin Plus Simple Que Jamais + 90+ Blockchains\n16,000+ Actifs Échange via le fournisseur Vos actifs Le montant comprend :\n• les frais du fournisseur de services\n• les frais de réseau pour l\'envoi de %s depuis l\'échange vers l\'adresse de l\'utilisateur. diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 1b7dd158fb..45d98693f9 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -16,12 +16,14 @@ Fatto Errore Impossibile ottenere la commissione + in Costi della rete OK Mantieni le modifiche Invia Impossibile inviare la transazione Con successo + Token %d gettone %d gettoni diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index b500909414..62c071a8af 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -80,6 +80,9 @@ トークンの追加 受け取りたいトークンを選択します スワップしたいトークンを選択します + 資金を追加 + スワップ + 送金 ポートフォリオに追加 トークンを追加 並べ替え・グループ化 @@ -87,6 +90,10 @@ ネットワークを選択 カスタムトークンの追加 トークンの管理 + クレジットカードまたは銀行口座 + アドレスまたはQRコードを共有 + 自分のポートフォリオ間で + 受け取る このアドレスには%3$s ネットワークから%1$s (%2$s) のみを送信してください。他のトークンやネットワークを使用すると、資金を失う可能性があります。 デフォルト レガシー @@ -214,6 +221,7 @@ アクセスが拒否されました アカウント アカウント + %s に失敗しました 有効化 追加 資金を追加 @@ -230,6 +238,8 @@ 適用する 承認 承認 + 承認済み + 承認中 注意 利用可能なネットワーク バックアップ @@ -309,10 +319,14 @@ 非表示 %sまで長押し 時間 + + %d 時間 + %d時間前 インポート + 進行中 残高不足 後で @@ -355,6 +369,8 @@ %1$s — %2$s 続きを読む 受け取る + 受け取り済み + 受け取り中 おすすめ 拒否 リロード @@ -373,6 +389,8 @@ 送る 送金: 取引の送信に失敗しました + 送金中 + 送金済み サーバーが利用できません。しばらくしてからもう一度お試しください。 共有 リンクを共有 @@ -383,6 +401,7 @@ スキップ 問題が発生しました ステーキング + ステーキング済み ステーキング 始める 送信 @@ -390,6 +409,8 @@ サポート 対応ネットワーク スワップ + スワップ済み + スワップ中 Tangem Tangem Wallet タップして長押し @@ -398,6 +419,7 @@ 宛先 %sへ 今日 + トークン 送信するトークン %d トークン @@ -406,6 +428,8 @@ 取引状況 取引 送金 + 送金済み + しばらくしてからもう一度お試しください データを読み込めません… わかりました 理解して続行 @@ -415,10 +439,12 @@ ステーキング解除 %1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。 値がコピーされました + 投票 ウォレット 警告 + 引き出し中 はい 利息モード コントラクトアドレスをコピーしました! @@ -580,12 +606,14 @@ ベストレート FCA警告リスト 固定レートは利用できません + スワッププロバイダー お得なレート FCA警告リストに掲載されたプロバイダー 最大 %s まで使用可能 %s 以上で利用可能 このペアは利用できません 許可が必要です + 権限が必要です 推奨 %sを買い付けました %sを買い付けています @@ -633,6 +661,7 @@ 承認機能は、別のアドレスに特定の量のトークンを使用する許可を与えるために必要です。設計上スマートコントラクトは、承認しない限りトークンにアクセスできません。トークンを「ロック解除」すると、StakeKitスマート コントラクトがトークンを使用する権限が与えられます。ネットワークのマイナーは、このアクションをブロックチェーンに記録するためにガス料金(あなたが支払う)を受け取ります。承認後、トークンをステーキングできます。 続行するには、Polygonスマートコントラクトが%sを使用することを許可する必要があります 続行するには、%1sスマートコントラクトに%2sを使用する権限を付与してください + 分散型取引所がウォレットと連携するには、許可が必要です。%1s 許可を与える 無制限 アドレスはTangemハードウェアウォレット上で直接生成され、そのまま安全に使用できます。 @@ -660,6 +689,8 @@ 暗号資産をオフラインで厳重保管。カードサイズで、金庫以上の安心を。 実行すると、最初からやり直す必要があります。 Googleドライブのバックアップから既存のウォレットを復元する + ウォレットの復元をさらに簡単にするため、Googleドライブバックアップ機能を準備しています。 + Googleドライブバックアップは近日対応予定です Googleドライブのバックアップ さらに強固なセキュリティのために、新しいウォレットを作成して資産を移動しましょう。 新しいウォレットを作成 @@ -1022,6 +1053,7 @@ その他のオプション 秘密鍵はチップ内で安全に生成されます。シードフレーズは存在しないので、誰もエクスポートしたり盗んだりすることはできません。 秘密鍵を非公開で生成する + 続行すると、以下に同意したものとみなされます。 カードは有効化され、使用可能になりました 成功! ウォレットの設定が完了し、使用できるようになりました。 @@ -1103,6 +1135,8 @@ 別の方法を選択してください。 現地の規制要件に準拠するため、%@の利用には本人確認が必要です。 決済プロバイダーによる本人確認が必要です。 + 認証する + 重要事項 オンランプ機能を使用することにより、プロバイダの%1$sおよび%2$sに同意するものとします サービスは外部プロバイダーによって提供されます。 \nTangemは責任を負いません。 買付金額は%s以下にしてください @@ -1166,7 +1200,11 @@ 不明なパラメータ クレジットカードまたは銀行口座 アドレスまたはQRコードを共有してください + 暗号資産を安全に売却 + 別のウォレットに送信 ポートフォリオ間で + その他 + クイック入金 メモ不要 %3$sネットワーク上の%1$s ( %2$s ) %2$sネットワーク上の%1$s @@ -1410,6 +1448,7 @@ ステーキングから資金の引き出しを要求した後、トークンが利用可能になるまでの待機期間。 ウォームアップ期間 ステーキングへの参加を有効にするために割り当てられた時間。 + ステーキングが有効です 現在、利用可能なバリデーターは見つかりません。しばらくしてからもう一度お試しください。 ステーキングは利用できません ネットワークは、ステーキングのためにトークンの使用を承認していることを確認するために、トークン承認手数料を請求します。 @@ -1520,23 +1559,26 @@ 続行するには少なくとも%1$sの受信取引が必要です 残高不足 承認すると、このスマートコントラクトが今後の取引でトークンを利用できるようになります。 + 詳細モード 固定レート ネットワークは、あなたがトークンのスワップを承認していることを確認するために、トークン承認手数料を請求します。 + スワップ中 より多くのトークンをより良いレートで、ウォレット内にて直接交換します。 新しいスワッププロバイダーが利用可能になりました! 他のものをお探しですか?\n検索してみるか、別の暗号資産をチェックしてみましょう! どのトークンでも検索できます。まだ一覧に表示されていないものでも検索可能です。 必要なものは検索して見つけましょう。 + シンプルモード 24時間体制のサポートであらゆる問題に対応します。 いつもここに 複数の信頼できるプロバイダーが一箇所に集結。ウォレット内で様々な暗号資産を簡単に交換できます。 - Tangem内で暗号資産を直接交換\n追加の送金は不要\n取引所に資産を移す必要なし + Tangem内で暗号資産を直接交換できます\n追加の送金は不要\n取引所に資金を移す必要もありません ぜひスワップしてください ウォレット内でスワップ 失敗も死角もありません。取引は常に保護されます。 - スワップは信頼できるプロバイダーを通じて実行されます。秘密鍵は常にTangemウォレット内に保持されます。シンプル。透明。自己管理。 + スワップは信頼できるプロバイダーを通じて実行されます。秘密鍵は常にTangemウォレット内に保持されます。明確で、透明性が高く、自己管理型です。 難攻不落の防御 - 主導権はあなたの手にあります + 主導権はあなたの手に 幅広いネットワークの中から、常に最適なプロバイダーと料金レートを選択します TangemはDEXとCEXを含む複数のプロバイダーを比較し、最良レートを自動で選択します。別のプロバイダーを希望する場合は、手動で選択できます。 破格のレート @@ -1555,7 +1597,9 @@ すべての分散型取引所は、スマートコントラクトがあなたの許可なくウォレットにアクセスするのを防ぐために承認を必要とします。設計上、スマートコントラクトは承認なしでトークンにアクセスできません。トークンを「ロック解除」することで、あなたは1-inchのスマートコントラクトがトークンを使うことを承認します。ネットワークのマイナーは、このアクションをブロックチェーンに記録するためのガス料金(あなたが支払う)を受け取ります。承認後、トークンを交換することができます。 承認 手数料見積りエラーです。サポートにフィードバックをお送りください。 + 送信元 スワップする + 送信 選択したトークンをこの量を交換すると、価格に大きな影響が生じ、結果が減少します。 流動性が低いため、受取額が大幅に少なくなる可能性があります。金額を減らすか、別のプロバイダーをお試しください。 価格への影響が甚大です @@ -1564,11 +1608,14 @@ 許可を与える スワップ スワップ中… + 受け取り先 受け取る トークンを選択 利用不可 この取引に十分な流動性がありません。\n金額を減らすか、別のプロバイダーを選択してください。 取引額が大きすぎます + 送金 + 送金... 皆様からのフィードバックをお待ちしております Tangem Payのベータ版を公開しました カード名を変更できません @@ -1645,6 +1692,7 @@ カードを交換する 英字と数字のみ使用できます 無効な文字が含まれています + カード名 表示 詳細を表示 ポートフォリオ内のあらゆる資産をカードと交換 @@ -1657,6 +1705,7 @@ 現在、出金ができません 現在の処理が完了するまでは、スワップや新しい出金を開始することはできません。 出金処理中です + カード名 %s以上の金額を設定してください 限度額を設定できませんでした。もう一度お試しください 変更 @@ -1667,6 +1716,9 @@ 1日の上限を設定しました 1日の利用限度額 カード設定 + + %d枚のカード + PINコードを変更 忘れた場合はアプリに戻って確認できます。 %s 〜 %sの範囲で上限を設定 @@ -1681,8 +1733,15 @@ 無料のTangem Visaバーチャルカードを入手 Tangem Payを入手 サポートへ移動 + 新しいカード情報が生成されます。 + 発行手数料 + 発行手数料を支払うため、決済口座にUSDCを入金してください + 手数料を支払えません + 追加カードを発行しますか? + カードを発行 通常は最大で15分ほどかかります Tangemカードのセットアップ + 新しいデジタルカードを発行中 カードを発行しています カードは通常、5分以内に自動で発行されます。手動での審査が必要な稀な場合は、最大48時間かかることがあります。 Tangem Pay @@ -1699,6 +1758,8 @@ KYCブロックを非表示 申し訳ございませんが、 本人確認ができませんでした。 + 最大3枚までカードを保有できます。新しいカードを追加するには、いずれかのカードを削除してください。 + カード発行枚数の上限に達しました 無料のTangem Visaバーチャルカードを入手 日常の支払いにUSDCを利用 カードをGET @@ -1708,6 +1769,8 @@ 余計な費用なしで、表示金額のみを支払う あなたの住所や資産を開示することなく、個別の支払い用アカウントが作成されます。 他に類を見ないプライバシー + そして支払いカードを連携します + ウォレットを設定します 無料のTangem Payカードを数分でゲットしましょう Payサポート 支払いアカウント @@ -1728,6 +1791,7 @@ 現在、データを表示できませんが、カードでのお支払いは引き続きご利用いただけます。 \nPINコードの設定 カード無効化済み + カードを交換中 セッションの有効期限が切れました セッションを更新 日常の支払いにUSDCを利用 @@ -1757,7 +1821,7 @@ 承認が取り消されましたが、あなたの資金は引き続き利息モードです。操作を行うには、利息モードに移動し、再度承認を付与してください。 利用可能残高 合計残高 - 年間で最大%sを獲得 + 年利最大%s XPUBを生成する 非表示 このトークンをメイン画面から非表示にします。トークンの管理ページからいつでも再度追加できます。 @@ -1783,18 +1847,28 @@ 売却できません %sからのスワップは利用できません スワップはできません + 報酬を請求中 コントラクト: %s + 利息モードを無効化中 + ステーキングで獲得した金額 まだ取引はありません 取引履歴の読み込みに失敗しました。\n情報を更新するには、リロードボタンをクリックしてください。 + %%image%% %s から 複数のアドレス 現在、このブロックチェーンでは取引履歴はサポートされていません。しかしご心配なく!弊社で対応中です。その間、エクスプローラーで確認することができます。 オペレーション + 保留中 + 報酬を再ステーキングしました + 報酬の再ステーキング + ステーキング報酬 + %%image%% %s へ 対象:%s 送金元: %s 送金先: %s バリデーター: %s 通知は有効になっていますが、デバイスの設定で通知を許可するまでは機能しません。 取引通知 + 送金処理中 最小%s 最小取引金額は%1$sです。 Tronネットワークの人気トークンの手数料は高い可能性があります。TRXをステーキングすると、取引コストを削減できる可能性があります。 @@ -1826,7 +1900,9 @@ 受信取引の通知を受け取る 新しいプロモーション情報をいち早く入手しましょう 新しい機能や限定オファーへの早期アクセス。 + 価格変動アラート、製品ニュース、限定オファー 機能およびニュースのアップデート + オファー・最新情報 プッシュ通知を使用しますか? プッシュ通知を有効にすると、ウォレットに着金したときにアラートを受信できます。 取引を見逃さない @@ -2064,7 +2140,10 @@ MATICはPOLに移行中です。ただし、期限は設定されておらず、MATICはまだ廃止されていません。MATICトークンを引き続き安全に使用することも、POLに交換することもできます。 MATICからPOLへの移行 - %d ネットワークのアドレスを取得するために、カードまたはリングを利用してください + カードまたはリングを使って、%dネットワークのアドレスを取得します + + + アドレスを同期して、%dネットワークのアドレスを取得します 一部のアドレスが見つかりません 現在、ネットワークにアクセスできません。しばらくしてからもう一度お試しください。 diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml index 890ff18a50..64d51c812e 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -80,6 +80,9 @@ Adicionar tokens Escolha o token que deseja receber. Escolha o token que deseja trocar. + Adicionar fundos + Trocar + Transferir Adicione ao seu portfólio Adicionar tokens Classificação e agrupamento @@ -87,6 +90,10 @@ Escolha a rede Adicionar token personalizado Gerenciar tokens + Cartão de crédito ou conta bancária + Compartilhe seu endereço ou código QR. + Entre seus portfólios + Você recebe Enviar somente %1$s (%2$s) de %3$s A rede não está vinculada a este endereço. O uso de outros tokens e redes pode resultar na perda de fundos. Padrão Legado @@ -217,6 +224,7 @@ Acesso negado Conta Contas + %s fracassado Ativar Adicionar Adicionar fundos @@ -233,6 +241,8 @@ Aplicar Aprovação Aprovar + Aprovado + Aprovando Atenção Redes disponíveis Backup @@ -315,11 +325,16 @@ Esconder Mantenha-se em %s hora + + %d hora + %d horas + UM OUTRO Importar + em Em andamento Saldo insuficiente Mais tarde @@ -364,6 +379,8 @@ %1$s — %2$s Leia mais Receber + Recebido + Recebendo Recomendado Rejeitar Recarregar @@ -382,6 +399,8 @@ Enviar Enviar: Falha ao enviar a transação + Enviando + Enviado O servidor não está disponível. Tente novamente mais tarde. Compartilhar Compartilhar link @@ -392,6 +411,7 @@ Pular Algo deu errado Stake + Estacado Apostas Começar Enviar @@ -399,6 +419,8 @@ Suporte Redes suportadas Trocar + Trocado + Troca Tangem Carteira Tangem Toque e segure @@ -407,6 +429,7 @@ Para Para %s Hoje + Token Token a ser enviado token @@ -416,6 +439,8 @@ Status da transação Transações Transferir + Transferido + Por favor, tente novamente mais tarde. Não foi possível carregar os dados… Eu entendo Entendi, continue. @@ -425,10 +450,12 @@ Unstake Devido a %1$s limitações apenas %2$d Os UTXOs podem caber em uma única transação. Isso significa que você só pode enviar %3$s ou menos. Você precisa reduzir a quantidade. Valor copiado + Votação Carteiras Aviso semana com + Retirada Sim Modo de rendimento Endereço do contrato copiado! @@ -590,6 +617,7 @@ Melhor tarifa Lista de advertências da FCA A tarifa fixa não está disponível. + Fornecedor para troca Tarifa competitiva Fornecedor na lista de advertências da FCA Disponível até %s @@ -671,6 +699,8 @@ Mantém suas criptomoedas seguras e offline. Fino como um cartão de crédito, mais seguro que um cofre de banco. Se fizer isso, terá que começar tudo de novo. Recupere sua carteira existente por meio do backup do Google Drive. + Estamos trabalhando no backup do Google Drive para facilitar ainda mais a recuperação da carteira. + O backup do Google Drive estará disponível em breve. Backup do Google Drive Crie uma carteira segura e transfira seus fundos para obter proteção adicional. Criar nova carteira @@ -1041,6 +1071,7 @@ Outras opções Suas chaves serão geradas com segurança dentro do chip. Não há frase mnemônica, o que significa que ninguém pode exportá-las ou roubá-las. Gere chaves de forma privada + Ao continuar, você concorda com os termos. %1$s Seu cartão está ativado e pronto para uso. Sucesso! Sua carteira está configurada e pronta para uso! @@ -1166,6 +1197,8 @@ via %s Você pagará Grupo + Agrupar por redes + Ordenar por saldo Por equilíbrio Organizar tokens Desagrupar @@ -1191,6 +1224,8 @@ Parâmetros desconhecidos Cartão de crédito ou conta bancária Compartilhe seu endereço ou código QR. + Venda criptomoedas com segurança + Enviar para outra carteira Entre seus portfólios Outro Recarga rápida @@ -1440,6 +1475,7 @@ O período que você deve aguardar após solicitar o saque de fundos do staking para que os tokens fiquem disponíveis. Período de aquecimento O período alocado para ativar a participação no staking. + Staking ativado Não há validadores disponíveis no momento. Tente novamente mais tarde. Staking indisponível A rede cobrará uma taxa de aprovação de token para verificar se você está autorizando o uso do seu token para staking. @@ -1550,6 +1586,7 @@ Uma transação de entrada de pelo menos %1$s é necessário prosseguir Fundos insuficientes Ao aprovar, você permite que o contrato inteligente utilize seus tokens em transações futuras. + Modo detalhado Taxa fixa A rede cobrará uma taxa de aprovação de token para verificar se você está autorizando o uso do seu token para a troca. Troca em andamento @@ -1558,12 +1595,13 @@ Procurando algo diferente?\n Experimente pesquisar ou explore outra criptomoeda! Pesquise qualquer token, mesmo que ele ainda não esteja na sua lista. Use a busca para encontrar o que você precisa. + Modo simples Sinta-se seguro com o suporte disponível 24 horas por dia, 7 dias por semana, para ajudá-lo com qualquer problema. Sempre aqui Diversos provedores confiáveis ​​em um só lugar — troque qualquer ativo facilmente em sua carteira. Troque criptomoedas diretamente na Tangem\nSem transferências adicionais\nSem movimentação de fundos para corretoras Troque conosco - Troque o conteúdo da sua carteira. + Troque dentro de sua carteira Sem erros, sem perdas de posse, sem pontos cegos — sua transação está sempre protegida. As trocas são executadas por meio de provedores confiáveis. Suas chaves permanecem em sua carteira Tangem o tempo todo. Clara. Transparente. Autocustódia. Defesa Impenetrável @@ -1575,7 +1613,7 @@ Sem complicações e intuitivo, permitindo que você troque tokens com apenas alguns toques Troca entre as principais redes e milhares de tokens 0% Taxa em trocas de stablecoins Simplesmente conveniente - Mais de 90 blockchains | Mais de 16.000 ativos + mais de 90 blockchains\n16.000+ ativos Trocar através do provedor Seus ativos O valor inclui:\n• taxa do provedor de serviços\n• taxa de rede para envio %s da central de distribuição de volta para o endereço do usuário. @@ -1603,6 +1641,8 @@ não disponível Liquidez insuficiente para esta transação. \n o valor ou escolha outro provedor. Negociação em grande escala + Transferir + Transferir... Teremos todo o prazer em receber seu feedback. O Tangem Pay agora está em versão beta. Não foi possível renomear o cartão. @@ -1679,6 +1719,7 @@ Substituir cartão Somente letras e números são permitidos. Caracteres inválidos + Nome do cartão Revelar Mostrar detalhes Troque qualquer ativo da sua carteira por um cartão. @@ -1691,6 +1732,7 @@ Retirada indisponível agora Você não pode iniciar uma troca ou um novo saque até que o atual seja concluído. Retirada em andamento + Nome do cartão Definir um limite a partir de %s Não foi possível definir o limite. Tente novamente. Mudar @@ -1701,6 +1743,10 @@ O limite diário está definido. Limite diário Configurações do cartão + + %d cartão + %d cartões + Alterar código PIN Volte ao aplicativo se você se esquecer. Defina um limite a partir de %s para %s @@ -1715,8 +1761,15 @@ Obtenha seu cartão virtual Visa Tangem grátis. Obtenha o Tangem Pay Acesse o Suporte + Isso gera um novo conjunto de detalhes do cartão. + Taxa de emissão + Deposite USDC na conta de pagamento para cobrir a taxa de emissão. + Não foi possível cobrir a taxa. + Emitir um cartão adicional? + Emitir cartão Geralmente leva até 15 minutos. Configurando seu cartão Tangem + Emissão de um novo cartão digital Emissão do seu cartão O cartão geralmente é emitido automaticamente em até 5 minutos. Em casos raros, se for necessária uma análise manual, o processo pode levar até 48 horas. Tangem Pay @@ -1733,6 +1786,8 @@ Ocultar bloco KYC Desculpe, não foi possível verificar. Seu perfil. + Você pode ter até 3 cartões. Exclua um para adicionar um novo. + Número máximo de cartões emitidos Obtenha seu cartão virtual Visa Tangem grátis. Use USDC para pagamentos do dia a dia. Obter cartão @@ -1742,6 +1797,8 @@ Pague exatamente o que você vê. Uma conta de pagamento separada será criada sem divulgar seus endereços e bens. Privacidade incomparável + E vincule um cartão de pagamento a ele. + Vamos configurar uma carteira. Obtenha seu cartão Tangem Pay gratuito em minutos. Suporte de Pay Conta de pagamento @@ -1762,6 +1819,7 @@ Não foi possível exibir os detalhes. No entanto, os pagamentos com cartão ainda estão funcionando. Defina o código PIN. Cartão desativado + Substituindo seu cartão Sessão expirada Restaurar acesso Use USDC para pagamentos do dia a dia. @@ -1817,18 +1875,28 @@ Não disponível para venda Não disponível para troca a partir de %s Indisponível para troca + Reivindicar recompensa contrato: %s + Desativar o modo Yield + Ganhos com participação Você ainda não possui nenhuma transação. Falha ao carregar o histórico de transações. Clique no botão \n para atualizar as informações. + de: %%image%% %s Vários endereços O histórico de transações ainda não é suportado nesta blockchain. Mas não se preocupe, estamos trabalhando nisso! Enquanto isso, você pode conferir no explorador. Operação + Pendente + Recompensas redefinidas + Recompensas por reconfiguração + Recompensa de staking + para: %%image%% %s para: %s de: %s para: %s validador: %s As notificações estão ativadas, mas não funcionarão até que você as permita nas configurações do seu dispositivo. Notificações de transação + Transferência em andamento Mínimo %s O valor mínimo da transação é %1$s. As taxas da rede Tron para tokens populares podem ser mais altas. Fazer staking de TRX pode ajudar a reduzir os custos de transação. @@ -1860,7 +1928,9 @@ Receba notificações de transações recebidas. Seja o primeiro a saber sobre novas promoções. Acesso antecipado a novas funcionalidades e ofertas exclusivas. + Alertas de alteração de preços, novidades sobre produtos e ofertas exclusivas. Atualizações de notícias e recursos + Ofertas e atualizações Você gostaria de usar notificações push? Ative as notificações push para receber alertas quando os fundos chegarem à sua carteira. Não perca nenhuma transação. @@ -2099,8 +2169,12 @@ O MATIC está sendo migrado para o POL. No entanto, não há prazo definido e o MATIC ainda não foi descontinuado. Você pode continuar usando o token MATIC com segurança ou trocá-lo pelo POL. Migração de MATIC para POL - Use seu Cartão ou Anel para obter o endereço de uma rede. - Use seu Cartão ou Anel para obter endereços de rede. + Use seu Cartão ou Anel para obter o endereço de uma rede %d. + Use seu Cartão ou Anel para obter endereços de redes %d. + + + Sincronizar endereços para obter um endereço de %d rede + Sincronizar endereços para obter endereços para %d rede Alguns endereços estão faltando. A rede está inacessível no momento. Tente novamente mais tarde. diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index b8e2e9e0e4..bfd2cb942b 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -80,6 +80,9 @@ Добавьте токены Выберите токен для получения Выберите токен для обмена + Пополнить + Обмен + Перевод Добавить в портфель Добавить токен Сортировка и группировка @@ -87,6 +90,9 @@ Выберите сеть Добавить токен Валюты + Поделитесь своим адресом или QR-кодом. + Между вашими портфелями + Вы получите Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. По умолчанию Устаревший @@ -326,6 +332,12 @@ Скрыть Удерживайте, чтобы %s час + + %d час + %d часa + %d часов + %d часа + %dч назад %dч назад @@ -333,6 +345,7 @@ %dч назад Импортировать + в В процессе Недостаточный баланс Позже @@ -423,6 +436,7 @@ На На %s Сегодня + Токен Токен к отправке %d токен @@ -504,7 +518,7 @@ Подписано Отправить отзыв Подробности - У вас может быть только один мобильный кошелек. Вы можете апгрейдить его до аппаратного кошелька Tangem или добавить новый аппаратный кошелек. + Мобильный кошелек может быть только один. Перенесите его на аппаратный кошелек Tangem или добавьте новый аппаратный кошелек. Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования Основной адрес @@ -681,6 +695,8 @@ Хранит вашу криптовалюту в безопасности и офлайн. Тонкая, как банковская карта — надёжнее банковского хранилища. Если вы это сделаете, придётся начать заново. Восстановить существующий кошелёк через резервную копию Google Drive + Мы работаем над резервным копированием в Google Drive, чтобы сделать восстановление кошелька еще проще. + Резервное копирование в Google Drive скоро появится Google Drive бэкап Создайте новый защищённый кошелёк и переведите свои средства для максимальной защиты. Создать новый кошелек @@ -1036,6 +1052,13 @@ Необходимо повторить операцию, при этом карта будет сброшена к заводским настройкам Ошибка активации Добавление токенов + + В вашем кошельке %d токен. Синхронизируйте адреса, чтобы продолжить. + В вашем кошельке %d токена. Синхронизируйте адреса, чтобы продолжить. + В вашем кошельке %d токенов. Синхронизируйте адреса, чтобы продолжить. + В вашем кошельке %d токенов. Синхронизируйте адреса, чтобы продолжить. + + Синхронизируйте ваш кошелек Вы добавили одну резервную карту или кольцо. После того, как процесс будет завершен, Вы больше не сможете добавить еще. Если у Вас есть еще одна карта или кольцо, добавьте ее в резервную копию. Хотите продолжить? Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас. Парольная фраза — это дополнительная функция безопасности, которая добавляет слово или фразу к вашей фразе восстановления, создавая новый набор адресов кошелька для дополнительной защиты. @@ -1057,6 +1080,7 @@ Другие опции Ваши ключи будут надежно сгенерированы внутри чипа. Никакой seed-фразы, а это значит, что никто не может экспортировать или украсть ее. Cоздавайте ключи приватно + Продолжая, вы соглашаетесь с \n%s Ваша карта активирована и готова к использованию Успешно! Ваш кошелек настроен и готов к использованию! @@ -1178,6 +1202,8 @@ через %s Вы заплатите Группы + Сгруппировать по сети + Сортировать по балансу По балансу Упорядочить токены Список @@ -1571,13 +1597,21 @@ Чувствуйте уверенность с круглосуточной поддержкой, готовой помочь в любой ситуации! Круглосуточная поддержка Надежные провайдеры в одном месте — обменивайте любые активы легко и быстро прямо в своем кошельке! + Обменивайте крипту прямо в Tangem\nНикаких лишних переводов\nНе нужно отправлять средства на биржи Обменивайте с нами + Обмен прямо в кошельке Высший уровень безопасности и проверенные партнёры защищают каждую транзакцию + Обмен осуществляется через надежных провайдеров. Ваши ключи всегда остаются в кошельке Tangem. Понятно. Прозрачно. Некастодиально. Полная защита + Всё Под Вашим Контролем Автоматически находим самые выгодные предложения среди проверенных провайдеров + Tangem сравнивает несколько провайдеров, как DEX, так и CEX. Лучший курс выбирается автоматически. Предпочитаете другого провайдера? Вы можете выбрать его вручную. Лучшие курсы + Лучший Доступный Курс Интуитивный обмен в пару касаний — без сложностей и ожидания + Обменивайте тысячи токенов между любыми сетями. Комиссия 0% на обмен стейблкоинов Проще простого + 90+ Блокчейнов\n16 000+ Активов Обмен через провайдера Ваши средства В сумму включено: \n• комиссия провайдера сервиса\n• комиссия сети за отправку %s от биржи обратно на адрес пользователя. @@ -1823,6 +1857,7 @@ валидатор: %s Уведомления включены, но не будут работать, пока вы не разрешите их в настройках устройства. Уведомления о транзакциях + Перевод в процессе Минимум %s Минимальная сумма транзакции равна %1$s. Комиссии сети Tron для популярных токенов могут быть выше. Стейкинг TRX может помочь снизить расходы на транзакции. @@ -1965,7 +2000,7 @@ Установить Код Доступа Настройки кошелька Tangem - Используйте %s или отсканируйте карту/кольцо, чтобы разблокировать доступ к вашему кошельку + Воспользуйтесь %s или отсканируйте карту/кольцо, чтобы разблокировать доступ к вашему кошельку Процесс выдачи разрешения уже в работе и скоро будет завершен Выдача разрешения Похоже, что процесс активации карт или кольца не был завершен корректно. Это могло быть вызвано проблемой взаимодействия с модулем NFC либо некорректным прикладыванием карты или кольца к телефону. Пожалуйста, обратитесь в нашу службу поддержки для уточнения деталей. @@ -2039,6 +2074,12 @@ Используйте вашу карту или кольцо, чтобы получить адреса для %d сетей Используйте вашу карту или кольцо, чтобы получить адреса для %d сетей + + Синхронизируйте адреса, чтобы получить адрес для %d сети + Синхронизируйте адреса, чтобы получить адреса для %d сетей + Синхронизируйте адреса, чтобы получить адреса для %d сетей + Синхронизируйте адреса, чтобы получить адреса для %d сетей + Некоторые адреса отсутствуют В данный момент сеть недоступна. Пожалуйста, попробуйте позже. Сеть недоступна diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 0e1fecd93d..e246aefdb0 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -321,6 +321,7 @@ %d годин тому Імпортувати + у В процесі Пізніше Дізнатися більше @@ -408,6 +409,7 @@ До На %s Сьогодні + Токен %d токен %d токени @@ -644,6 +646,8 @@ Зберігає ваші криптовалюти в безпеці та в режимі офлайн. Тонкий, як кредитна картка, безпечніший за банківське сховище. Якщо ви це зробите, доведеться почати спочатку. Відновлення існуючого гаманця за допомогою резервної копії Google Диску + Ми працюємо над резервним копіюванням у Google Drive, щоб зробити відновлення гаманця ще простішим. + Резервне копіювання в Google Drive незабаром з\'явиться Google Диск бекап Створіть новий захищений гаманець і переведіть свої кошти для додаткового захисту. Створити новий гаманець @@ -715,6 +719,9 @@ Прикладіть, щоб відсканувати Прикладіть, щоб підписати Прикладіть картку або кільце + Ваш гаманець синхронізовано й готово до використання.\nНе бачите деякі токени? + Гаманець успішно імпортовано + Відновлення %d%% Ви оновили дані біометрії, відскануйте свою картку або кільце для входу Ваш баланс повинен перевищувати суму комісії для здійснення переказу Недостатньо балансу @@ -977,6 +984,13 @@ Необхідно повторити операцію, при цьому картка буде скинута до заводських налаштувань. Помилка активації Додати токени + + У вашому гаманці %d токен. Синхронізуйте адреси, щоб продовжити. + У вашому гаманці %d токени. Синхронізуйте адреси, щоб продовжити. + У вашому гаманці %d токенів. Синхронізуйте адреси, щоб продовжити. + У вашому гаманці %d токенів. Синхронізуйте адреси, щоб продовжити. + + Синхронізуйте свій гаманець Ви додали одну резервну картку або кільце. Після завершення процесу резервного копіювання ви не зможете додати їх більше. Якщо у вас є ще одна картка або кільце, додайте її до резервної копії. Бажаєте продовжити? Процес резервного копіювання частково завершено. Ви не можете вийти з нього зараз. Парольна фраза – це розширена функція безпеки, яку використовують криптогаманці. Вона додає додаткове слово або фразу на ваш вибір до вже існуючої seed - фрази, щоб розблокувати абсолютно новий набір адрес. @@ -998,6 +1012,7 @@ Інші опції Ваші ключі будуть надійно згенеровані всередині чіпу. Ніякої seed-фрази, а це означає, що ніхто не зможе її експортувати або вкрасти. Генеруйте ключі приватно + Продовжуючи, ви погоджуєтеся з \n%s Ваша картка активована та готова до використання Успішно! Ваш гаманець налаштований і готовий до використання! @@ -1504,13 +1519,21 @@ Надійна підтримка 24/7, щоб ваші фінансові операції були швидкими та надійними! Цілодобова підтримка Надійні провайдери дозволяють легко обмінювати активи, забезпечуючи повну безпеку у вашому гаманці + Обмінюйте крипту прямо в Tangem\nНіяких зайвих переказів\nНе потрібно відправляти кошти на біржі Обмінюйте з нами + Обмін прямо в гаманці Найвищий рівень безпеки та перевірені провайдери гарантують захист ваших активів при кожному обміні. + Обмін здійснюється через надійних провайдерів. Ваші ключі завжди залишаються в гаманці Tangem. Зрозуміло. Прозоро. Некастодіально. Безпечніше, ніж будь-коли + Усе Під Вашим Контролем Отримуйте максимальну вигоду з кращими курсами від надійних провайдерів + Tangem порівнює кілька провайдерів, як DEX, так і CEX. Найкращий курс обирається автоматично. Віддаєте перевагу іншому провайдеру? Ви можете обрати його вручну. Найкращі тарифи + Найкращий Доступний Курс Просто та зручно — обмін токенів в декілька дотиків + Обмінюйте тисячі токенів між будь-якими мережами. Комісія 0% на обмін стейблкоїнів Легше, ніж будь-коли + 90+ Блокчейнів\n16 000+ Активів Обмін через провайдера Ваші активи Сума включає: \n• комісію постачальника послуг\n• комісію мережі за відправлення %s з біржі назад на адресу користувача. @@ -1975,6 +1998,12 @@ Використовуйте вашу картку або кільце, щоб отримати адреси для %d мереж Використовуйте вашу картку або кільце, щоб отримати адреси для %d мереж + + Синхронізуйте адреси, щоб отримати адресу для %d мережі + Синхронізуйте адреси, щоб отримати адреси для %d мереж + Синхронізуйте адреси, щоб отримати адреси для %d мереж + Синхронізуйте адреси, щоб отримати адреси для %d мереж + Деякі адреси відсутні Мережа наразі недоступна. Будь ласка, спробуйте пізніше. Мережа недоступна diff --git a/core/res/src/main/res/values-zh-rCN/strings.xml b/core/res/src/main/res/values-zh-rCN/strings.xml index 336648fd49..1369fb0631 100644 --- a/core/res/src/main/res/values-zh-rCN/strings.xml +++ b/core/res/src/main/res/values-zh-rCN/strings.xml @@ -80,6 +80,9 @@ 添加代币 选择您想要接收的代币 选择您要兑换的代币 + 增加资金 + 兑换 + 转账 添加到您的投资组合 添加代币 排序和分组 @@ -87,6 +90,10 @@ 选择网络 添加自定义代币 管理代币 + 信用卡或银行账户 + 分享您的地址或二维码 + 在您的投资组合之间 + 您将收到 只能从 %3$s 网络发送 %1$s (%2$s) 到此地址。使用其他代币和网络可能会导致资金损失。 默认 传统 @@ -214,8 +221,10 @@ 拒绝访问 账户 账户 + %s 失败 激活 添加 + 增加资金 添加到投资组合 添加代币 添加代币 @@ -229,6 +238,8 @@ 申请 批准 批准 + 已批准 + 批准 请注意 可用网络 备份 @@ -308,10 +319,14 @@ 隐藏 保持到 %s 小时 + + %d小时 + 小时之前 导入 + 进行中 余额不足 稍后 @@ -354,6 +369,8 @@ %1$s — %2$s 阅读更多 接收 + 已收到 + 接收中 推荐 拒绝 重新加载 @@ -372,6 +389,8 @@ 发送 发送: 交易发送失败 + 发送中 + 发送 服务器不可用,请稍后再试。 分享 分享链接 @@ -382,6 +401,7 @@ 跳过 出问题了 质押 + 已质押 质押 开始 提交 @@ -389,6 +409,8 @@ 支持 支持的网络 兑换 + 已兑换 + 兑换... Tangem Tangem钱包 点击并按住 @@ -397,6 +419,7 @@ 到 %s 今天 + 代币 要发送的代币 %d代币 @@ -405,6 +428,8 @@ 交易状态 交易 转让 + 已转账 + 请稍后再试。 无法加载数据…… 我明白 我明白,请继续 @@ -414,10 +439,12 @@ 取消抵押 由于 %1$s 的限制,一次交易只能发送 %2$d 个UTXO。这意味着您只能发送 %3$s 或更少。您需要减少金额。 通用值已拷贝 + 表决 钱包 警告 + 撤回 收益模式 合约地址已复制! @@ -506,6 +533,7 @@ 在其他地址发现了资金。启用动态地址即可访问这些地址。 在其他地址发现的资金 动态地址 + 一旦网络 %@ 中的待处理交易完成,即可进行动态地址管理 最佳机会 清除筛选 列表正在刷新,暂时为空。请稍后再查看。 @@ -578,12 +606,14 @@ 最佳汇率 FCA警告清单 固定利率不可用 + 兑换提供商 有竞争力的费率 被列入 FCA 警告名单的提供商 最多可 %s 可用 %s 此交易对不可用 需要许可 + 需要许可 推荐 已购买 %s 购买 %s @@ -631,6 +661,7 @@ 您需要使用“批准”功能来授权其他地址使用您指定数量的代币。根据设计,智能合约只有在您批准后才能访问您的代币。通过“解锁”您的代币,您授权 StakeKit 智能合约使用它们。网络矿工会收到 Gas 费(由您支付)来记录区块链上的此操作。批准后,您可以质押您的代币。 要继续,您需要允许 Polygon 智能合约使用您的 %s 要继续,请授权 %1s 智能合约使用您的 %2s + 去中心化交易所需要获得许可才能与您的钱包互动。 %1s 给予许可 无限制 地址直接在您的 Tangem 硬件钱包上生成,随时可用,并受到全面保护。 @@ -658,6 +689,8 @@ 让您的加密货币安全离线存储。轻薄如信用卡,安全胜过银行金库。 如果确定现在退出,您需要从头再来。 通过 Google 云端硬盘备份恢复现有钱包 + 我们正在改进 Google 云盘备份功能,使钱包恢复更加便捷。 + Google 云盘备份功能即将推出 Google 云端硬盘备份 创建一个安全的钱包并转移资金,以加强保护。 创建新钱包 @@ -819,6 +852,7 @@ 收益模式 质押是获取加密货币奖励的最简单方式。 %s 年利率最高可达 %s + 在另一个网络或帐户中 代币已添加 关于 %s @@ -1019,6 +1053,7 @@ 其他选项 您的密钥将在芯片内部安全生成,没有助记词,这意味着任何人都无法导出或窃取它。 私下生成密钥 + 如继续,即表示您同意 您的卡已激活,可以使用了。 成功! 您的钱包已设置完毕,可以使用了! @@ -1094,6 +1129,14 @@ 此交易已处理完毕,无需进一步操作。 获得最佳利率... 即时 + 验证是免费的,通常需要 1-2 分钟。 + Tangem无法获取您的身份信息,您直接与受监管的服务提供商共享数据。 + 通过验证后,即可完全访问该提供商的未来交易 + 选择其他方法 + 为遵守当地监管要求 %@ 需要进行身份验证。 + 支付提供商要求进行身份验证 + 验证 + 什么是重要的 使用 onramp 功能即表示您同意提供商的 %1$s 和 %2$s 服务由外部供应商提供。\nTangem对此不承担任何责任。 购买金额不应超过 %s @@ -1132,6 +1175,8 @@ 通过 %s 您将支付 分组 + 按网络分组 + 按余额排序 按余额 整理代币 取消分组 @@ -1157,7 +1202,11 @@ 未知参数 信用卡或银行账户 分享您的地址或二维码 + 安全出售加密货币 + 发送到另一个钱包 在您的投资组合之间 + 其他 + 快速充值 无需备忘录 %1$s (%2$s) 在 %3$s 网络 %1$s 在 %2$s 网络 @@ -1401,6 +1450,7 @@ 从质押账户提取资金后,您必须等待一段时间才能获得代币。 热身期 激活参与质押的指定时间。 + 已启用质押 目前没有可用的验证节点。请稍后再试。 质押功能不可用 网络将收取代币批准费,以验证您是否授权使用您的代币进行质押。 @@ -1511,13 +1561,16 @@ 至少需要有 %1$s 的转入交易才能继续进行 资金不足 批准后,您即允许智能合约在未来的交易中使用您的代币。 + 详细模式 固定利率 网络将收取代币批准费,以验证您是否授权使用您的代币进行兑换。 + 兑换中 直接在您的钱包中以更优惠的汇率兑换更多代币。 新增兑换服务提供商! 还在寻找其他代币?\n尝试搜索或探索其他加密货币! 搜索任何代币,即使它还不在你的列表中。 使用搜索查找所需内容 + 简易模式 我们提供全天候支持,让您安心无忧,任何问题都能得到帮助。 永远在这里 多个值得信赖的供应商汇聚一处——在您的钱包中轻松兑换任何资产 @@ -1546,7 +1599,9 @@ 所有去中心化交易所都要求用户授权,以防止智能合约未经许可访问您的钱包。根据设计,智能合约只有在您授权后才能访问您的代币。通过“解锁”您的代币,您授权 1-inch 智能合约使用这些代币。网络矿工会收到 Gas 费(由您支付)来记录区块链上的此操作。授权后,您可以兑换您的代币。 批准 费用估算错误。请联系客服反馈。 + 您发送自 您兑换 + 您发送 兑换如此数量的选定代币将对价格产生重大影响,并降低您的收益。 由于流动性低,您收到的资金可能会大大减少。请尝试较小的金额或另一个提供商。 价格影响大 @@ -1555,11 +1610,14 @@ 给予许可 兑换 互换... + 您收到 您收到 选择代币 无法使用 此交易流动性不足,请减少金额或选择其他供应商。 交易额过大 + 转账 + 转账... 我们非常乐意收到您的反馈。 Tangem Pay 现已进入测试阶段 无法重命名卡片 @@ -1636,6 +1694,7 @@ 重新发行卡片 只允许输入字母和数字。 无效字符 + 卡片名称 显示 显示详情 将您投资组合中的任何资产互换到卡片 @@ -1648,6 +1707,7 @@ 目前无法提款 当前提款交易完成之前,您无法发起新的提款交易或互换交易。 提款进行中 + 卡片名称 从 %s设定一个限额 我们无法设置限额,请稍后再试。 改变 @@ -1658,6 +1718,9 @@ 每日限额已设定 每日限额 卡片设置 + + %d卡片 + 更改PIN码 如果忘记了,请返回应用程序。 设置限额从 %s 到 %s @@ -1672,8 +1735,15 @@ 免费领取您的 Tangem Visa 虚拟卡 获取 Tangem Pay 前往支持页面 + 它会生成一组新的卡片详细信息。 + 发行费 + 将USDC存入支付账户以支付发行费用 + 无法支付费用 + 是否再发一张卡? + 发卡 通常需要最多15分钟 设置您的 Tangem 卡 + 发行新的数字卡 正在发放您的卡片 该卡通常会在 5 分钟内自动发放。极少数情况下,如果需要人工审核,则可能需要长达 48 小时。 Tangem Pay @@ -1690,6 +1760,8 @@ 隐藏 KYC 块 抱歉,我们无法验证 您的个人资料。 + 您最多可以添加 3 张卡片。删除一张即可添加新卡片。 + 最大发卡量 免费领取您的 Tangem Visa 虚拟卡 使用 USDC 进行日常支付 获取卡片 @@ -1699,6 +1771,8 @@ 实际支付金额与所示金额一致 将在不透露您的地址和资产的情况下创建一个单独的付款账户 无与伦比的隐私保护 + 并将其与支付卡关联。 + 我们将设置一个钱包。 几分钟内即可获得免费的 Tangem Pay 卡 支付支持 支付账户 @@ -1719,6 +1793,7 @@ 无法显示详细信息。但刷卡支付功能仍然可用。 设置 PIN 码 卡片已停用 + 更换您的卡片 会话已过期 恢复访问权限 使用 USDC 进行日常支付 @@ -1774,18 +1849,28 @@ 无法出售 无法从 %s兑换 无法兑换 + 领取奖励 合约: %s + 禁用收益模式 + 质押收益 您目前还没有任何交易记录。 加载交易历史记录失败。\n点击刷新按钮更新信息。 + %%image%%%s 多个地址 本区块链目前不支持交易历史记录。不过不用担心,我们正在努力!在此期间,您可以在资源管理器中查看。 操作 + 待定 + 奖励已再质押 + 奖励再质押 + 质押奖励 + %%image%%%s 为 %s 来自 %s 到: %s 验证节点: %s 通知功能已启用,但需要您在设备设置中允许通知才能生效。 交易通知 + 转账中 最少 %s 最低交易金额为 %1$s。 热门代币在波场网络上的交易费用可能较高。质押 TRX 或许有助于降低交易成本。 @@ -1817,7 +1902,9 @@ 获取新交易通知 抢先了解最新促销活动 抢先体验最新功能和专属优惠。 + 价格变动提醒、产品资讯和独家优惠 专题报道和新闻更新 + 优惠与更新 您想使用推送通知吗? 启用推送通知,即可在资金到账时收到提醒。 不要错过任何一笔交易 @@ -2057,6 +2144,9 @@ 使用您的卡片或指环获取%d网络地址 + + 同步地址以获取%d网络地址 + 部分地址缺失 目前网络无法连接,请稍后再试。 网络无法访问 diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 69ae0df956..bd779ef436 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -68,6 +68,7 @@ 錯誤 獲取費用失敗 導入 + 進行中 網路費 @@ -92,6 +93,7 @@ 成功 交換 條款和條件 + 代幣 %d 代幣 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 73aedfe1a8..3871825936 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -80,6 +80,9 @@ Add tokens Choose the token you want to receive Choose the token you want to swap + Add funds + Swap + Transfer Add to your portfolio Add tokens Sorting and grouping @@ -87,6 +90,10 @@ Choose network Add custom token Manage tokens + Credit card or bank account + Share your address or QR-code + Between your portfolios + You receive Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. Default Legacy @@ -217,6 +224,7 @@ Access denied Account Accounts + %s failed Activate Add Add funds @@ -233,6 +241,8 @@ Apply Approval Approve + Approved + Approving Attention Available networks Backup @@ -315,11 +325,16 @@ Hide Hold to %s hour + + %d hour + %d hours + %dh ago %dh ago Import + in In progress Insufficient balance Later @@ -364,6 +379,8 @@ %1$s — %2$s Read more Receive + Received + Receiving Recommended Reject Reload @@ -382,6 +399,8 @@ Send Send: Failed to send transaction + Sending + Sent The server is not available, please try again later Share Share Link @@ -392,6 +411,7 @@ Skip Something went wrong Stake + Staked Staking Start Submit @@ -399,6 +419,8 @@ Support Supported networks Swap + Swapped + Swapping Tangem Tangem Wallet Tap and hold @@ -407,6 +429,7 @@ To To %s Today + Token Token to send %d token @@ -416,6 +439,8 @@ Transaction status Transactions Transfer + Transferred + Please try again later. Unable to load data… I understand I understand, continue @@ -425,10 +450,12 @@ Unstake Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. Value copied + Voting Wallets Warning week with + Withdrawing Yes Yield Mode Contract address copied! @@ -590,6 +617,7 @@ Best rate FCA Warning List Fixed rate is unavailable + Provider for swap Competitive rate Provider in FCA warning list Available up to %s @@ -1044,6 +1072,7 @@ Other options Your keys will be securely generated inside the chip. There is no seed phrase, which means nobody can export or steal it. Generate keys privately + By continuing, you agree to the \n%s Your card is activated and ready to be used Success! Your wallet is set up and ready to use! @@ -1133,12 +1162,16 @@ Service is provided by an external provider.\nTangem is not responsible. The purchase amount should be no more than %s The amount to buy must be at least %s + Cumulative transaction amount over %1s may require identity verification with %2s + By clicking Pay, you agree to %1s\'s %2s and %3s. No available providers for this currency Quickest processing Pay with Payment method Available up to %s Available from %s + US and UK issued cards can\'t be processed via this method. Provider may require additional identity verification + Provider requirements %d provider %d providers @@ -1169,10 +1202,19 @@ via %s You will pay Group + Group by networks + Sort by balance By balance Organize tokens Ungroup %s support + Product news, exclusive offers, and activity reminders. + Offers & Updates + Get notified about price changes for top market coins. + Price Alerts + Notification Settings + Real-time alerts for transactions, exchanges, and critical updates. + Transaction Alerts More info You can enable Notifications for Tangem in Settings. Enable Later @@ -1194,6 +1236,8 @@ Unknown Parameters Credit card or bank account Share your address or QR-code + Sell crypto securely + Send to another wallet Between your portfolios Other Quick top up @@ -1443,6 +1487,7 @@ The period you must wait after requesting to withdraw funds from staking before the tokens become available. Warmup period The allocated time for activating participation in staking. + Staking enabled No available validators at the moment. Please try again later. Staking Unavailable The network will charge a token approval fee to verify that you are authorizing the use of your token for the staking. @@ -1553,6 +1598,7 @@ An incoming transaction of at least %1$s is required to proceed Insufficient funds By approving, you allow the smart contract to use your tokens in future transactions. + Detailed mode Fixed Rate The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap. Swap in progress @@ -1561,6 +1607,7 @@ Looking for something else?\nTry searching or explore another crypto! Search for any token, even if it’s not in your list yet. Use search to find what you need + Simple mode Feel confident with round-the-clock support to help with any issues Always Here Multiple trusted providers in one place—swap any asset effortlessly in your wallet @@ -1576,7 +1623,7 @@ Unbeatable Rates Best Available Rate Hassle-free and intuitive, allowing you to swap tokens in just a few taps - Swap across major networks
and thousands of tokens 0% fee on stablecoin-to-stablecoin swaps + Swap across major networks
and thousands of tokens 0% fee on stablecoin-to-stablecoin swaps Simply Convenient 90+ Blockchains\n16,000+ Assets Swap via provider @@ -1606,6 +1653,8 @@ not available Not enough liquidity for this trade.\nReduce the amount or choose another provider. Trade too large + Transfer + Transfer... We would be happy to receive your feedback Tangem Pay is now in beta Unable to rename card @@ -1657,7 +1706,7 @@ CVC Failed to load data. Try again later. Expiry - Freeze Card + Freeze card Hide details Hide Open Google Wallet @@ -1682,6 +1731,7 @@ Replace card Only letters and numbers are allowed Invalid characters + Card name Reveal Show details Swap any asset in your portfolio for card @@ -1705,6 +1755,10 @@ Daily limit is set Daily limit Card settings + + %d card + %d cards + Change PIN-code Come back to the app if you forget it. Set a limit from %s to %s @@ -1714,13 +1768,20 @@ Failed to issue card A technical error has occurred, please try again by clicking the button below. A technical error has occurred, please contact support. - Feature will be availible soon + Feature will be available soon You will be available to issue additional cards for your payment account Get your free Tangem Visa virtual card Get Tangem Pay Go to Support + It generates a new set of card details + Issue fee + Deposit USDC to payment account to cover the issuing fee + Unable to cover fee + Issue an additional card? + Issue card It usually takes up to 15 minutes Setting up your Tangem Card + Issuing a new digital card Issuing your card The card is usually issued automatically within 5 minutes. In rare cases, if manual review is required, it may take up to 48 hours. Tangem Pay @@ -1737,6 +1798,8 @@ Hide KYC block Sorry, we couldn\'t verify your profile. + You can have up to 3 cards. Delete one to add a new card. + Maximum Cards Issued Get your free Tangem Visa virtual card Use USDC for everyday payments Get card @@ -1746,6 +1809,8 @@ Pay exactly what you see A separate payment account will be created without disclosing your addresses and assets Unrivaled privacy + And link a payment card to it + We\'ll set up a wallet Get your free Tangem Pay Card in minutes Pay Support Payment account @@ -1796,7 +1861,8 @@ Approval has been revoked. Your funds remain in Yield mode. To perform actions, please go to Yield mode and grant permission again. Available balance Total balance - Earn up to %s a year + Up to %s APR + Up to %s APY Generate XPUB Hide You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page. @@ -1822,18 +1888,28 @@ Unavailable to sell Unavailable for swap from %s Unavailable for swap + Claiming reward contract: %s + Disabling Yield mode + Earned from stake You don\'t have any transactions yet Failed to load transaction history.\nClick on reload button to update the information. + from: %%image%% %s Multiple addresses Transaction history is currently not supported for this blockchain. But don\'t worry, we\'re working on it! In the meantime you can check it in the explorer. Operation + Pending + Rewards restaked + Rewards restaking + Staking reward + to: %%image%% %s for: %s from: %s to: %s validator: %s Notifications are enabled but won\'t work until you allow notifications in your device settings. Transaction Notifications + Transfer in progress Minimum %s The minimum transaction amount is %1$s. Tron network fees for popular tokens can be higher. Staking TRX may help reduce transaction costs. @@ -1865,7 +1941,9 @@ Get notified of incoming transactions Be the first to know about new promotions Early access to fresh features and exclusive offers. + Price change alerts, product news, and exclusive offers Feature and News Updates + Offers & Updates Would you like to use\nPush-notifications? Enable push notifications to receive alerts when funds arrive in your wallet. Don\'t Miss a Transaction @@ -2105,7 +2183,11 @@ MATIC to POL Migration Use your card or ring to get an address for %d network - Use your card or ring to get an addresses for %d networks + Use your card or ring to get addresses for %d networks + + + Sync addresses to get an address for %d network + Sync addresses to get an addresses for %d networks Some addresses are missing The network is currently unreachable. Please try again later. @@ -2265,6 +2347,14 @@ No, send all Reduce by %s XTZ To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ + Activate Yield Mode for the first time and get up to 3x yield for your first 30 days + First month APR bonus + You get market yield + Bonus. Bonus is paid once in USDT or USDC within 14 days after the 30-day period ends. Available while promo budget lasts. Terms and conditions apply + Summary + Keep funds in Yield Mode for 30 consecutive days. Bonus is based on the yield you actually earn during that period + How to qualify + 3 × market yield for first 30 days\nMinimum eligibility: $1 of market yield accumulated for 30 days\nMaximum bonus: $50 + How much you get When Yield Mode is active, all future top-ups to this address will be supplied to Aave. You can still manage your funds freely. Your %s is supplied to Aave Supplying %1$s %2$s to Aave From 5fb1090653b0eddf8ef89758198207fd0b071db9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 May 2026 07:13:13 -0700 Subject: [PATCH 27/29] Updated on 2026-08-14 --- .../components/TangemPayCardPageScreenComponent.kt | 4 +--- .../tangempay/entity/TangemPayDetailsUM.kt | 2 +- .../model/TangemPayCardDetailsBlockModel.kt | 14 ++++++++------ .../tangempay/ui/TangemPayCardDetailsBlock.kt | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt index 87a347f661..938132dbc9 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt @@ -60,9 +60,7 @@ internal class TangemPayCardPageScreenComponent( TangemPayCardPageScreen( state = state, cardDetailsBlockComponent = cardDetailsBlockComponent, - cardDetailsState = cardDetailsState.copy( - isActive = !state.isReissueInProgress, - ), + cardDetailsState = cardDetailsState, modifier = modifier, ) bottomSheet.child?.instance?.BottomSheet() diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt index a86c20d4d4..52a72107d6 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt @@ -31,7 +31,7 @@ internal data class TangemPayCardDetailsUM( val isLoading: Boolean = false, val cardFrozenState: TangemPayCardFrozenState, val displayNameState: DisplayNameState?, - val isActive: Boolean = true, + val isActionsAvailable: Boolean = false, ) internal sealed interface DisplayNameState { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt index 4e97b58c7d..eac06a5a95 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt @@ -74,7 +74,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor( private val showCardDetailsTimerJobHolder = JobHolder() init { - subscribeToCardNameChanges(cardId = params.params.config.cardId, userWalletId = params.params.userWalletId) + subscribeToCardChanges(cardId = params.params.config.cardId, userWalletId = params.params.userWalletId) subscribeToCardFrozenState() modelScope.launch { cardDetailsEventListener.event.collectLatest { event -> @@ -86,7 +86,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor( } } - private fun subscribeToCardNameChanges(cardId: String, userWalletId: UserWalletId) { + private fun subscribeToCardChanges(cardId: String, userWalletId: UserWalletId) { paymentAccountStatusSupplier.invoke(userWalletId) .onEach { state -> val status = state.value @@ -97,7 +97,11 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor( val card = status.requireCardWithId(cardId) val displayName = card.displayName ?: return@onEach + if (card.isReissuing) { + hideCardDetails() + } uiState.update(TangemPayCardDetailsUpdateNameTransformer(displayName)) + uiState.update { it.copy(isActionsAvailable = !card.isReissuing) } } } .launchIn(modelScope) @@ -141,10 +145,8 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor( } fun hideCardDetails() { - modelScope.launch { - revealCardDetailsJobHolder.cancel() - uiState.transformerUpdate(transformer = DetailsHiddenStateTransformer(stateFactory)) - } + revealCardDetailsJobHolder.cancel() + uiState.transformerUpdate(transformer = DetailsHiddenStateTransformer(stateFactory)) } private fun showError() { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt index 18bf622b32..5a21bce025 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt @@ -97,7 +97,7 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M shape = RoundedCornerShape(16.dp), ), ) { - if (shouldShowDetails && state.isActive) { + if (shouldShowDetails) { TangemPayCardDetailsShownBlock( cardNumber = state.number, expiry = state.expiry, @@ -147,7 +147,7 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif ) } - if (state.isActive) { + if (state.isActionsAvailable) { ConstraintLayout( modifier = Modifier .align(Alignment.BottomCenter) From 99f228ee338413826f1d8d894f21b58c376e00c8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 May 2026 08:04:22 -0700 Subject: [PATCH 28/29] Updated on 2026-08-14 --- ...rd_48x32.webp => img_visa_card_48_32.webp} | Bin .../drawable/img_visa_card_inactive_48_32.xml | 18 ++++++++++++++++ .../tangempay/ui/TangemPayDetailsScreen.kt | 20 +++++++----------- 3 files changed, 26 insertions(+), 12 deletions(-) rename core/ui/src/main/res/drawable/{img_visa_card_48x32.webp => img_visa_card_48_32.webp} (100%) create mode 100644 core/ui/src/main/res/drawable/img_visa_card_inactive_48_32.xml diff --git a/core/ui/src/main/res/drawable/img_visa_card_48x32.webp b/core/ui/src/main/res/drawable/img_visa_card_48_32.webp similarity index 100% rename from core/ui/src/main/res/drawable/img_visa_card_48x32.webp rename to core/ui/src/main/res/drawable/img_visa_card_48_32.webp diff --git a/core/ui/src/main/res/drawable/img_visa_card_inactive_48_32.xml b/core/ui/src/main/res/drawable/img_visa_card_inactive_48_32.xml new file mode 100644 index 0000000000..70bce64243 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_visa_card_inactive_48_32.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index 7044edc626..589b8d751c 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -302,20 +302,16 @@ private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modi ) { Image( modifier = Modifier.fillMaxSize(), - painter = painterResource(R.drawable.img_visa_card_48x32), + painter = painterResource( + if (card.isReissuing) { + R.drawable.img_visa_card_inactive_48_32 + } else { + R.drawable.img_visa_card_48_32 + }, + ), contentDescription = null, ) - if (card.isReissuing) { - Icon( - modifier = Modifier - .size(16.dp) - .align(Alignment.BottomStart) - .padding(2.dp), - painter = painterResource(R.drawable.ic_update_32), - contentDescription = null, - tint = TangemTheme.colors.text.constantWhite, - ) - } else { + if (!card.isReissuing) { Text( modifier = Modifier .align(Alignment.BottomStart) From d1da95f2b602184a2493d1077fbef75326f3d514 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 May 2026 15:04:50 +0000 Subject: [PATCH 29/29] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 76d48569f9..257bb0ac32 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.38-1512" +tangemBlockchainSdk = "develop-1520" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.38-615" +tangemCardSdk = "develop-614" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^