From dc7e286c59dea59309f4694bd33c2ff725c0fce4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Jul 2026 12:15:35 +0300 Subject: [PATCH 01/21] 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 0f32ca5313..7cee5f99e1 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.39-1589" +tangemBlockchainSdk = "releases-5.39-1601" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.39-623" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From ee35d8f3011535dfba5a0acd63516295a81bb63e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Jul 2026 14:48:10 +0500 Subject: [PATCH 02/21] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 26 +-- .../swap/domain/fee/DexSwapFeeCalculator.kt | 5 - .../SwapInteractorImplFindBestQuoteTest.kt | 193 ++++++++++++++++++ .../domain/fee/DexSwapFeeCalculatorTest.kt | 93 +++++++-- .../tangem/feature/swap/model/SwapModel.kt | 1 + 5 files changed, 284 insertions(+), 34 deletions(-) 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 4d57c736b2..d50f8618cc 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 @@ -62,6 +62,7 @@ import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero import com.tangem.utils.logging.TangemLogger import jakarta.inject.Inject @@ -420,13 +421,20 @@ internal class SwapInteractorImpl @Inject constructor( val fromToken = fromSwapCurrencyStatus.currency val toToken = toSwapCurrencyStatus.currency - val includeFeeInAmount = getIncludeFeeInAmountInternal( - fromSwapCurrencyStatus = fromSwapCurrencyStatus, - amount = amount, - reduceBalanceBy = reduceBalanceBy, - feeValue = BigDecimal.ZERO, + val nativeBalance = walletManagersFacade.getNativeTokenBalance( + userWalletId = fromSwapCurrencyStatus.userWalletId, + networkId = fromToken.network.rawId, + derivationPath = fromSwapCurrencyStatus.currency.network.derivationPath.value, ) + val includeFeeInAmount = if (nativeBalance.isZero()) { + IncludeFeeInAmountInternal.Excluded + } else { + IncludeFeeInAmountInternal.Included( + SwapAmount(nativeBalance - reduceBalanceBy, fromToken.decimals), + ) + } + val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmountInternal.Included) { includeFeeInAmount.amountSubtractFee } else { @@ -446,12 +454,6 @@ internal class SwapInteractorImpl @Inject constructor( rateType = RateType.FLOAT, ) - val quoteBalanceStatus = if (includeFeeInAmount == IncludeFeeInAmountInternal.BalanceNotEnough) { - SwapBalanceStatus.InsufficientAmount - } else { - SwapBalanceStatus.Pending // fee not resolved yet - } - return provider to getQuotesState( provider = provider, quoteDataModel = quotes, @@ -459,7 +461,7 @@ internal class SwapInteractorImpl @Inject constructor( fromSwapCurrencyStatus = fromSwapCurrencyStatus, toSwapCurrencyStatus = toSwapCurrencyStatus, isAllowedToSpend = true, - quoteBalanceStatus = quoteBalanceStatus, + quoteBalanceStatus = SwapBalanceStatus.Pending, ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculator.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculator.kt index 8f711b7b88..99c5658eef 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculator.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculator.kt @@ -127,11 +127,6 @@ class DexSwapFeeCalculator( derivationPath = fromSwapCurrencyStatus.currency.network.derivationPath.value, ) - // if native balance is zero - we can't calculate fee - if (nativeBalance.signum() == 0) { - raise(ExpressDataError.UnknownError()) - } - try { val txAmountValue = transaction.txValue ?: error("unable to get txValue") val amountToSend = createNativeAmountForDex(txAmountValue, fromSwapCurrencyStatus.currency.network) diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt index 5e5d2ab44b..ead54dc918 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt @@ -20,12 +20,15 @@ import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel +import com.tangem.feature.swap.domain.models.domain.SwapBalanceStatus import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.ui.SwapState import io.mockk.coEvery +import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.slot import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -717,6 +720,196 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( } } + /** + * `manageCex` no longer derives `includeFeeInAmount` through `getIncludeFeeInAmountInternal`. + * It now reads the native-coin balance directly: + * - native balance non-zero → request the whole `nativeBalance - reduceBalanceBy` as `fromAmount` + * - native balance zero → request the original swap `amount` + * The resulting quote balance status is always `Pending` (resolved later by the fee selector). + */ + @Nested + inner class CexNativeBalanceAmount { + + @Test + fun `should request nativeBalance as fromAmount when native balance is non-zero`() = runTest { + // Given — native balance 10 (from base stub), decimals 18, reduceBalanceBy 0 + val cexProvider = buildSwapProvider(ExchangeProviderType.CEX) + val fromStatus = buildSwapCurrencyStatus( + networkRawId = ethNetwork, + isCoin = true, + amount = BigDecimal("10"), + decimals = 18, + ) + val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) + val quoteModel = buildQuoteModel() + val fromAmountSlot = slot() + + coEvery { + repository.findBestQuote( + userWallet = any(), + fromContractAddress = any(), + fromNetwork = any(), + toContractAddress = any(), + toNetwork = any(), + fromAmount = capture(fromAmountSlot), + fromDecimals = any(), + toDecimals = any(), + providerId = cexProvider.providerId, + rateType = any(), + ) + } returns quoteModel.right() + + // When + val result = sut.findBestQuote( + fromSwapCurrencyStatus = fromStatus, + toSwapCurrencyStatus = toStatus, + providers = listOf(cexProvider), + amountToSwap = "1.0", + reduceBalanceBy = BigDecimal.ZERO, + ) + + // Then — fromAmount is the full native balance (10 * 1e18), not the "1.0" swap amount + assertThat(fromAmountSlot.isCaptured).isTrue() + assertThat(fromAmountSlot.captured).isEqualTo("10000000000000000000") + assertThat(result[cexProvider]).isInstanceOf(SwapState.QuotesLoadedState::class.java) + val loaded = (result[cexProvider] ?: error("state must not be null")) as SwapState.QuotesLoadedState + assertThat(loaded.preparedSwapConfigState.balanceStatus).isEqualTo(SwapBalanceStatus.Pending) + } + + @Test + fun `should subtract reduceBalanceBy from native balance when building fromAmount`() = runTest { + // Given — native balance 10, reduceBalanceBy 2 → fromAmount = 8 * 1e18 + val cexProvider = buildSwapProvider(ExchangeProviderType.CEX) + val fromStatus = buildSwapCurrencyStatus( + networkRawId = ethNetwork, + isCoin = true, + amount = BigDecimal("10"), + decimals = 18, + ) + val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) + val quoteModel = buildQuoteModel() + val fromAmountSlot = slot() + + coEvery { + repository.findBestQuote( + userWallet = any(), + fromContractAddress = any(), + fromNetwork = any(), + toContractAddress = any(), + toNetwork = any(), + fromAmount = capture(fromAmountSlot), + fromDecimals = any(), + toDecimals = any(), + providerId = cexProvider.providerId, + rateType = any(), + ) + } returns quoteModel.right() + + // When + sut.findBestQuote( + fromSwapCurrencyStatus = fromStatus, + toSwapCurrencyStatus = toStatus, + providers = listOf(cexProvider), + amountToSwap = "1.0", + reduceBalanceBy = BigDecimal("2"), + ) + + // Then + assertThat(fromAmountSlot.captured).isEqualTo("8000000000000000000") + } + + @Test + fun `should request the original swap amount as fromAmount when native balance is zero`() = runTest { + // Given — native balance ZERO → includeFeeInAmount Excluded → fromAmount = swap amount (1.0) + val cexProvider = buildSwapProvider(ExchangeProviderType.CEX) + val fromStatus = buildSwapCurrencyStatus( + networkRawId = ethNetwork, + isCoin = true, + amount = BigDecimal("10"), + decimals = 18, + ) + val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) + val quoteModel = buildQuoteModel() + val fromAmountSlot = slot() + + coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal.ZERO + coEvery { + repository.findBestQuote( + userWallet = any(), + fromContractAddress = any(), + fromNetwork = any(), + toContractAddress = any(), + toNetwork = any(), + fromAmount = capture(fromAmountSlot), + fromDecimals = any(), + toDecimals = any(), + providerId = cexProvider.providerId, + rateType = any(), + ) + } returns quoteModel.right() + + // When + val result = sut.findBestQuote( + fromSwapCurrencyStatus = fromStatus, + toSwapCurrencyStatus = toStatus, + providers = listOf(cexProvider), + amountToSwap = "1.0", + reduceBalanceBy = BigDecimal.ZERO, + ) + + // Then — 1.0 * 1e18, not the native balance + assertThat(fromAmountSlot.captured).isEqualTo("1000000000000000000") + val loaded = (result[cexProvider] ?: error("state must not be null")) as SwapState.QuotesLoadedState + assertThat(loaded.preparedSwapConfigState.balanceStatus).isEqualTo(SwapBalanceStatus.Pending) + } + + @Test + fun `should read native token balance for the from-token network`() = runTest { + // Given + val cexProvider = buildSwapProvider(ExchangeProviderType.CEX) + val fromStatus = buildSwapCurrencyStatus( + networkRawId = ethNetwork, + isCoin = true, + amount = BigDecimal("10"), + ) + val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) + val quoteModel = buildQuoteModel() + + coEvery { + repository.findBestQuote( + userWallet = any(), + fromContractAddress = any(), + fromNetwork = any(), + toContractAddress = any(), + toNetwork = any(), + fromAmount = any(), + fromDecimals = any(), + toDecimals = any(), + providerId = cexProvider.providerId, + rateType = any(), + ) + } returns quoteModel.right() + + // When + sut.findBestQuote( + fromSwapCurrencyStatus = fromStatus, + toSwapCurrencyStatus = toStatus, + providers = listOf(cexProvider), + amountToSwap = "1.0", + reduceBalanceBy = BigDecimal.ZERO, + ) + + // Then — the CEX path resolves the fee-paying native balance for the from-token network + coVerify { + walletManagersFacade.getNativeTokenBalance( + userWalletId = any(), + networkId = ethNetwork, + derivationPath = any(), + ) + } + } + } + @Nested inner class MixedProviderDispatch { diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculatorTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculatorTest.kt index 9d57af4369..ef941b47ba 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculatorTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/fee/DexSwapFeeCalculatorTest.kt @@ -122,29 +122,88 @@ internal class DexSwapFeeCalculatorTest { } // ------------------------------------------------------------------------- - // EVM zero-balance short-circuit + // EVM zero-balance no longer short-circuits (guard removed) + // + // Previously a zero native balance raised UnknownError *before* any fee call. That guard was + // removed, so a zero-balance quote must still surface a fee: when the tx amount fits the (zero) + // balance the normal getFeeUseCase path runs; when it does not, the balance check throws and the + // calculator falls back to getEthSpecificFeeUseCase via the IllegalStateException branch. // ------------------------------------------------------------------------- @Test - fun `EVM DEX swap with native balance ZERO returns Left UnknownError`() = runTest { - val fromStatus = buildSwapCurrencyStatus(networkRawId = ethNetwork, isCoin = true) - val transaction = buildDex(txValue = "0") - coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal.ZERO + fun `EVM DEX swap with native balance ZERO no longer short-circuits and computes fee via getFeeUseCase`() = + runTest { + val fromStatus = buildSwapCurrencyStatus(networkRawId = ethNetwork, isCoin = true) + // txValue "0" → amountToSend 0, so `nativeBalance(0) < 0` is false and the main path runs. + val transaction = buildDex(txValue = "0") + coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal.ZERO + coEvery { + getFeeUseCase.invoke(userWallet = any(), network = any(), transactionData = any()) + } returns TransactionFee.Single(normal = ethLegacyFee()).right() - val result = sut.calculate(fromStatus, transaction) + val result = sut.calculate(fromStatus, transaction) - assertThat(result.isLeft()).isTrue() - result.onLeft { assertThat(it).isEqualTo(ExpressDataError.UnknownError()) } - // getFeeUseCase should not have been called because balance check short-circuits first. - // Use a more permissive verify to avoid clashing with the other overload signatures. - coVerify(exactly = 0) { - getFeeUseCase.invoke( - userWallet = any(), - network = any(), - transactionData = any(), - ) + // The removed guard means the fee is now computed instead of raising UnknownError. + assertThat(result.isRight()).isTrue() + coVerify(exactly = 1) { + getFeeUseCase.invoke( + userWallet = any(), + network = any(), + transactionData = any(), + ) + } + coVerify(exactly = 0) { + getEthSpecificFeeUseCase.invoke( + userWallet = any(), + cryptoCurrency = any(), + gasLimit = any(), + gasPrice = any(), + ) + } + } + + @Test + fun `EVM DEX swap with native balance ZERO falls back to getEthSpecificFeeUseCase when txValue exceeds balance`() = + runTest { + val fromStatus = buildSwapCurrencyStatus(networkRawId = ethNetwork, isCoin = true) + val gas = BigInteger.valueOf(120_000L) + // txValue 0.001 ETH > zero balance → `nativeBalance < amountToSend` throws → gas fallback. + val transaction = buildDex(txValue = "1000000000000000", gas = gas) + coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal.ZERO + coEvery { + getEthSpecificFeeUseCase.invoke( + userWallet = any(), + cryptoCurrency = any(), + gasLimit = any(), + gasPrice = any(), + ) + } returns TransactionFee.Choosable( + minimum = ethLegacyFee(), + normal = ethLegacyFee(), + priority = ethLegacyFee(), + ).right() + + val result = sut.calculate(fromStatus, transaction) + + // Zero balance now falls back instead of raising UnknownError up-front. + assertThat(result.isRight()).isTrue() + coVerify(exactly = 1) { + getEthSpecificFeeUseCase.invoke( + userWallet = any(), + cryptoCurrency = any(), + gasLimit = gas, + gasPrice = any(), + ) + } + // The balance check throws before the main fee call, so getFeeUseCase is never reached. + coVerify(exactly = 0) { + getFeeUseCase.invoke( + userWallet = any(), + network = any(), + transactionData = any(), + ) + } } - } // ------------------------------------------------------------------------- // EVM IllegalStateException → fallback to GetEthSpecificFeeUseCase 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 45bc3613a8..453067c8a0 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 @@ -1728,6 +1728,7 @@ internal class SwapModel @Inject constructor( if (provider != null && swapState != null && isNotNullCurrency) { modelScope.launch { feeSelectorRepository.state.value = FeeSelectorUM.Loading + updateFeePaidCryptoCurrencyFor(fromSwapCurrencyStatus) feeSelectorReloadTrigger.triggerUpdate() } analyticsEventHandler.send(SwapEvents.ProviderChosen(provider)) From fa91188e722c6b683678cbea98aedbeaa8e4d364 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Jul 2026 15:44:05 +0200 Subject: [PATCH 03/21] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 4 + .../toggles/DefaultStakingFeatureToggles.kt | 6 + .../DefaultStakingFeatureTogglesTest.kt | 26 +++ .../staking/toggles/StakingFeatureToggles.kt | 2 + .../impl/presentation/model/StakingModel.kt | 7 + .../amount/AmountChangeStateTransformer.kt | 2 + .../amount/AmountMaxValueStateTransformer.kt | 2 + .../AmountRequirementStateTransformer.kt | 41 ++++- .../model/StakingModelTestBase.kt | 5 + .../AmountRequirementStateTransformerTest.kt | 163 ++++++++++++++++++ 10 files changed, 253 insertions(+), 5 deletions(-) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index dec9126e3d..b1ff87ea9d 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -166,5 +166,9 @@ { "name": "AND_14829_WARNINGS_REFACTORING_ENABLED", "version": "undefined" + }, + { + "name": "AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED", + "version": "6.0" } ] diff --git a/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt b/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt index 6f62c8bc06..bc6f6e44a5 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt @@ -14,6 +14,12 @@ internal class DefaultStakingFeatureToggles( return featureTogglesManager.isFeatureEnabled(toggle) } + override fun isSolanaUnstakeValidationEnabled(): Boolean { + return featureTogglesManager.isFeatureEnabled( + FeatureToggles.AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED, + ) + } + private fun StakingIntegrationID.getFeatureToggle(): FeatureToggles? = when (this) { is StakingIntegrationID.P2PEthPool -> FeatureToggles.STAKING_ETH_ENABLED is StakingIntegrationID.StakeKit -> this.getStakeKitFeatureToggle() diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/toggles/DefaultStakingFeatureTogglesTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/toggles/DefaultStakingFeatureTogglesTest.kt index ca6766c8c8..73586fe538 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/toggles/DefaultStakingFeatureTogglesTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/toggles/DefaultStakingFeatureTogglesTest.kt @@ -58,4 +58,30 @@ internal class DefaultStakingFeatureTogglesTest { verify(exactly = 0) { featureTogglesManager.isFeatureEnabled(any()) } } + + @Test + fun `isSolanaUnstakeValidationEnabled returns true when toggle enabled`() { + every { + featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED) + } returns true + + assertThat(toggles.isSolanaUnstakeValidationEnabled()).isTrue() + + verify(exactly = 1) { + featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED) + } + } + + @Test + fun `isSolanaUnstakeValidationEnabled returns false when toggle disabled`() { + every { + featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED) + } returns false + + assertThat(toggles.isSolanaUnstakeValidationEnabled()).isFalse() + + verify(exactly = 1) { + featureTogglesManager.isFeatureEnabled(FeatureToggles.AND_16148_SOLANA_UNSTAKE_VALIDATION_ENABLED) + } + } } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt b/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt index 80761562fc..8da36f24fb 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt @@ -5,4 +5,6 @@ import com.tangem.domain.staking.model.StakingIntegrationID interface StakingFeatureToggles { fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean + + fun isSolanaUnstakeValidationEnabled(): Boolean } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index e808f05372..e03f0537dc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -60,6 +60,7 @@ import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.repositories.P2PEthPoolRepository +import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.domain.tokens.* import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.* @@ -157,6 +158,7 @@ internal class StakingModel @Inject constructor( private val innerRouter: InnerStakingRouter, private val messageSender: UiMessageSender, private val giveApprovalFeatureToggles: GiveApprovalFeatureToggles, + private val stakingFeatureToggles: StakingFeatureToggles, appRouter: AppRouter, ) : Model(), StakingClickIntents { @@ -382,6 +384,8 @@ internal class StakingModel @Inject constructor( minimumTransactionAmount = minimumTransactionAmount, actionType = uiState.value.actionType, integration = integration, + isSolanaUnstakeValidationEnabled = stakingFeatureToggles + .isSolanaUnstakeValidationEnabled(), ) addAll( @@ -644,6 +648,7 @@ internal class StakingModel @Inject constructor( minimumTransactionAmount = minimumTransactionAmount, value = value, integration = integration, + isSolanaUnstakeValidationEnabled = stakingFeatureToggles.isSolanaUnstakeValidationEnabled(), ), ) checkSumLimitExceeded() @@ -685,6 +690,7 @@ internal class StakingModel @Inject constructor( minimumTransactionAmount = minimumTransactionAmount, actionType = uiState.value.actionType, integration = integration, + isSolanaUnstakeValidationEnabled = stakingFeatureToggles.isSolanaUnstakeValidationEnabled(), ), ) checkSumLimitExceeded() @@ -1472,6 +1478,7 @@ internal class StakingModel @Inject constructor( value = amountValue, minimumTransactionAmount = minimumTransactionAmount, integration = integration, + isSolanaUnstakeValidationEnabled = stakingFeatureToggles.isSolanaUnstakeValidationEnabled(), ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt index 9c9c0d1595..7dd219e1b3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountChangeStateTransformer.kt @@ -14,6 +14,7 @@ internal class AmountChangeStateTransformer( private val minimumTransactionAmount: EnterAmountBoundary?, private val value: String, private val integration: StakingIntegration, + private val isSolanaUnstakeValidationEnabled: Boolean, ) : Transformer { private val maxEnterAmountConverter = MaxEnterAmountConverter() @@ -43,6 +44,7 @@ internal class AmountChangeStateTransformer( maxAmount = maxEnterAmount, integration = integration, actionType = prevState.actionType, + isSolanaUnstakeValidationEnabled = isSolanaUnstakeValidationEnabled, ).transform(updatedAmountState), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt index 94476c6be9..0e250e8b3c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountMaxValueStateTransformer.kt @@ -14,6 +14,7 @@ internal class AmountMaxValueStateTransformer( private val minimumTransactionAmount: EnterAmountBoundary?, private val actionType: StakingActionCommonType, private val integration: StakingIntegration, + private val isSolanaUnstakeValidationEnabled: Boolean, ) : Transformer { private val maxEnterAmountConverter = MaxEnterAmountConverter() @@ -40,6 +41,7 @@ internal class AmountMaxValueStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, integration = integration, actionType = prevState.actionType, + isSolanaUnstakeValidationEnabled = isSolanaUnstakeValidationEnabled, ).transform(updatedAmountState), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt index 05a795e7cf..8874243fb5 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformer.kt @@ -16,6 +16,7 @@ import com.tangem.domain.staking.model.StakingIntegration import com.tangem.domain.staking.model.common.StakingAmountRequirement import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.R +import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.extensions.isPositive import com.tangem.utils.isNullOrZero @@ -28,6 +29,7 @@ internal class AmountRequirementStateTransformer( private val maxAmount: EnterAmountBoundary, private val integration: StakingIntegration, private val actionType: StakingActionCommonType, + private val isSolanaUnstakeValidationEnabled: Boolean = false, ) : Transformer { override fun transform(prevState: AmountState): AmountState { return if (prevState is AmountState.Data) { @@ -103,11 +105,17 @@ internal class AmountRequirementStateTransformer( ) } is StakingActionCommonType.Exit -> { - integration.exitArgs?.amountRequirement?.getError( - amount = amountDecimal, - minErrorRes = R.string.staking_unstake_amount_requirement_error, - maxErrorRes = R.string.staking_max_amount_requirement_error, - ) + if (isSolanaUnstakeValidationEnabled && + isSolana(cryptoCurrencyStatus.currency.network.rawId) + ) { + getSolanaUnstakeError(amount = amountDecimal, staked = maxAmount.amount) + } else { + integration.exitArgs?.amountRequirement?.getError( + amount = amountDecimal, + minErrorRes = R.string.staking_unstake_amount_requirement_error, + maxErrorRes = R.string.staking_max_amount_requirement_error, + ) + } } else -> null } @@ -124,6 +132,29 @@ internal class AmountRequirementStateTransformer( return isEnterOrExit && isTron && !isIntegerOnly } + private fun getSolanaUnstakeError(amount: BigDecimal, staked: BigDecimal?): TextReference? { + val minimum = integration.exitMinimumAmount?.takeIf { it.isPositive() } + ?: integration.enterMinimumAmount + if (minimum == null || staked == null) return null + + // Full unstake is always allowed regardless of minimum delegation. + if (amount.compareTo(staked) == 0) return null + + if (amount < minimum) { + val formatted = minimum.format { crypto(cryptoCurrencyStatus.currency) } + return resourceReference( + R.string.staking_unstake_amount_requirement_error, + wrappedList(formatted), + ) + } + + if (staked - amount < minimum) { + return resourceReference(R.string.staking_notification_low_staked_balance_text) + } + + return null + } + private fun StakingAmountRequirement.getError( amount: BigDecimal, @StringRes minErrorRes: Int, diff --git a/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/model/StakingModelTestBase.kt b/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/model/StakingModelTestBase.kt index dccea3b0b6..e5e657a1fe 100644 --- a/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/model/StakingModelTestBase.kt +++ b/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/model/StakingModelTestBase.kt @@ -29,6 +29,7 @@ import com.tangem.domain.staking.* import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.repositories.P2PEthPoolRepository +import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.domain.tokens.* import com.tangem.domain.transaction.usecase.* import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -115,6 +116,9 @@ internal abstract class StakingModelTestBase { protected val innerRouter: InnerStakingRouter = mockk() protected val messageSender: UiMessageSender = mockk() protected val giveApprovalFeatureToggles: GiveApprovalFeatureToggles = mockk() + protected val stakingFeatureToggles: StakingFeatureToggles = mockk { + every { isSolanaUnstakeValidationEnabled() } returns false + } @BeforeEach fun setUp() { @@ -208,6 +212,7 @@ internal abstract class StakingModelTestBase { innerRouter = innerRouter, messageSender = messageSender, giveApprovalFeatureToggles = giveApprovalFeatureToggles, + stakingFeatureToggles = stakingFeatureToggles, appRouter = appRouter, ) } diff --git a/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformerTest.kt b/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformerTest.kt index b470fd8de2..10b2ec7971 100644 --- a/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformerTest.kt +++ b/features/staking/impl/src/test/kotlin/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountRequirementStateTransformerTest.kt @@ -70,6 +70,30 @@ internal class AmountRequirementStateTransformerTest { ) } + private fun solanaCryptoStatus(): CryptoCurrencyStatus = mockk(relaxed = true) { + every { currency.network.rawId } returns "solana" + } + + private fun solanaExitIntegration(exitMin: BigDecimal?, enterMin: BigDecimal? = null): StakingIntegration = + mockk { + every { exitMinimumAmount } returns exitMin + every { enterMinimumAmount } returns enterMin + every { exitArgs } returns null + } + + private fun solanaTransformer( + staked: BigDecimal, + exitMin: BigDecimal?, + enterMin: BigDecimal? = null, + enabled: Boolean = true, + ) = AmountRequirementStateTransformer( + cryptoCurrencyStatus = solanaCryptoStatus(), + maxAmount = EnterAmountBoundary(amount = staked, fiatAmount = null, fiatRate = null), + integration = solanaExitIntegration(exitMin = exitMin, enterMin = enterMin), + actionType = StakingActionCommonType.Exit(partiallyUnstakeDisabled = false), + isSolanaUnstakeValidationEnabled = enabled, + ) + @Test fun `WHEN amount exceeds positive maximum THEN max amount error string is used`() { val transformer = AmountRequirementStateTransformer( @@ -149,4 +173,143 @@ internal class AmountRequirementStateTransformerTest { assertThat((result.amountTextField.error as TextReference.Res).id) .isEqualTo(R.string.staking_max_amount_requirement_error) } + + @Test + fun `WHEN Solana full unstake THEN no error`() { + val transformer = solanaTransformer(staked = BigDecimal("5"), exitMin = BigDecimal("1")) + + val result = transformer.transform(amountState(BigDecimal("5"))) as AmountState.Data + + assertThat(result.amountTextField.isError).isFalse() + assertThat(result.isPrimaryButtonEnabled).isTrue() + } + + @Test + fun `WHEN Solana full unstake of small stake below minimum THEN no error`() { + val small = BigDecimal("0.098090754") + val transformer = solanaTransformer(staked = small, exitMin = BigDecimal("1")) + + val result = transformer.transform(amountState(small)) as AmountState.Data + + assertThat(result.amountTextField.isError).isFalse() + assertThat(result.isPrimaryButtonEnabled).isTrue() + } + + @Test + fun `WHEN Solana partial unstake below minimum THEN unstake min error and button disabled`() { + val transformer = solanaTransformer(staked = BigDecimal("5"), exitMin = BigDecimal("1")) + + val result = transformer.transform(amountState(BigDecimal("0.5"))) as AmountState.Data + + assertThat(result.amountTextField.isError).isTrue() + assertThat(result.isPrimaryButtonEnabled).isFalse() + assertThat((result.amountTextField.error as TextReference.Res).id) + .isEqualTo(R.string.staking_unstake_amount_requirement_error) + } + + @Test + fun `WHEN Solana partial unstake leaving remainder below minimum THEN low staked balance error and button disabled`() { + val transformer = solanaTransformer(staked = BigDecimal("5"), exitMin = BigDecimal("1")) + + val result = transformer.transform(amountState(BigDecimal("4.5"))) as AmountState.Data + + assertThat(result.amountTextField.isError).isTrue() + assertThat(result.isPrimaryButtonEnabled).isFalse() + assertThat((result.amountTextField.error as TextReference.Res).id) + .isEqualTo(R.string.staking_notification_low_staked_balance_text) + } + + @Test + fun `WHEN Solana partial unstake violating both minimums THEN unstake min error takes priority`() { + val transformer = solanaTransformer(staked = BigDecimal("1.5"), exitMin = BigDecimal("1")) + + val result = transformer.transform(amountState(BigDecimal("0.7"))) as AmountState.Data + + assertThat(result.amountTextField.isError).isTrue() + assertThat((result.amountTextField.error as TextReference.Res).id) + .isEqualTo(R.string.staking_unstake_amount_requirement_error) + } + + @Test + fun `WHEN Solana partial unstake with both parts above minimum THEN no error`() { + val transformer = solanaTransformer(staked = BigDecimal("5"), exitMin = BigDecimal("1")) + + val result = transformer.transform(amountState(BigDecimal("1.5"))) as AmountState.Data + + assertThat(result.amountTextField.isError).isFalse() + assertThat(result.isPrimaryButtonEnabled).isTrue() + } + + @Test + fun `WHEN Solana amount exactly at minimum THEN no error`() { + val transformer = solanaTransformer(staked = BigDecimal("5"), exitMin = BigDecimal("1")) + + val result = transformer.transform(amountState(BigDecimal("1"))) as AmountState.Data + + assertThat(result.amountTextField.isError).isFalse() + assertThat(result.isPrimaryButtonEnabled).isTrue() + } + + @Test + fun `WHEN Solana remainder exactly at minimum THEN no error`() { + val transformer = solanaTransformer(staked = BigDecimal("5"), exitMin = BigDecimal("1")) + + val result = transformer.transform(amountState(BigDecimal("4"))) as AmountState.Data + + assertThat(result.amountTextField.isError).isFalse() + } + + @Test + fun `WHEN Solana exit minimum is zero THEN falls back to enter minimum`() { + val transformer = solanaTransformer( + staked = BigDecimal("5"), + exitMin = BigDecimal.ZERO, + enterMin = BigDecimal("1"), + ) + + val result = transformer.transform(amountState(BigDecimal("0.5"))) as AmountState.Data + + assertThat(result.amountTextField.isError).isTrue() + assertThat((result.amountTextField.error as TextReference.Res).id) + .isEqualTo(R.string.staking_unstake_amount_requirement_error) + } + + @Test + fun `WHEN Solana both minimums null THEN partial unstake allowed`() { + val transformer = solanaTransformer(staked = BigDecimal("5"), exitMin = null, enterMin = null) + + val result = transformer.transform(amountState(BigDecimal("0.5"))) as AmountState.Data + + assertThat(result.amountTextField.isError).isFalse() + assertThat(result.isPrimaryButtonEnabled).isTrue() + } + + @Test + fun `WHEN Solana validation disabled THEN partial unstake below minimum allowed`() { + val transformer = solanaTransformer( + staked = BigDecimal("5"), + exitMin = BigDecimal("1"), + enabled = false, + ) + + val result = transformer.transform(amountState(BigDecimal("0.5"))) as AmountState.Data + + assertThat(result.amountTextField.isError).isFalse() + } + + @Test + fun `WHEN validation enabled but currency is not Solana THEN Solana rule does not apply`() { + val transformer = AmountRequirementStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, // relaxed mock: network.rawId is not "solana" + maxAmount = EnterAmountBoundary(amount = BigDecimal("5"), fiatAmount = null, fiatRate = null), + integration = exitIntegrationWith(minimum = null, maximum = null), + actionType = StakingActionCommonType.Exit(partiallyUnstakeDisabled = false), + isSolanaUnstakeValidationEnabled = true, + ) + + val result = transformer.transform(amountState(BigDecimal("0.5"))) as AmountState.Data + + // Non-Solana: falls through to legacy exitArgs path (minimum null → no error), NOT the Solana remainder rule. + assertThat(result.amountTextField.isError).isFalse() + } } \ No newline at end of file From 50ed36218178f2aa4a694ab5b2e7fad178211c24 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Jul 2026 19:35:12 +0500 Subject: [PATCH 04/21] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 39 +--- .../SwapInteractorImplFindBestQuoteTest.kt | 206 +++++++++++++++--- 2 files changed, 180 insertions(+), 65 deletions(-) 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 d50f8618cc..957e5188f0 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 @@ -62,7 +62,6 @@ import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* import com.tangem.utils.coroutines.runSuspendCatching -import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero import com.tangem.utils.logging.TangemLogger import jakarta.inject.Inject @@ -236,7 +235,6 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus = toSwapCurrencyStatus, provider = provider, amount = amount, - reduceBalanceBy = reduceBalanceBy, expressOperationType = ExpressOperationType.SWAP, ) } else { @@ -245,7 +243,6 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus = toSwapCurrencyStatus, provider = provider, amount = amount, - reduceBalanceBy = reduceBalanceBy, expressOperationType = ExpressOperationType.SWAP, ) } @@ -256,7 +253,6 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus = toSwapCurrencyStatus, provider = provider, amount = amount, - reduceBalanceBy = reduceBalanceBy, ) } } @@ -271,7 +267,6 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus: SwapCurrencyStatus, provider: SwapProvider, amount: SwapAmount, - reduceBalanceBy: BigDecimal, expressOperationType: ExpressOperationType, ): Pair { if (fromSwapCurrencyStatus.status.value.yieldSupplyStatus?.isActive == true) { @@ -301,7 +296,6 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus = toSwapCurrencyStatus, provider = provider, amount = amount, - reduceBalanceBy = reduceBalanceBy, ) } @@ -359,7 +353,6 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus: SwapCurrencyStatus, provider: SwapProvider, amount: SwapAmount, - reduceBalanceBy: BigDecimal, expressOperationType: ExpressOperationType, ): Pair { val maybeQuotes = repository.findBestQuote( @@ -381,7 +374,6 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus = toSwapCurrencyStatus, provider = provider, amount = amount, - reduceBalanceBy = reduceBalanceBy, ) } @@ -416,38 +408,21 @@ internal class SwapInteractorImpl @Inject constructor( toSwapCurrencyStatus: SwapCurrencyStatus, provider: SwapProvider, amount: SwapAmount, - reduceBalanceBy: BigDecimal, ): Pair { val fromToken = fromSwapCurrencyStatus.currency val toToken = toSwapCurrencyStatus.currency - val nativeBalance = walletManagersFacade.getNativeTokenBalance( - userWalletId = fromSwapCurrencyStatus.userWalletId, - networkId = fromToken.network.rawId, - derivationPath = fromSwapCurrencyStatus.currency.network.derivationPath.value, - ) - - val includeFeeInAmount = if (nativeBalance.isZero()) { - IncludeFeeInAmountInternal.Excluded - } else { - IncludeFeeInAmountInternal.Included( - SwapAmount(nativeBalance - reduceBalanceBy, fromToken.decimals), - ) - } - - val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmountInternal.Included) { - includeFeeInAmount.amountSubtractFee - } else { - amount - } - + // Always request the user-entered amount. The real balance/fee decision is deferred to the fee + // selector (`computeBalanceStatus` / `applySwapFee`), which correctly handles gasless (token) fee + // payment even when the native coin balance is zero. Do NOT derive the quote amount from the native + // balance here — that discards the entered amount ([REDACTED_TASK_KEY] regression: CEX always sent max). val quotes = repository.findBestQuote( userWallet = fromSwapCurrencyStatus.userWallet, fromContractAddress = fromToken.getContractAddress(), fromNetwork = fromToken.network.rawId, toContractAddress = toToken.getContractAddress(), toNetwork = toToken.network.rawId, - fromAmount = amountToRequest.toStringWithRightOffset(), + fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, toDecimals = toToken.decimals, providerId = provider.providerId, @@ -1388,8 +1363,8 @@ internal class SwapInteractorImpl @Inject constructor( * same-currency-token path: balance check on the from-token's own balance. * - Otherwise → native-fee branch via [getIncludeFeeInAmountForNative]. * - * Used both by [loadCexQuoteData] (with `feeValue = ZERO` at quote stage) and by - * [computeBalanceStatus] (with the actual fee once the selector resolves). + * Used by [computeBalanceStatus] with the actual fee once the fee selector resolves. The quote stage + * ([manageCex]) no longer consults this — it always requests the user-entered amount. */ private suspend fun getIncludeFeeInAmountInternal( fromSwapCurrencyStatus: SwapCurrencyStatus, diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt index ead54dc918..c29b33cdde 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/SwapInteractorImplFindBestQuoteTest.kt @@ -24,7 +24,6 @@ import com.tangem.feature.swap.domain.models.domain.SwapBalanceStatus import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.ui.SwapState import io.mockk.coEvery -import io.mockk.coVerify import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic @@ -721,18 +720,18 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( } /** - * `manageCex` no longer derives `includeFeeInAmount` through `getIncludeFeeInAmountInternal`. - * It now reads the native-coin balance directly: - * - native balance non-zero → request the whole `nativeBalance - reduceBalanceBy` as `fromAmount` - * - native balance zero → request the original swap `amount` - * The resulting quote balance status is always `Pending` (resolved later by the fee selector). + * [REDACTED_TASK_KEY]: the CEX quote stage must request the **user-entered** amount as `fromAmount`, regardless of + * the native-coin balance or `reduceBalanceBy`. A prior fix derived the quote amount from the native + * balance (`nativeBalance - reduceBalanceBy`), which discarded the entered amount and made CEX always + * quote the max balance (and, for tokens, sent the native balance under the token's decimals). The real + * balance/fee decision is deferred to the fee selector, so the quote status is always `Pending`. */ @Nested - inner class CexNativeBalanceAmount { + inner class CexQuoteAmount { @Test - fun `should request nativeBalance as fromAmount when native balance is non-zero`() = runTest { - // Given — native balance 10 (from base stub), decimals 18, reduceBalanceBy 0 + fun `should request the entered amount for a coin with non-zero native balance`() = runTest { + // Given — coin balance 10, native balance 10 (base stub); user enters 0.014 (the reported case) val cexProvider = buildSwapProvider(ExchangeProviderType.CEX) val fromStatus = buildSwapCurrencyStatus( networkRawId = ethNetwork, @@ -764,21 +763,21 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( fromSwapCurrencyStatus = fromStatus, toSwapCurrencyStatus = toStatus, providers = listOf(cexProvider), - amountToSwap = "1.0", + amountToSwap = "0.014", reduceBalanceBy = BigDecimal.ZERO, ) - // Then — fromAmount is the full native balance (10 * 1e18), not the "1.0" swap amount + // Then — fromAmount is the entered 0.014 (0.014 * 1e18), NOT the full balance assertThat(fromAmountSlot.isCaptured).isTrue() - assertThat(fromAmountSlot.captured).isEqualTo("10000000000000000000") + assertThat(fromAmountSlot.captured).isEqualTo("14000000000000000") assertThat(result[cexProvider]).isInstanceOf(SwapState.QuotesLoadedState::class.java) val loaded = (result[cexProvider] ?: error("state must not be null")) as SwapState.QuotesLoadedState assertThat(loaded.preparedSwapConfigState.balanceStatus).isEqualTo(SwapBalanceStatus.Pending) } @Test - fun `should subtract reduceBalanceBy from native balance when building fromAmount`() = runTest { - // Given — native balance 10, reduceBalanceBy 2 → fromAmount = 8 * 1e18 + fun `should ignore reduceBalanceBy when building the CEX quote fromAmount`() = runTest { + // Given — reduceBalanceBy must NOT affect the CEX quote amount anymore val cexProvider = buildSwapProvider(ExchangeProviderType.CEX) val fromStatus = buildSwapCurrencyStatus( networkRawId = ethNetwork, @@ -814,13 +813,13 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( reduceBalanceBy = BigDecimal("2"), ) - // Then - assertThat(fromAmountSlot.captured).isEqualTo("8000000000000000000") + // Then — still the entered 1.0 * 1e18, unaffected by reduceBalanceBy + assertThat(fromAmountSlot.captured).isEqualTo("1000000000000000000") } @Test - fun `should request the original swap amount as fromAmount when native balance is zero`() = runTest { - // Given — native balance ZERO → includeFeeInAmount Excluded → fromAmount = swap amount (1.0) + fun `should request the entered amount for a coin with zero native balance`() = runTest { + // Given — native balance ZERO must not block or override the entered amount val cexProvider = buildSwapProvider(ExchangeProviderType.CEX) val fromStatus = buildSwapCurrencyStatus( networkRawId = ethNetwork, @@ -857,24 +856,75 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( reduceBalanceBy = BigDecimal.ZERO, ) - // Then — 1.0 * 1e18, not the native balance + // Then — 1.0 * 1e18, status Pending assertThat(fromAmountSlot.captured).isEqualTo("1000000000000000000") val loaded = (result[cexProvider] ?: error("state must not be null")) as SwapState.QuotesLoadedState assertThat(loaded.preparedSwapConfigState.balanceStatus).isEqualTo(SwapBalanceStatus.Pending) } @Test - fun `should read native token balance for the from-token network`() = runTest { - // Given + fun `should request the entered token amount with token decimals for a token with non-zero native balance`() = + runTest { + // Given — token (6 decimals) balance 100, native ETH balance 10 (base stub); user enters 5. + // The quote must send 5 in token units, NOT the native balance under token decimals. + val cexProvider = buildSwapProvider(ExchangeProviderType.CEX) + val fromStatus = buildSwapCurrencyStatus( + networkRawId = ethNetwork, + contractAddress = "0xToken", + isCoin = false, + amount = BigDecimal("100"), + decimals = 6, + ) + val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) + val quoteModel = buildQuoteModel() + val fromAmountSlot = slot() + + coEvery { + repository.findBestQuote( + userWallet = any(), + fromContractAddress = any(), + fromNetwork = any(), + toContractAddress = any(), + toNetwork = any(), + fromAmount = capture(fromAmountSlot), + fromDecimals = any(), + toDecimals = any(), + providerId = cexProvider.providerId, + rateType = any(), + ) + } returns quoteModel.right() + + // When + val result = sut.findBestQuote( + fromSwapCurrencyStatus = fromStatus, + toSwapCurrencyStatus = toStatus, + providers = listOf(cexProvider), + amountToSwap = "5", + reduceBalanceBy = BigDecimal.ZERO, + ) + + // Then — 5 * 1e6 (token decimals), NOT 10 (native balance) + assertThat(fromAmountSlot.captured).isEqualTo("5000000") + val loaded = (result[cexProvider] ?: error("state must not be null")) as SwapState.QuotesLoadedState + assertThat(loaded.preparedSwapConfigState.balanceStatus).isEqualTo(SwapBalanceStatus.Pending) + } + + @Test + fun `should not block a token swap with zero native balance (gasless)`() = runTest { + // Given — the original [REDACTED_TASK_KEY] case: token with zero native (ETH) balance, gasless supported. val cexProvider = buildSwapProvider(ExchangeProviderType.CEX) val fromStatus = buildSwapCurrencyStatus( networkRawId = ethNetwork, - isCoin = true, - amount = BigDecimal("10"), + contractAddress = "0xToken", + isCoin = false, + amount = BigDecimal("100"), + decimals = 6, ) val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) val quoteModel = buildQuoteModel() + val fromAmountSlot = slot() + coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal.ZERO coEvery { repository.findBestQuote( userWallet = any(), @@ -882,7 +932,7 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( fromNetwork = any(), toContractAddress = any(), toNetwork = any(), - fromAmount = any(), + fromAmount = capture(fromAmountSlot), fromDecimals = any(), toDecimals = any(), providerId = cexProvider.providerId, @@ -891,22 +941,112 @@ internal class SwapInteractorImplFindBestQuoteTest : SwapInteractorImplTestBase( } returns quoteModel.right() // When - sut.findBestQuote( + val result = sut.findBestQuote( fromSwapCurrencyStatus = fromStatus, toSwapCurrencyStatus = toStatus, providers = listOf(cexProvider), - amountToSwap = "1.0", + amountToSwap = "5", reduceBalanceBy = BigDecimal.ZERO, ) - // Then — the CEX path resolves the fee-paying native balance for the from-token network - coVerify { - walletManagersFacade.getNativeTokenBalance( - userWalletId = any(), - networkId = ethNetwork, - derivationPath = any(), + // Then — entered token amount is quoted and status is Pending (not InsufficientAmount) + assertThat(fromAmountSlot.captured).isEqualTo("5000000") + val loaded = (result[cexProvider] ?: error("state must not be null")) as SwapState.QuotesLoadedState + assertThat(loaded.preparedSwapConfigState.balanceStatus).isEqualTo(SwapBalanceStatus.Pending) + } + + @Test + fun `should request the full entered balance for a coin when max is tapped`() = runTest { + // Given — "Max" sets the entered amount to the full coin balance (10). The native balance stub is + // deliberately different (3) so a regression to the old `nativeBalance - reduceBalanceBy` logic + // would flip the asserted value (3e18) instead of the entered 10e18. + val cexProvider = buildSwapProvider(ExchangeProviderType.CEX) + val fromStatus = buildSwapCurrencyStatus( + networkRawId = ethNetwork, + isCoin = true, + amount = BigDecimal("10"), + decimals = 18, + ) + val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) + val quoteModel = buildQuoteModel() + val fromAmountSlot = slot() + + coEvery { walletManagersFacade.getNativeTokenBalance(any(), any(), any()) } returns BigDecimal("3") + coEvery { + repository.findBestQuote( + userWallet = any(), + fromContractAddress = any(), + fromNetwork = any(), + toContractAddress = any(), + toNetwork = any(), + fromAmount = capture(fromAmountSlot), + fromDecimals = any(), + toDecimals = any(), + providerId = cexProvider.providerId, + rateType = any(), ) - } + } returns quoteModel.right() + + // When — user taps Max: entered amount == full coin balance + val result = sut.findBestQuote( + fromSwapCurrencyStatus = fromStatus, + toSwapCurrencyStatus = toStatus, + providers = listOf(cexProvider), + amountToSwap = "10", + reduceBalanceBy = BigDecimal.ZERO, + ) + + // Then — full entered balance 10 * 1e18, NOT the native balance (3); no quote-stage fee subtraction + assertThat(fromAmountSlot.captured).isEqualTo("10000000000000000000") + val loaded = (result[cexProvider] ?: error("state must not be null")) as SwapState.QuotesLoadedState + assertThat(loaded.preparedSwapConfigState.balanceStatus).isEqualTo(SwapBalanceStatus.Pending) + } + + @Test + fun `should request the full entered token balance when max is tapped`() = runTest { + // Given — token (6 decimals) balance 100, native ETH balance 10 (base stub). "Max" enters 100. + // native (10) naturally differs from the token balance (100), so a regression to the native-balance + // logic would send 10 (as "10000000") instead of the entered 100 (as "100000000"). + val cexProvider = buildSwapProvider(ExchangeProviderType.CEX) + val fromStatus = buildSwapCurrencyStatus( + networkRawId = ethNetwork, + contractAddress = "0xToken", + isCoin = false, + amount = BigDecimal("100"), + decimals = 6, + ) + val toStatus = buildSwapCurrencyStatus(networkRawId = btcNetwork) + val quoteModel = buildQuoteModel() + val fromAmountSlot = slot() + + coEvery { + repository.findBestQuote( + userWallet = any(), + fromContractAddress = any(), + fromNetwork = any(), + toContractAddress = any(), + toNetwork = any(), + fromAmount = capture(fromAmountSlot), + fromDecimals = any(), + toDecimals = any(), + providerId = cexProvider.providerId, + rateType = any(), + ) + } returns quoteModel.right() + + // When — user taps Max: entered amount == full token balance + val result = sut.findBestQuote( + fromSwapCurrencyStatus = fromStatus, + toSwapCurrencyStatus = toStatus, + providers = listOf(cexProvider), + amountToSwap = "100", + reduceBalanceBy = BigDecimal.ZERO, + ) + + // Then — full entered token balance 100 * 1e6, NOT the native balance (10) + assertThat(fromAmountSlot.captured).isEqualTo("100000000") + val loaded = (result[cexProvider] ?: error("state must not be null")) as SwapState.QuotesLoadedState + assertThat(loaded.preparedSwapConfigState.balanceStatus).isEqualTo(SwapBalanceStatus.Pending) } } From 276a28800e28a995f2b946d8759fa339c13bdc0a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Jul 2026 23:42:13 +0500 Subject: [PATCH 05/21] Updated on 2026-08-14 --- .../choosetoken/market/state/SwapMarketCategory.kt | 10 +++------- .../impl/choosetoken/model/MarketBlockDelegate.kt | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/market/state/SwapMarketCategory.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/market/state/SwapMarketCategory.kt index 5e480142ac..926aeecb47 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/market/state/SwapMarketCategory.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/market/state/SwapMarketCategory.kt @@ -16,13 +16,9 @@ internal enum class SwapMarketCategory( val title: TextReference, val order: TokenMarketListConfig.Order, ) { - Trending( - title = resourceReference(R.string.markets_sort_by_trending_title), - order = TokenMarketListConfig.Order.Trending, - ), - ExperiencedBuyers( - title = resourceReference(R.string.markets_sort_by_experienced_buyers_title), - order = TokenMarketListConfig.Order.Buyers, + MarketCap( + title = resourceReference(R.string.markets_sort_by_rating_title), + order = TokenMarketListConfig.Order.ByRating, ), TopGainers( title = resourceReference(R.string.markets_sort_by_top_gainers_title), diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt index 15fffa4ab4..b5f33f004b 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/model/MarketBlockDelegate.kt @@ -51,7 +51,7 @@ internal class MarketBlockDelegate @AssistedInject constructor( private val visibleMarketItemIds = MutableStateFlow>(emptyList()) private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) - private val selectedCategoryFlow = MutableStateFlow(SwapMarketCategory.Trending) + private val selectedCategoryFlow = MutableStateFlow(SwapMarketCategory.MarketCap) val addToPortfolioSlot: SlotNavigation = SlotNavigation() val addToPortfolioManager: AddToPortfolioManager = addToPortfolioManagerFactory.create( From 14726d2fb4a4626cf0ff559dfef274e1a57211dd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Jul 2026 14:16:41 +0300 Subject: [PATCH 06/21] Updated on 2026-08-14 --- .../presentation/tokendetails/model/TokenDetailsModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 48d4cc262f..f4b55056ba 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -558,9 +558,9 @@ internal class TokenDetailsModel @Inject constructor( override fun onTransferClick() { val amount = cryptoCurrencyStatus?.value?.amount if (amount == null || amount.signum() <= 0) { - uiMessageSender.send( - message = SnackbarMessage( - message = resourceReference(R.string.token_button_unavailability_reason_empty_balance_send), + handleUnavailabilityReason( + unavailabilityReason = ScenarioUnavailabilityReason.EmptyBalance( + withdrawalScenario = ScenarioUnavailabilityReason.WithdrawalScenario.SEND, ), ) return From 5b031d5c0ef7b04803559fa7cc688f567cfa96ef Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 13:24:17 +0500 Subject: [PATCH 07/21] Updated on 2026-08-14 --- .../common/ui/account/PortfolioSelectRow.kt | 15 + core/res/src/main/res/values/strings.xml | 1 + .../AddToPortfolioBottomSheet.kt | 8 +- .../DefaultAddToPortfolioComponent.kt | 2 +- .../model/AddToPortfolioModel.kt | 4 +- .../model/AddToPortfolioRouteUiSpec.kt | 5 +- .../model/AddToPortfolioRoutes.kt | 2 +- .../addtoportfolio/model/AddTokenModel.kt | 14 +- .../addtoportfolio/model/AddTokenUiBuilder.kt | 13 +- .../converter/ChooseTokenListItemConverter.kt | 12 +- .../impl/choosetoken/ui/ChooseTokenScreen.kt | 332 +++++++++++++++++- .../DefaultManageFundsComponent.kt | 24 +- .../managefunds/model/ManageFundsModel.kt | 17 +- .../model/ManageFundsRouteUiSpec.kt | 2 +- .../model/TokenActionsUiBuilder.kt | 67 ++-- 15 files changed, 448 insertions(+), 70 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt index 9dd905225f..2e6d8cc553 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt @@ -20,6 +20,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.common.ui.R +import com.tangem.common.ui.userwallet.CardImage +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.ui.components.SpacerW12 import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.ds.row.TangemRowContainer @@ -111,6 +113,18 @@ fun PortfolioSelectRowV2( size = AccountIconSize.RedesignedDefault, ) } + } else if (state.imageState != null) { + Box( + modifier = Modifier + .layoutId(TangemRowLayoutId.HEAD) + .padding(end = TangemTheme.dimens2.x3), + contentAlignment = Alignment.Center, + ) { + CardImage( + modifier = Modifier.size(TangemTheme.dimens2.x10), + imageState = state.imageState, + ) + } } Row( @@ -156,6 +170,7 @@ data class PortfolioSelectUM( val isAccountMode: Boolean, val isMultiChoice: Boolean, val onClick: () -> Unit, + val imageState: UserWalletItemUM.ImageState? = null, ) @Preview(widthDp = 360, showBackground = true) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b72b7efbc3..a2da937bc0 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -727,6 +727,7 @@ An error occurred An error occurred. Code: %s. Requires memo + Get %1$s Transaction By approving, you allow the smart contract to use your tokens in future transactions. Amount %s diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt index 3f2288d1f4..e1d4f00182 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt @@ -18,6 +18,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent import com.tangem.features.commonfeatures.impl.R @@ -69,7 +70,7 @@ internal fun AddToPortfolioBottomSheet( AddToPortfolioRoutes.AddToken, AddToPortfolioRoutes.Empty, is AddToPortfolioRoutes.NetworkSelector, - AddToPortfolioRoutes.TokenActions, + is AddToPortfolioRoutes.TokenActions, -> true } if (isScrollableContent) { @@ -92,11 +93,12 @@ private fun AddToPortfolioBottomSheetTitle( onBackClick: () -> Unit, modifier: Modifier = Modifier, ) { - val title: TextReference = when (stack.active.configuration) { + val title: TextReference = when (val config = stack.active.configuration) { AddToPortfolioRoutes.AddToken -> resourceReference(R.string.common_add_token) AddToPortfolioRoutes.Empty -> TextReference.EMPTY is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network) - AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token) + is AddToPortfolioRoutes.TokenActions -> + resourceReference(R.string.get_token_title, wrappedList(config.currencyName)) AddToPortfolioRoutes.UserPortfolio -> resourceReference(R.string.markets_portfolio_block_title) AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent) .title.collectAsStateWithLifecycle().value diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt index 6c9970ee38..d3652b2d02 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt @@ -104,7 +104,7 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor( ): ComposableContentComponent = when (config) { AddToPortfolioRoutes.AddToken -> addTokenComponent AddToPortfolioRoutes.PortfolioSelector -> portfolioSelectorComponent - AddToPortfolioRoutes.TokenActions -> tokenActionsComponent + is AddToPortfolioRoutes.TokenActions -> tokenActionsComponent AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY AddToPortfolioRoutes.UserPortfolio -> createUserPortfolioComponent(componentContext) is AddToPortfolioRoutes.NetworkSelector -> chooseNetworkComponentFactory.create( 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 1cc4c726fb..5369f159bc 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 @@ -305,7 +305,9 @@ internal class AddToPortfolioModel @Inject constructor( setupTokenActionsFlow(selectedPortfolioSnapshot, addedToken) .onEach { cryptoCurrencyData -> tokenActionsData.emit(cryptoCurrencyData) - navigation.replaceAll(AddToPortfolioRoutes.TokenActions) + navigation.replaceAll( + AddToPortfolioRoutes.TokenActions(cryptoCurrencyData.status.currency.name), + ) } .onEmpty { finishSuccessFlow(result) } .launchIn(this) diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRouteUiSpec.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRouteUiSpec.kt index 68d861ed3a..4a2311cea0 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRouteUiSpec.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRouteUiSpec.kt @@ -2,6 +2,7 @@ package com.tangem.features.commonfeatures.impl.addtoportfolio.model import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.features.commonfeatures.impl.R internal data class AddToPortfolioRouteUiSpec( @@ -42,8 +43,8 @@ internal fun AddToPortfolioRoutes.uiSpec(): AddToPortfolioRouteUiSpec = when (th shouldApplyHorizontalPadding = false, footer = AddToPortfolioFooterKind.UserPortfolioAdd, ) - AddToPortfolioRoutes.TokenActions -> AddToPortfolioRouteUiSpec( - title = resourceReference(R.string.common_get_token), + is AddToPortfolioRoutes.TokenActions -> AddToPortfolioRouteUiSpec( + title = resourceReference(R.string.get_token_title, wrappedList(currencyName)), isScrollable = false, shouldApplyHorizontalPadding = true, footer = AddToPortfolioFooterKind.None, diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt index 8c01391c47..80e3cf7632 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt @@ -27,5 +27,5 @@ internal sealed interface AddToPortfolioRoutes : Route { data object UserPortfolio : AddToPortfolioRoutes @Serializable - data object TokenActions : AddToPortfolioRoutes + data class TokenActions(val currencyName: String) : AddToPortfolioRoutes } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenModel.kt index 7b7facbc0e..871aa665ed 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenModel.kt @@ -17,18 +17,23 @@ import com.tangem.features.commonfeatures.api.addtoportfolio.SelectedPortfolio import com.tangem.features.commonfeatures.impl.R import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddTokenUiBuilder.Companion.toggleProgress +import com.tangem.features.wallet.utils.UserWalletImageFetcher import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.operations.attestation.ArtworkSize import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @ModelScoped @Suppress("LongParameterList") +@OptIn(ExperimentalCoroutinesApi::class) internal class AddTokenModel @Inject constructor( paramsContainer: ParamsContainer, + private val walletImageFetcher: UserWalletImageFetcher, private val uiBuilder: AddTokenUiBuilder, private val messageSender: UiMessageSender, private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, @@ -46,15 +51,22 @@ internal class AddTokenModel @Inject constructor( field = MutableStateFlow(value = null) init { + val walletImageFlow = params.selectedPortfolio + .map { it.userWallet } + .distinctUntilChanged() + .flatMapLatest { walletImageFetcher.walletImage(it, ArtworkSize.SMALL) } + combine( flow = params.selectedNetwork.distinctUntilChanged(), flow2 = params.selectedPortfolio.distinctUntilChanged(), - transform = { selectedNetwork, selectedPortfolio -> + flow3 = walletImageFlow, + transform = { selectedNetwork, selectedPortfolio, walletImage -> addTokenJob.join() val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio) uiBuilder.updateContent( selectedPortfolio = selectedPortfolio, selectedNetwork = selectedNetwork, + walletImage = walletImage, isTangemIconVisible = isTangemIconVisible, onConfirmClick = { onAddClick(selectedNetwork, selectedPortfolio).saveIn(addTokenJob) }, ) diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenUiBuilder.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenUiBuilder.kt index fc6d0e2afd..662e9f6754 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenUiBuilder.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenUiBuilder.kt @@ -5,6 +5,7 @@ import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.PortfolioSelectUM import com.tangem.common.ui.account.toUM import com.tangem.common.ui.addtoken.AddTokenUM +import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.ParamsContainer import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter @@ -35,13 +36,18 @@ internal class AddTokenUiBuilder @Inject constructor( ) } - private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM { + private fun createPortfolio( + selectedPortfolio: SelectedPortfolio, + walletImage: UserWalletItemUM.ImageState, + ): PortfolioSelectUM { val accountIcon: AccountIconUM? val portfolioName: TextReference + val imageState: UserWalletItemUM.ImageState? when (selectedPortfolio.isAccountMode) { false -> { accountIcon = null portfolioName = stringReference(selectedPortfolio.userWallet.name) + imageState = walletImage } true -> { val accountStatus = selectedPortfolio.account.account @@ -50,6 +56,7 @@ internal class AddTokenUiBuilder @Inject constructor( is CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) is Payment -> AccountIconUM.Payment } + imageState = null } } return PortfolioSelectUM( @@ -58,12 +65,14 @@ internal class AddTokenUiBuilder @Inject constructor( isAccountMode = selectedPortfolio.isAccountMode, isMultiChoice = selectedPortfolio.isAvailableMorePortfolio, onClick = { params.callbacks.onChangePortfolioClick() }, + imageState = imageState, ) } fun updateContent( selectedPortfolio: SelectedPortfolio, selectedNetwork: SelectedNetwork, + walletImage: UserWalletItemUM.ImageState, isTangemIconVisible: Boolean, onConfirmClick: () -> Unit, ): AddTokenUM { @@ -78,7 +87,7 @@ internal class AddTokenUiBuilder @Inject constructor( onConfirmClick = onConfirmClick, ) val networkUM = createNetwork(selectedNetwork) - val portfolioUM = createPortfolio(selectedPortfolio) + val portfolioUM = createPortfolio(selectedPortfolio, walletImage) val currency = selectedNetwork.cryptoCurrency val tokenToAdd = TokenItemState.Content( id = currency.id.value, diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/converter/ChooseTokenListItemConverter.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/converter/ChooseTokenListItemConverter.kt index 3ca744cb50..84238b8387 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/converter/ChooseTokenListItemConverter.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/converter/ChooseTokenListItemConverter.kt @@ -8,7 +8,6 @@ import com.tangem.common.ui.tokens.TokenItemGrouping.toGroupedItems import com.tangem.common.ui.tokens.TokenItemGrouping.toUngroupedItems import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.icons.IconTint import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM @@ -55,12 +54,11 @@ internal class ChooseTokenListItemConverter( } private val fiatAmountStateProvider: ((TotalFiatBalance, isExpanded: Boolean) -> FiatAmountState?) = - { totalBalance, isExpanded -> - when { - isSearchingState -> FiatAmountState.Empty - !isExpanded -> FiatAmountState.Icon(R.drawable.ic_chewron_down_20, IconTint.Informative) - else -> AccountCryptoPortfolioItemStateConverter - .createFiatAmountState(totalBalance, appCurrency) + { totalBalance, _ -> + if (isSearchingState) { + FiatAmountState.Empty + } else { + AccountCryptoPortfolioItemStateConverter.createFiatAmountState(totalBalance, appCurrency) } } diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt index a9af5504aa..65166caad5 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/choosetoken/ui/ChooseTokenScreen.kt @@ -1,5 +1,16 @@ package com.tangem.features.commonfeatures.impl.choosetoken.ui +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.BoundsTransform +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.animation.core.snap +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -7,6 +18,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -15,16 +27,21 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.lerp import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp -import com.tangem.common.ui.tokens.portfolioTokensList +import com.tangem.common.ui.tokens.NonContentItemContent +import com.tangem.common.ui.tokens.SlideInItemVisibility import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.components.account.toBoxSize import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.SearchBar @@ -34,6 +51,8 @@ import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.token.AccountItemPreviewData import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.NON_CONTENT_TOKENS_LIST_KEY +import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem import com.tangem.core.ui.components.tokenlist.TokenListItem import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM @@ -41,26 +60,42 @@ import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.ds.image.TangemDeviceIcon +import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.header.TangemHeaderRow +import com.tangem.core.ui.ds.row.internal.TangemRowTailUM +import com.tangem.core.ui.ds.row.token.TangemTokenRow +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.ds.row.token.internal.TokenRowTitle import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.BuyTokenScreenTestTags +import com.tangem.core.ui.utils.ProvideSharedTransitionScope import com.tangem.core.ui.utils.TangemSharedTransitionLayout import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection +import com.tangem.core.ui.utils.sharedBoundsSafely import com.tangem.features.commonfeatures.api.choosetoken.model.ChooseTokenPortfolioFullBlockUM import com.tangem.features.commonfeatures.api.choosetoken.model.TokenListUMData import com.tangem.features.commonfeatures.api.choosetoken.model.WalletListUM import com.tangem.features.commonfeatures.api.choosetoken.model.WalletTabUM import com.tangem.features.commonfeatures.impl.R import com.tangem.features.commonfeatures.impl.choosetoken.market.state.SwapMarketState +import com.tangem.utils.StringsSigns.DOT import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlin.random.Random private const val LOAD_MORE_BUFFER = 25 +private const val ACCOUNT_COLLAPSE_STEP_MS = 50 +private const val ACCOUNT_COLLAPSE_MAX_DELAY_MS = 250 +private const val ACCOUNT_COLLAPSE_BASE_DELAY_MS = 150 +private const val ACCOUNT_CONTENT_ANIM_MS = 350 +private const val ACCOUNT_CONTENT_ANIM_DELAY_MS = 90 +private const val ACCOUNT_BOUNDS_ANIM_MS = 250 private val ChooseTokenFullUM.isNotFoundState: Boolean get() { @@ -298,11 +333,10 @@ private fun LazyListScope.tokensListItems(tokensListData: TokenListUMData, isBal when (tokensListData) { is TokenListUMData.AccountList -> { tokensListData.tokensList.forEachIndexed { index, item -> - portfolioTokensList( + accountWithTokens( portfolio = item, portfolioIndex = index, isBalanceHidden = isBalanceHidden, - testTag = BuyTokenScreenTestTags.LAZY_LIST_ITEM, ) } } @@ -338,6 +372,298 @@ private fun LazyListScope.tokensList(items: ImmutableList, isB ) } +@Suppress("LongMethod") +private fun LazyListScope.accountWithTokens( + portfolio: TokensListItemUM.Portfolio, + portfolioIndex: Int, + isBalanceHidden: Boolean, +) { + val tokens = portfolio.tokens + val isExpanded = portfolio.isExpanded + val lastIndex = maxOf(tokens.lastIndex.inc(), 1) + + item(key = "account-${portfolio.id}", contentType = "choose-token-account") { + val effectiveLastIndex by animateIntAsState( + targetValue = if (isExpanded) lastIndex else 0, + animationSpec = if (isExpanded) { + snap() + } else { + snap( + delayMillis = minOf( + ACCOUNT_COLLAPSE_STEP_MS * maxOf(tokens.lastIndex, 0), + ACCOUNT_COLLAPSE_MAX_DELAY_MS, + ) + ACCOUNT_COLLAPSE_BASE_DELAY_MS, + ) + }, + label = "accountLastIndex", + ) + AccountRow( + portfolio = portfolio, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) + .semantics { lazyListItemPosition = portfolioIndex } + .roundedShapeItemDecoration( + currentIndex = 0, + radius = TangemTheme.dimens.radius14, + lastIndex = effectiveLastIndex, + backgroundColor = TangemTheme.colors.background.primary, + ), + ) + } + + if (portfolio.content is PortfolioItemContentUM.Empty) { + item(key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}") { + SlideInItemVisibility( + visible = isExpanded, + currentIndex = 1, + lastIndex = lastIndex, + modifier = Modifier.roundedShapeItemDecoration( + currentIndex = 1, + radius = TangemTheme.dimens.radius14, + lastIndex = 1, + backgroundColor = TangemTheme.colors.background.primary, + ), + ) { + NonContentItemContent(modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16)) + } + } + return + } + + itemsIndexed( + items = tokens, + key = { _, token -> "${token.id}-choose-account-${portfolio.id}" }, + contentType = { _, token -> token::class.java }, + ) { tokenIndex, token -> + val indexWithHeader = tokenIndex.inc() + val lastTokenBottomPadding = TangemTheme.dimens.spacing8 + SlideInItemVisibility( + visible = isExpanded, + currentIndex = tokenIndex, + lastIndex = lastIndex, + modifier = Modifier + .testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) + .roundedShapeItemDecoration( + currentIndex = indexWithHeader, + radius = TangemTheme.dimens.radius14, + lastIndex = lastIndex, + backgroundColor = TangemTheme.colors.background.primary, + ), + ) { + PortfolioTokensListItem( + state = token, + isBalanceHidden = isBalanceHidden, + modifier = Modifier.conditional(indexWithHeader == lastIndex) { + padding(bottom = lastTokenBottomPadding) + }, + ) + } + } +} + +@Suppress("LongMethod") +@OptIn(ExperimentalSharedTransitionApi::class) +@Composable +private fun AccountRow( + portfolio: TokensListItemUM.Portfolio, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + val tokenRowUM = accountTokenRowUM(portfolio) + val subtitle = (tokenRowUM.subtitleUM as? TangemTokenRowUM.SubtitleUM.Content)?.text + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + ProvideSharedTransitionScope(Modifier.weight(1f)) { + val iconSharedContentState = rememberSharedContentState(key = "account-icon-${portfolio.id}") + val titleSharedContentState = rememberSharedContentState(key = "account-title-${portfolio.id}") + val boundsTransform = BoundsTransform { _, _ -> tween(ACCOUNT_BOUNDS_ANIM_MS) } + + AnimatedContent( + targetState = portfolio.isExpanded, + transitionSpec = { + fadeIn(animationSpec = tween(ACCOUNT_CONTENT_ANIM_MS, delayMillis = ACCOUNT_CONTENT_ANIM_DELAY_MS)) + .togetherWith(fadeOut(animationSpec = tween(ACCOUNT_CONTENT_ANIM_MS))) + }, + label = "accountExpand", + ) { isExpandedState -> + val animatedContentScope = this + val composables = remember(tokenRowUM) { + AccountRowComposables( + icon = { iconModifier -> + val iconSize = if (isExpandedState) { + AccountIconSize.RedesignExtraSmall + } else { + AccountIconSize.RedesignedDefault + } + val sizedIcon = when (val icon = tokenRowUM.headIconUM) { + is TangemIconUM.Currency -> icon.copy( + currencyIconState = when (val iconState = icon.currencyIconState) { + is CurrencyIconState.CryptoPortfolio.Icon -> iconState.copy(size = iconSize) + is CurrencyIconState.CryptoPortfolio.Letter -> iconState.copy(size = iconSize) + else -> iconState + }, + ) + else -> icon + } + TangemIcon( + tangemIconUM = sizedIcon, + modifier = iconModifier + .size(iconSize.toBoxSize()) + .sharedBoundsSafely( + sharedContentState = iconSharedContentState, + animatedVisibilityScope = animatedContentScope, + boundsTransform = boundsTransform, + ), + ) + }, + title = { titleModifier -> + val targetFraction = if (isExpandedState) 0f else 1f + val animationFraction = animateFloatAsState( + targetValue = targetFraction, + animationSpec = tween(durationMillis = ACCOUNT_CONTENT_ANIM_MS), + label = "accountTitle", + ) + val startStyle = TangemTheme.typography2.captionSemibold12 + val stopStyle = TangemTheme.typography2.bodySemibold16 + val textStyle by remember(animationFraction.value) { + derivedStateOf { lerp(startStyle, stopStyle, animationFraction.value) } + } + val resizedTitle = when (val titleUM = tokenRowUM.titleUM) { + is TangemTokenRowUM.TitleUM.Content -> titleUM.copy( + text = styledStringReference( + titleUM.text.resolveReference(), + { textStyle.toSpanStyle() }, + ), + ) + else -> titleUM + } + TokenRowTitle( + titleUM = resizedTitle, + modifier = titleModifier.sharedBoundsSafely( + sharedContentState = titleSharedContentState, + animatedVisibilityScope = animatedContentScope, + boundsTransform = boundsTransform, + resizeMode = scaleToBounds(ContentScale.Fit, Alignment.CenterStart), + ), + ) + }, + ) + } + + if (isExpandedState) { + TangemHeaderRow( + subtitle = subtitle, + isBalanceHidden = isBalanceHidden, + titleContent = composables.title, + headContent = composables.icon, + tailUM = TangemRowTailUM.Empty, + onItemClick = tokenRowUM.onItemClick, + ) + } else { + TangemTokenRow( + tokenRowUM = tokenRowUM, + isBalanceHidden = isBalanceHidden, + headComponent = composables.icon, + titleComponent = composables.title, + ) + } + } + } + AccountTail( + isExpanded = portfolio.isExpanded, + onClick = { tokenRowUM.onItemClick?.invoke() }, + ) + } +} + +@Composable +private fun AccountTail(isExpanded: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .padding(start = TangemTheme.dimens2.x2, end = TangemTheme.dimens2.x4) + .size(TangemTheme.dimens2.x9) + .clip(CircleShape) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + AnimatedContent( + targetState = isExpanded, + contentAlignment = Alignment.Center, + label = "accountTail", + ) { expanded -> + if (expanded) { + Icon( + modifier = Modifier + .offset(x = TangemTheme.dimens.spacing2) + .size(TangemTheme.dimens2.x4), + painter = painterResource(id = R.drawable.ic_minimize_24), + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + contentDescription = null, + ) + } else { + AccountExpandButton() + } + } + } +} + +@Composable +private fun AccountExpandButton(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(TangemTheme.dimens2.x9) + .clip(CircleShape) + .background(TangemTheme.colors2.button.backgroundSecondary), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens2.x5), + painter = painterResource(id = R.drawable.ic_chewron_down_20), + tint = TangemTheme.colors2.button.iconPrimary, + contentDescription = null, + ) + } +} + +@Stable +private class AccountRowComposables( + val title: @Composable (Modifier) -> Unit, + val icon: @Composable (Modifier) -> Unit, +) + +private fun accountTokenRowUM(portfolio: TokensListItemUM.Portfolio): TangemTokenRowUM.Content { + val account = portfolio.tokenItemUM + val name = (account.titleState as? TokenItemState.TitleState.Content)?.text ?: TextReference.EMPTY + val tokensCount = (account.subtitleState as? TokenItemState.SubtitleState.TextContent)?.value + val balance = (account.fiatAmountState as? TokenItemState.FiatAmountState.Content)?.text + return TangemTokenRowUM.Content( + id = portfolio.id, + headIconUM = TangemIconUM.Currency(currencyIconState = account.iconState), + titleUM = TangemTokenRowUM.TitleUM.Content(text = name), + subtitleUM = buildAccountSubtitle(tokensCount, balance) + ?.let { TangemTokenRowUM.SubtitleUM.Content(text = it) } + ?: TangemTokenRowUM.SubtitleUM.Empty, + topEndContentUM = TangemTokenRowUM.EndContentUM.Empty, + bottomEndContentUM = TangemTokenRowUM.EndContentUM.Empty, + tailUM = TangemRowTailUM.Empty, + onItemClick = { account.onItemClick?.invoke(account) }, + onItemLongClick = null, + ) +} + +private fun buildAccountSubtitle(tokensCount: TextReference?, balance: String?): TextReference? { + return when { + tokensCount != null && balance != null -> + combinedReference(tokensCount, stringReference(" $DOT "), stringReference(balance)) + tokensCount != null -> tokensCount + balance != null -> stringReference(balance) + else -> null + } +} + private fun LazyListScope.emptyTokensList(modifier: Modifier = Modifier) { item("EmptyTokensList") { Box( diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/DefaultManageFundsComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/DefaultManageFundsComponent.kt index 8354a085f3..b2ae5d9dda 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/DefaultManageFundsComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/DefaultManageFundsComponent.kt @@ -30,7 +30,6 @@ import com.tangem.common.ui.markets.action.TokenActionsContext import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent import com.tangem.features.commonfeatures.impl.userportfolio.UserPortfolioComponent import com.tangem.features.commonfeatures.impl.R -import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -42,15 +41,12 @@ internal class DefaultManageFundsComponent @AssistedInject constructor( chooseTokenComponentFactory: ChooseTokenComponent.Factory, tokenActionsComponentFactory: TokenActionsComponent.Factory, userPortfolioComponentFactory: UserPortfolioComponent.Factory, - walletFeatureToggles: WalletFeatureToggles, ) : AppComponentContext by appComponentContext, ManageFundsComponent { private val model: ManageFundsModel = getOrCreateModel(params) private val isCompactTokenActions: Boolean = params.launchMode is ManageFundsComponent.LaunchMode.TokenActionsOnly - private val isAddFundsStage1Enabled: Boolean = walletFeatureToggles.isAddFundsStage1Enabled - private val tokenActionsComponent: TokenActionsComponent by lazy { tokenActionsComponentFactory.create( context = child(key = "manageFundsTokenActions"), @@ -107,7 +103,7 @@ internal class DefaultManageFundsComponent @AssistedInject constructor( ) } - WithOptionalRedesignTheme(isEnabled = isAddFundsStage1Enabled) { + TangemThemeRedesign { TangemBottomSheet( onBack = if (canGoBack) model::onBack else ::dismiss, config = TangemBottomSheetConfig( @@ -179,7 +175,7 @@ internal class DefaultManageFundsComponent @AssistedInject constructor( ) { userPortfolioComponent.Content(modifier) } - ManageFundsModel.UiRoute.TokenActions -> tokenActionsComponent.Content(modifier) + is ManageFundsModel.UiRoute.TokenActions -> tokenActionsComponent.Content(modifier) } } @@ -191,8 +187,13 @@ internal class DefaultManageFundsComponent @AssistedInject constructor( onCloseClick: () -> Unit, ) { val spec = route.uiSpec(model.flowType) + val title = if (route is ManageFundsModel.UiRoute.TokenActions) { + route.title + } else { + spec.title + } TangemTopBar( - title = spec.title, + title = title, subtitle = spec.subtitle, type = TangemTopBarType.BottomSheet, startContent = if (canGoBack) { @@ -219,15 +220,6 @@ internal class DefaultManageFundsComponent @AssistedInject constructor( ) } - @Composable - private fun WithOptionalRedesignTheme(isEnabled: Boolean, content: @Composable () -> Unit) { - if (isEnabled) { - TangemThemeRedesign(content = content) - } else { - content() - } - } - @AssistedFactory interface Factory : ManageFundsComponent.Factory { override fun create( diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt index 71f307963d..a1178676e5 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt @@ -10,6 +10,9 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler 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.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase @@ -26,6 +29,7 @@ import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenAnalyticsPa import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult import com.tangem.features.commonfeatures.api.tokenactions.BottomAction +import com.tangem.features.commonfeatures.impl.R import com.tangem.features.commonfeatures.impl.managefunds.analytics.ManageFundsAnalyticsEvent import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent import com.tangem.features.commonfeatures.impl.userportfolio.state.UserPortfolioStateController @@ -206,7 +210,7 @@ internal class ManageFundsModel @Inject constructor( return@launch } tokenActionsTrigger.value = TokenActionsRequest(wallet, match.first, match.second) - replaceRoot(UiRoute.TokenActions) + replaceRoot(tokenActionsRoute(match.second)) } } @@ -218,7 +222,7 @@ internal class ManageFundsModel @Inject constructor( 1 -> { val entry = entries.first() tokenActionsTrigger.value = TokenActionsRequest(entry.userWallet, entry.account, entry.status) - replaceRoot(UiRoute.TokenActions) + replaceRoot(tokenActionsRoute(entry.status)) } else -> { filteredEntries.value = entries @@ -261,7 +265,12 @@ internal class ManageFundsModel @Inject constructor( private fun openTokenActions(request: TokenActionsRequest, bottomAction: BottomAction) { tokenActionsTrigger.value = request currentBottomAction.value = bottomAction - pushRoute(UiRoute.TokenActions) + pushRoute(tokenActionsRoute(request.status)) + } + + private fun tokenActionsRoute(status: CryptoCurrencyStatus): UiRoute.TokenActions { + val title = resourceReference(R.string.get_token_title, wrappedList(status.currency.name)) + return UiRoute.TokenActions(title = title) } private fun replaceRoot(route: UiRoute) { @@ -277,7 +286,7 @@ internal class ManageFundsModel @Inject constructor( data object Loading : UiRoute data object ChooseToken : UiRoute data object UserPortfolio : UiRoute - data object TokenActions : UiRoute + data class TokenActions(val title: TextReference) : UiRoute } private data class TokenActionsRequest( diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsRouteUiSpec.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsRouteUiSpec.kt index 7fd98aecd9..a9911c0b98 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsRouteUiSpec.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsRouteUiSpec.kt @@ -33,7 +33,7 @@ internal fun ManageFundsModel.UiRoute.uiSpec(flowType: ManageFundsComponent.Flow shouldApplyHorizontalPadding = false, shouldFillHeight = false, ) - ManageFundsModel.UiRoute.TokenActions -> ManageFundsRouteUiSpec( + is ManageFundsModel.UiRoute.TokenActions -> ManageFundsRouteUiSpec( title = resourceReference(if (isTransfer) R.string.common_transfer else R.string.common_get_token), subtitle = null, shouldApplyHorizontalPadding = true, diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/model/TokenActionsUiBuilder.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/model/TokenActionsUiBuilder.kt index cc1ec83458..e6ad94e41a 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/model/TokenActionsUiBuilder.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/model/TokenActionsUiBuilder.kt @@ -31,6 +31,7 @@ import com.tangem.features.commonfeatures.impl.R import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.wallets.usecase.GetWalletIconUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent import com.tangem.features.commonfeatures.impl.tokenactions.ui.state.PortfolioBadgeUM import com.tangem.features.commonfeatures.impl.tokenactions.ui.state.TokenActionsUM @@ -42,6 +43,7 @@ internal class TokenActionsUiBuilder @Inject constructor( paramsContainer: ParamsContainer, private val designFeatureToggles: DesignFeatureToggles, private val getWalletIconUseCase: GetWalletIconUseCase, + private val getWalletsUseCase: GetWalletsUseCase, private val walletIconUMConverter: WalletIconUMConverter, ) { private val params = paramsContainer.require() @@ -145,38 +147,47 @@ internal class TokenActionsUiBuilder @Inject constructor( } private fun createPortfolioBadge(cryptoCurrencyData: CryptoCurrencyData): PortfolioBadgeUM { - return if (cryptoCurrencyData.isAccountMode) { - val icon = CryptoPortfolioIconConverter.convert(cryptoCurrencyData.account.account.icon) - val name = cryptoCurrencyData - .account - .account - .accountName - .toUM() - .value - PortfolioBadgeUM.Account( - badge = TangemBadgeUM( - text = name, - tangemIconUM = TangemIconUM.Icon( - iconRes = icon.value.getResId(), - tintReference = { icon.color.getUiColor() }, + return when { + cryptoCurrencyData.isAccountMode -> { + val icon = CryptoPortfolioIconConverter.convert(cryptoCurrencyData.account.account.icon) + val name = cryptoCurrencyData + .account + .account + .accountName + .toUM() + .value + PortfolioBadgeUM.Account( + badge = TangemBadgeUM( + text = name, + tangemIconUM = TangemIconUM.Icon( + iconRes = icon.value.getResId(), + tintReference = { icon.color.getUiColor() }, + ), + size = TangemBadgeSize.X6, + shape = TangemBadgeShape.Rounded, + iconPosition = TangemBadgeIconPosition.Start, + shouldRespectIconTint = true, ), - size = TangemBadgeSize.X6, - shape = TangemBadgeShape.Rounded, - iconPosition = TangemBadgeIconPosition.Start, - shouldRespectIconTint = true, - ), - ) - } else { - val userWallet = cryptoCurrencyData.userWallet - PortfolioBadgeUM.Wallet( - name = stringReference(userWallet.name), - deviceIcon = walletIconUMConverter.convert( - getWalletIconUseCase(cryptoCurrencyData.userWallet), - ), - ) + ) + } + isSingleWallet() -> PortfolioBadgeUM.None + else -> { + val userWallet = cryptoCurrencyData.userWallet + PortfolioBadgeUM.Wallet( + name = stringReference(userWallet.name), + deviceIcon = walletIconUMConverter.convert( + getWalletIconUseCase(cryptoCurrencyData.userWallet), + ), + ) + } } } + private fun isSingleWallet(): Boolean { + val count = runCatching { getWalletsUseCase.invokeSync().size }.getOrNull() ?: return false + return count <= 1 + } + private fun createSubtitle2State(status: CryptoCurrencyStatus): TokenItemState.Subtitle2State? { return when (status.value) { is CryptoCurrencyStatus.Loaded, From 095d992b4e5c4426e352e1041cbb6dd48137cbf9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 13:14:26 +0300 Subject: [PATCH 08/21] 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 bafb894a94..e752ccee30 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-6.0-1599" +tangemBlockchainSdk = "releases-6.0-1603" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-6.0-626" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 7b979e704a9dcec1ae89e1a12857f9efc606c8a2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 10:15:40 +0000 Subject: [PATCH 09/21] 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 7cee5f99e1..e752ccee30 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.39-1601" +tangemBlockchainSdk = "releases-6.0-1603" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.39-623" +tangemCardSdk = "releases-6.0-626" #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 aa89d11b7c9b3f63ea16236b170d2e802254f7d5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 12:31:42 +0200 Subject: [PATCH 10/21] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 86 ++++++++++++++++++- core/res/src/main/res/values-es/strings.xml | 7 +- core/res/src/main/res/values-fr/strings.xml | 9 +- core/res/src/main/res/values-ja/strings.xml | 7 +- .../src/main/res/values-pt-rBR/strings.xml | 83 +++++++++++++++++- core/res/src/main/res/values-ru/strings.xml | 9 +- .../src/main/res/values-uk-rUA/strings.xml | 7 +- .../src/main/res/values-zh-rCN/strings.xml | 20 ++++- core/res/src/main/res/values/strings.xml | 72 +++++++++++++++- .../state/TokenDetailsStateController.kt | 13 +-- .../wallet/state/model/WalletActionButtons.kt | 6 +- 11 files changed, 284 insertions(+), 35 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index e37cc25476..a038bc4d36 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -98,6 +98,8 @@ Adresse hinzufügen Adresse hinzufügen und Netzwerk auswählen Kontakt hinzufügen + Adresse kopiert + Diese Adresse ist bereits gespeichert als %1$s Adresse Adressen @@ -106,8 +108,11 @@ Kontakt Name der Kontaktperson Adresse kopieren + Kontakt hinzugefügt Es konnte kein Kontakt hergestellt werden. Bitte versuchen Sie es später erneut. + Kontakt löschen Dieser Kontakt wird aus all Ihren Adressbüchern gelöscht. + „%1$s“ hat nur eine Adresse. Durch das Löschen wird auch der Kontakt gelöscht. Möchten Sie fortfahren? Der Kontakt konnte nicht gelöscht werden. Bitte versuchen Sie es später erneut. Verwalten von Kontakten und Adressen Verwerfen @@ -115,11 +120,20 @@ Adresse eingeben Ungültige Adresse Weiter bearbeiten + Sie können maximal 20 Adressen erstellen. Löschen Sie eine, um eine neue hinzuzufügen. + Neue Adresse kann nicht hinzugefügt werden + Der Name des Ansprechpartners ist erforderlich + Der Name des Ansprechpartners enthält ungültige Zeichen + Der Name des Ansprechpartners darf nicht länger als 50 Zeichen sein + Dieser Name ist bei dieser Wallet bereits vergeben. Neuer Kontakt Noch keine Kontakte Die von Ihnen hinzugefügten Kontakte werden hier angezeigt Adresse entfernen + Kontakt speichern + In Wallet speichern Dieser Kontakt wird mit dem Adressbuch dieser Wallet verknüpft. + Es wurden keine Ergebnisse gefunden.\nVersuchen Sie es mit einem anderen Namen Netzwerk auswählen Adressbuch Nicht gespeicherte Änderungen @@ -348,10 +362,12 @@ Vom Von %s Adressen synchronisieren + Erhalten Erste Schritte Token erhalten Zum Anbieter gehen Zum Token + Zur Verifizierung gehen Verstanden Ausblenden Halten bis %s @@ -405,11 +421,13 @@ oder Hauptkarte Primärring + Sonstiges Passphrase Einfügen Datenschutzrichtlinie %1$s-%2$s %1$s — %2$s + Zinssatz Weiterlesen Empfangen Erhalten @@ -471,6 +489,7 @@ %d Token Transaktion fehlgeschlagen + Transaktions-ID Transaktionsstatus Transaktionen Überweisung @@ -700,6 +719,9 @@ Feedback zu Tangem Eine Transaktion kann nicht gesendet werden Fehler in der Coinbeschreibung + Portfolio prüfen und Verdienstmöglichkeiten erkunden + Portfolio-Überprüfung + Für dich Jetzt aktualisieren Aktualisiere die Anwendung auf die neueste Version, um die ordnungsgemäße Funktionalität zu gewährleisten Aktualisierung erforderlich @@ -715,6 +737,7 @@ Es ist ein Fehler aufgetreten Es ist ein Fehler aufgetreten. Code: %s. Memo erforderlich + Erhalten %1$s Nur diese Mit Deiner Zustimmung erlaubst Du dem Smart Contract, Deine Token in zukünftigen Transaktionen zu verwenden. Betrag %s @@ -731,6 +754,8 @@ Schlüsselgenerierung Alle kryptografischen Vorgänge finden innerhalb des sicheren Chips statt, der gegen Klonen und physische Manipulation zertifiziert ist. Sicherheit auf Hardwareebene + Das Netzwerk ist derzeit stark ausgelastet. Sie können jetzt fortfahren oder es später erneut versuchen, wenn die Gebühren möglicherweise niedriger sind. + Die Netzwerkgebühr ist höher als üblich Vorhandene Wallet hinzufügen Neues Wallet erstellen Karte oder Ring bestellen @@ -866,6 +891,16 @@ Der ausgewählte Token ist derzeit nicht für Aktionen innerhalb der Krypto-Wallet verfügbar. Aber keine Sorge, du kannst dein Interesse bekunden, indem du den Token hochstufen. Hochstimmen Das Wallet unterstützt nicht mehr als ein Netzwerk + KI Gesamt: + Zusammenfassung der KI anfordern + + Vermögenswert + Vermögenswerte + + Keine Daten + Gesamtwert + Daten konnten nicht geladen werden + Top-Halterung %s Über diesen Coin Um dieses Asset zu kaufen, zu tauschen oder zu erhalten, füge diesen Deinem Portfolio hinzu Dieses Asset wird derzeit in der Wallet nicht unterstützt. @@ -886,6 +921,7 @@ **Hinzufügen zu Ihrem Portfolio**, um mit dem Kauf, Tausch oder Erhalt dieses Vermögenswerts zu beginnen Zum Portfolio hinzufügen Dein Portfolio + **Token nicht unterstützt**. Dieser Token wird derzeit in der Wallet nicht unterstützt. Marktimpuls Schnelle Aktionen Alles löschen @@ -1215,6 +1251,7 @@ Der zu kaufende Betrag muss mindestens %s betragen Kumulierte Transaktionsbeträge über %1s können eine Identitätsüberprüfung mit %2s Kumulierte Transaktionsbeträge über dem Gegenwert von %1s können eine Identitätsprüfung mit %2s + Apple Pay-Transaktionen erfordern möglicherweise eine Identitätsprüfung mit %1s Indem du auf \"Bezahlen\" klicken, stimmen Sie %1s\'s %2s und %3szu. Keine verfügbaren Anbieter für diese Währung Schnellste Bearbeitung @@ -1247,6 +1284,8 @@ Erhältlich ab Verfügbar bis zu Du erhältst + Dieser Token wird nicht unterstützt. Bitte wählen Sie einen anderen Token zum Kauf. + %s wird nicht unterstützt Der Service wird von einem externen Anbieter bereitgestellt. \n Tangem übernimmt keine Verantwortung. Du kannst diesen Bildschirm schließen und den Transaktionsstatus auf dem Bildschirm mit den Token-Details überprüfen. Bis zu @@ -1390,6 +1429,7 @@ Ziel-Tag Adresse eingeben ENS-Name oder Adresse + Adresse, ENS oder Name Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein Minimaler Betrag ist %s Minimaler Wechselgeld ist %s @@ -1401,6 +1441,7 @@ Memo Überprüfe deine Netzwerkverbindung Informationen zur Netzwerkgebühr nicht erreichbar + aus „ %1$s “ in %2$s Sie senden Von %s Grenzwert Gasgebühr @@ -1551,6 +1592,7 @@ Zurzeit sind keine Validierer verfügbar. Bitte versuche es später noch einmal. Staking nicht verfügbar Staking ist in Ihrer Region nicht verfügbar. + Staking ist in Ihrer Region derzeit nicht verfügbar. Falls Sie ein VPN aktiviert haben, deaktivieren Sie es bitte. Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Du die Verwendung Deines Tokens für das Staking genehmigst. Indem Du die Staking-Funktionalität nutzt, stimmst Du den %1$s und %2$s des Anbieters zu. Gesperrt @@ -1768,6 +1810,8 @@ MCC Eine Gebühr wird gemäß den Servicetarifen erhoben Die Transaktion wurde vom Händler teilweise oder vollständig storniert + Wir führen das System schrittweise ein und werden Sie informieren, sobald Tangem Pay hier verfügbar ist. + Tangem Pay ist in Ihrer Region nicht verfügbar. Nutze Dein Geld weiterhin. Du kannst es jederzeit einfrieren. Karte entsperren? Entsperren der Karte fehlgeschlagen. Versuchen Sie es später erneut. @@ -1777,6 +1821,16 @@ Ihr Konto wurde geschlossen Auf gerooteten Geräten nicht nutzbar. Verfügbares Guthaben + ACH + FedWire + Gebühr für die Auffahrt + Eingegangene USD werden im Verhältnis 1:1 in USDC umgewandelt. + Eine Banküberweisung kann 1-2 Werktage dauern. + Durch die Nutzung des Dienstes stimmen Sie den Bedingungen des Anbieters zu. %1$s Und %2$s + Details anzeigen + Dies kann etwas Zeit in Anspruch nehmen. + Vorbereitung Ihrer Bankdaten + Einzahlungen sind ausschließlich per ACH oder FedWire möglich. SWIFT-Überweisungen werden zurückgebucht. KYC vom Hauptbildschirm ausblenden Tangem Pay Karte 1 Guthaben hinzufügen @@ -1846,6 +1900,10 @@ PIN-Code ändern Kehren Sie zur App zurück, falls Sie ihn vergessen. Karte + Tarif wechseln + Kartenbezogen + Planbezogen + Aktueller Plan Limit von %s bis %s festlegen Limits festlegen Unzureichendes Guthaben @@ -1950,6 +2008,13 @@ Konto löschen Tangem Pay wird vom Hauptbildschirm entfernt und erscheint auch nach einer Neuinstallation der App nicht wieder. Konto löschen? + Abbrechen + Tarif wechseln + Auswählen + Tarif wechseln + Tarife vergleichen + Auswahl bestätigen + Plan auswählen Wir beheben ein technisches Problem. Bitte versuchen Sie es später erneut. Service vorübergehend nicht verfügbar Daten können derzeit nicht angezeigt werden, Kartenzahlungen funktionieren jedoch weiterhin. @@ -1965,6 +2030,8 @@ Nutzen Sie USDC für alltägliche Zahlungen Tangem Pay ist vorübergehend nicht erreichbar. Tangem Pay + Empfangen Sie Fiat-USD per ACH/FedWire + Banküberweisung Senden Sie USDC Polygon an die Adresse Ihres Kontos Von einer anderen Wallet oder Börse Laden Sie Ihr Konto mit einem beliebigen Token aus Ihrer Wallet auf @@ -2011,6 +2078,9 @@ %s kann nicht ausgeblendet werden N / A QR-Code anzeigen + Die Daten des Tokens konnten nicht geladen werden. + Gehe zum Tauschen + Token-Zusammenfassung 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 @@ -2059,6 +2129,7 @@ Tippe auf die Doppelkarte oder Ring mit der Nummer %s und entferne sie erst am Ende des Vorgangs. Aufladen Aufgeladen + Du hast bezahlt Bitte versuche es später noch einmal. Sollte das Problem weiterhin bestehen, wende Dich bitte an den Support. Etwas ist schiefgelaufen! Es ist ein Fehler aufgetreten. Fehlercode: %s. Bitte kontaktiere unseren Support. @@ -2090,6 +2161,12 @@ Wallet umbenennen Alle freischalten Alle mit %s freischalten + Kontonummer + Bankadresse + Name der Bank + Adresse des Begünstigten + Name des Begünstigten + Bankleitzahl Virtuelles Konto Noch keine Transaktionen. Beginnen Sie mit dem Einkaufen und sehen Sie sich hier den Verlauf an AML-geprüft @@ -2311,6 +2388,8 @@ Dieses Token muss mit deinem Hedera-Konto verknüpft sein, bevor du ihn erhalten kannst. Verknüpfe deinen Token Nicht genug %s. Lade dein Hedera-Konto auf, um dieses Token zuzuordnen + Es scheint, dass die Aktivierung der Karte oder des Rings nicht korrekt abgeschlossen wurde. Dies kann an einem Problem mit dem NFC-Modul deines Gerätes oder an einem falschen Tippen der Karte oder Ring auf dein Gerät liegen. Bitte wende dich an unser Support-Team, um Unterstützung zu erhalten. + Handeln ist gefragt. Benutzen Sie nicht Ihr Wallet! Bist Du sicher, dass Du die Transaktion stornieren willst? Du kannst die Transaktion nicht erneut starten. Deine Transaktion mit einem Betrag von %1$s %2$s wurde nicht abgeschlossen. Du kannst versuchen, diese erneut abzuschließen. Du hast eine nicht abgeschlossene Transaktion @@ -2329,6 +2408,7 @@ Adressen synchronisieren, um eine Adresse für das Netzwerk %d zu erhalten Einige Adressen fehlen + Verwenden %s oder Zugangscode, um den Zugriff auf Ihre Wallet freizuschalten Das Netzwerk ist derzeit nicht erreichbar. Bitte versuchen Sie es später erneut. Netzwerk ist nicht erreichbar Lade dein Guthaben auf @@ -2336,8 +2416,8 @@ Fehlende Sicherung Diese Karte oder Ring wurde bereits für Transaktionen verwendet. Wenn die Karte oder Ring aus einer nicht vertrauenswürdigen Quelle stammt, solltest du den gesamten Betrag abheben. Wenn es sich um deine Karte oder Ring handelt, sind keine Maßnahmen erforderlich. Karte oder Ring hat bereits Transaktionen unterzeichnet - Wird so schnell wie möglich aktualisiert. - Es fehlen einige Token-Guthaben. + Einige Token-Guthaben konnten nicht aktualisiert werden + Guthaben sind möglicherweise nicht aktuell Deine Bewertung motiviert uns, die Tangem Wallet noch besser zu machen. Gefällt dir Tangem? Du musst deinen Token zuordnen, bevor du Token erhalten kannst @@ -2599,6 +2679,8 @@ %1$s aus Aave zurücküberwiesen Yield-Modus initialisieren Yield-Modus reaktivieren + Zurückgegeben + Geliefert Lieferung an Aave %1$s geliefert an Aave Abheben von Aave diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 3a5403aaec..d3a635d982 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1208,6 +1208,7 @@ La cantidad a comprar debe ser como mínimo %s El importe acumulado de la transacción superior a %1s puede requerir la verificación de la identidad con %2s El importe acumulado de la transacción superior al equivalente de %1s puede requerir la verificación de la identidad con %2s + Las transacciones de Apple Pay pueden requerir verificación de identidad con %1s Al hacer clic en Pagar, usted acepta %1s\'s %2s y %3s. No hay proveedores disponibles para esta moneda Procesamiento más rápido @@ -2296,6 +2297,8 @@ Este token debe estar asociado con su cuenta de Hedera antes de poder recibirlo. Asocie su token No hay suficiente %s. Recargue su cuenta de Hedera para asociar este token + Hemos detectado que el proceso de activación de la tarjeta no se ha completado correctamente debido a problemas con el módulo NFC de su dispositivo o a que no ha acercado la tarjeta al teléfono de forma adecuada. Póngase en contacto con nuestro equipo de soporte para obtener más información. + Es necesario actuar. ¡No use su billetera! ¿Está seguro de que desea cancelar la transacción? No podrá volver a reintentarlo. Su transacción con un importe de %1$s %2$s no se ha completado. Puede volver a intentarlo para completarla. Tiene una transacción sin terminar @@ -2321,8 +2324,8 @@ Falta backup Esta tarjeta se ha utilizado previamente para transacciones. Si la recibió de una fuente no confiable, considere retirar todos los fondos. Si es su tarjeta, no se requiere ninguna acción. La tarjeta ya ha firmado transacciones - Se actualizará lo antes posible - Faltan algunos saldos de tokens + No se han podido actualizar los saldos de algunos tokens + Los saldos pueden no estar actualizados Su opinión nos motiva a hacer Tangem Wallet aún mejor ¿Disfrutando de Tangem? Deba asociar su token antes de recibir tokens diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index c1d9e544ef..a10ceb497d 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1112,6 +1112,7 @@ Le montant à acheter doit être au moins %s Si le montant cumulé des transactions dépasse %1s, une vérification d\'identité via %2s pourrait être requise Si le montant cumulé des transactions dépasse l\'équivalent de %1s, une vérification d\'identité via %2s pourrait être requise + Les transactions Apple Pay peuvent nécessiter une vérification d\'identité avec %1s En appuyant sur Acheter, vous acceptez %1s %2s et %3s. Aucun fournisseur disponible pour cette devise Le plus rapide @@ -1531,8 +1532,8 @@ Compatible avec Web 3.0 Une transaction entrante d\'au moins de %1$s est requise pour continuer Fonds insuffisants - Discussion avec le support - Joindre les logs de l\'application + Support via chat + Joindre les logs de l\'app Données du SWAP :\nDepuis :%1$s%2$s\nVers :%3$s%4$s\nPar :%5$s-%6$s Accéder au chat Ouvrir un email @@ -2158,8 +2159,8 @@ Sauvegarde manquante Cette carte a déjà été utilisée pour des transactions. Si elle provient d\'une source non fiable, envisagez de retirer tous les fonds. S\'il s\'agit de votre carte, aucune action n\'est requise. La carte a déjà signé des transactions - Sera mis à jour dès que possible - Données de soldes incomplètes + Certains soldes de jetons n\'ont pas pu être mis à jour + Les soldes peuvent ne pas être à jour Votre avis nous motive à améliorer encore le Portefeuille Tangem Vous appréciez Tangem ? Vous devez associer votre jeton avant de recevoir des jetons diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 1a9a334e93..0cb5e2e92d 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -1191,6 +1191,7 @@ 買付金額は少なくとも%sである必要があります 累計取引額が%1sを超えると、%2sでの本人確認が必要になる場合があります。 累計取引額が%1s相当額を超えると、%2sでの本人確認が必要になる場合があります。 + Apple Payの取引では、%1sによる本人確認が必要になる場合があります 「支払う」をタップすると、%1sの%2sおよび%3sに同意したものとみなされます。 この通貨で利用可能なプロバイダーはありません 最短で処理 @@ -2274,6 +2275,8 @@ このトークンを受け取るには、Hederaアカウントに関連付ける必要があります。 トークンを関連付ける %sが不足しています。このトークンを関連付けるには、Hederaアカウントに資金を追加してください。 + カードの有効化が正常に完了していないことが判明しました。原因として、お使いの端末のNFC機能の不具合、またはカードをスマートフォンに正しくかざせていない可能性があります。詳しくはサポートまでお問い合わせください。 + 対応が必要です。ウォレットは使用しないでください。 取引をキャンセルしてもよろしいですか? 取引を再試行することはできません %1$s %2$sの取引が完了しませんでした。もう一度試して完了してください。 未完了の取引があります @@ -2297,8 +2300,8 @@ バックアップがありません このカードは以前取引に使用されたことがあります。信頼できない出所から受け取った場合は、全資金を引き出すことを検討してください。あなたのカードであれば、何もする必要はありません。 カードはすでに取引に署名済みです - 順次更新されます。 - 一部トークンの残高が表示されていません。 + 一部のトークン残高を更新できませんでした + 残高が最新でない可能性があります あなたのレビューは、Tangemウォレットをさらに良くするためのモチベーションになります Tangemを楽しんでいますか? トークンを受け取る前に、トークンを関連付ける必要があります。 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 490096fa80..0b5e395924 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -98,15 +98,21 @@ Adicionar endereço Adicione o endereço e selecione a rede. Adicionar contato + Endereço copiado + Este endereço já está salvo como %1$s %d endereço %d endereços + Escolha o endereço Contato Nome do contato Copiar endereço + Contato adicionado Não foi possível estabelecer contato. Tente novamente mais tarde. + Excluir contato Este contato será excluído de toda a sua agenda de contatos. + "%1$s\"Possui apenas um endereço. Excluí-lo também excluirá o contato. Continuar?" Não foi possível excluir o contato. Tente novamente mais tarde. Gerenciar contatos e endereços Descartar @@ -114,11 +120,20 @@ Insira o endereço Endereço inválido Continue editando + Você não pode criar mais de 20 endereços. Exclua um para adicionar um novo. + Não é possível adicionar um novo endereço. + É necessário nome para contato. + O nome do contato contém caracteres inválidos. + O nome de contato não deve exceder 50 caracteres. + Esse nome já está em uso nesta carteira. Novo contato Ainda não há contatos. Os contatos que você adicionar aparecerão aqui. Remover endereço + Salvar contato + Salvar na carteira Este contato será vinculado à agenda de endereços desta carteira. + Nenhum resultado encontrado.\nTente outro nome Selecione a rede Agenda de endereços Alterações não salvas @@ -347,10 +362,12 @@ De De %s Sincronizar endereços + Obter Comece agora Obter token Ir para o provedor Ir para o token + Ir para a verificação Entendi Esconder Mantenha-se em %s @@ -404,11 +421,13 @@ ou Cartão principal Anel primário + Outro Senha Colar política de Privacidade %1$s-%2$s %1$s — %2$s + Taxa Leia mais Receber Recebido @@ -431,6 +450,7 @@ Enviar Enviar: Falha ao enviar a transação + Enviar e trocar Enviando Enviado O servidor não está disponível. Tente novamente mais tarde. @@ -469,6 +489,7 @@ tokens Transação falhou + ID da transação Status da transação Transações Transferir @@ -698,9 +719,17 @@ Feedback Tangem Não foi possível enviar uma transação. Erro na descrição da moeda + Analise seu portfólio e explore oportunidades de ganhos. + Avaliação de portfólio + Para você + Atualize agora Atualize o aplicativo para a versão mais recente para garantir o funcionamento correto. Atualização necessária + Esta versão do aplicativo não é mais compatível e não pode ser atualizada neste dispositivo. + Atualização indisponível Atualizar + Seu sistema operacional está desatualizado. Atualize-o para continuar usando o aplicativo. + Atualize seu sistema operacional Por favor, atualize o aplicativo para a versão mais recente para garantir o funcionamento correto. Atualização necessária Fundos insuficientes @@ -708,6 +737,7 @@ Ocorreu um erro. Ocorreu um erro. Código: %s. Requer memorando + Obter %1$s Transação Ao aprovar, você permite que o contrato inteligente utilize seus tokens em transações futuras. Quantia %s @@ -724,6 +754,8 @@ Geração de chaves Todas as operações criptográficas ocorrem dentro do chip de segurança, certificado contra clonagem e adulteração física. Segurança em nível de hardware + A atividade na rede está alta. Você pode continuar agora ou tentar novamente mais tarde, quando as tarifas poderão ser menores. + A tarifa de rede está mais alta do que o normal. Adicionar carteira existente Criar nova carteira Encomendar Tangem @@ -859,6 +891,16 @@ O token selecionado está atualmente indisponível para ações na carteira de criptomoedas. Mas não se preocupe, você pode demonstrar seu interesse votando a favor dele. Voto positivo A carteira não suporta mais de uma rede. + Total de IA: + Solicite um resumo de IA + + %d ativo + %d ativos + + Sem dados + Valor total + Não foi possível carregar os dados. + Posição superior %s Sobre a moeda Para comprar, trocar ou receber este ativo, adicione-o à sua carteira. Este ativo não é atualmente suportado na carteira. @@ -879,6 +921,7 @@ **Adicione ao seu portfólio** para começar a comprar, trocar ou receber este ativo. Em seu portfólio Seu portfólio + **Token não suportado**. Este token não é suportado na carteira neste momento. Pulso do mercado Ações rápidas Limpar tudo @@ -1131,12 +1174,12 @@ Biometria Leia mais sobre a frase-semente. - + Escreva esta 1palavra na ordem indicada abaixo e guarde-a em um local seguro e secreto. Escreva estas %dpalavras na ordem indicada abaixo e guarde-as em um local seguro e secreto. Sua frase-semente - + %dpalavra %dpalavras Para importar sua carteira, insira sua frase mnemônica no campo abaixo. @@ -1208,6 +1251,7 @@ O valor da compra deve ser de pelo menos %s Valor total acumulado das transações acima de %1s pode exigir verificação de identidade com %2s Valor total acumulado das transações acima do equivalente a %1s pode exigir verificação de identidade com %2s + As transações do Apple Pay podem exigir verificação de identidade. %1s Ao clicar em Pagar, você concorda com %1s\'s %2s e %3s. Não há fornecedores disponíveis para esta moeda. Processamento mais rápido @@ -1287,6 +1331,7 @@ Cartão de crédito ou conta bancária Compartilhe seu endereço ou código QR. Venda criptomoedas com segurança + Enviar com troca para outro token Enviar para outra carteira Entre seus portfólios Outro @@ -1382,6 +1427,7 @@ Etiqueta de destino Insira o endereço Nome ou endereço ENS + Endereço, ENS ou nome O endereço é o mesmo que o endereço da carteira. O valor mínimo é %s A mudança mínima é %s @@ -1393,6 +1439,7 @@ Memorando Verifique sua conexão de rede. Informações sobre tarifas de rede indisponíveis + de %1$s em %2$s Você envia De %s Limite de gás @@ -1542,6 +1589,8 @@ Staking ativado Não há validadores disponíveis no momento. Tente novamente mais tarde. Staking indisponível + O staking não está disponível na sua região. + O staking está indisponível na sua região. Se você tiver uma VPN ativada, tente desativá-la. A rede cobrará uma taxa de aprovação de token para verificar se você está autorizando o uso do seu token para staking. Ao utilizar a funcionalidade de staking, você concorda com os termos do provedor. %1$s e %2$s Trancado @@ -1621,6 +1670,8 @@ Stake %s Desvincule %s A transação está sendo processada! A validação está em andamento na blockchain. Isso pode levar alguns minutos. + %s O staking está proibido devido a verificações de segurança. + %s Apostar é suspeito. Aposte por sua conta e risco. Desvinculação Desbloquear Desbloqueando @@ -1649,6 +1700,8 @@ Compatível com Web 3.0 Uma transação de entrada de pelo menos %1$s é necessário prosseguir Fundos insuficientes + Chat de suporte + Anexar logs do aplicativo Dados da operação SWAP:\nDe: %1$s %2$s\nPara: %3$s %4$s\nPor %5$s - %6$s Abra o e-mail Abra o e-mail @@ -1755,6 +1808,8 @@ MCC Uma taxa é cobrada de acordo com as tarifas de serviço A transação foi parcial ou totalmente revertida pelo comerciante. + Estamos implementando gradualmente e avisaremos quando o Tangem Pay estiver disponível aqui. + O Tangem Pay não está disponível na sua região. Continue usando seu dinheiro. Você pode congelar a qualquer momento. Descongelar seu cartão? Não foi possível desbloquear o cartão. Tente novamente mais tarde. @@ -1832,6 +1887,11 @@ Alterar código PIN Volte ao aplicativo se você se esquecer. + Cartão + Alterar plano + relacionado a cartões + Plano relacionado + Plano atual Defina um limite a partir de %s para %s Definir limites saldo insuficiente @@ -1936,6 +1996,13 @@ Remover conta O Tangem Pay será removido da tela principal e não aparecerá novamente, mesmo após a reinstalação do aplicativo. Remover conta? + Cancelar + Plano de downgrade + Selecionar + Plano de atualização + Comparar planos + Confirmar seleção + Selecione um plano Estamos resolvendo um problema técnico. Por favor, tente novamente mais tarde. Serviço temporariamente indisponível Não foi possível exibir os detalhes. No entanto, os pagamentos com cartão ainda estão funcionando. @@ -2043,6 +2110,9 @@ Gêmeo Tangem Essa ação é irreversível. Você não terá mais acesso à sua carteira antiga. Toque no cartão gêmeo com o número %s e não remova até o final da operação. + Recarregar + Recarregado + Você pagou Por favor, tente novamente mais tarde. Se o problema persistir, entre em contato com o suporte. Algo deu errado! Ocorreu um erro. Código do erro: %sPor favor, entre em contato com nosso suporte. @@ -2074,6 +2144,8 @@ Renomear carteira Desbloquear tudo Desbloqueie tudo com %s + Conta virtual + Ainda não há transações. Comece a gastar e veja o histórico aqui. Verificado AML Disponível Bloqueado @@ -2293,6 +2365,8 @@ Este token precisa ser associado à sua conta Hedera antes que você possa recebê-lo. Associe seu token Não é suficiente %sRecarregue sua conta Hedera para associar este token. + Descobrimos que o processo de ativação do cartão não foi concluído corretamente devido a problemas com o módulo NFC do seu aparelho ou à maneira incorreta de aproximar os cartões do celular. Entre em contato com nossa equipe de Suporte para obter mais detalhes. + É preciso agir. Não use sua carteira! Tem certeza de que deseja cancelar a transação? Você não poderá tentar realizá-la novamente. Sua transação com um valor de %1$s %2$s Não foi concluído. Você pode tentar novamente para concluí-lo. Você tem uma transação pendente. @@ -2311,6 +2385,7 @@ Sincronizar endereços para obter endereços para %d rede Alguns endereços estão faltando. + Usar %s ou o código de acesso para desbloquear o acesso à sua carteira. A rede está inacessível no momento. Tente novamente mais tarde. A rede está inacessível. Recarregue sua carteira @@ -2318,8 +2393,8 @@ Backup ausente Este cartão já foi usado anteriormente para transações. Se o recebeu de uma fonte não confiável, considere retirar todos os fundos. Se o cartão for seu, nenhuma ação é necessária. O cartão já registrou transações. - Será atualizado assim que possível. - Faltam alguns saldos de tokens + Alguns saldos de tokens não puderam ser atualizados. + Os saldos podem estar desatualizados Sua avaliação nos motiva a aprimorar ainda mais a Tangem Wallet. Gostando de Tangem? Você precisa associar seu token antes de receber tokens. diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 74baf3a874..0354f68c22 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -82,7 +82,7 @@ Выберите токен для обмена Пополнить Обмен - Перевод + Перевести Добавить в портфель Добавить токены Сортировка и группировка @@ -1251,6 +1251,7 @@ Сумма покупки должна составлять минимум %s Общая сумма транзакций свыше %1s может потребовать верификации личности через %2s Общая сумма транзакций, превышающая эквивалент %1s, может потребовать верификации личности через %2s + Для транзакций через Apple Pay может потребоваться верификация личности в %1s Нажимая «Оплатить», вы соглашаетесь с %1s\'s %2s и %3s. Нет доступных провайдеров для выбранной валюты Самый быстрый @@ -2291,6 +2292,8 @@ Этот токен должен быть ассоциирован с вашей учетной записью Hedera, прежде чем вы сможете его принять Ассоциируете свой токен Недостаточно %s. Пополните ваш аккаунт Hedera для ассоциации этого токена + Мы обнаружили, что процесс активации карты не завершен должным образом из-за проблем с NFC-модулем устройства или неправильного прикладывания карт к телефону. Пожалуйста, обратитесь в службу поддержки для уточнения деталей. + Требуется действие. Не используйте кошелек! Вы уверены что хотите отменить эту транзакцию? Вы не сможете отправить ее еще раз Ваша транзакция %1$s %2$s не была завершена. Вы можете попробовать отправить ее еще раз. У вас есть незавершенная транзакция @@ -2320,8 +2323,8 @@ Резервная копия отсутствует Эта карта ранее использовалась для подписи транзакций. Если она получена от ненадежного источника, рассмотрите возможность вывода своих средств. Если это ваша карта, дополнительных действий не требуется. Карта уже подписывала транзакции - Обновление будет произведено в кратчайшие сроки. - Отсутствует часть баланса токенов. + Некоторые балансы токенов не удалось обновить + Балансы могут быть неактуальны Ваш отзыв мотивирует нас сделать кошелек Tangem еще лучше Нравится Tangem? Вам необходимо провести ассоциацию токена для того, чтобы иметь возможность принимать его 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 7924e3da11..1176df0144 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1251,6 +1251,7 @@ Сума покупки повинна бути не менше %s Загальна сума транзакцій понад %1s може вимагати верифікації особи через %2s Загальна сума транзакцій, що перевищує еквівалент %1s, може вимагати верифікації особи через %2s + Для транзакцій через Apple Pay може знадобитися верифікація особи в %1s Натискаючи «Оплатити», ви погоджуєтеся з %1s\'s %2s і %3s. Для данної валюти немає доступних провайдерів Найшвидший @@ -2306,6 +2307,8 @@ Цей токен повинен бути асоційований з вашим обліковим записом Hedera, перш ніж ви зможете його прийняти Асоціюйте свій токен Недостатньо %s. Поповніть ваш обліковий запис Hedera для асоціації цього токена + Ми виявили, що процес активації картки не завершено належним чином через проблеми з NFC-модулем пристрою або неправильне прикладання карток до телефону. Будь ласка, зверніться до служби підтримки для уточнення деталей. + Потрібна дія. Не використовуйте гаманець! Ви впевнені, що хочете скасувати цю транзакцію? Ви не зможете відправити її ще раз Ваша транзакція %1$s %2$s не була завершена. Ви можете спробувати відправити її ще раз. У вас є незавершена транзакція @@ -2335,8 +2338,8 @@ Резервна копія відсутня Ця картка вже використовувалася для здійснення транзакцій. Якщо вона отримана з ненадійного джерела, подумайте про те, щоб зняти всі кошти. Якщо це ваша картка, ніяких додаткових дій не потрібно. Картка вже підписувала транзакції - Буде оновлено якнайшвидше. - Відсутня частина балансу токенів. + Деякі баланси токенів не вдалося оновити + Баланси можуть бути неактуальними Ваш відгук мотивує нас робити гаманець Tangem Wallet ще кращим Подобається Tangem? Вам необхідно провести асоціацію токена, щоб мати можливість приймати його 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 e1ea9ae35f..3a428f777a 100644 --- a/core/res/src/main/res/values-zh-rCN/strings.xml +++ b/core/res/src/main/res/values-zh-rCN/strings.xml @@ -98,14 +98,20 @@ 添加地址 添加地址并选择网络 添加联系人 + 地址已复制 + 此地址已保存为 %1$s %d地址\n%d地址 + 选择地址 联系人 联系人姓名 复制地址 + 联系人已保存 无法创建联系人。请稍后再试。 + 删除联系人 该联系人将从您所有的通讯录中删除 + “%1$s“只有一个地址。删除此地址也会删除联系人。继续吗?” 无法删除联系人,请稍后再试。 管理联系人及地址 取消 @@ -113,11 +119,20 @@ 输入地址 无效地址 继续编辑 + 您最多只能创建 20 个地址。删除一个地址即可添加新地址。 + 无法添加新地址 + 必须填写联系人姓名 + 联系人姓名包含无效字符 + 联系人姓名不得超过 50 个字符 + 这个钱包的名字已经被注册了 新联系人 尚无联系人 添加的联系人将显示在此处 移除地址 + 保存联系人 + 保存到钱包 该联系人将与该钱包的通讯录关联。 + 未找到结果。\n请尝试其他名称 选择网络 地址簿 未保存的更改 @@ -1185,6 +1200,7 @@ 购买金额必须至少 %s 累计交易金额超过 %1s 时,可能需要通过 %2s进行身份验证 累计交易金额超过等值金额 %1s 可能需要通过 %2s进行身份验证 + Apple Pay 交易可能需要通过以下方式进行身份验证: %1s 点击“支付”即表示您同意 %1s的 %2s 和 %3s。 目前没有提供此货币的供应商 最快处理 @@ -2261,6 +2277,8 @@ 您必须先将此代币与您的 Hedera 账户关联才能收到它。 关联您的代币 %s不够。请为您的 Hedera 账户充值以关联此代币 + 我们发现,由于您的设备NFC模块存在问题,或者将卡片轻触手机的方式不正确,导致卡片激活流程未能正常完成。请联系我们的客服团队以获取更多详情。 + 必须采取行动。不要用你的钱包! 您确定要取消交易吗?取消后您将无法再次尝试交易。 您金额为 %1$s %2$s 的交易未完成。您可以再次尝试完成交易。 您有未完成的交易 @@ -2284,7 +2302,7 @@ 缺少备份 此卡曾用于交易。如果是从不可信来源收到的,请考虑提取所有资金。如果是您的卡,则无需采取任何措施。 卡片已签署交易 - 将尽快更新 + 部分代币余额无法更新 缺少部分代币余额 您的评价激励我们不断改进 Tangem Wallet。 喜欢 Tangem 吗? diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index a2da937bc0..7540b870fa 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -98,6 +98,8 @@ Add address Add address and select network Add contact + Address copied + This address is already saved as %1$s %d address %d addresses @@ -106,8 +108,11 @@ Contact Contact name Copy address + Contact saved Couldn\'t create contact. Please try again later. + Delete contact This contact will be deleted from all your address books + \"%1$s\" has only one address. Deleting it will also delete the contact. Continue? Couldn\'t delete contact. Please try again later. Manage contacts & addresses Discard @@ -116,7 +121,7 @@ Invalid address Keep editing You can not create more than 20 addresses. Delete one to add new. - Can\'t add new address + Can\'t add new address Contact name is required Contact name contains invalid characters Contact name must not exceed 50 characters @@ -125,8 +130,10 @@ No contacts yet Contacts added will appear here Remove address + Save contact Save to Wallet This contact will be linked to this wallet’s address book. + No results found.\nTry another name Select network Address book Unsaved changes @@ -355,6 +362,7 @@ From From %s Synchronize addresses + Get Get started Get token Go to provider @@ -413,6 +421,7 @@ or Primary card Primary ring + Other Passphrase Paste Privacy Policy @@ -480,6 +489,7 @@ %d tokens Transaction failed + Transaction ID Transaction status Transactions Transfer @@ -711,6 +721,7 @@ Can\'t send a transaction Coin description error Review portfolio and explore earn opportunities + Portfolio review For You Update now Update the app to its latest version to ensure proper functionality @@ -881,6 +892,16 @@ The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it. Upvote The wallet doesn\'t support more than one network + AI Total: + Ask for AI summary + + %d asset + %d assets + + No data + Total value + Can’t load data + Top holding %s About coin To buy, exchange, or receive this asset, add it to your portfolio This asset is currently not supported in the wallet @@ -1231,6 +1252,7 @@ The amount to buy must be at least %s Cumulative transaction amount over %1s may require identity verification with %2s Cumulative transaction amount over equivalent of %1s may require identity verification with %2s + Apple Pay transactions may require identity verification with %1s By clicking Pay, you agree to %1s\'s %2s and %3s. No available providers for this currency Quickest processing @@ -1263,6 +1285,8 @@ Available from Available up to You get + This token is not supported. Please choose a different token to buy. + %s is not supported Service is provided by an external provider. \nTangem is not responsible. You can close this screen and check the transaction status on the token details screen. Up to @@ -1406,6 +1430,7 @@ Destination Tag Enter address ENS name or address + Address, ENS or name Address is the same as wallet address Minimum amount is %s Minimum change is %s @@ -1568,6 +1593,7 @@ No available validators at the moment. Please try again later. Staking Unavailable Staking is unavailable in your region + Staking is currently unavailable in your region. If you have VPN enabled – try disabling it. The network will charge a token approval fee to verify that you are authorizing the use of your token for the staking. By using staking functionality, you agree with provider’s %1$s and %2$s Locked @@ -1785,6 +1811,8 @@ MCC A fee is charged due to the service tariffs The transaction was partially or fully reversed by the merchant + We are rolling it out gradually and let you know when Tangem Pay will be available here + Tangem Pay is unavailable in your region Keep using your money. You can freeze anytime. Unfreeze your card? Failed to unfreeze the card. Try again later. @@ -1794,6 +1822,16 @@ Your account has been closed Unable to use on rooted device Available balance + ACH + FedWire + Fee for onramp + Received USD will be converted to USDC by 1:1 rate + Bank transfer might take 1-2 business days + By using service, you agree with provider %1$s and %2$s + Show details + This may take a little time + Preparing your banking details + Deposit via ACH or FedWire only. SWIFT transfers will be returned. Hide KYC from main screen Tangem Pay Card 1 Add funds @@ -1863,6 +1901,10 @@ Change PIN-code Come back to the app if you forget it. Card + Change plan + Card related + Plan related + Current plan Set a limit from %s to %s Set limits insufficient funds @@ -1967,6 +2009,13 @@ Remove account Tangem Pay will be removed from the main screen and won\'t appear again, even after reinstalling the app. Remove account? + Cancel + Downgrade plan + Select + Upgrade plan + Compare plans + Confirm selection + Select plan We’re fixing a technical issue. Please try again later. Service temporarily unavailable Service unreachable. However, card payments are still working. @@ -1982,6 +2031,8 @@ Use USDC for everyday payments Connection issues Tangem Pay + Receive fiat USD via ACH/FedWire + Bank transfer Send USDC Polygon to your account’s address From another wallet or exchange Use crypto from your wallet to top up your payment account @@ -2028,6 +2079,9 @@ Unable to hide %s N/A Show QR code + Can’t load data of the token + Go to swap + Token summary Exchange this token for another at %1$s service fees from February %2$s-%3$s. Swap with Changelly, %s fees Swap now @@ -2076,6 +2130,7 @@ Tap the twin card with number %s and do not remove until the end of the operation Top up Topped up + You paid Please try again later. If the issue persists, please contact support. Something went wrong! We\'ve encountered an error. Error code: %s. Please contact our support. @@ -2107,6 +2162,12 @@ Rename wallet Unlock all Unlock all with %s + Account number + Bank address + Bank name + Beneficiary address + Beneficiary name + Routing number Virtual account No transactions yet. Start spending and see history here AML verifired @@ -2328,6 +2389,8 @@ This token must be associated with your Hedera account before you can receive it Associate your token Not enough %s. Top up your Hedera account to associate this token + We found out that the card activation process has not been completed accordingly due to issues with your device\'s NFC module or the incorrect way of tapping the cards to the phone. Please get in touch with our Support team for more details. + Action is required. Don\'t use your wallet! Are you sure you want to cancel the transaction? You will not be able to retry the transaction again Your transaction with an amount of %1$s %2$s was not completed. You can try again to complete it. You have unfinished transaction @@ -2346,6 +2409,7 @@ Sync addresses to get an addresses for %d networks Some addresses are missing + Use %s or access code to unlock access to your wallet The network is currently unreachable. Please try again later. Network is unreachable Top up your wallet @@ -2353,8 +2417,8 @@ Missing backup This card has been previously used for transactions. If received from an untrusted source, consider withdrawing all funds. If it\'s your card, no action is required. Card has already signed transactions - Will be updated as soon as possible - Missing some token balances + Some token balances couldn\'t be updated. + Balances may be outdated Your review keeps us motivated to make Tangem Wallet even better Enjoying Tangem? You must associate your token before receiving tokens @@ -2617,6 +2681,8 @@ %1$s withdrawn from Aave Yield Mode initialized Yield Mode reactivated + Returned + Supplied Supply to Aave %1$s supplied to Aave Withdraw from Aave diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt index c1e5c1fafb..a75eb5c7de 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt @@ -13,12 +13,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.features.tokendetails.impl.R import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* import javax.inject.Inject @ModelScoped @@ -51,14 +46,14 @@ internal class TokenDetailsStateController @Inject constructor() { ), balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( addFundsButton = TangemButtonUM( - text = resourceReference(R.string.tangempay_card_details_add_funds), + text = resourceReference(R.string.actionbutton_addfunds_title), tangemIconUM = TangemIconUM.Icon(iconRes = R.drawable.ic_arrow_down_24), onClick = { }, isEnabled = true, type = TangemButtonType.Secondary, ), swapButton = TangemButtonUM( - text = resourceReference(R.string.common_swap), + text = resourceReference(R.string.actionbutton_swap_title), tangemIconUM = TangemIconUM.Icon( iconRes = R.drawable.ic_exchange_default_24, tintReference = { TangemTheme.colors2.graphic.neutral.quaternary }, @@ -68,7 +63,7 @@ internal class TokenDetailsStateController @Inject constructor() { type = TangemButtonType.Secondary, ), transferButton = TangemButtonUM( - text = resourceReference(R.string.common_transfer), + text = resourceReference(R.string.actionbutton_transfer_title), tangemIconUM = TangemIconUM.Icon(iconRes = R.drawable.ic_arrow_up_24), onClick = { }, isEnabled = true, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt index 31daa69aca..10ed719757 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt @@ -55,7 +55,7 @@ internal sealed class WalletActionButtons( override val onClick: () -> Unit, override val isEnabled: Boolean, ) : WalletActionButtons( - text = resourceReference(R.string.common_add_funds), + text = resourceReference(R.string.actionbutton_addfunds_title), iconRes = R.drawable.ic_arrow_down_24, ) @@ -63,7 +63,7 @@ internal sealed class WalletActionButtons( override val onClick: () -> Unit, override val isEnabled: Boolean, ) : WalletActionButtons( - text = resourceReference(R.string.common_swap), + text = resourceReference(R.string.actionbutton_swap_title), iconRes = R.drawable.ic_exchange_default_24, ) @@ -79,7 +79,7 @@ internal sealed class WalletActionButtons( override val onClick: () -> Unit, override val isEnabled: Boolean, ) : WalletActionButtons( - text = resourceReference(R.string.common_transfer), + text = resourceReference(R.string.actionbutton_transfer_title), iconRes = R.drawable.ic_arrow_up_24, ) } \ No newline at end of file From 177176b3ee5b898af381b4adf41abb104441a544 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 15:32:30 +0500 Subject: [PATCH 11/21] Updated on 2026-08-14 --- .../common/ui/markets/action/QuickActions.kt | 3 ++ .../markets/action/QuickActionsConverter.kt | 51 ++++++++++++++----- .../action/QuickActionsConverterTest.kt | 51 +++++++++++++++++++ .../tokens/actions/BaseActionsFactory.kt | 9 ++-- .../tokens/actions/CommonActionsFactory.kt | 2 +- .../actions/OutdatedDataActionsFactory.kt | 2 +- .../tokenactions/ui/TokenActionsContentV2.kt | 6 ++- 7 files changed, 104 insertions(+), 20 deletions(-) diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActions.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActions.kt index 27d4bfac71..104176d4c6 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActions.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActions.kt @@ -1,9 +1,12 @@ package com.tangem.common.ui.markets.action import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet +import kotlinx.collections.immutable.persistentSetOf data class QuickActions( val actions: ImmutableList, val onQuickActionClick: (QuickActionUM) -> Unit, val onQuickActionLongClick: (QuickActionUM) -> Unit, + val disabledActions: ImmutableSet = persistentSetOf(), ) \ No newline at end of file diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverter.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverter.kt index de1e9704d1..63add1f9fe 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverter.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverter.kt @@ -4,6 +4,7 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet object QuickActionsConverter { @@ -13,8 +14,9 @@ object QuickActionsConverter { isRedesignEnabled: Boolean, context: TokenActionsContext = TokenActionsContext.Markets, ): QuickActions { + val states = toQuickActionStates(cryptoData.actions, isRedesignEnabled, context) return QuickActions( - actions = toQuickActions(cryptoData.actions, isRedesignEnabled, context), + actions = states.map { it.action }.toImmutableList(), onQuickActionClick = { quickActionUM -> tokenActionsHandler.handle( action = quickActionUM.toHandledAction(), @@ -30,6 +32,7 @@ object QuickActionsConverter { ) } }, + disabledActions = states.filterNot { it.isEnabled }.map { it.action }.toImmutableSet(), ) } @@ -45,30 +48,52 @@ object QuickActionsConverter { } /** - * Returns available actions filtered to [context]'s allow-list and ordered by it. - * Omitting [context] (default [TokenActionsContext.Markets]) yields all available actions in source order; - * a context with a non-null [TokenActionsContext.allowedActionsInOrder] filters to and orders by that list. + * Returns actions filtered and ordered for [context]. + * Omitting [context] (default [TokenActionsContext.Markets]) yields only available actions in source order. + * A context with a non-null [TokenActionsContext.allowedActionsInOrder] returns that list's actions in order, + * including unavailable ones (they are meant to be shown disabled by the caller). */ fun toQuickActions( actions: List, isRedesignEnabled: Boolean, context: TokenActionsContext = TokenActionsContext.Markets, - ): ImmutableList { - val available = actions.filter { it.unavailabilityReason == ScenarioUnavailabilityReason.None } - val allowed = context.allowedActionsInOrder - ?: return available.mapNotNull { it.toQuickActionUM(isRedesignEnabled) }.toImmutableList() + ): ImmutableList = + toQuickActionStates(actions, isRedesignEnabled, context).map { it.action }.toImmutableList() - val byBsAction = available.associateBy { it.toBsAction() } - val hasExchange = byBsAction.containsKey(TokenActionsBSContentUM.Action.Exchange) + private fun toQuickActionStates( + actions: List, + isRedesignEnabled: Boolean, + context: TokenActionsContext, + ): List { + val allowed = context.allowedActionsInOrder + ?: return actions + .filter { it.unavailabilityReason == ScenarioUnavailabilityReason.None } + .mapNotNull { action -> + action.toQuickActionUM(isRedesignEnabled)?.let { QuickActionState(it, isEnabled = true) } + } + + val byBsAction = actions.associateBy { it.toBsAction() } + val isExchangeAvailable = byBsAction[TokenActionsBSContentUM.Action.Exchange] + ?.unavailabilityReason == ScenarioUnavailabilityReason.None return allowed.mapNotNull { action -> when (action) { TokenActionsBSContentUM.Action.SendWithSwap -> - if (hasExchange) swapAndSendUM(isRedesignEnabled) else null - else -> byBsAction[action]?.toQuickActionUM(isRedesignEnabled) + if (isExchangeAvailable) { + QuickActionState(swapAndSendUM(isRedesignEnabled), isEnabled = true) + } else { + null + } + else -> { + val state = byBsAction[action] ?: return@mapNotNull null + val um = state.toQuickActionUM(isRedesignEnabled) ?: return@mapNotNull null + QuickActionState(um, isEnabled = state.unavailabilityReason == ScenarioUnavailabilityReason.None) + } } - }.toImmutableList() + } } + private data class QuickActionState(val action: QuickActionUM, val isEnabled: Boolean) + private fun swapAndSendUM(isRedesignEnabled: Boolean): QuickActionUM = if (isRedesignEnabled) QuickActionUM.V2.SwapAndSend else QuickActionUM.V1.SwapAndSend diff --git a/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverterTest.kt b/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverterTest.kt index b0028b644c..d6cecd4063 100644 --- a/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverterTest.kt +++ b/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverterTest.kt @@ -127,4 +127,55 @@ internal class QuickActionsConverterTest { assertThat(result).doesNotContain(QuickActionUM.V2.SwapAndSend) assertThat(result).doesNotContain(QuickActionUM.V2.Exchange(shouldShowBadge = false)) } + + @Test + fun `GIVEN buy and swap unavailable WHEN context is AddFunds THEN they are still shown (disabled) not hidden`() { + // Arrange + val actions = listOf( + TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.BuyUnavailable(cryptoCurrencyName = "BTC")), + TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.SingleWallet, shouldShowBadge = false), + TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None), + ) + + // Act + val result = QuickActionsConverter.toQuickActions( + actions = actions, + isRedesignEnabled = true, + context = TokenActionsContext.AddFunds, + ) + + // Assert + assertThat(result).containsExactly( + QuickActionUM.V2.Buy, + QuickActionUM.V2.Exchange(shouldShowBadge = false), + QuickActionUM.V2.Receive, + ).inOrder() + } + + @Test + fun `GIVEN swap and sell unavailable WHEN context is Transfer THEN swap and sell shown disabled and no swapAndSend`() { + // Arrange + val actions = listOf( + TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.SingleWallet, shouldShowBadge = false), + TokenActionsState.ActionState.Sell( + ScenarioUnavailabilityReason.NotSupportedBySellService(cryptoCurrencyName = "BTC"), + ), + ) + + // Act + val result = QuickActionsConverter.toQuickActions( + actions = actions, + isRedesignEnabled = true, + context = TokenActionsContext.Transfer, + ) + + // Assert + assertThat(result).containsExactly( + QuickActionUM.V2.Send, + QuickActionUM.V2.Exchange(shouldShowBadge = false), + QuickActionUM.V2.Sell, + ).inOrder() + assertThat(result).doesNotContain(QuickActionUM.V2.SwapAndSend) + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt index 75b406ee1b..6c821b7893 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt @@ -104,17 +104,20 @@ internal open class BaseActionsFactory( /** * Determines the unavailability reason for the SELL action * - * @param userWalletId the ID of the user's wallet + * @param userWallet the user's wallet * @param status the status of the cryptocurrency * @param sendUnavailabilityReason the reason for unavailability of the send action */ protected suspend fun getSellUnavailabilityReason( - userWalletId: UserWalletId, + userWallet: UserWallet, status: CryptoCurrencyStatus, sendUnavailabilityReason: ScenarioUnavailabilityReason, ): ScenarioUnavailabilityReason { + if (userWallet is UserWallet.Cold && userWallet.cardTypesResolver.isStart2Coin()) { + return ScenarioUnavailabilityReason.NotSupportedBySellService(status.currency.name) + } return rampStateManager.availableForSell( - userWalletId = userWalletId, + userWalletId = userWallet.walletId, status = status, sendUnavailabilityReason = sendUnavailabilityReason, ).fold( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt index 4d9837958f..f158738630 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt @@ -104,7 +104,7 @@ internal class CommonActionsFactory( // region Sell val sellUnavailabilityReason = getSellUnavailabilityReason( - userWalletId = userWallet.walletId, + userWallet = userWallet, status = cryptoCurrencyStatus, sendUnavailabilityReason = sendUnavailabilityReason, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt index 49084a3a21..f432fc0495 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt @@ -120,7 +120,7 @@ internal class OutdatedDataActionsFactory( // region Sell if (sendUnavailabilityReason == ScenarioUnavailabilityReason.None) { val sellUnavailabilityReason = getSellUnavailabilityReason( - userWalletId = userWallet.walletId, + userWallet = userWallet, status = cryptoCurrencyStatus, sendUnavailabilityReason = sendUnavailabilityReason, ) diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/ui/TokenActionsContentV2.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/ui/TokenActionsContentV2.kt index 20c555716f..bf4f21300b 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/ui/TokenActionsContentV2.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/ui/TokenActionsContentV2.kt @@ -127,14 +127,16 @@ private fun QuickActionsList(state: TokenActionsUM, modifier: Modifier = Modifie Modifier.testTag(TokenActionsTestTags.BUY_ACTION) else -> Modifier } + val isEnabled = actionUM !in state.quickActions.disabledActions TokenActionRow( modifier = actionModifier, iconRes = actionUM.icon, title = actionUM.title, description = actionUM.description, - onClick = { state.quickActions.onQuickActionClick(actionUM) }, + isEnabled = isEnabled, + onClick = { state.quickActions.onQuickActionClick(actionUM) }.takeIf { isEnabled }, onLongClick = { state.quickActions.onQuickActionLongClick(actionUM) } - .takeIf { actionUM.isLongClickAvailable }, + .takeIf { actionUM.isLongClickAvailable && isEnabled }, ) } } From e6968d7b35ed894f9ec12cedd2084f97b85378c7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 12:39:42 +0200 Subject: [PATCH 12/21] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 5 +++-- .../com/tangem/common/routing/AppRoute.kt | 1 + .../DefaultMarketsTokenDetailsComponent.kt | 2 +- .../market/details/MarketsTokenDetailsModel.kt | 1 + .../tokendetails/TokenDetailsComponent.kt | 1 + .../DefaultTokenDetailsComponent.kt | 18 ++++++++++-------- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index c43ead18f2..f7c46d1d64 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -21,7 +21,6 @@ import com.tangem.features.feed.entry.components.FeedEntryRoute import com.tangem.features.home.api.HomeComponent import com.tangem.features.hotwallet.* import com.tangem.features.kyc.KycComponent -import com.tangem.features.survey.SurveyComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensMode @@ -37,9 +36,10 @@ import com.tangem.features.send.api.NFTSendComponent import com.tangem.features.send.api.SendComponent import com.tangem.features.send.api.SendEntryPointComponent import com.tangem.features.staking.api.StakingComponent +import com.tangem.features.survey.SurveyComponent import com.tangem.features.swap.SwapComponent -import com.tangem.features.tangempay.components.TangemPayHotWalletOnboardingComponent import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent +import com.tangem.features.tangempay.components.TangemPayHotWalletOnboardingComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent import com.tangem.features.tangempay.components.TangemPayOnboardingComponent.Params.* import com.tangem.features.tokendetails.TokenDetailsComponent @@ -307,6 +307,7 @@ internal class ChildFactory @Inject constructor( userWalletId = route.userWalletId, currency = route.currency, navigationAction = route.navigationAction, + shouldShowMarketBlock = route.shouldShowMarketBlock, ), componentFactory = tokenDetailsComponentFactory, ) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 3b46824c4d..cd1e1253a0 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -64,6 +64,7 @@ sealed class AppRoute(val path: String) : Route { val userWalletId: UserWalletId, val currency: CryptoCurrency, val navigationAction: NavigationAction? = null, + val shouldShowMarketBlock: Boolean = true, ) : AppRoute(path = "/currency_details/${userWalletId.stringValue}/${currency.id.value}") @Serializable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt index a185cc8ed4..efbc99093b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt @@ -76,7 +76,7 @@ internal class DefaultMarketsTokenDetailsComponent( } private val portfolioBlockComponent: PortfolioBlockComponent? = - if (designFeatureToggles.isRedesignEnabled) { + if (updatedParams.shouldShowPortfolio && designFeatureToggles.isRedesignEnabled) { portfolioBlockComponentFactory.create( context = child("portfolio_block"), params = PortfolioBlockComponent.Params(token = updatedParams.token), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt index a87365998c..d5976c19fa 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt @@ -401,6 +401,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( AppRoute.CurrencyDetails( userWalletId = result.wallet.walletId, currency = result.addedCurrency.currency, + shouldShowMarketBlock = false, ), ) } diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/TokenDetailsComponent.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/TokenDetailsComponent.kt index 8192a29732..05acec665c 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/TokenDetailsComponent.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/TokenDetailsComponent.kt @@ -12,6 +12,7 @@ interface TokenDetailsComponent : ComposableContentComponent { val userWalletId: UserWalletId, val currency: CryptoCurrency, val navigationAction: NavigationAction? = null, + val shouldShowMarketBlock: Boolean = true, ) interface Factory : ComponentFactory diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index aec7dd1d42..2fdff86eb3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -22,12 +22,12 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreenLegacy import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.ChooseAddressBottomSheetComponent -import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.CloreMigrationBottomSheetComponent import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.DynamicAddressesBottomSheetComponent import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.TransferBottomSheetComponent -import com.tangem.features.rating.RatingComponent +import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent import com.tangem.features.markets.token.block.TokenMarketBlockComponent +import com.tangem.features.rating.RatingComponent import com.tangem.features.tokendetails.ExpressTransactionsComponent import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokenreceive.TokenReceiveComponent @@ -88,12 +88,14 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( }, ) - private val tokenMarketBlockComponent = params.currency.toTokenMarketParam()?.let { tokenMarketParams -> - tokenMarketBlockComponentFactory.create( - appComponentContext = child("tokenMarketBlockComponent"), - params = tokenMarketParams, - ) - } + private val tokenMarketBlockComponent = params.currency.toTokenMarketParam() + ?.takeIf { params.shouldShowMarketBlock } + ?.let { tokenMarketParams -> + tokenMarketBlockComponentFactory.create( + appComponentContext = child("tokenMarketBlockComponent"), + params = tokenMarketParams, + ) + } private val yieldSupplyComponent = yieldSupplyComponentFactory.create( context = child("tokenYieldSupplyComponent"), From 4d08c4404d76187d4a33a1d0399d896a7b2a3b20 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 13:59:09 +0300 Subject: [PATCH 13/21] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index b1ff87ea9d..a5de61882f 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -57,7 +57,7 @@ }, { "name": "TWI_1326_YIELD_MODE_SWAP_ENABLED", - "version": "6.0" + "version": "6.1" }, { "name": "ADDRESS_SYNC_ENABLED", From b7ea0b5d2cd3627820e8315a305f8557057366af Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 18:47:45 +0500 Subject: [PATCH 14/21] Updated on 2026-08-14 --- data/markets/build.gradle.kts | 4 + .../converters/TokenMarketListConverter.kt | 4 +- .../TokenMarketListConverterTest.kt | 145 ++++++++++++++++++ .../blockchainsdk/compatibility/L2Networks.kt | 47 +++++- .../compatibility/L2NetworksTest.kt | 92 +++++++++++ 5 files changed, 283 insertions(+), 9 deletions(-) create mode 100644 data/markets/src/test/java/com/tangem/data/markets/converters/TokenMarketListConverterTest.kt create mode 100644 libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/compatibility/L2NetworksTest.kt diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index bce4fe8a70..e25a0fa07d 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -49,4 +49,8 @@ dependencies { ksp(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion + + // region Tests dependencies + testImplementation(projects.test.core) + // endregion } diff --git a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt index 86b29f8006..0ef9c79726 100644 --- a/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt +++ b/data/markets/src/main/java/com/tangem/data/markets/converters/TokenMarketListConverter.kt @@ -1,5 +1,6 @@ package com.tangem.data.markets.converters +import com.tangem.blockchainsdk.compatibility.applyL2Compatibility import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse import com.tangem.domain.markets.TokenMarket import com.tangem.domain.markets.TokenMarketListWithMaxApy @@ -19,7 +20,8 @@ internal object TokenMarketListConverter : Converter + val tokens = value.tokens.map { rawToken -> + val token = rawToken.applyL2Compatibility() val stakingRate = token.stakingOpportunities ?.mapNotNull { it.apy } ?.max() diff --git a/data/markets/src/test/java/com/tangem/data/markets/converters/TokenMarketListConverterTest.kt b/data/markets/src/test/java/com/tangem/data/markets/converters/TokenMarketListConverterTest.kt new file mode 100644 index 0000000000..e48c695c8e --- /dev/null +++ b/data/markets/src/test/java/com/tangem/data/markets/converters/TokenMarketListConverterTest.kt @@ -0,0 +1,145 @@ +package com.tangem.data.markets.converters + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchainsdk.compatibility.l2BlockchainsList +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse +import com.tangem.domain.markets.TokenMarket +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +internal class TokenMarketListConverterTest { + + @Test + fun `GIVEN ethereum coin with networks WHEN convert THEN L2 networks are appended`() { + // Arrange + val response = createResponse( + createToken(id = "ethereum", networks = listOf(createNetwork(networkId = "ethereum"))), + ) + + // Act + val actual = TokenMarketListConverter.convert(response) + + // Assert + val networkIds = actual.tokens.single().networks?.map(TokenMarket.Network::networkId) + val expectedNetworkIds = listOf("ethereum") + l2BlockchainsList.map { it.toNetworkId() } + assertThat(networkIds).containsExactlyElementsIn(expectedNetworkIds) + assertThat(networkIds).containsAtLeast("arbitrum-one", "optimistic-ethereum", "base") + } + + @Test + fun `GIVEN ethereum coin with networks WHEN convert THEN appended L2 networks are native coin entries`() { + // Arrange + val response = createResponse( + createToken(id = "ethereum", networks = listOf(createNetwork(networkId = "ethereum"))), + ) + + // Act + val actual = TokenMarketListConverter.convert(response) + + // Assert + val l2Networks = actual.tokens.single().networks.orEmpty().filter { it.networkId != "ethereum" } + assertThat(l2Networks).isNotEmpty() + assertThat(l2Networks.mapNotNull(TokenMarket.Network::contractAddress)).isEmpty() + } + + @Test + fun `GIVEN ethereum coin with backend-provided L2 network WHEN convert THEN backend entry wins without duplicates`() { + // Arrange + val backendArbitrum = createNetwork(networkId = "arbitrum-one", decimalCount = 18) + val response = createResponse( + createToken(id = "ethereum", networks = listOf(createNetwork(networkId = "ethereum"), backendArbitrum)), + ) + + // Act + val actual = TokenMarketListConverter.convert(response) + + // Assert + val networks = actual.tokens.single().networks.orEmpty() + assertThat(networks.map(TokenMarket.Network::networkId)).containsNoDuplicates() + val arbitrum = networks.single { it.networkId == "arbitrum-one" } + assertThat(arbitrum.decimalCount).isEqualTo(18) + } + + @Test + fun `GIVEN non-ethereum token with networks WHEN convert THEN networks stay unchanged`() { + // Arrange + val tetherNetworks = listOf( + createNetwork( + networkId = "ethereum", + contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7", + decimalCount = 6, + ), + createNetwork( + networkId = "tron", + contractAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + decimalCount = 6, + ), + ) + val response = createResponse(createToken(id = "tether", networks = tetherNetworks)) + + // Act + val actual = TokenMarketListConverter.convert(response) + + // Assert + val expected = tetherNetworks.map { network -> + TokenMarket.Network( + networkId = network.networkId, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + } + assertThat(actual.tokens.single().networks).isEqualTo(expected) + } + + @Test + fun `GIVEN ethereum coin without networks WHEN convert THEN networks stay null`() { + // Arrange + val response = createResponse(createToken(id = "ethereum", networks = null)) + + // Act + val actual = TokenMarketListConverter.convert(response) + + // Assert + assertThat(actual.tokens.single().networks).isNull() + } + + private fun createResponse(vararg tokens: TokenMarketListResponse.Token) = TokenMarketListResponse( + imageHost = "https://img.tangem.org/", + tokens = tokens.toList(), + total = tokens.size, + limit = 20, + offset = 0, + timestamp = 1L, + summary = null, + ) + + private fun createToken( + id: String, + networks: List?, + name: String = id, + symbol: String = id.take(n = 3).uppercase(), + ) = TokenMarketListResponse.Token( + id = id, + name = name, + symbol = symbol, + currentPrice = BigDecimal.ONE, + priceChangePercentage = null, + marketRating = null, + marketCap = null, + isUnderMarketCapLimit = null, + stakingOpportunities = null, + maxYieldApy = null, + networks = networks, + ) + + private fun createNetwork( + networkId: String, + contractAddress: String? = null, + decimalCount: Int? = null, + ) = TokenMarketListResponse.Token.Network( + networkId = networkId, + contractAddress = contractAddress, + decimalCount = decimalCount, + ) +} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt index 782e3e88ff..f60e5218ee 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/compatibility/L2Networks.kt @@ -4,6 +4,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse +import com.tangem.datasource.api.markets.models.response.TokenMarketListResponse import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse @@ -36,18 +37,48 @@ fun List.applyL2Compatibility(coinId: String): List< fun TokenMarketInfoResponse.applyL2Compatibility(coinId: String): TokenMarketInfoResponse { val networks = this.networks ?: return this - return if (coinId == ETHEREUM_COIN_ID) { - val l2Networks = l2BlockchainsList.map { blockchain -> + if (coinId != ETHEREUM_COIN_ID) return this + + val networksWithL2 = networks.appendMissingL2Networks( + networkId = { it.networkId }, + createNetwork = { networkId -> TokenMarketInfoResponse.Network( - networkId = blockchain.toNetworkId(), + networkId = networkId, contractAddress = null, decimalCount = null, ) - } - this.copy(networks = networks + l2Networks) - } else { - this - } + }, + ) + return this.copy(networks = networksWithL2) +} + +fun TokenMarketListResponse.Token.applyL2Compatibility(): TokenMarketListResponse.Token { + val networks = this.networks ?: return this + if (id != ETHEREUM_COIN_ID) return this + + val networksWithL2 = networks.appendMissingL2Networks( + networkId = { it.networkId }, + createNetwork = { networkId -> + TokenMarketListResponse.Token.Network( + networkId = networkId, + contractAddress = null, + decimalCount = null, + ) + }, + ) + return this.copy(networks = networksWithL2) +} + +private inline fun List.appendMissingL2Networks( + networkId: (T) -> String, + createNetwork: (networkId: String) -> T, +): List { + val existingNetworkIds = mapTo(hashSetOf(), networkId) + val missingL2Networks = l2BlockchainsList + .map { it.toNetworkId() } + .filterNot { it in existingNetworkIds } + .map(createNetwork) + return this + missingL2Networks } fun getTokenIdIfL2Network(tokenId: String): String { diff --git a/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/compatibility/L2NetworksTest.kt b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/compatibility/L2NetworksTest.kt new file mode 100644 index 0000000000..1dc7fda86c --- /dev/null +++ b/libs/blockchain-sdk/src/test/java/com/tangem/blockchainsdk/compatibility/L2NetworksTest.kt @@ -0,0 +1,92 @@ +package com.tangem.blockchainsdk.compatibility + +import com.google.common.truth.Truth.assertThat +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.datasource.api.markets.models.response.TokenMarketInfoResponse +import org.junit.jupiter.api.Test +import java.math.BigDecimal + +internal class L2NetworksTest { + + @Test + fun `GIVEN ethereum info with networks WHEN applyL2Compatibility THEN missing L2 networks are appended`() { + // Arrange + val response = createInfoResponse(id = "ethereum", networks = listOf(createNetwork(networkId = "ethereum"))) + + // Act + val actual = response.applyL2Compatibility(coinId = "ethereum") + + // Assert + val networkIds = actual.networks?.map(TokenMarketInfoResponse.Network::networkId) + val expectedNetworkIds = listOf("ethereum") + l2BlockchainsList.map { it.toNetworkId() } + assertThat(networkIds).containsExactlyElementsIn(expectedNetworkIds) + } + + @Test + fun `GIVEN ethereum info with backend-provided L2 network WHEN applyL2Compatibility THEN backend entry wins without duplicates`() { + // Arrange + val backendArbitrum = createNetwork(networkId = "arbitrum-one", decimalCount = 18) + val response = createInfoResponse( + id = "ethereum", + networks = listOf(createNetwork(networkId = "ethereum"), backendArbitrum), + ) + + // Act + val actual = response.applyL2Compatibility(coinId = "ethereum") + + // Assert + val networks = actual.networks.orEmpty() + assertThat(networks.map(TokenMarketInfoResponse.Network::networkId)).containsNoDuplicates() + assertThat(networks.single { it.networkId == "arbitrum-one" }).isEqualTo(backendArbitrum) + } + + @Test + fun `GIVEN non-ethereum info WHEN applyL2Compatibility THEN networks stay unchanged`() { + // Arrange + val tetherNetworks = listOf( + createNetwork( + networkId = "ethereum", + contractAddress = "0xdAC17F958D2ee523a2206206994597C13D831ec7", + decimalCount = 6, + ), + ) + val response = createInfoResponse(id = "tether", networks = tetherNetworks) + + // Act + val actual = response.applyL2Compatibility(coinId = "tether") + + // Assert + assertThat(actual.networks).isEqualTo(tetherNetworks) + } + + private fun createInfoResponse( + id: String, + networks: List?, + ) = TokenMarketInfoResponse( + id = id, + name = id, + symbol = id.take(n = 3).uppercase(), + currentPrice = BigDecimal.ONE, + priceChangePercentage = null, + networks = networks, + shortDescription = null, + fullDescription = null, + insights = null, + metrics = null, + securityData = null, + links = null, + pricePerformance = null, + exchangesAmount = null, + ) + + private fun createNetwork( + networkId: String, + contractAddress: String? = null, + decimalCount: Int? = null, + ) = TokenMarketInfoResponse.Network( + networkId = networkId, + exchangeable = false, + contractAddress = contractAddress, + decimalCount = decimalCount, + ) +} \ No newline at end of file From c6c7cf61ab18da6fe60cf62a613d1502e453d3b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 15:49:08 +0200 Subject: [PATCH 15/21] Updated on 2026-08-14 --- .../common/ui/markets/MarketListItemV2.kt | 2 + .../onramp/analytics/OnrampAnalyticsEvent.kt | 14 +++ .../tokens/actions/BaseActionsFactory.kt | 31 ------ .../tokens/actions/CommonActionsFactory.kt | 10 +- .../actions/OutdatedDataActionsFactory.kt | 10 +- .../actions/UnreachableActionsFactory.kt | 10 +- features/onramp/impl/build.gradle.kts | 4 + .../main/entity/OnrampMainComponentUM.kt | 5 + .../main/entity/factory/OnrampStateFactory.kt | 47 ++++++++- .../main/model/OnrampMainComponentModel.kt | 68 ++++++++++--- .../onramp/main/ui/OnrampAmountContent.kt | 4 +- .../main/ui/OnrampMainComponentContent.kt | 15 ++- .../tokenlist/model/OnrampTokenListModel.kt | 16 +--- .../entity/factory/OnrampStateFactoryTest.kt | 95 +++++++++++++++++++ 14 files changed, 241 insertions(+), 90 deletions(-) create mode 100644 features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactoryTest.kt diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItemV2.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItemV2.kt index 985380f91c..ba70d244ec 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItemV2.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItemV2.kt @@ -3,6 +3,7 @@ package com.tangem.common.ui.markets import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -73,6 +74,7 @@ fun MarketListItemContentV2(model: MarketsListItemUM, modifier: Modifier = Modif tangemIconUM = TangemIconUM.Url(model.iconUrl, fallbackRes = R.drawable.ic_custom_token_44), modifier = Modifier .size(TangemTheme.dimens2.x10) + .clip(RoundedCornerShape(8.dp)) .layoutId(layoutId = TangemRowLayoutId.HEAD), ) diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt index 5010202433..1711ccdcbd 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/analytics/OnrampAnalyticsEvent.kt @@ -1,6 +1,7 @@ package com.tangem.domain.onramp.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_DESCRIPTION import com.tangem.core.analytics.models.AnalyticsParam.Key.PAYMENT_METHOD @@ -208,4 +209,17 @@ sealed class OnrampAnalyticsEvent( event = "Button - All Offers", params = emptyMap(), ) + + data class NoticeBuyNotSupported( + private val source: OnrampSource, + private val tokenSymbol: String, + private val blockchain: String, + ) : OnrampAnalyticsEvent( + event = "Notice - Buy Not Supported", + params = mapOf( + SOURCE to source.analyticsName, + TOKEN_PARAM to tokenSymbol, + BLOCKCHAIN to blockchain, + ), + ) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt index 6c821b7893..4cc9fc0ed5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt @@ -54,37 +54,6 @@ internal open class BaseActionsFactory( } } - /** - * Determines the unavailability reason for the BUY action - * - * @param userWallet the user's cold wallet - * @param currency the cryptocurrency to check - * @param requirementsDeferred a deferred object containing the asset requirements condition - */ - protected suspend fun getOnrampUnavailabilityReason( - userWallet: UserWallet, - currency: CryptoCurrency, - requirementsDeferred: Deferred?, - ): ScenarioUnavailabilityReason { - // Start2Coin (S2C) are legacy single-currency cards that do not support buying crypto in-app - // (historically only Receive/Send were offered for them). - if (userWallet is UserWallet.Cold && userWallet.cardTypesResolver.isStart2Coin()) { - return ScenarioUnavailabilityReason.BuyUnavailable(currency.name) - } - - val onrampUnavailabilityReason = rampStateManager.availableForBuy( - userWallet = userWallet, - cryptoCurrency = currency, - ) - val shouldCheckAssetRequirements = - onrampUnavailabilityReason == ScenarioUnavailabilityReason.None && requirementsDeferred != null - return if (shouldCheckAssetRequirements) { - getReceiveScenario(requirementsDeferred.await()) - } else { - onrampUnavailabilityReason - } - } - /** * Determines the unavailability reason for the SEND action * diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt index f158738630..f244724fd4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt @@ -50,14 +50,6 @@ internal class CommonActionsFactory( null } - val onrampUnavailabilityReasonDeferred = async { - getOnrampUnavailabilityReason( - userWallet = userWallet, - currency = cryptoCurrencyStatus.currency, - requirementsDeferred = requirementsDeferred, - ) - } - val sendUnavailabilityReasonDeferred = async { getSendUnavailabilityReason(userWalletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus) } @@ -99,7 +91,7 @@ internal class CommonActionsFactory( // endregion // region Buy - addBuyAction(reason = onrampUnavailabilityReasonDeferred.await()) + addBuyAction(reason = ScenarioUnavailabilityReason.None) // endregion // region Sell diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt index f432fc0495..111e4685a5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt @@ -52,14 +52,6 @@ internal class OutdatedDataActionsFactory( null } - val onrampUnavailabilityReasonDeferred = async { - getOnrampUnavailabilityReason( - userWallet = userWallet, - currency = cryptoCurrencyStatus.currency, - requirementsDeferred = requirementsDeferred, - ) - } - val sendUnavailabilityReasonDeferred = if (sources.networkSource == StatusSource.ACTUAL) { async { getSendUnavailabilityReason( @@ -87,7 +79,7 @@ internal class OutdatedDataActionsFactory( // endregion // region Buy - addBuyAction(reason = onrampUnavailabilityReasonDeferred.await()) + addBuyAction(reason = ScenarioUnavailabilityReason.None) // endregion // region Stake diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt index 11a8155d1d..23e188e3dd 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt @@ -35,14 +35,6 @@ internal class UnreachableActionsFactory( null } - val onrampUnavailabilityReasonDeferred = async { - getOnrampUnavailabilityReason( - userWallet = userWallet, - currency = cryptoCurrencyStatus.currency, - requirementsDeferred = requirementsDeferred, - ) - } - val hideTokenUnavailabilityReason = getTokenHideUnavailabilityReason(userWallet) // endregion @@ -52,7 +44,7 @@ internal class UnreachableActionsFactory( // endregion // region Buy - addBuyAction(reason = onrampUnavailabilityReasonDeferred.await()) + addBuyAction(reason = ScenarioUnavailabilityReason.None) // endregion // region Receive diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 87252298e9..6a21e2425b 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -82,4 +82,8 @@ dependencies { /** Other */ implementation(deps.decompose.ext.compose) implementation(deps.kotlin.immutable.collections) + + /** Tests */ + testImplementation(projects.test.core) + testImplementation(projects.common.test) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt index b9e86bee09..07967d59c2 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -3,6 +3,7 @@ package com.tangem.features.onramp.main.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.ds.message.TangemMessageUM import com.tangem.core.ui.extensions.TextReference @Immutable @@ -11,9 +12,12 @@ internal sealed interface OnrampMainComponentUM { val topBarConfig: OnrampMainTopBarUM val errorNotification: NotificationUM? + val buyNotSupportedMessage: TangemMessageUM? + data class InitialLoading( override val topBarConfig: OnrampMainTopBarUM, override val errorNotification: NotificationUM?, + override val buyNotSupportedMessage: TangemMessageUM? = null, ) : OnrampMainComponentUM data class Content( @@ -22,6 +26,7 @@ internal sealed interface OnrampMainComponentUM { val amountBlockState: OnrampAmountBlockUM, val offersBlockState: OnrampOffersBlockUM, val onrampAmountButtonUMState: OnrampAmountButtonUMState, + override val buyNotSupportedMessage: TangemMessageUM? = null, ) : OnrampMainComponentUM } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index 46e69fe061..132bb8aa5b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -7,10 +7,12 @@ import androidx.compose.ui.text.input.KeyboardType import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.core.ui.ds.message.TangemMessageIconPosition +import com.tangem.core.ui.ds.message.TangemMessageUM +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.error.OnrampError @@ -21,6 +23,7 @@ import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.main.entity.* import com.tangem.utils.Provider import java.math.BigDecimal +import com.tangem.core.ui.R as CoreUiR internal class OnrampStateFactory( private val currentStateProvider: Provider, @@ -120,6 +123,42 @@ internal class OnrampStateFactory( } } + fun getBuyNotSupportedState(state: OnrampMainComponentUM = currentStateProvider()): OnrampMainComponentUM { + val message = buildBuyNotSupportedMessage() + + return when (state) { + is OnrampMainComponentUM.Content -> state.copy( + buyNotSupportedMessage = message, + errorNotification = null, + offersBlockState = OnrampOffersBlockUM.Empty, + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, + amountBlockState = state.amountBlockState.copy( + amountFieldModel = state.amountBlockState.amountFieldModel.copy(isError = true), + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, + ), + ) + is OnrampMainComponentUM.InitialLoading -> state.copy( + buyNotSupportedMessage = message, + errorNotification = null, + ) + } + } + + private fun buildBuyNotSupportedMessage(): TangemMessageUM = TangemMessageUM( + id = "buy_not_supported", + title = resourceReference( + id = R.string.onramp_token_is_not_supported_banner_title, + formatArgs = wrappedList(cryptoCurrency.name), + ), + subtitle = resourceReference(R.string.onramp_token_is_not_supported_banner_subtitle), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = CoreUiR.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + iconPosition = TangemMessageIconPosition.Leading, + ) + private fun getNoPairsErrorState(): OnrampMainComponentUM { val state = currentStateProvider() val contentState = state as? OnrampMainComponentUM.Content ?: return state diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 86098da81b..5c85ec5a96 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -2,6 +2,7 @@ package com.tangem.features.onramp.main.model import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -9,12 +10,15 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.components.fields.InputManager import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.onramp.* import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.OnrampAvailability +import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.main.OnrampMainComponent import com.tangem.features.onramp.main.entity.* @@ -30,9 +34,9 @@ import com.tangem.utils.coroutines.PeriodicTask import com.tangem.utils.coroutines.SingleTaskScheduler import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.isNullOrZero +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import com.tangem.utils.logging.TangemLogger import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -46,6 +50,7 @@ internal class OnrampMainComponentModel @Inject constructor( private val fetchQuotesUseCase: OnrampFetchQuotesUseCase, private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase, private val fetchPairsUseCase: OnrampFetchPairsUseCase, + private val rampStateManager: RampStateManager, private val amountInputManager: InputManager, private val getOnrampOffersUseCase: GetOnrampOffersUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, @@ -222,6 +227,8 @@ internal class OnrampMainComponentModel @Inject constructor( } private fun handleOnrampAvailability(availability: OnrampAvailability) { + // "Buy not supported" notification has priority over the residency flow. + if (state.value.buyNotSupportedMessage != null) return when (availability) { is OnrampAvailability.Available -> Unit is OnrampAvailability.ConfirmResidency, @@ -274,23 +281,30 @@ internal class OnrampMainComponentModel @Inject constructor( ifLeft = ::handleOnrampError, ifRight = { country -> if (country == null) return@onEach - state.update { prevState -> - when (prevState) { - is OnrampMainComponentUM.Content -> { - amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) - } - is OnrampMainComponentUM.InitialLoading -> { - stateFactory.getReadyState(country.defaultCurrency, params.initialFiatAmount) - } - } + // Resolve token-level buy support BEFORE emitting any Content state, so an + // unsupported token never briefly shows an enabled amount field — otherwise it + // would grab focus and flash the keyboard before being disabled. + if (isTokenNotSupportedForBuy()) { + showBuyNotSupported(country) + } else { + state.update { prevState -> getCountryUpdatedState(prevState, country) } + updatePairsAndQuotes() } - updatePairsAndQuotes() }, ) } .launchIn(modelScope) } + private fun getCountryUpdatedState( + prevState: OnrampMainComponentUM, + country: OnrampCountry, + ): OnrampMainComponentUM = when (prevState) { + is OnrampMainComponentUM.Content -> amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) + is OnrampMainComponentUM.InitialLoading -> + stateFactory.getReadyState(country.defaultCurrency, params.initialFiatAmount) + } + private fun subscribeToQuotesUpdate() { getOnrampQuotesUseCase.invoke() .conflate() @@ -356,11 +370,43 @@ internal class OnrampMainComponentModel @Inject constructor( ) } + private suspend fun isTokenNotSupportedForBuy(): Boolean { + // Token-level "cannot be bought", independent of country: the asset is either flagged as + // not onrampable (BuyUnavailable) or absent from the express asset list (AssetNotFound). + // Transient express states (loading/unreachable) are NOT treated as "not supported". + val reason = rampStateManager.availableForBuy( + userWallet = userWallet, + cryptoCurrency = params.cryptoCurrency, + ) + return reason is ScenarioUnavailabilityReason.BuyUnavailable || + reason is ScenarioUnavailabilityReason.AssetNotFound + } + private fun handleOnrampError(onrampError: OnrampError) { TangemLogger.e(onrampError.toString()) state.update { stateFactory.getOnrampErrorState(onrampError) } } + private fun showBuyNotSupported(country: OnrampCountry) { + if (state.value.buyNotSupportedMessage != null) return + + analyticsEventHandler.send( + OnrampAnalyticsEvent.NoticeBuyNotSupported( + source = params.source, + tokenSymbol = params.cryptoCurrency.symbol, + blockchain = params.cryptoCurrency.network.name, + ), + ) + quotesTaskScheduler.cancelTask() + // "Not supported" has priority: hide the residency bottom sheet if it was already shown. + bottomSheetNavigation.dismiss() + // Emit the not-supported state in a single update built from the ready state, so the amount + // field never appears enabled first (no focus/keyboard flash). + state.update { prevState -> + stateFactory.getBuyNotSupportedState(getCountryUpdatedState(prevState, country)) + } + } + private fun sendOnrampQuotesErrorAnalytic(quotes: List) { quotes.forEach { errorState -> when (errorState) { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt index e0cedd00a0..5afa3dab84 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt @@ -125,7 +125,9 @@ private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: Strin ) LaunchedEffect(key1 = Unit) { - requester.requestFocus() + if (!amountField.isError) { + requester.requestFocus() + } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt index 2b24ad2fb9..2c61db8413 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.ds.message.TangemMessage import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.WindowInsetsZero @@ -75,7 +76,7 @@ private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { OnrampAmountContentLoading() - if (state.errorNotification != null) Notification(config = state.errorNotification.config) + OnrampNotifications(state = state) } } @@ -134,6 +135,16 @@ private fun Content(state: OnrampMainComponentUM.Content, modifier: Modifier = M OnrampOffersContent(state = state.offersBlockState) - if (state.errorNotification != null) Notification(config = state.errorNotification.config) + OnrampNotifications(state = state) + } +} + +@Composable +private fun OnrampNotifications(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { + val buyNotSupportedMessage = state.buyNotSupportedMessage + val errorNotification = state.errorNotification + when { + buyNotSupportedMessage != null -> TangemMessage(messageUM = buyNotSupportedMessage, modifier = modifier) + errorNotification != null -> Notification(config = errorNotification.config, modifier = modifier) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index b11f85dce7..18210052a3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -23,7 +23,6 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.tokens.GetAssetRequirementsUseCase -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM @@ -270,9 +269,7 @@ internal class OnrampTokenListModel @Inject constructor( val isNotUnreachable = status.value !is CryptoCurrencyStatus.Unreachable val isAvailable = when (params.filterOperation) { - OnrampOperation.BUY -> { - isAvailableForBuy - } // unreachable state is available for Buy operation + OnrampOperation.BUY -> true OnrampOperation.SELL -> isNotUnreachable OnrampOperation.SWAP -> { isNotUnreachable && isAvailableForBuy @@ -295,12 +292,7 @@ internal class OnrampTokenListModel @Inject constructor( private suspend fun checkAvailabilityByOperation(status: CryptoCurrencyStatus): Boolean { return when (params.filterOperation) { - OnrampOperation.BUY -> { - rampStateManager.availableForBuy( - userWallet = userWallet, - cryptoCurrency = status.currency, - ).isAvailable() - } + OnrampOperation.BUY -> true OnrampOperation.SELL -> { rampStateManager.availableForSell( userWalletId = userWallet.walletId, @@ -318,8 +310,4 @@ internal class OnrampTokenListModel @Inject constructor( } } } - - private fun ScenarioUnavailabilityReason.isAvailable(): Boolean { - return this == ScenarioUnavailabilityReason.None - } } \ No newline at end of file diff --git a/features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactoryTest.kt b/features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactoryTest.kt new file mode 100644 index 0000000000..e5beb49d45 --- /dev/null +++ b/features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactoryTest.kt @@ -0,0 +1,95 @@ +package com.tangem.features.onramp.main.entity.factory + +import com.google.common.truth.Truth.assertThat +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.onramp.model.OnrampCurrency +import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUMState +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.OnrampOffersBlockUM +import com.tangem.features.onramp.main.entity.OnrampSecondaryFieldErrorUM +import com.tangem.utils.Provider +import io.mockk.mockk +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +internal class OnrampStateFactoryTest { + + private lateinit var currentState: OnrampMainComponentUM + + private val cryptoCurrency = MockCryptoCurrencyFactory().ethereum + + private val factory = OnrampStateFactory( + currentStateProvider = Provider { currentState }, + onrampAmountButtonUMStateFactory = OnrampAmountButtonUMStateFactory(), + cryptoCurrency = cryptoCurrency, + onrampIntents = mockk(relaxed = true), + ) + + @Test + fun `GIVEN initial loading with error WHEN getBuyNotSupportedState THEN shows None message and clears error`() { + // Arrange + currentState = factory.getInitialState(currency = "BTC", onClose = {}, openSettings = {}) + .copy(errorNotification = mockk()) + + // Act + val result = factory.getBuyNotSupportedState() + + // Assert + val message = result.buyNotSupportedMessage + assertThat(message).isNotNull() + assertThat(message!!.messageEffect).isEqualTo(TangemMessageEffect.None) + assertThat(message.title).isEqualTo( + resourceReference( + id = R.string.onramp_token_is_not_supported_banner_title, + formatArgs = wrappedList(cryptoCurrency.name), + ), + ) + assertThat(message.subtitle).isEqualTo( + resourceReference(R.string.onramp_token_is_not_supported_banner_subtitle), + ) + assertThat(result.errorNotification).isNull() + } + + @Test + fun `GIVEN content with errors WHEN getBuyNotSupportedState THEN message has priority and other errors hidden`() { + // Arrange + currentState = factory.getInitialState(currency = "BTC", onClose = {}, openSettings = {}) + val content = factory.getReadyState(currency = USD_CURRENCY) as OnrampMainComponentUM.Content + currentState = content.copy( + errorNotification = mockk(), + offersBlockState = OnrampOffersBlockUM.Loading, + onrampAmountButtonUMState = OnrampAmountButtonUMState.Loaded(persistentListOf()), + amountBlockState = content.amountBlockState.copy( + amountFieldModel = content.amountBlockState.amountFieldModel.copy(isError = true), + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error(stringReference("error")), + ), + ) + + // Act + val result = factory.getBuyNotSupportedState() as OnrampMainComponentUM.Content + + // Assert + assertThat(result.buyNotSupportedMessage).isNotNull() + assertThat(result.errorNotification).isNull() + assertThat(result.offersBlockState).isEqualTo(OnrampOffersBlockUM.Empty) + assertThat(result.onrampAmountButtonUMState).isEqualTo(OnrampAmountButtonUMState.None) + assertThat(result.amountBlockState.secondaryFieldModel).isEqualTo(OnrampSecondaryFieldErrorUM.Empty) + // Amount input is locked (disabled via isError) — like the unsupported-country case. + assertThat(result.amountBlockState.amountFieldModel.isError).isTrue() + } + + private companion object { + val USD_CURRENCY = OnrampCurrency( + name = "US Dollar", + code = "USD", + image = null, + precision = 2, + unit = "$", + ) + } +} \ No newline at end of file From 9bcc68222d392c01b53c85e0247cd336a4016fd3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Jul 2026 17:40:34 +0200 Subject: [PATCH 16/21] Updated on 2026-08-14 --- .../core/ui/ds/tabs/TangemSegmentedPicker.kt | 6 +++++ .../portfolioblock/ui/PortfolioBlock.kt | 19 ++++++++++++--- .../feed/ui/market/list/components/Options.kt | 8 ++++++- .../ui/components/TokenDetailsBalanceBlock.kt | 24 ++++++++++++------- .../wallet/ui/components/WalletItemBlocks.kt | 10 +++++++- .../ui/components/common/WalletBalance.kt | 17 ++++++++++++- .../ui/components/common/WalletTopBar.kt | 16 +++++++++---- 7 files changed, 81 insertions(+), 19 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemSegmentedPicker.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemSegmentedPicker.kt index e8cf99da89..54d5069530 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemSegmentedPicker.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemSegmentedPicker.kt @@ -29,6 +29,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEachIndexed import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.utils.extensions.indexOfFirstOrNull @@ -281,6 +283,7 @@ private fun Segment( modifier: Modifier = Modifier, minSegmentWidth: Dp = Dp.Unspecified, ) { + val hapticManager = LocalHapticManager.current Box( modifier = modifier .defaultMinSize(minWidth = minSegmentWidth) @@ -288,6 +291,9 @@ private fun Segment( indication = null, interactionSource = remember { MutableInteractionSource() }, ) { + if (selectedIndex.value != index) { + hapticManager.perform(TangemHapticEffect.View.SegmentTick) + } selectedIndex.value = index onClick() }, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/PortfolioBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/PortfolioBlock.kt index 22a447786c..77df065ac6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/PortfolioBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolioblock/ui/PortfolioBlock.kt @@ -35,6 +35,8 @@ import com.tangem.core.ui.ds.row.TangemRowContainer import com.tangem.core.ui.ds.row.TangemRowLayoutId import com.tangem.core.ui.ds2.fade.TangemFade import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -96,6 +98,7 @@ internal fun PortfolioBlock(state: PortfolioBlockUM, modifier: Modifier = Modifi @Composable private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = Modifier) { + val hapticManager = LocalHapticManager.current FloatingCard(modifier = modifier) { TangemRowContainer(modifier = Modifier.clickableSingle(onClick = state.onRowClick)) { Text( @@ -128,7 +131,10 @@ private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = M iconPosition = TangemButtonIconPosition.Start, shape = TangemButtonShape.Rounded, size = TangemButtonSize.X9, - onClick = state.onAddFundsClick, + onClick = { + hapticManager.perform(TangemHapticEffect.View.ContextClick) + state.onAddFundsClick() + }, ), ) @@ -144,7 +150,10 @@ private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = M ), shape = TangemButtonShape.Rounded, size = TangemButtonSize.X9, - onClick = state.onRowClick, + onClick = { + hapticManager.perform(TangemHapticEffect.View.ContextClick) + state.onRowClick() + }, ), ) } @@ -153,6 +162,7 @@ private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = M @Composable private fun AddTokenBlock(state: PortfolioBlockUM.AddToken, modifier: Modifier = Modifier) { + val hapticManager = LocalHapticManager.current FloatingCard(modifier = modifier) { Row( modifier = Modifier @@ -189,7 +199,10 @@ private fun AddTokenBlock(state: PortfolioBlockUM.AddToken, modifier: Modifier = text = resourceReference(R.string.common_add), shape = TangemButtonShape.Rounded, size = TangemButtonSize.X9, - onClick = state.onAddClick, + onClick = { + hapticManager.perform(TangemHapticEffect.View.ContextClick) + state.onAddClick() + }, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/Options.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/Options.kt index 104d4fef3f..a69b65bd57 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/Options.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/list/components/Options.kt @@ -19,6 +19,8 @@ import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.tabs.TangemSegmentUM import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.features.feed.impl.R @@ -117,6 +119,7 @@ private fun OptionsV2( modifier: Modifier = Modifier, ) { var isShowDropdownMenu by rememberSaveable { mutableStateOf(false) } + val hapticManager = LocalHapticManager.current val segmentItems = remember { persistentListOf( @@ -147,7 +150,10 @@ private fun OptionsV2( horizontalArrangement = Arrangement.SpaceBetween, ) { PrimaryInverseTangemButton( - onClick = { isShowDropdownMenu = true }, + onClick = { + hapticManager.perform(TangemHapticEffect.View.ContextClick) + isShowDropdownMenu = true + }, iconPosition = RedesignTangemButtonIconPosition.End, tangemIconUM = TangemIconUM.Icon( iconRes = R.drawable.ic_chewron_down_20, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt index a74d169214..427c7c7b30 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsBalanceBlock.kt @@ -14,11 +14,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -35,11 +35,9 @@ import com.tangem.core.ui.ds.button.TangemButtonType import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.ds.button.action.ActionButtons import com.tangem.core.ui.ds.image.TangemIconUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.orMaskWithStars -import com.tangem.core.ui.extensions.resolveAnnotatedReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.test.TokenDetailsScreenTestTags @@ -47,6 +45,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBala import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.features.tokendetails.impl.R import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList private val CurrencyIconSize: Dp = 70.dp private val NetworkBadgeSize: Dp = 24.dp @@ -90,16 +89,25 @@ internal fun TokenDetailsBalanceBlock( } if (!balanceBlockUM.isBalanceZeroContent()) { SpacerH(TangemTheme.dimens2.x10) + val hapticManager = LocalHapticManager.current val buttons = remember( balanceBlockUM.addFundsButton, balanceBlockUM.swapButton, balanceBlockUM.transferButton, + hapticManager, ) { persistentListOf( balanceBlockUM.addFundsButton, balanceBlockUM.swapButton, balanceBlockUM.transferButton, - ) + ).map { button -> + button.copy( + onClick = { + hapticManager.perform(TangemHapticEffect.View.ContextClick) + button.onClick() + }, + ) + }.toPersistentList() } ActionButtons(buttons = buttons) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt index 05924d40c5..31d053f08a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt @@ -5,6 +5,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import com.tangem.core.ui.ds.button.TangemButton import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.test.MainScreenTestTags import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM @@ -31,13 +33,19 @@ internal fun LazyListScope.organizeTokens2(state: WalletUM, itemModifier: Modifi key = "OrganizeTokensButton", contentType = "OrganizeTokensButton", ) { + val hapticManager = LocalHapticManager.current val testTag = if (organizeButton.text == resourceReference(R.string.main_add_and_manage_tokens)) { MainScreenTestTags.ADD_AND_MANAGE_BUTTON } else { MainScreenTestTags.ORGANIZE_TOKENS_BUTTON } TangemButton( - buttonUM = organizeButton, + buttonUM = organizeButton.copy( + onClick = { + hapticManager.perform(TangemHapticEffect.View.ContextClick) + organizeButton.onClick() + }, + ), modifier = itemModifier.testTag(testTag), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt index 562d21f546..ceb9e0f51b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -37,6 +38,8 @@ import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior import com.tangem.core.ui.ds.topbar.collapsing.snapToExitUntilCollapsed import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.test.MainScreenTestTags @@ -47,6 +50,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditiona import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList private const val MIN_SCALE = 0.75f private const val MAX_SCALE = 1f @@ -64,6 +68,17 @@ internal fun WalletBalance( val alpha = 1f - collapsedFraction val scale = alpha.coerceIn(MIN_SCALE, MAX_SCALE) val density = LocalDensity.current + val hapticManager = LocalHapticManager.current + val hapticButtons = remember(buttons, hapticManager) { + buttons.map { button -> + button.copy( + onClick = { + hapticManager.perform(TangemHapticEffect.View.ContextClick) + button.onClick() + }, + ) + }.toImmutableList() + } Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -99,7 +114,7 @@ internal fun WalletBalance( } } SpacerH(TangemTheme.dimens2.x2) - ActionButtons(buttons, modifier = Modifier.fillMaxWidth()) + ActionButtons(buttons = hapticButtons, modifier = Modifier.fillMaxWidth()) SpacerH(TangemTheme.dimens2.x6) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt index b03fc85b81..da656845e9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt @@ -28,10 +28,8 @@ import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedS import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.res.LocalRootBackgroundColor -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.haptic.TangemHapticEffect +import com.tangem.core.ui.res.* import com.tangem.core.ui.test.MainScreenTestTags import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy @@ -58,6 +56,7 @@ internal fun WalletTopBar( isBalanceHidden: Boolean, behavior: TangemCollapsingAppBarBehavior, ) { + val hapticManager = LocalHapticManager.current Surface( color = Color.Unspecified, contentColor = Color.Unspecified, @@ -95,7 +94,14 @@ internal fun WalletTopBar( ) { topBarConfig.endActions.forEach { action -> TangemTopBarActionContent( - action, + action.copy( + onClick = action.onClick?.let { onClick -> + { + hapticManager.perform(TangemHapticEffect.View.ContextClick) + onClick() + } + }, + ), modifier = Modifier.testTag(MainScreenTestTags.MORE_BUTTON), ) } From 03f702640e9ff1b7dd8d3a1da065d21e55b4d91f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Jul 2026 09:35:23 +0300 Subject: [PATCH 17/21] Updated on 2026-08-14 --- .../main/model/OnrampMainComponentModel.kt | 5 + ...OnrampProviderCalculatedAnalyticsSender.kt | 21 +++ ...mpProviderCalculatedAnalyticsSenderTest.kt | 123 ++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSender.kt create mode 100644 features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSenderTest.kt diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 5c85ec5a96..ba9858ba48 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -27,6 +27,7 @@ import com.tangem.features.onramp.main.entity.factory.OnrampAmountStateFactory import com.tangem.features.onramp.main.entity.factory.OnrampOffersStateFactory import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory import com.tangem.features.onramp.utils.sendOnrampErrorEvent +import com.tangem.features.onramp.utils.sendProviderCalculatedEvent import com.tangem.features.onramp.utils.showDemoModeWarningIfNeeded import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -330,6 +331,10 @@ internal class OnrampMainComponentModel @Inject constructor( state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } } else -> { + analyticsEventHandler.sendProviderCalculatedEvent( + quotes = quotes, + tokenSymbol = params.cryptoCurrency.symbol, + ) state.update { prevState -> val resetState = amountStateFactory.getAmountSecondaryFieldResetState() if (prevState is OnrampMainComponentUM.Content && diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSender.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSender.kt new file mode 100644 index 0000000000..cd0f9f8480 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSender.kt @@ -0,0 +1,21 @@ +package com.tangem.features.onramp.utils + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent +import com.tangem.domain.onramp.model.OnrampQuote + +internal fun AnalyticsEventHandler.sendProviderCalculatedEvent(quotes: List, tokenSymbol: String) { + val quote = quotes.findBestRateQuote() ?: return + + send( + OnrampAnalyticsEvent.ProviderCalculated( + providerName = quote.provider.info.name, + tokenSymbol = tokenSymbol, + paymentMethod = quote.paymentMethod.name, + ), + ) +} + +private fun List.findBestRateQuote(): OnrampQuote.Data? { + return filterIsInstance().maxByOrNull { it.toAmount.value } +} \ No newline at end of file diff --git a/features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSenderTest.kt b/features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSenderTest.kt new file mode 100644 index 0000000000..1e1cc15ee8 --- /dev/null +++ b/features/onramp/impl/src/test/kotlin/com/tangem/features/onramp/utils/OnrampProviderCalculatedAnalyticsSenderTest.kt @@ -0,0 +1,123 @@ +package com.tangem.features.onramp.utils + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent +import com.tangem.domain.onramp.model.OnrampQuote +import com.tangem.test.core.ProvideTestModels +import io.mockk.Called +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest +import java.math.BigDecimal + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class OnrampProviderCalculatedAnalyticsSenderTest { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxUnitFun = true) + + @BeforeEach + fun resetMocks() { + clearMocks(analyticsEventHandler) + } + + @ParameterizedTest + @ProvideTestModels + fun `GIVEN quotes WHEN send THEN provider calculated sent for best-rate provider`(model: SelectionModel) { + // Act + analyticsEventHandler.sendProviderCalculatedEvent(quotes = model.quotes, tokenSymbol = TOKEN_SYMBOL) + + // Assert + verify(exactly = 1) { + analyticsEventHandler.send( + OnrampAnalyticsEvent.ProviderCalculated( + providerName = model.expectedProviderName, + tokenSymbol = TOKEN_SYMBOL, + paymentMethod = PAYMENT_METHOD, + ), + ) + } + } + + @Test + fun `GIVEN no quotes WHEN send THEN no event sent`() { + // Act + analyticsEventHandler.sendProviderCalculatedEvent(quotes = emptyList(), tokenSymbol = TOKEN_SYMBOL) + + // Assert + verify { analyticsEventHandler wasNot Called } + } + + @Test + fun `GIVEN only non-loaded quotes WHEN send THEN no event sent`() { + // Arrange + val quotes = listOf(mockk(), mockk()) + + // Act + analyticsEventHandler.sendProviderCalculatedEvent(quotes = quotes, tokenSymbol = TOKEN_SYMBOL) + + // Assert + verify { analyticsEventHandler wasNot Called } + } + + private fun provideTestModels() = listOf( + SelectionModel( + name = "highest-rate quote among several is selected", + quotes = listOf( + createQuote(providerName = "Low", rate = BigDecimal("100")), + createQuote(providerName = "High", rate = BigDecimal("120")), + createQuote(providerName = "Mid", rate = BigDecimal("90")), + ), + expectedProviderName = "High", + ), + SelectionModel( + name = "SEPA quote with lower rate is NOT prioritized, higher-rate quote wins", + quotes = listOf( + createQuote(providerName = "SepaLowerRate", rate = BigDecimal("100")), + createQuote(providerName = "CardHigherRate", rate = BigDecimal("105")), + ), + expectedProviderName = "CardHigherRate", + ), + SelectionModel( + name = "single loaded quote is selected", + quotes = listOf( + createQuote(providerName = "Single", rate = BigDecimal("100")), + ), + expectedProviderName = "Single", + ), + SelectionModel( + name = "best-rate loaded quote is selected even when error quotes are present", + quotes = listOf( + mockk(), + createQuote(providerName = "Loaded", rate = BigDecimal("100")), + mockk(), + ), + expectedProviderName = "Loaded", + ), + ) + + private fun createQuote(providerName: String, rate: BigDecimal): OnrampQuote.Data { + return mockk { + every { provider.info.name } returns providerName + every { paymentMethod.name } returns PAYMENT_METHOD + every { toAmount.value } returns rate + } + } + + internal data class SelectionModel( + val name: String, + val quotes: List, + val expectedProviderName: String, + ) { + override fun toString(): String = name + } + + private companion object { + const val TOKEN_SYMBOL = "BTC" + const val PAYMENT_METHOD = "Card" + } +} \ No newline at end of file From abf6fc7d697de469dc5edd6c91d161fdeeaa36d7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Jul 2026 14:20:04 +0500 Subject: [PATCH 18/21] Updated on 2026-08-14 --- .../models/event/MainScreenAnalyticsEvent.kt | 4 ++ .../models/event/TransferAnalyticsEvent.kt | 23 ++++++++++ .../analytics/TokenScreenAnalyticsEvent.kt | 24 +++++++++++ .../analytics/ManageFundsAnalyticsEvent.kt | 5 +-- .../managefunds/model/ManageFundsModel.kt | 42 +++++++++++++++---- .../details/MarketsTokenDetailsModel.kt | 1 + .../analytics/MarketDetailsAnalyticsEvent.kt | 5 +++ .../tokendetails/model/TokenDetailsModel.kt | 19 +++++++++ .../transformer/UpdateTransferTransformer.kt | 7 ++++ .../UpdateTransferTransformerTest.kt | 9 ++++ .../model/intents/WalletClickIntents.kt | 1 + 11 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TransferAnalyticsEvent.kt diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt index b09ed9c19d..aeea25ea21 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/MainScreenAnalyticsEvent.kt @@ -42,6 +42,10 @@ sealed class MainScreenAnalyticsEvent( event = "Button - Add Funds", ) + class ButtonTransfer : MainScreenAnalyticsEvent( + event = "Button - Transfer", + ) + class LimitsClicked : MainScreenAnalyticsEvent( event = "Limits Clicked", ) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TransferAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TransferAnalyticsEvent.kt new file mode 100644 index 0000000000..c197e44afd --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/TransferAnalyticsEvent.kt @@ -0,0 +1,23 @@ +package com.tangem.core.analytics.models.event + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam + +sealed class TransferAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = "Transfer", event = event, params = params) { + + class MethodScreenOpened(source: AnalyticsParam.ScreensSources) : TransferAnalyticsEvent( + event = "Method Screen Opened", + params = mapOf(AnalyticsParam.SOURCE to source.value), + ) + + class ButtonSell : TransferAnalyticsEvent(event = "Button - Sell") + + class ButtonSwap : TransferAnalyticsEvent(event = "Button - Swap") + + class ButtonSend : TransferAnalyticsEvent(event = "Button - Send") + + class ButtonSwapAndSend : TransferAnalyticsEvent(event = "Button - Swap&Send") +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt index ba7056df00..cc59832b8d 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/analytics/TokenScreenAnalyticsEvent.kt @@ -136,6 +136,30 @@ sealed class TokenScreenAnalyticsEvent( status = status, blockchain = blockchain, ) + + class ButtonAddFunds( + token: String, + blockchain: String, + derivationIndex: Int? = null, + ) : ButtonWithParams( + event = "Button - Add Funds", + token = token, + status = null, + blockchain = blockchain, + derivationIndex = derivationIndex, + ) + + class ButtonTransfer( + token: String, + blockchain: String, + derivationIndex: Int? = null, + ) : ButtonWithParams( + event = "Button - Transfer", + token = token, + status = null, + blockchain = blockchain, + derivationIndex = derivationIndex, + ) } class ActionButtonDisabled( diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/analytics/ManageFundsAnalyticsEvent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/analytics/ManageFundsAnalyticsEvent.kt index 19c26683b6..e59d62bbe1 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/analytics/ManageFundsAnalyticsEvent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/analytics/ManageFundsAnalyticsEvent.kt @@ -8,9 +8,9 @@ internal sealed class ManageFundsAnalyticsEvent( params: Map = emptyMap(), ) : AnalyticsEvent(category = CATEGORY, event = event, params = params) { - class MethodScreenOpened(source: String) : ManageFundsAnalyticsEvent( + class MethodScreenOpened(source: AnalyticsParam.ScreensSources) : ManageFundsAnalyticsEvent( event = "Method Screen Opened", - params = mapOf(AnalyticsParam.SOURCE to source), + params = mapOf(AnalyticsParam.SOURCE to source.value), ) class ButtonBuy : ManageFundsAnalyticsEvent(event = "Button - Buy") @@ -21,6 +21,5 @@ internal sealed class ManageFundsAnalyticsEvent( companion object { private const val CATEGORY = "Add Funds" - const val SOURCE_MAIN_SCREEN = "Main Screen" } } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt index a1178676e5..94b88e2469 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt @@ -7,6 +7,8 @@ import com.tangem.common.routing.AppRouter import com.tangem.common.ui.markets.action.CryptoCurrencyData import com.tangem.common.ui.markets.action.TokenActionsBSContentUM import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.TransferAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -150,11 +152,20 @@ internal class ManageFundsModel @Inject constructor( } override fun onQuickActionClick(action: TokenActionsBSContentUM.Action, shouldDismiss: Boolean) { - val event = when (action) { - TokenActionsBSContentUM.Action.Buy -> ManageFundsAnalyticsEvent.ButtonBuy() - TokenActionsBSContentUM.Action.Exchange -> ManageFundsAnalyticsEvent.ButtonSwap() - TokenActionsBSContentUM.Action.Receive -> ManageFundsAnalyticsEvent.ButtonReceive() - else -> null + val event = when (flowType) { + ManageFundsComponent.FlowType.AddFunds -> when (action) { + TokenActionsBSContentUM.Action.Buy -> ManageFundsAnalyticsEvent.ButtonBuy() + TokenActionsBSContentUM.Action.Exchange -> ManageFundsAnalyticsEvent.ButtonSwap() + TokenActionsBSContentUM.Action.Receive -> ManageFundsAnalyticsEvent.ButtonReceive() + else -> null + } + ManageFundsComponent.FlowType.Transfer -> when (action) { + TokenActionsBSContentUM.Action.Send -> TransferAnalyticsEvent.ButtonSend() + TokenActionsBSContentUM.Action.Exchange -> TransferAnalyticsEvent.ButtonSwap() + TokenActionsBSContentUM.Action.SendWithSwap -> TransferAnalyticsEvent.ButtonSwapAndSend() + TokenActionsBSContentUM.Action.Sell -> TransferAnalyticsEvent.ButtonSell() + else -> null + } } event?.let { analyticsEventHandler.send(it) } if (shouldDismiss) { @@ -178,9 +189,7 @@ internal class ManageFundsModel @Inject constructor( private fun initChooseToken(mode: ManageFundsComponent.LaunchMode.ChooseToken) { chooseTokenBridge.selectWalletTab(mode.userWalletId) - analyticsEventHandler.send( - ManageFundsAnalyticsEvent.MethodScreenOpened(source = ManageFundsAnalyticsEvent.SOURCE_MAIN_SCREEN), - ) + sendMethodScreenOpenedEvent() replaceRoot(UiRoute.ChooseToken) modelScope.launch { chooseTokenBridge.onCurrencyChosen.receiveAsFlow().collect(::openTokenActionsFromBridge) @@ -209,6 +218,7 @@ internal class ManageFundsModel @Inject constructor( params.onDismiss() return@launch } + sendMethodScreenOpenedEvent() tokenActionsTrigger.value = TokenActionsRequest(wallet, match.first, match.second) replaceRoot(tokenActionsRoute(match.second)) } @@ -217,6 +227,9 @@ internal class ManageFundsModel @Inject constructor( private fun initFilteredByRawId(mode: ManageFundsComponent.LaunchMode.FilteredByRawId) { modelScope.launch { val entries = collectFilteredEntries(mode.rawCurrencyId) + if (entries.isNotEmpty()) { + sendMethodScreenOpenedEvent() + } when (entries.size) { 0 -> params.onDismiss() 1 -> { @@ -250,6 +263,19 @@ internal class ManageFundsModel @Inject constructor( } } + private fun sendMethodScreenOpenedEvent() { + val source = when (launchMode) { + is ManageFundsComponent.LaunchMode.ChooseToken -> AnalyticsParam.ScreensSources.Main + is ManageFundsComponent.LaunchMode.TokenActionsOnly -> AnalyticsParam.ScreensSources.Token + is ManageFundsComponent.LaunchMode.FilteredByRawId -> AnalyticsParam.ScreensSources.Market + } + val event = when (flowType) { + ManageFundsComponent.FlowType.AddFunds -> ManageFundsAnalyticsEvent.MethodScreenOpened(source = source) + ManageFundsComponent.FlowType.Transfer -> TransferAnalyticsEvent.MethodScreenOpened(source = source) + } + analyticsEventHandler.send(event) + } + private fun openTokenActionsFromBridge(result: ChooseTokenResult) { val account = result.account as? AccountStatus.CryptoPortfolio ?: return openTokenActions( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt index d5976c19fa..e79b45966a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt @@ -393,6 +393,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( } fun openAddFunds(rawCurrencyId: com.tangem.domain.models.currency.CryptoCurrency.RawID) { + analyticsEventHandler.send(analyticsEventBuilder.addFundsClicked()) addFundsSheetNavigation.activate(AddFundsSlotRoute(rawCurrencyId = rawCurrencyId)) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/analytics/MarketDetailsAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/analytics/MarketDetailsAnalyticsEvent.kt index 1ae3f86fab..907705a88b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/analytics/MarketDetailsAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/analytics/MarketDetailsAnalyticsEvent.kt @@ -70,6 +70,11 @@ internal class MarketDetailsAnalyticsEvent( event = "Button - Share", params = mapOf("Token" to token.symbol), ) + + fun addFundsClicked() = MarketDetailsAnalyticsEvent( + event = "Button - Add Funds", + params = mapOf("Token" to token.symbol), + ) } enum class IntervalType(val source: String) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index f4b55056ba..73fd82bed5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -20,6 +20,7 @@ import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent +import com.tangem.core.analytics.models.event.TransferAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -368,6 +369,7 @@ internal class TokenDetailsModel @Inject constructor( actions = state.states, networkSource = networkSource, clickIntents = this@TokenDetailsModel, + analyticsEventHandler = analyticsEventsHandler, onActionDispatched = bottomSheetNavigation::dismiss, ), ) @@ -547,6 +549,13 @@ internal class TokenDetailsModel @Inject constructor( } override fun onAddFundsClick() { + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.ButtonWithParams.ButtonAddFunds( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + derivationIndex = getAccountIndexOrNull(), + ), + ) bottomSheetNavigation.activate( TokenDetailsBottomSheetConfig.AddFunds( userWalletId = userWalletId, @@ -556,6 +565,13 @@ internal class TokenDetailsModel @Inject constructor( } override fun onTransferClick() { + analyticsEventsHandler.send( + TokenScreenAnalyticsEvent.ButtonWithParams.ButtonTransfer( + token = cryptoCurrency.symbol, + blockchain = cryptoCurrency.network.name, + derivationIndex = getAccountIndexOrNull(), + ), + ) val amount = cryptoCurrencyStatus?.value?.amount if (amount == null || amount.signum() <= 0) { handleUnavailabilityReason( @@ -565,6 +581,9 @@ internal class TokenDetailsModel @Inject constructor( ) return } + analyticsEventsHandler.send( + TransferAnalyticsEvent.MethodScreenOpened(source = AnalyticsParam.ScreensSources.Token), + ) bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.Transfer) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformer.kt index 46f2b89ec5..c33785b3a3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformer.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformer.kt @@ -1,5 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.event.TransferAnalyticsEvent import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState @@ -13,6 +15,7 @@ internal class UpdateTransferTransformer( private val actions: List, private val networkSource: StatusSource, private val clickIntents: TokenDetailsClickIntents, + private val analyticsEventHandler: AnalyticsEventHandler, private val onActionDispatched: () -> Unit, ) : Transformer { @@ -28,6 +31,7 @@ internal class UpdateTransferTransformer( isLoading = action.unavailabilityReason.isOutdatedLoading(), isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, onClick = { + analyticsEventHandler.send(TransferAnalyticsEvent.ButtonSend()) onActionDispatched() clickIntents.onSendClick(action.unavailabilityReason) }, @@ -38,6 +42,7 @@ internal class UpdateTransferTransformer( isLoading = action.unavailabilityReason.isLoading, isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, onClick = { + analyticsEventHandler.send(TransferAnalyticsEvent.ButtonSwap()) onActionDispatched() clickIntents.onSwapFromClick(action.unavailabilityReason) }, @@ -53,6 +58,7 @@ internal class UpdateTransferTransformer( isLoading = false, isEnabled = true, onClick = { + analyticsEventHandler.send(TransferAnalyticsEvent.ButtonSwapAndSend()) onActionDispatched() clickIntents.onSwapAndSendClick(it.unavailabilityReason) }, @@ -63,6 +69,7 @@ internal class UpdateTransferTransformer( isLoading = action.unavailabilityReason.isOutdatedLoading(), isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, onClick = { + analyticsEventHandler.send(TransferAnalyticsEvent.ButtonSell()) onActionDispatched() clickIntents.onSellClick(action.unavailabilityReason) }, diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformerTest.kt index e50e0b4c1a..3fcf8b3ee0 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformerTest.kt @@ -1,6 +1,8 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer import com.google.common.truth.Truth.assertThat +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.event.TransferAnalyticsEvent import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.extensions.stringReference @@ -24,6 +26,7 @@ import org.junit.jupiter.api.Test class UpdateTransferTransformerTest { private val clickIntents: TokenDetailsClickIntents = mockk(relaxed = true) + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) private val onActionDispatched: () -> Unit = mockk(relaxed = true) @Test @@ -112,6 +115,7 @@ class UpdateTransferTransformerTest { // THEN verifyOrder { + analyticsEventHandler.send(ofType()) onActionDispatched.invoke() clickIntents.onSendClick(ScenarioUnavailabilityReason.None) } @@ -130,6 +134,7 @@ class UpdateTransferTransformerTest { // THEN verifyOrder { + analyticsEventHandler.send(ofType()) onActionDispatched.invoke() clickIntents.onSellClick(ScenarioUnavailabilityReason.None) } @@ -225,6 +230,7 @@ class UpdateTransferTransformerTest { // THEN verifyOrder { + analyticsEventHandler.send(ofType()) onActionDispatched.invoke() clickIntents.onSwapFromClick(ScenarioUnavailabilityReason.None) } @@ -347,6 +353,7 @@ class UpdateTransferTransformerTest { // Assert verifyOrder { + analyticsEventHandler.send(ofType()) onActionDispatched.invoke() clickIntents.onSwapAndSendClick(ScenarioUnavailabilityReason.None) } @@ -405,6 +412,7 @@ class UpdateTransferTransformerTest { // THEN verify(exactly = 0) { onActionDispatched.invoke() } verify(exactly = 0) { clickIntents.onSendClick(any()) } + verify(exactly = 0) { analyticsEventHandler.send(any()) } } private fun createTransformer( @@ -414,6 +422,7 @@ class UpdateTransferTransformerTest { actions = actions, networkSource = networkSource, clickIntents = clickIntents, + analyticsEventHandler = analyticsEventHandler, onActionDispatched = onActionDispatched, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index d983fe385c..0ed6aa49de 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -125,6 +125,7 @@ internal class WalletClickIntents @Inject constructor( } fun onTransferClick(userWalletId: UserWalletId) { + analyticsEventHandler.send(MainScreenAnalyticsEvent.ButtonTransfer()) router.openTransfer(userWalletId) } From 2f5a544541f0b3eb084e043bae627420e03c458f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Jul 2026 01:31:20 -0700 Subject: [PATCH 19/21] Updated on 2026-08-14 --- .../DefaultPaymentAccountStatusFetcher.kt | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 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 0c90bc8ef9..57447e1f15 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 @@ -93,10 +93,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( .fold( ifLeft = { error -> logger.e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}") - when (error) { - is VisaApiError.NotPaeraCustomer -> constructNotCreatedOrEmptyStatus(params.userWalletId) - else -> PaymentAccountStatusValue.Error.Unavailable - } + error.toStatusValueWhenTangemPayStatusUnknown(params.userWalletId) }, ifRight = { hasTangemPay -> proceedHasTangemPayResult(account = account, hasTangemPay = hasTangemPay) @@ -169,14 +166,11 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( if (cache != null && cache.value.hasAccountData()) { cache.value.copySealed( source = StatusSource.ONLY_CACHE, - error = when (error) { - is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced - else -> PaymentAccountStatusValue.Error.Unavailable - }, + error = error.toErrorValue(), ) } else { logger.e("proceedWithoutOrder ${account.userWalletId} error: $error") - error.mapToPaymentAccountStatus(account.userWalletId) + error.toStatusValueWhenHasTangemPay(account.userWalletId) } }, ifRight = { customerInfo -> @@ -197,7 +191,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( val customerInfo = onboardingRepository.getCustomerInfo(account.userWalletId).fold( ifLeft = { error -> logger.e("proceedWithOrderId KYC check ${account.userWalletId} error: $error") - return error.mapToPaymentAccountStatus(account.userWalletId) + return error.toStatusValueWhenHasTangemPay(account.userWalletId) }, ifRight = { it }, ) @@ -216,7 +210,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( return customerOrderRepository.getOrderData(userWalletId = account.userWalletId, orderId = orderId).fold( ifLeft = { error -> logger.e("proceedWithOrderId ${account.userWalletId} orderId: $orderId error: $error") - error.mapToPaymentAccountStatus(account.userWalletId) + error.toStatusValueWhenHasTangemPay(account.userWalletId) }, ifRight = { orderData -> logger.i("proceedWithOrderId ${account.userWalletId}: $orderId status: ${orderData.status}") @@ -283,7 +277,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( onboardingRepository.clearOrderId(account.userWalletId) return onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId) .fold( - ifLeft = { it.mapToPaymentAccountStatus(account.userWalletId) }, + ifLeft = { it.toStatusValueWhenHasTangemPay(account.userWalletId) }, ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus(account.userWalletId) }, ) } @@ -467,14 +461,39 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( state = TangemPayCardState.Issuing, ) - private suspend fun VisaApiError.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue { + private suspend fun VisaApiError.toStatusValueWhenHasTangemPay( + userWalletId: UserWalletId, + ): PaymentAccountStatusValue { return when (this) { - is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced is VisaApiError.NotPaeraCustomer -> constructNotCreatedOrEmptyStatus(userWalletId) - else -> PaymentAccountStatusValue.Error.Unavailable + else -> toErrorValue() } } + private suspend fun VisaApiError.toStatusValueWhenTangemPayStatusUnknown( + userWalletId: UserWalletId, + ): PaymentAccountStatusValue { + return when (this) { + is VisaApiError.NotPaeraCustomer -> constructNotCreatedOrEmptyStatus(userWalletId) + else -> { + val previousValue = paymentAccountStatusesStore.getSyncOrNull(userWalletId)?.value + if (previousValue != null && previousValue.hasAccountData()) { + previousValue.copySealed( + source = StatusSource.ONLY_CACHE, + error = toErrorValue(), + ) + } else { + constructNotCreatedOrEmptyStatus(userWalletId) + } + } + } + } + + private fun VisaApiError.toErrorValue(): PaymentAccountStatusValue.Error = when (this) { + is VisaApiError.RefreshTokenExpired -> PaymentAccountStatusValue.Error.NotSynced + else -> PaymentAccountStatusValue.Error.Unavailable + } + private suspend fun constructNotCreatedOrEmptyStatus(userWalletId: UserWalletId): PaymentAccountStatusValue { val entryPoint = TangemPayEntryPoint.BANNER val shouldShowBanner = !eligibilityManager.isPaeraCustomerForAnyWallet(entryPoint) && From 16baed2dc1742b088700f9190de98a83517b1463 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Jul 2026 08:31:46 +0000 Subject: [PATCH 20/21] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index e752ccee30..8493666708 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,16 +5,16 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-6.0-1603" +tangemBlockchainSdk = "develop-1607" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-6.0-626" +tangemCardSdk = "develop-630" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ tangemHotSdk = "develop-550" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ - - +tangemUsedeskSdk = "main-12" +#tangemUsedeskSdk = "0.0.1" # Keep it! - used for local builds ^ [libraries] blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } @@ -23,6 +23,9 @@ card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tange hot-core = { module = "com.tangem.tangem-hot-sdk-kotlin:core", version.ref = "tangemHotSdk" } hot-android = { module = "com.tangem.tangem-hot-sdk-kotlin:android", version.ref = "tangemHotSdk" } +usedesk-chat-sdk = { module = "com.tangem.usedesk:chat-sdk", version.ref = "tangemUsedeskSdk" } +usedesk-chat-gui = { module = "com.tangem.usedesk:chat-gui", version.ref = "tangemUsedeskSdk" } + vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } From 6107f1d82ee15c159709a2218ec7df9c55ee72c6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Jul 2026 14:16:48 +0300 Subject: [PATCH 21/21] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 27 ++++++++- core/res/src/main/res/values-es/strings.xml | 7 ++- core/res/src/main/res/values-fr/strings.xml | 29 +++++++-- core/res/src/main/res/values-ja/strings.xml | 22 ++++++- .../src/main/res/values-pt-rBR/strings.xml | 60 ++++++++++++++++--- core/res/src/main/res/values-ru/strings.xml | 38 ++++++++---- .../src/main/res/values-uk-rUA/strings.xml | 36 ++++++++--- .../src/main/res/values-zh-rCN/strings.xml | 9 ++- core/res/src/main/res/values/strings.xml | 33 ++++++++-- 9 files changed, 215 insertions(+), 46 deletions(-) diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index a038bc4d36..5914434127 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -105,6 +105,7 @@ Adressen Adresse auswählen + Alles löschen Kontakt Name der Kontaktperson Adresse kopieren @@ -134,6 +135,7 @@ In Wallet speichern Dieser Kontakt wird mit dem Adressbuch dieser Wallet verknüpft. Es wurden keine Ergebnisse gefunden.\nVersuchen Sie es mit einem anderen Namen + Alle auswählen Netzwerk auswählen Adressbuch Nicht gespeicherte Änderungen @@ -1298,6 +1300,19 @@ Nach Guthaben Token organisieren Gruppe löschen + Berechtigte Cashback-Zahlungen werden wie folgt ausgezahlt: + Sie sind bereits eingeschrieben in %1$s + Zulässige Token + Anmelden + Sie sind erfolgreich eingeschrieben in %1$s + Diese Aktion existiert nicht mehr oder ist abgelaufen. + Kampagne nicht aktiv + Cashback-Konto auswählen + Melden Sie sich an %1$s + Ich stimme zu, dass %1$s + Ich stimme zu, dass + %1$s Bedingungen + Erhalten Sie bis Ende Juli bei jedem Swap ab 10.000 $ Cashback.\n\nDie Sätze steigen mit dem Volumen: 0,10% ab 10.000 $, 0,20% ab 20.000 $, 0,50% ab 100.000 $.\n\nMaximale Auszahlung: 500 $ pro Swap und 10.000 $ pro Wallet und Swap-Richtung, solange die Aktion läuft. Swaps von Stablecoins in andere Stablecoins sind ausgeschlossen.\n\nDie Auszahlung erfolgt wöchentlich an eine USDT- oder USDC-Adresse Ihrer Wahl. %s Unterstützung Push-Benachrichtigungen sind aktiviert, funktionieren aber erst, nachdem du Benachrichtigungen in den Geräteeinstellungen zugelassen hast. Benachrichtigungen zulassen @@ -1629,9 +1644,9 @@ Um mit dem Staking zu beginnen, musst Du zuerst Dein TON-Konto aktivieren. Kontoaktivierung Um mit dem Staking in TON zu beginnen, sende zunächst eine kleine Transaktion an Deine eigene Adresse – dadurch wird Deine Wallet aktiviert. - Für den Abschluss der Transaktion können zusätzlich zur Netzwerkgebühr bis zu 0,2 TON erforderlich sein. Nicht genutzte Beträge werden zurückerstattet. - Für diesen Vorgang sind zusätzlich zur Netzwerkgebühr 0,2 TON erforderlich. Bitte lade Dein Guthaben auf. - TON-Reserve erforderlich + Für den Abschluss der Transaktion können zusätzlich zur Netzwerkgebühr bis zu 0,2 GRAM erforderlich sein. Nicht genutzte Beträge werden zurückerstattet. + Für diesen Vorgang sind zusätzlich zur Netzwerkgebühr 0,2 GRAM erforderlich. Bitte lade Dein Guthaben auf. + GRAM-Reserve erforderlich Durch diese Aktion werden andere Positionen geschlossen oder gemäß den Netzwerkregeln in den Auszahlungsstatus versetzt. Positionsstatus Entsperre dein Geld, um es aus dem Staking-Prozess abzuheben. Das Freischalten nimmt %s. @@ -1900,6 +1915,7 @@ PIN-Code ändern Kehren Sie zur App zurück, falls Sie ihn vergessen. Karte + Fehler beim Laden Tarif wechseln Kartenbezogen Planbezogen @@ -2037,6 +2053,7 @@ Laden Sie Ihr Konto mit einem beliebigen Token aus Ihrer Wallet auf Aus Ihrer Tangem Wallet USDC im Polygon + Visavorteile Gelder aus erstatteten Käufen werden nicht auf Ihr On-Chain-Guthaben zurückerstattet und stehen nicht für Abhebungen zur Verfügung, bleiben aber auf Ihrem Kartenguthaben für Einkäufe verfügbar Bitte beachten Sie Ihr PIN-Code @@ -2080,6 +2097,10 @@ QR-Code anzeigen Die Daten des Tokens konnten nicht geladen werden. Gehe zum Tauschen + Letzte Aktualisierung: %1$s + Negativer Ausblick + Neutraler Ausblick + Positiver Ausblick Token-Zusammenfassung Tausche diesen Token gegen einen anderen zu %1$s Servicegebühren von Februar %2$s-%3$s. Tausche mit Changelly, %s Gebühren diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index d3a635d982..60ae725cbf 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -1288,6 +1288,7 @@ Tarjeta de crédito o cuenta bancaria Comparta su dirección o código QR Venda criptomonedas de forma segura + Enviar con intercambio a otro token Enviar a otra billetera Entre sus portafolios Otro @@ -1580,9 +1581,9 @@ Para empezar a hacer staking, primero debes activar tu cuenta de TON. Activación de cuenta Para empezar a hacer staking en TON, realice primero una transacción de salida de cualquier importe - esto activará su billetera. - Es posible que se requieran hasta 0.2 TON además de la tarifa de red para completar la transacción. Cualquier cantidad no utilizada será reembolsada. - Se requieren 0.2 TON para realizar esta operación, además de la tarifa de red. Por favor, recargue su saldo. - Se requiere reserva de TON + Es posible que se requieran hasta 0.2 GRAM además de la tarifa de red para completar la transacción. Cualquier cantidad no utilizada será reembolsada. + Se requieren 0.2 GRAM para realizar esta operación, además de la tarifa de red. Por favor, recargue su saldo. + Se requiere reserva de GRAM Esta acción cerrará otras posiciones o las cambiará al estado de retiro, de acuerdo con las reglas de la red. Estado de las posiciones Desbloquee su dinero para retirarlo del proceso de staking. El desbloqueo demora %s minuto. diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index a10ceb497d..bebe09f137 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -94,26 +94,44 @@ Ajouter une adresse Ajouter une adresse et sélectionner un réseau Ajouter le contact + Adresse copiée + Cette adresse est déjà enregistrée sous le nom %1$s adresse adresses + Choisir une adresse + Tout supprimer Contact Nom du contact Copier l\'adresse + Contact sauvegardé Nous n\'avons pas pu créer le contact. Veuillez réessayer plus tard. + Supprimer le contact Ce contact sera supprimé de tous vos carnets d\'adresses + \"%1$s\" n\'a qu\'une seule adresse. La supprimer va aussi supprimer le contact. Continuer? Nous n\'avons pas pu supprimer le contact. Veuillez réessayer plus tard. Gérer les contacts & adresses Oui, annuler Modifier l\'adresse Entrer l\'adresse + Adresse invalide Non, continuer + Vous ne pouvez pas créer plus de 20 adresses. Supprimez-en une pour en ajouter une nouvelle. + Impossible d\'ajouter une nouvelle adresse + Le nom du contact est requis + Le nom du contact contient des caractères invalides + Le nom du contact ne doit pas dépasser 50 caractères + Ce nom est déjà utilisé dans ce portefeuille Nouveau contact Aucun contact pour le moment Les contacts que vous ajouterez vont apparaître ici Supprimer l\'adresse + Enregistrer le contact + Enregistrer dans le portefeuille Ce contact va être lié au carnet d\'adresses de ce portefeuille. + Aucun résultat.\nEssayez un autre nom + Tout sélectionner Sélectionner un réseau Carnet d\'adresses Modifications non enregistrées @@ -1182,6 +1200,7 @@ Aucun jeton pris en charge n\'a été trouvé Ce code QR contient des paramètres non reconnus : %s. Si vous continuez, certaines informations de paiement risquent d\'être perdues. Paramètres inconnus + Envoyer en échangeant vers un autre token Recharge rapide Aucun mémo requis %1$s (%2$s) sur le réseau %3$s @@ -1355,7 +1374,7 @@ Le total dépasse le solde Êtes-vous sûr de vouloir modifier le token de réception ? Cela réinitialisera les données que vous avez saisies précédemment. Changement de token - Échanger et envoyer + Échanger et Envoyer Poursuivre l\'échange ? Cela effacera vos données précédentes. Confirmer la conversion L\'envoi de toute autre crypto entraînera sa perte irréversible. @@ -1459,9 +1478,9 @@ Pour commencer le staking, vous devez d’abord activer votre compte TON Activation du compte Pour commencer à staker TON, effectuez d\'abord une transaction sortante de n\'importe quel montant — cela activera votre portefeuille. - Jusqu\'à 0,2 TON peuvent être requis en plus des frais de réseau pour terminer la transaction. Tout montant non utilisé sera remboursé. - 0,2 TON requis pour effectuer cette opération, en plus des frais de réseau. Veuillez recharger votre solde. - Réserve de TON requise + Jusqu\'à 0,2 GRAM peuvent être requis en plus des frais de réseau pour terminer la transaction. Tout montant non utilisé sera remboursé. + 0,2 GRAM requis pour effectuer cette opération, en plus des frais de réseau. Veuillez recharger votre solde. + Réserve de GRAM requise Cette action fermera d\'autres positions ou les fera passer au statut de retrait, conformément aux règles du réseau. Statut des positions Débloquez votre argent pour le retirer du processus de staking. Le déverrouillage prend %s. @@ -2138,6 +2157,8 @@ Ce jeton doit être associé à votre compte Hedera avant que vous puissiez le recevoir Associez votre jeton %s insuffisant. Renflouez votre compte Hedera pour associer ce jeton + Nous avons constaté que le processus d\'activation de la carte n\'a pas été effectué correctement en raison de problèmes avec le module NFC de votre appareil ou d\'une méthode incorrecte pour maintenir les cartes sur votre téléphone. Veuillez contacter notre équipe d\'assistance pour plus de détails. + Action requise. N\'utilisez pas votre portefeuille! Êtes-vous sûr(e) de vouloir annuler la transaction ? Vous ne pourrez plus réessayer. Votre transaction d\'un montant de %1$s %2$s n\'a pas été finalisée. Vous pouvez réessayer plus tard pour la finaliser. Vous avez une transaction inachevée diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 0cb5e2e92d..2c2737a84a 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -98,14 +98,20 @@ アドレスを追加 アドレスを追加し、ネットワークを選択してください。 連絡先を追加 + アドレスをコピーしました + このアドレスはすでに%1$sとして保存されています %d件のアドレス + アドレスを選択 連絡先 連絡先名 アドレスをコピー + 連絡先を保存しました 連絡先を作成できませんでした。しばらくしてからもう一度お試しください。 + 連絡先を削除 この連絡先は、すべてのアドレス帳から削除されます。 + 「%1$s」には1つのアドレスしかありません。削除すると、連絡先も削除されます。続行しますか? 連絡先を削除できませんでした。しばらくしてからもう一度お試しください。 連絡先とアドレスを管理 破棄 @@ -113,11 +119,20 @@ アドレスを入力 無効なアドレス 編集を続ける + 20件を超えるアドレスは作成できません。新しく追加するには、既存のアドレスを1件削除してください。 + 新しいアドレスを追加できません + 連絡先名を入力してください + 連絡先名に使用できない文字が含まれています + 連絡先名は50文字以内で入力してください + その名前はこのウォレットですでに使用されています 新しい連絡先 連絡先はまだありません 追加した連絡先はここに表示されます。 アドレスを削除 + 連絡先を保存 + ウォレットに保存 この連絡先は、このウォレットのアドレス帳に紐付けられます。 + 結果が見つかりませんでした。\n別の名前を試してください。 ネットワークを選択 連絡先 保存されていない変更 @@ -1269,6 +1284,7 @@ クレジットカードまたは銀行口座 アドレスまたはQRコードを共有してください 暗号資産を安全に売却 + 別のトークンにスワップして送る 別のウォレットに送信 ポートフォリオ間で その他 @@ -1558,9 +1574,9 @@ ステーキングを開始するには、まずTONアカウントを有効化してください。 アカウントの有効化 TONのステーキングを開始するには、まず自分のアドレスに少額の取引を送信します。これによりウォレットが有効になります。 - 取引を完了するには、ネットワーク手数料に加えて最大0.2TONが必要になる場合があります。未使用分は返金されます。 - この操作を続行するには、ネットワーク手数料に加えて0.2 TONが必要です。残高を補充してください。 - TONの準備金が必要です + 取引を完了するには、ネットワーク手数料に加えて最大0.2GRAMが必要になる場合があります。未使用分は返金されます。 + この操作を続行するには、ネットワーク手数料に加えて0.2 GRAMが必要です。残高を補充してください。 + GRAMの準備金が必要です このアクションは、ネットワークのルールに従って、他のポジションをクローズするか、引き出しステータスに切り替えます。 ポジション状況 資金をステーキングから引き出すには、ロックを解除してください。ロック解除には%sかかります。 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 0b5e395924..7e2292db62 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -105,6 +105,7 @@ %d endereços Escolha o endereço + Limpar tudo Contato Nome do contato Copiar endereço @@ -134,6 +135,7 @@ Salvar na carteira Este contato será vinculado à agenda de endereços desta carteira. Nenhum resultado encontrado.\nTente outro nome + Selecionar tudo Selecione a rede Agenda de endereços Alterações não salvas @@ -1174,13 +1176,13 @@ Biometria Leia mais sobre a frase-semente. - Escreva esta 1palavra na ordem indicada abaixo e guarde-a em um local seguro e secreto. - Escreva estas %dpalavras na ordem indicada abaixo e guarde-as em um local seguro e secreto. + Escreva esta %d palavra na ordem indicada abaixo e guarde-a em um local seguro e secreto. + Escreva estas %d palavras na ordem indicada abaixo e guarde-as em um local seguro e secreto. Sua frase-semente - %dpalavra - %dpalavras + %d palavra + %d palavras Para importar sua carteira, insira sua frase mnemônica no campo abaixo. Gerar frase-semente @@ -1284,6 +1286,8 @@ Disponível em Disponível até Você recebe + Este token não é compatível. Escolha outro token para comprar. + %s não é suportado O serviço é fornecido por um provedor externo. A Tangem não se responsabiliza por ele. Você pode fechar esta tela e verificar o status da transação na tela de detalhes do token. Até @@ -1296,6 +1300,19 @@ Por equilíbrio Organizar tokens Desagrupar + O cashback elegível será distribuído para: + Você já está matriculado(a) em %1$s + Tokens elegíveis + Inscreva-se + Você se inscreveu com sucesso em %1$s + Esta campanha não existe mais ou expirou. + Campanha inativa + Selecione a conta de cashback + Inscreva-se em %1$s + Concordo com %1$s + Concordo com + %1$s Termos + Ganhe cashback em todas as suas transações a partir de US$ 10.000 até o final de julho.\n\nAs taxas aumentam conforme o tamanho da transação: 0,10% a partir de US$ 10 mil, 0,20% a partir de US$ 20 mil, 0,50% A partir de US$ 100 mil.\n\nPagamento máximo: US$ 500 por troca e US$ 10.000 por carteira por direção de troca, enquanto durar a campanha. Trocas de stablecoin por stablecoin estão excluídas.\n\nO pagamento é feito semanalmente em USDT ou USDC, no endereço de sua escolha. %s suporte As notificações push estão ativadas, mas só funcionarão depois que você as permitir nas configurações do seu dispositivo. Permitir notificações @@ -1627,9 +1644,9 @@ Para começar a fazer staking, você precisa ativar sua conta TON primeiro. Ativação de conta Para começar a fazer staking de TON, primeiro envie uma pequena transação para o seu próprio endereço — isso ativará sua carteira. - Poderá ser necessário um valor adicional de até 0,2 TON, além da taxa de rede, para concluir a transação. Qualquer valor não utilizado será reembolsado. - Para prosseguir com esta operação, é necessário um saldo de 0,2 TON, além da taxa de rede. Por favor, recarregue seu saldo. - Reserva TON necessária + Poderá ser necessário um valor adicional de até 0,2 GRAM, além da taxa de rede, para concluir a transação. Qualquer valor não utilizado será reembolsado. + Para prosseguir com esta operação, é necessário um saldo de 0,2 GRAM, além da taxa de rede. Por favor, recarregue seu saldo. + Reserva GRAM necessária Essa ação encerrará outras posições ou as converterá para o status de saque, de acordo com as regras da rede. Status das posições Desbloqueie seu dinheiro para retirá-lo do processo de staking. O desbloqueio leva... %s. @@ -1819,6 +1836,16 @@ Sua conta foi encerrada Não é possível usar em dispositivos com root. Saldo disponível + ACH + FedWire + Taxa de acesso à rampa + Os USD recebidos serão convertidos para USDC na proporção de 1:1. + A transferência bancária pode levar de 1 a 2 dias úteis + Ao utilizar o serviço, você concorda com o provedor. %1$s e %2$s + Mostrar detalhes + Isso pode levar um pouco de tempo. + Preparando seus dados bancários + Depósito somente via ACH ou FedWire. Transferências SWIFT serão devolvidas. Ocultar KYC da tela principal Cartão Tangem Pay 1 Adicionar fundos @@ -1888,6 +1915,7 @@ Alterar código PIN Volte ao aplicativo se você se esquecer. Cartão + Erro ao carregar Alterar plano relacionado a cartões Plano relacionado @@ -2018,11 +2046,14 @@ Use USDC para pagamentos do dia a dia. O serviço Tangem Pay está temporariamente inacessível. Tangem Pay + Receba USD fiduciário via ACH/FedWire + Transferência bancária Envie USDC Polygon para o endereço da sua conta De outra carteira ou exchange Recarregue sua conta com qualquer token da carteira Da sua Tangem Wallet USDC na rede Polygon + Benefícios do visto Os fundos de compras reembolsadas não serão devolvidos ao seu saldo na blockchain nem estarão disponíveis para saque, mas permanecerão no saldo do seu cartão para compras futuras Observe Seu código PIN @@ -2064,6 +2095,13 @@ Não foi possível ocultar %s N/A Mostrar código QR + Não foi possível carregar os dados do token. + Ir para o swap + Última atualização: %1$s + Perspectiva negativa + Perspectiva neutra + Perspectiva positiva + Resumo do token Troque este token por outro em %1$s taxas de serviço a partir de fevereiro %2$s-%3$s. Trocar com Changelly, %s tarifas Troque agora @@ -2144,6 +2182,12 @@ Renomear carteira Desbloquear tudo Desbloqueie tudo com %s + Número de conta + Endereço do banco + Nome do banco + Endereço do beneficiário + Nome do beneficiário + Número de roteamento Conta virtual Ainda não há transações. Comece a gastar e veja o histórico aqui. Verificado AML @@ -2656,6 +2700,8 @@ %1$s retirado de Aave Modo de rendimento inicializado Modo de rendimento reativado + Devolvido + Fornecido Fornecimento para Aave %1$s fornecido à Aave Retirar do Aave diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 0354f68c22..d111e36499 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -81,7 +81,7 @@ Выберите токен для получения Выберите токен для обмена Пополнить - Обмен + Обменять Перевести Добавить в портфель Добавить токены @@ -98,17 +98,24 @@ Добавить адрес Добавить адрес и выбрать сеть Добавить контакт + Адрес скопирован + Этот адрес уже сохранен как %1$s - адрес - адреса - адресов - адресов + %d адрес + %d адреса + %d адресов + %d адресов + Выберите адрес + Очистить все Контакт Имя контакта Копировать адрес + Контакт сохранен Не удалось создать контакт. Пожалуйста, попробуйте позже. + Удалить контакт Этот контакт будет удален из всех ваших адресных книг + У \"%1$s\" только один адрес. Его удаление также приведет к удалению контакта. Продолжить? Не удалось удалить контакт. Пожалуйста, попробуйте позже. Управление контактами и адресами Отменить @@ -116,11 +123,21 @@ Ввести адрес Неверный адрес Продолжить + Вы не можете создать более 20 адресов. Удалите один, чтобы добавить новый. + Невозможно добавить новый адрес + Имя контакта обязательно + Имя контакта содержит недопустимые символы + Имя контакта должно содержать не более 50 символов + Это имя уже используется в этом кошельке Новый контакт Нет добавленных контактов Здесь отобразятся добавленные вами контакты. Удалить адрес + Сохранить контакт + Сохранить в кошелек Этот контакт будет привязан к этому кошельку в адресной книге. + Ничего не найдено.\nПопробуйте другое имя + Выбрать все Выбрать сеть Адресная книга Несохраненные изменения @@ -1335,6 +1352,7 @@ Банковская карта или банковский счет Поделитесь своим адресом или QR-кодом Продавайте криптовалюту безопасно + Отправить с обменом на другой токен Отправить на другой кошелек Между вашими портфелями Другие @@ -1526,7 +1544,7 @@ Отправляемая сумма превышает остаток Вы уверены, что хотите изменить токен для получения? Это действие сбросит ранее введённые данные. Изменение токена - Обмен и отправка + Обменять и отправить Продолжить с обменом? Это действие удалит предыдущие данные Подтвердить конвертацию Отправка любой другой валюты приведёт к её безвозвратной потере. @@ -1631,9 +1649,9 @@ Чтобы начать стейкинг, сначала активируйте свой TON-аккаунт. Активация аккаунта Чтобы начать стейкинг в TON, сначала отправьте небольшую транзакцию на свой же адрес — это активирует ваш кошелёк. - До 0.2 TON может потребоваться сверх сетевой комиссии для завершения транзакции. Неиспользованная часть будет возвращена. - Для выполнения операции требуется дополнительно 0.2 TON, помимо сетевой комиссии. Пожалуйста, пополните баланс. - Требуется резерв TON + До 0.2 GRAM может потребоваться сверх сетевой комиссии для завершения транзакции. Неиспользованная часть будет возвращена. + Для выполнения операции требуется дополнительно 0.2 GRAM, помимо сетевой комиссии. Пожалуйста, пополните баланс. + Требуется резерв GRAM Это действие закроет другие позиции или переведёт их в статус вывода средств в соответствии с правилами сети. Статус позиций Разблокируйте свои средства, чтобы вывести их из стейкинга. Разблокировка займёт %s. @@ -2047,7 +2065,7 @@ Токен в сети %%image%% %1$s %s сеть %1$s в сети %2$s - %1$sв %%image%% %2$s + %1$s в %%image%% %2$s %1$s в %2$s %%image%% Токен %1$s (%2$s) является основной валютой в сети %3$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети Невозможно скрыть %s diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 1176df0144..87fbf2490b 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -81,7 +81,7 @@ Оберіть токен для отримання Оберіть токен для обміну Поповнити - Обмін + Обміняти Переказ Додати до портфеля Додати токени @@ -98,17 +98,24 @@ Додати адресу Додати адресу та вибрати мережу Додати контакт + Адресу скопійовано + Цю адресу вже збережено як %1$s - адреса - адреси - адрес - адрес + %d адреса + %d адреси + %d адрес + %d адрес + Оберіть адресу + Очистити все Контакт Ім\'я контакту Скопіювати адресу + Контакт збережено Не вдалося створити контакт. Будь ласка, спробуйте пізніше. + Видалити контакт Цей контакт буде видалено з усіх ваших адресних книг + У \"%1$s\" лише одна адреса. Її видалення також призведе до видалення контакту. Продовжити? Не вдалося видалити контакт. Будь ласка, спробуйте пізніше. Керування контактами та адресами Скасувати @@ -116,11 +123,21 @@ Ввести адресу Недійсна адреса Продовжити + Ви не можете створити більше 20 адрес. Видаліть одну, щоб додати нову. + Неможливо додати нову адресу + Ім\'я контакту обов\'язкове + Ім\'я контакту містить недопустимі символи + Ім\'я контакту повинно містити не більше 50 символів + Це ім\'я вже використовується в цьому гаманці Новий контакт Немає доданих контактів Тут відображатимуться додані вами контакти. Видалити адресу + Зберегти контакт + Зберегти в гаманець Цей контакт буде прив\'язано до цього гаманця в адресній книзі. + Нічого не знайдено.\nСпробуйте інше ім\'я + Вибрати все Вибрати мережу Адресна книга Незбережені зміни @@ -1335,6 +1352,7 @@ Банківська картка або банківський рахунок Поділіться своєю адресою або QR-кодом Безпечно продавайте криптовалюту + Надіслати з обміном на інший токен Надіслати на інший гаманець Між вашими портфелями Інші @@ -1526,7 +1544,7 @@ Сума, що відправляється, перевищує залишок Ви впевнені, що хочете змінити токен отримання? Це призведе до скидання раніше введених даних. Зміна токену - Обмін та надсилання + Обміняти та надіслати Продовжити обмін? Це очистить ваші попередні дані. Підтвердити конвертацію Надсилання будь-якої іншої валюти призведе до її незворотної втрати. @@ -1631,9 +1649,9 @@ Щоб розпочати стейкінг, спочатку активуйте свій TON-акаунт. Активація акаунту Щоб почати стейкінг в TON, спочатку здійсніть вихідну транзакцію на будь-яку суму — це активує ваш гаманець. - До 0.2 TON може знадобитися додатково мережевої комісії для завершення транзакції. Невикористана частина буде повернута. - Для виконання операції потребується додатково 0.2 TON, крім мережевої комісії. Будь ласка, поповніть баланс. - Потрібен резерв TON + До 0.2 GRAM може знадобитися додатково мережевої комісії для завершення транзакції. Невикористана частина буде повернута. + Для виконання операції потребується додатково 0.2 GRAM, крім мережевої комісії. Будь ласка, поповніть баланс. + Потрібен резерв GRAM Ця дія закриє інші позиції або переведе їх у статус виведення, відповідно до правил мережі. Статус позицій Розблокуйте свої кошти, щоб вивести їх зі стейкінгу. Розблокування займе %s. 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 3a428f777a..ef6f3f2e6c 100644 --- a/core/res/src/main/res/values-zh-rCN/strings.xml +++ b/core/res/src/main/res/values-zh-rCN/strings.xml @@ -104,6 +104,7 @@ %d地址\n%d地址 选择地址 + 全部清除 联系人 联系人姓名 复制地址 @@ -133,6 +134,7 @@ 保存到钱包 该联系人将与该钱包的通讯录关联。 未找到结果。\n请尝试其他名称 + 全选 选择网络 地址簿 未保存的更改 @@ -1278,6 +1280,7 @@ 信用卡或银行账户 分享您的地址或二维码 安全出售加密货币 + 发送并兑换成另一种代币 发送到另一个钱包 在您的投资组合之间 其他 @@ -1565,9 +1568,9 @@ 要开始质押,您需要先激活您的 TON 账户。 激活账户 要开始在 TON 上进行质押,首先向您自己的地址发送一笔小额交易——这将激活您的钱包。 - 除网络费用外,完成交易可能还需要额外支付最多 0.2 TON 的费用。任何未使用的金额将予以退还。 - 除网络费用外,本次操作还需要 0.2 TON。请充值。 - 需要TON储备 + 除网络费用外,完成交易可能还需要额外支付最多 0.2 GRAM 的费用。任何未使用的金额将予以退还。 + 除网络费用外,本次操作还需要 0.2 GRAM。请充值。 + 需要GRAM储备 根据网络规则,此操作将关闭其他仓位或将其切换为提现状态。 仓位状态 解锁您的资金即可从质押过程中提款。解锁需要一定时间。 %s。 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 7540b870fa..f2b53e3b74 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -105,6 +105,7 @@ %d addresses Choose address + Clear All Contact Contact name Copy address @@ -134,6 +135,7 @@ Save to Wallet This contact will be linked to this wallet’s address book. No results found.\nTry another name + Select All Select network Address book Unsaved changes @@ -636,6 +638,8 @@ Long transaction time The transaction amount was refunded in %1$s to your wallet due to OKX or bridge rules. %2$s The amount was refunded in %1$s (%2$s network) + Your funds have been refunded in %1$s to your wallet on the %2$s network, in accordance with OKX exchange rules. + Refunded in %s Visit provider’s website for verification KYC verification required by provider Purchase completed @@ -1299,6 +1303,21 @@ By balance Organize tokens Ungroup + Eligible cashback will be distributed to: + You\'re already enrolled in %1$s + Eligible tokens + Enroll + You\'re successfully enrolled in %1$s + This campaign no longer exists or has expired + Campaign not active + Earn 0.5% cashback on every swap over $500, on any pair except stable to stable. Max payout $50 per swap.\n\nComplete five qualifying swaps and unlock an extra $10 bonus.\n\nRewards are paid weekly in USDT or USDC on the address selected. + Select cashback account + Select token + Enroll in %1$s + I agree with %1$s + I agree with + %1$s Terms + Earn cashback on every swap from $10K until the end of July.\n\nRates step up with size: 0.10% from $10K, 0.20% from $20K, 0.50% from $100K.\n\nMax payout: $500 per swap, and $10,000 per wallet per swap direction until campaign lasts. Stable coin into stablecoin swaps are excluded.\n\nPayout arrives weekly in USDT or USDC address of your choice. %s support Push Notifications are enabled but won\'t work until you allow them Allow notifications @@ -1522,7 +1541,7 @@ Total amount exceeds balance Are you sure you want to change the receiving token? This will reset your previously entered data. Changing token - Swap and send + Swap & Send Proceed with swap? This will clear your previous data. Confirm Conversion Sending any other currency will result in its irreversible loss. @@ -1630,9 +1649,9 @@ To begin staking, you need to activate your TON account first. Account activation To start staking in TON, first send a small transaction to your own address — this will activate your wallet. - Up to 0.2 TON may be required in addition to the network fee to complete the transaction. Any unused amount will be refunded. - 0.2 TON is required to proceed with this operation, in addition to the network fee. Please top up your balance. - TON reserve required + Up to 0.2 GRAM may be required in addition to the network fee to complete the transaction. Any unused amount will be refunded. + 0.2 GRAM is required to proceed with this operation, in addition to the network fee. Please top up your balance. + GRAM reserve required This action will close other positions or switch them to withdrawal status, according to network rules. Positions status Unlock your money to withdraw it from staking process. Unlocking takes %s. @@ -1901,6 +1920,7 @@ Change PIN-code Come back to the app if you forget it. Card + Error loading Change plan Card related Plan related @@ -2038,6 +2058,7 @@ Use crypto from your wallet to top up your payment account From your Tangem Wallet USDC on Polygon network + Visa Benefits Funds from refunded purchases won’t be returned your on-chain Polygon balance or be available for withdrawal, but will stay on your card balance for purchases Please note Your PIN code @@ -2081,6 +2102,10 @@ Show QR code Can’t load data of the token Go to swap + Last update: %1$s + Negative outlook + Neutral outlook + Positive outlook Token summary Exchange this token for another at %1$s service fees from February %2$s-%3$s. Swap with Changelly, %s fees