From bec533452908fbe7102a20a25f56231be16d9d2b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 11:12:10 +0400 Subject: [PATCH 01/33] Updated on 2026-08-14 --- .../model/CustomTokenFormModel.kt | 3 +- .../list/CustomTokenFormUseCasesFacade.kt | 48 +++++++++---------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index ff7a6c3c36..1ce00a7b83 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -55,7 +55,8 @@ internal class CustomTokenFormModel @Inject constructor( private val params: CustomTokenFormComponent.Params = paramsContainer.require() private var createdCurrency: CryptoCurrency? = null - private var useCasesFacade: CustomTokenFormUseCasesFacade = customTokenFormUseCasesFacadeFactory.create(params.mode) + private val useCasesFacade: CustomTokenFormUseCasesFacade = + customTokenFormUseCasesFacadeFactory.create(params.mode.userWalletId) private val customCurrencyValidator = CustomCurrencyValidator( userWalletId = params.mode.userWalletId, useCasesFacade = useCasesFacade, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt index 4d2071cf69..51a46bd0e0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt @@ -18,9 +18,9 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase -import com.tangem.features.managetokens.component.AddCustomTokenMode import com.tangem.lib.crypto.derivation.AccountNodeRecognizer import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -29,7 +29,7 @@ import timber.log.Timber @Suppress("LongParameterList") internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( - @Assisted private val mode: AddCustomTokenMode, + @Assisted private val userWalletId: UserWalletId, private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val derivePublicKeysUseCase: DerivePublicKeysUseCase, private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase, @@ -39,26 +39,24 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( private val accountsFeatureToggles: AccountsFeatureToggles, ) { - suspend fun addCryptoCurrenciesUseCase(currency: CryptoCurrency): Either = when (mode) { - is AddCustomTokenMode.Account -> either { - val accountId = getAccountId(currency) + suspend fun addCryptoCurrenciesUseCase(currency: CryptoCurrency): Either { + return if (accountsFeatureToggles.isFeatureEnabled) { + either { + val accountId = getAccountId(currency) - manageCryptoCurrenciesUseCase(accountId = accountId, add = currency).bind() - } - is AddCustomTokenMode.Wallet -> { - addCryptoCurrenciesUseCase.invoke( - userWalletId = mode.userWalletId, - currency = currency, - ) + manageCryptoCurrenciesUseCase(accountId = accountId, add = currency).bind() + } + } else { + addCryptoCurrenciesUseCase.invoke(userWalletId = userWalletId, currency = currency) } } - suspend fun derivePublicKeysUseCase(currencies: List): Either = when (mode) { - is AddCustomTokenMode.Account -> Unit.right() - is AddCustomTokenMode.Wallet -> derivePublicKeysUseCase.invoke( - userWalletId = mode.userWalletId, - currencies = currencies, - ) + suspend fun derivePublicKeysUseCase(currencies: List): Either { + return if (accountsFeatureToggles.isFeatureEnabled) { + Unit.right() + } else { + derivePublicKeysUseCase.invoke(userWalletId = userWalletId, currencies = currencies) + } } suspend fun checkIsCurrencyNotAddedUseCase( @@ -67,7 +65,7 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( contractAddress: String?, ): Either = if (accountsFeatureToggles.isFeatureEnabled) { getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = mode.userWalletId, + userWalletId = userWalletId, networkId = networkId, derivationPath = derivationPath, contractAddress = contractAddress, @@ -76,7 +74,7 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( .right() } else { checkIsCurrencyNotAddedUseCase.invoke( - userWalletId = mode.userWalletId, + userWalletId = userWalletId, networkId = networkId, derivationPath = derivationPath, contractAddress = contractAddress, @@ -85,15 +83,15 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( private suspend fun Raise.getAccountId(currency: CryptoCurrency): AccountId { val accountList = singleAccountListSupplier.getSyncOrNull( - params = SingleAccountListProducer.Params(userWalletId = mode.userWalletId), + params = SingleAccountListProducer.Params(userWalletId = userWalletId), ) ensureNotNull(accountList) { - IllegalStateException("Account list not found: ${mode.userWalletId}") + IllegalStateException("Account list not found: $userWalletId") } if (accountList.activeAccounts == 1) { - return AccountId.forMainCryptoPortfolio(userWalletId = mode.userWalletId) + return AccountId.forMainCryptoPortfolio(userWalletId = userWalletId) } val currencyAccountIndex = currency.getAccountIndex().bind() @@ -104,7 +102,7 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( cryptoPortfolioAccount?.derivationIndex?.value == currencyAccountIndex } - return account?.accountId ?: AccountId.forMainCryptoPortfolio(userWalletId = mode.userWalletId) + return account?.accountId ?: AccountId.forMainCryptoPortfolio(userWalletId = userWalletId) } private fun CryptoCurrency.getAccountIndex(): Either = either { @@ -140,6 +138,6 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(mode: AddCustomTokenMode): CustomTokenFormUseCasesFacade + fun create(userWalletId: UserWalletId): CustomTokenFormUseCasesFacade } } \ No newline at end of file From 343cf1ed2065df4561c1aac52de91f893d337188 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 15:31:18 +0400 Subject: [PATCH 02/33] Updated on 2026-08-14 --- .../tangem/core/analytics/models/AnalyticsParam.kt | 12 +++++++++++- features/swap/impl/build.gradle.kts | 1 + .../com/tangem/feature/swap/analytics/SwapEvents.kt | 3 +++ .../java/com/tangem/feature/swap/model/SwapModel.kt | 5 ++++- .../feature/wallet/child/wallet/model/WalletModel.kt | 3 +++ .../wallet/analytics/WalletScreenAnalyticsEvent.kt | 7 +++---- .../supply/api/analytics/YieldSupplyAnalytics.kt | 5 +++++ features/yield-supply/impl/build.gradle.kts | 1 + .../model/YieldSupplyStartEarningModel.kt | 3 +++ .../stopearning/model/YieldSupplyStopEarningModel.kt | 5 ++++- 10 files changed, 38 insertions(+), 7 deletions(-) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index cff2fe1375..ff196b2f0e 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -1,5 +1,8 @@ package com.tangem.core.analytics.models +import com.tangem.core.analytics.models.AnalyticsParam.Key.REFERRAL +import com.tangem.core.analytics.models.AnalyticsParam.Key.REFERRAL_ID + sealed class AnalyticsParam { sealed class CardBalanceState(val value: String) { @@ -268,5 +271,12 @@ sealed class AnalyticsParam { const val ENS = "ENS" const val ENS_ADDRESS = "ENS Address" const val ACCOUNT_DERIVATION_FROM = "Account Derivation (from)" + const val REFERRAL = "Referral" + const val REFERRAL_ID = "Referral_ID" } -} \ No newline at end of file +} + +fun getReferralParams(referralId: String?): List> = listOf( + REFERRAL to (!referralId.isNullOrBlank()).toString().replaceFirstChar(Char::titlecase), + REFERRAL_ID to (referralId ?: "None"), +) \ No newline at end of file diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index 9be6f2453e..69dd4663a7 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.analytics.models) implementation(projects.core.configToggles) + implementation(projects.core.datasource) implementation(projects.core.navigation) implementation(projects.core.utils) implementation(projects.core.ui) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index fe69da29ce..b091a1fbfd 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -8,6 +8,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_TOKEN import com.tangem.core.analytics.models.AnalyticsParam.Key.SEND_TOKEN import com.tangem.core.analytics.models.AppsFlyerIncludedEvent +import com.tangem.core.analytics.models.getReferralParams import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.FeeType @@ -86,6 +87,7 @@ sealed class SwapEvents( val receiveToken: String, val fromDerivationIndex: Int?, val toDerivationIndex: Int?, + val referralId: String?, ) : SwapEvents( event = "Swap in Progress Screen Opened", params = mapOf( @@ -96,6 +98,7 @@ sealed class SwapEvents( "Send Blockchain" to sendBlockchain, "Receive Blockchain" to receiveBlockchain, "Account Derivation From or To (optional)" to "$fromDerivationIndex, $toDerivationIndex", + *getReferralParams(referralId).toTypedArray(), ), ), AppsFlyerIncludedEvent diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 65dba91ee9..96763be19f 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 @@ -20,6 +20,7 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.InputNumberFormatter +import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase @@ -118,6 +119,7 @@ internal class SwapModel @Inject constructor( private val accountsFeatureToggles: AccountsFeatureToggles, private val getTangemPayCurrencyStatusUseCase: GetTangemPayCurrencyStatusUseCase, private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase, + private val appsFlyerStore: AppsFlyerStore, ) : Model() { private val params = paramsContainer.require() @@ -888,7 +890,7 @@ internal class SwapModel @Inject constructor( } } - private fun sendSuccessEvent() { + private suspend fun sendSuccessEvent() { val provider = dataState.selectedProvider ?: return val fee = dataState.selectedFee?.feeType ?: return val fromCurrency = dataState.fromCryptoCurrency?.currency ?: return @@ -906,6 +908,7 @@ internal class SwapModel @Inject constructor( receiveToken = toCurrency.symbol, fromDerivationIndex = fromDerivationIndex, toDerivationIndex = toDerivationIndex, + referralId = appsFlyerStore.get()?.refcode, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index dc160b2505..eedc7314e9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -10,6 +10,7 @@ import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase @@ -105,6 +106,7 @@ internal class WalletModel @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val feedFeatureToggle: FeedFeatureToggle, private val bindRefcodeWithWalletUseCase: BindRefcodeWithWalletUseCase, + private val appsFlyerStore: AppsFlyerStore, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -235,6 +237,7 @@ internal class WalletModel @Inject constructor( accountsCount = accountsCount, theme = theme.value, isImported = selectedWallet.isImported(), + referralId = appsFlyerStore.get()?.refcode, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index e766bf4333..f92476350d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -1,9 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.analytics -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.AppsFlyerIncludedEvent -import com.tangem.core.analytics.models.OneTimeAnalyticsEvent +import com.tangem.core.analytics.models.* import com.tangem.domain.models.wallet.UserWalletId sealed class WalletScreenAnalyticsEvent { @@ -59,6 +56,7 @@ sealed class WalletScreenAnalyticsEvent { private val accountsCount: Int?, val theme: String, val isImported: Boolean, + val referralId: String?, ) : MainScreen( event = "Screen opened", params = buildMap { @@ -71,6 +69,7 @@ sealed class WalletScreenAnalyticsEvent { "Seedless" } put("Wallet Type", seedPhrase) + putAll(getReferralParams(referralId)) }, ), AppsFlyerIncludedEvent diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt index 02f6ba56aa..629181b412 100644 --- a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt +++ b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/analytics/YieldSupplyAnalytics.kt @@ -6,6 +6,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_DESCRIPTION import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM import com.tangem.core.analytics.models.AppsFlyerIncludedEvent +import com.tangem.core.analytics.models.getReferralParams sealed class YieldSupplyAnalytics( event: String, @@ -103,22 +104,26 @@ sealed class YieldSupplyAnalytics( data class FundsEarned( val token: String, val blockchain: String, + val referralId: String?, ) : YieldSupplyAnalytics( event = "Funds Earned", params = mapOf( TOKEN_PARAM to token, BLOCKCHAIN to blockchain, + *getReferralParams(referralId).toTypedArray(), ), ), AppsFlyerIncludedEvent data class FundsWithdrawn( val token: String, val blockchain: String, + val referralId: String?, ) : YieldSupplyAnalytics( event = "Funds Withdrawn", params = mapOf( TOKEN_PARAM to token, BLOCKCHAIN to blockchain, + *getReferralParams(referralId).toTypedArray(), ), ), AppsFlyerIncludedEvent diff --git a/features/yield-supply/impl/build.gradle.kts b/features/yield-supply/impl/build.gradle.kts index e79c976bd4..f1b9082389 100644 --- a/features/yield-supply/impl/build.gradle.kts +++ b/features/yield-supply/impl/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { /** Core */ implementation(projects.core.configToggles) + implementation(projects.core.datasource) implementation(projects.core.decompose) implementation(projects.core.ui) implementation(projects.core.navigation) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index 6a2d3d2597..52fe5d3df6 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -11,6 +11,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -65,6 +66,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( private val yieldSupplyGetMaxFeeUseCase: YieldSupplyGetMaxFeeUseCase, private val yieldSupplyGetCurrentFeeUseCase: YieldSupplyGetCurrentFeeUseCase, private val yieldSupplyRepository: YieldSupplyRepository, + private val appsFlyerStore: AppsFlyerStore, ) : Model(), YieldSupplyNotificationsComponent.ModelCallback { private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require() @@ -267,6 +269,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( YieldSupplyAnalytics.FundsEarned( blockchain = cryptoCurrency.network.name, token = cryptoCurrency.symbol, + referralId = appsFlyerStore.get()?.refcode, ), ) yieldSupplyFeeUM.transactionDataList.forEach { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt index f327d6c82a..0a54db1fee 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt @@ -11,6 +11,7 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrency @@ -61,6 +62,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor( private val yieldSupplyAlertFactory: YieldSupplyAlertFactory, private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase, private val yieldSupplyRepository: YieldSupplyRepository, + private val appsFlyerStore: AppsFlyerStore, ) : Model(), YieldSupplyNotificationsComponent.ModelCallback { private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require() @@ -160,7 +162,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor( yieldSupplyAlertFactory.onFailedTxEmailClick( userWallet = params.userWallet, cryptoCurrency = cryptoCurrency, - errorMessage = error.toString(), + errorMessage = errorMessage, ) } }, @@ -184,6 +186,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor( YieldSupplyAnalytics.FundsWithdrawn( token = cryptoCurrency.symbol, blockchain = cryptoCurrency.network.name, + referralId = appsFlyerStore.get()?.refcode, ), ) val event = AnalyticsParam.TxSentFrom.Earning( From 510923930c8f824708606c7f103e11318bfe9abf Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Feb 2026 15:31:19 +0400 Subject: [PATCH 03/33] Updated on 2026-08-14 --- .../TangemPayTxHistoryDetailsConverter.kt | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt index 8269d2423a..d5be0d999b 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayTxHistoryDetailsConverter.kt @@ -110,10 +110,11 @@ internal object TangemPayTxHistoryDetailsConverter : is TangemPayTxHistoryItem.Spend -> { val amountPrefix = when { this.amount.isZero() -> "" - this.status == TangemPayTxHistoryItem.Status.DECLINED -> "" - else -> StringsSigns.MINUS + this.status == TangemPayTxHistoryItem.Status.DECLINED || + this.amount.isPositive() -> StringsSigns.MINUS + else -> StringsSigns.PLUS } - val amount = this.amount.format { + val amount = this.amount.abs().format { fiat( fiatCurrencyCode = this@extractAmount.currency.currencyCode, fiatCurrencySymbol = this@extractAmount.currency.symbol, @@ -164,12 +165,19 @@ internal object TangemPayTxHistoryDetailsConverter : val localCurrency = this.localCurrency val localAmount = this.localAmount if (localCurrency != null && localAmount != null && localCurrency != currency) { - localAmount.format { + val amountPrefix = when { + localAmount.isZero() -> "" + this.status == TangemPayTxHistoryItem.Status.DECLINED || + localAmount.isPositive() -> StringsSigns.MINUS + else -> StringsSigns.PLUS + } + val amount = localAmount.abs().format { fiat( fiatCurrencyCode = localCurrency.currencyCode, fiatCurrencySymbol = localCurrency.symbol, ).price() } + amountPrefix + amount } else { null } From 427f8a832829d0c2a991727ea795afd2fd465550 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 14:48:30 +0300 Subject: [PATCH 04/33] Updated on 2026-08-14 --- .../gasless/CreateAndSendGaslessTransactionUseCase.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/CreateAndSendGaslessTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/CreateAndSendGaslessTransactionUseCase.kt index 70bafab5bd..6966933ca2 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/CreateAndSendGaslessTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/gasless/CreateAndSendGaslessTransactionUseCase.kt @@ -282,9 +282,12 @@ class CreateAndSendGaslessTransactionUseCase( val feeInTokenCurrency = txFee.transactionFee.normal as? Fee.Ethereum.TokenCurrency ?: error("Fee must be in token currency") + val maxTokenFeeAmount = feeInTokenCurrency.amount + val maxTokenFee = maxTokenFeeAmount.value?.movePointRight(maxTokenFeeAmount.decimals)?.toBigInteger() + ?: error("Max token fee amount is null") return GaslessTransactionData.Fee( feeToken = tokenForFee.contractAddress, - maxTokenFee = feeInTokenCurrency.gasLimit, + maxTokenFee = maxTokenFee, coinPriceInToken = feeInTokenCurrency.coinPriceInToken, feeTransferGasLimit = feeInTokenCurrency.feeTransferGasLimit, baseGas = feeInTokenCurrency.baseGas, From bbc40d7ddf319621c0dc3f3a219bf67f75655cc8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 30 Jan 2026 13:59:48 +0400 Subject: [PATCH 05/33] Updated on 2026-08-14 --- .../tangem/tap/di/TangemSdkManagerModule.kt | 16 ++++++ .../sdk/impl/DefaultTangemSdkManager.kt | 52 +++++++++++++++++-- core/res/src/main/res/values-de/strings.xml | 24 +++++++++ core/res/src/main/res/values-es/strings.xml | 4 +- core/res/src/main/res/values-fr/strings.xml | 12 +++++ core/res/src/main/res/values-ja/strings.xml | 29 ++++++++--- core/res/src/main/res/values-ru/strings.xml | 7 ++- .../src/main/res/values-uk-rUA/strings.xml | 2 + core/res/src/main/res/values/strings.xml | 8 ++- .../feedback/models/FeedbackEmailType.kt | 4 ++ .../feedback/SendFeedbackEmailUseCase.kt | 1 + .../utils/EmailMessageBodyResolver.kt | 1 + .../utils/EmailMessageTitleResolver.kt | 1 + .../feedback/utils/EmailSubjectResolver.kt | 1 + 14 files changed, 147 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt index 0487d0fad7..38e66db21b 100644 --- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -1,8 +1,13 @@ package com.tangem.tap.di import android.content.Context +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.domain.card.BuildConfig import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager @@ -10,6 +15,7 @@ import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.visa.VisaCardScanHandler +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -30,6 +36,11 @@ internal class TangemSdkManagerModule { visaCardActivationTaskFactory: VisaCardActivationTask.Factory, tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory, onboardingV2FeatureToggles: OnboardingV2FeatureToggles, + @GlobalUiMessageSender uiMessageSender: UiMessageSender, + appFinisher: AppFinisher, + sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + analyticsExceptionHandler: AnalyticsExceptionHandler, + dispatchers: CoroutineDispatcherProvider, ): TangemSdkManager { return if (BuildConfig.MOCK_DATA_SOURCE) { MockTangemSdkManager(resources = context.resources) @@ -41,6 +52,11 @@ internal class TangemSdkManagerModule { visaCardActivationTaskFactory = visaCardActivationTaskFactory, tangemPayChallengeTaskFactory = tangemPayChallengeTaskFactory, onboardingV2FeatureToggles = onboardingV2FeatureToggles, + uiMessageSender = uiMessageSender, + appFinisher = appFinisher, + sendFeedbackEmailUseCase = sendFeedbackEmailUseCase, + analyticsExceptionHandler = analyticsExceptionHandler, + dispatchers = dispatchers, ) } } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index ef82b7c01f..20046a427d 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -18,12 +18,21 @@ import com.tangem.common.extensions.hexToBytes import com.tangem.common.services.secure.SecureStorage import com.tangem.common.usersCode.UserCodeRepository import com.tangem.core.analytics.Analytics +import com.tangem.core.analytics.api.AnalyticsExceptionHandler +import com.tangem.core.analytics.models.ExceptionAnalyticsEvent +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.res.getStringSafe +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.crypto.bip39.DefaultMnemonic import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.repository.CardSdkConfigRepository +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId @@ -57,13 +66,14 @@ import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask import com.tangem.tap.domain.twins.FinalizeTwinTask import com.tangem.tap.domain.visa.VisaCardScanHandler +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.wallet.R import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlin.coroutines.resume -@Suppress("TooManyFunctions", "LargeClass") +@Suppress("TooManyFunctions", "LargeClass", "LongParameterList") internal class DefaultTangemSdkManager( private val cardSdkConfigRepository: CardSdkConfigRepository, private val resources: Resources, @@ -71,6 +81,11 @@ internal class DefaultTangemSdkManager( private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory, private val tangemPayChallengeTaskFactory: TangemPayGenerateAddressAndSignChallengeTask.Factory, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, + private val uiMessageSender: UiMessageSender, + private val appFinisher: AppFinisher, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val analyticsExceptionHandler: AnalyticsExceptionHandler, + dispatchers: CoroutineDispatcherProvider, ) : TangemSdkManager { private val awaitInitializationMutex = Mutex() @@ -99,6 +114,8 @@ internal class DefaultTangemSdkManager( override val userCodeRequestPolicy: UserCodeRequestPolicy get() = tangemSdk.config.userCodeRequestPolicy + private val coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.io) + override suspend fun checkNeedEnrollBiometrics(awaitInitialization: Boolean): Boolean { return try { needEnrollBiometrics @@ -413,9 +430,16 @@ internal class DefaultTangemSdkManager( break } else { if (attemps++ >= MAX_INITIALIZE_ATTEMPTS) { - error("Can't initialize authentication manager after $MAX_INITIALIZE_ATTEMPTS attempts") + analyticsExceptionHandler.sendException( + ExceptionAnalyticsEvent( + exception = IllegalStateException( + "Can't initialize authentication manager after $MAX_INITIALIZE_ATTEMPTS attempts", + ), + ), + ) + showAlert() } else { - delay(timeMillis = 200) + delay(timeMillis = 400) } } } while (true) @@ -424,6 +448,28 @@ internal class DefaultTangemSdkManager( } } + private fun showAlert() { + uiMessageSender.send( + message = DialogMessage( + message = resourceReference(id = R.string.alert_authentication_error_message), + title = resourceReference(id = R.string.alert_authentication_error_title), + isDismissable = false, + dismissOnFirstAction = false, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.alert_button_request_support), + onClick = { + coroutineScope.launch { + sendFeedbackEmailUseCase(FeedbackEmailType.BiometricsAuthenticationFailed) + } + }, + ) + }, + secondActionBuilder = { cancelAction(onClick = appFinisher::finish) }, + ), + ) + } + // region Twin-specific override suspend fun createFirstTwinWallet( diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 4175f06e1c..ab28c8f601 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -86,6 +86,8 @@ Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen. Standard Altbestand + Bei der biometrischen Authentifizierung ist ein Fehler aufgetreten. Bitte setzen Sie die Biometrie auf Ihrem Gerät zurück oder kontaktieren Sie den Support. + Authentifizierungsfehler So scannt man Hilfe anfordern Erneut versuchen @@ -299,6 +301,7 @@ Zum Token Verstanden Ausblenden + Halten bis %s Stunde Importieren In Arbeit @@ -374,6 +377,7 @@ Tauschen Tangem Tangem Wallet + Tippen und halten Allgemeine Geschäftsbedingungen Nutzungsbedingungen An @@ -463,6 +467,11 @@ Das Senden von Vermögenswerten in anderen Netzwerken führt zu dauerhaftem Verlust. %s Netzwerk Sende Geld nur mit + Beste Gelegenheiten + Alle Netzwerke + Alle Arten + Meist verwendet + Verdienen Hallo Support-Team, ich habe einen Fehler mit dem Code %s festgestellt. WalletConnect-Fehler Du hast eine Karte oder Ring aus einer anderen Wallet verwendet. Tippe auf die Karte oder Ring, die dieser Wallet zugeordnet ist. @@ -613,6 +622,7 @@ Zuerst die Sicherung abschließen Unvollständig Andere Methoden + Bewahre Deine Wiederherstellungsphrase an einem sicheren Ort auf und halte diese geheim, um Dein Guthaben zu schützen. Richte außerdem einen 8 stelligen Zugangscode für zusätzliche Sicherheit ein. Speicher Deinen Wiederherstellungssatz an einem sicheren Ort und halte diesen stets geheim, um Dein Geld zu schützen. Wiederherstellungs-Phrase Um Deine Wallet mit einem Zugangscode zu sichern, schließe den Sicherungsvorgang ab. @@ -1463,6 +1473,9 @@ Einfrieren Ihre Karte ist eingefroren. Hilfe erhalten + Grund: %s + %s · %s + MCC %s Andere Nicht nutzbar auf gerooteten Geräten Abgeschlossen @@ -1834,7 +1847,18 @@ OK, habe ich verstanden! Echt toll! Aktualisieren + Migration starten + Kopie + Um weiterhin Zugriff auf Deine Gelder zu haben, beginne die Migration gemäß den offiziellen Clore-Richtlinien. + Die Nachrichtensignatur wird für dieses Netzwerk nicht unterstützt. + Nachricht konnte nicht signiert werden. Bitte versuche es erneut. Laut der offiziellen Dokumentation von Clore werden alle Münzen, die vor dem 21. Dezember erhalten wurden, in Clore (ERC-20 Token) migriert; Münzen, die nach diesem Datum erhalten wurden, nicht. Eine Lösung für den Transfer ist in Arbeit — bleibt dran. + Nachricht + Claim-Portal öffnen + Um Deine Clore-Token weiterhin nutzen zu können, musst Du die Token-Migration gemäß den Informationen im Claim Portal durchführen. + Clore-Netzwerkmigration + signieren + Unterschrift Migration des Clore-Netzwerks Du befindest sich derzeit im Demo-Modus Demo-Modus aktiv diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index c3b8bb8a6a..4e0238e409 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -86,6 +86,8 @@ Envíe solo %1$s (%2$s) desde redes como %3$s a esta dirección. Usar otros tokens y redes puede resultar en la pérdida de fondos. Por defecto Legacy + Algo salió mal con la biometría. Por favor, intente restablecer la biometría en su dispositivo o contacte con soporte. + Error de inicio de sesión Cómo escanear Solicitar soporte Inténtelo de nuevo @@ -836,7 +838,7 @@ Hace %d minutos Resumen rápido - Noticias relacionadas + Noticias Tokens relacionados Fuentes Manténgase informado diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index d88cdd4e57..7eda943c3f 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -86,6 +86,8 @@ Envoyez uniquement %1$s (%2$s) depuis les réseaux %3$s à cette adresse. L\'utilisation d\'autres jetons et réseaux peut entraîner une perte de fonds. Défaut Héritage + Quelque chose s’est mal passé avec la biométrie. Veuillez réinitialiser la biométrie sur votre appareil ou contacter le support. + Erreur d’authentification Comment scanner Demander de l\'aide Réessayez @@ -1845,7 +1847,17 @@ Ok, compris! Vraiment cool ! Rafraîchir + Lancer la migration + Copier + La signature des messages n\'est pas prise en charge pour ce réseau. + Impossible de signer le message. Veuillez réessayer. Selon la documentation officielle de Clore, toutes les pièces reçues avant le 21 décembre seront migrées vers Clore (token ERC-20) ; les pièces reçues après cette date ne le seront pas. Une solution de transfert arrive — restez à l\'écoute. + Message + Ouvrir le portail des réclamations + Pour continuer à utiliser vos jetons Clore, vous devez effectuer la migration des jetons conformément aux informations fournies sur le portail de réclamation. + Migration du réseau Clore + Signer + Signature Migration du réseau Clore Vous êtes actuellement en mode démo Mode démo actif diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 770eb98276..c773a24141 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -86,6 +86,8 @@ このアドレスには%3$s ネットワークから%1$s (%2$s) のみを送信してください。他のトークンやネットワークを使用すると、資金を失う可能性があります。 デフォルト レガシー + デバイスの生体認証をリセットするか、サポートにお問い合わせください + 認証エラー スキャン方法 サポートをリクエストする もう一度やり直してください @@ -523,7 +525,7 @@ プロバイダー ベストレート FCA警告リスト - 最適な選択 + お得なレート FCA警告リストに掲載されたプロバイダー 最大 %s まで使用可能 %s 以上で利用可能 @@ -724,7 +726,7 @@ データなし マーケット動向 クイックアクション - マーケットから探す + トークンを探す 結果 時価総額10万ドル以下のトークンを見る トークンを表示 @@ -741,8 +743,8 @@ 経験豊富な買い手 時価総額 並べ替え - 上昇率上位 - 下落率上位 + 値上がり + 値下がり トレンド 利息モード ステーキングは暗号資産で報酬を受け取る最も簡単な方法です。 %s @@ -770,7 +772,7 @@ 取引所 経験豊富な買い手 少なくとも100の発信取引を持つネット・バイヤー - 経験豊富な買い手 + アクティブ投資家数 完全希薄化後評価額 現在流通していないものも含め、存在する可能性のあるすべてのコインが流通している場合の暗号資産の理論上の合計価値 完全希薄化後評価額 @@ -895,7 +897,7 @@ 事前申し込み受付中。他にない特別なカードを、いち早く体験しよう。 Tangem Visaカード 利用規約 - 100ドル以上を入金し、30日間保持すると、10ドルを受け取れます。 + $100以上を入金して30日間保有すると、$10を受け取れます Yield Mode キャンペーンに参加しよう! すべてのデバイスを保護するには、単一のアクセスコードを設定してください。 保護する @@ -1011,7 +1013,7 @@ 買付金額は%s以下にしてください 買付金額は少なくとも%sである必要があります この通貨で利用可能なプロバイダーはありません - 最速 + 最短処理 支払う 支払方法 最大 %s まで使用可能 @@ -1598,7 +1600,7 @@ このトークンは、2月%2$s-%3$s の間、%1$s のサービス手数料で別のトークンと交換できます。 Changellyでスワップ、手数料%s 今すぐスワップ - ホットな暗号資産🔥 + 市場トレンド🔥 買付できません 売却できません %sからのスワップは利用できません @@ -1825,7 +1827,18 @@ はい、わかりました! すごくクールだ! リフレッシュ + 移行を開始 + コピー + 資金へのアクセスを維持するため、Cloreの公式ガイドラインに従って移行を開始してください。 + このネットワークではメッセージ署名はサポートされていません + メッセージに署名できません。もう一度お試しください。 Cloreの公式ドキュメントによると、12月21日以前に受け取ったすべてのコインは Clore(ERC-20トークン)へ移行されますが、同日以降に受け取ったコインは移行されません。送金(移行)ソリューションは現在準備中です。続報をお待ちください。 + メッセージ + Claim Portalを開く + Cloreトークンを引き続き使用するには、Claim Portalの案内に従ってトークン移行を完了する必要があります。 + Cloreネットワーク移行 + 署名する + 署名 Cloreネットワークの移行 現在デモモードです デモモードが有効になっています diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index b0e42bdf47..cc50a2188a 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -86,6 +86,8 @@ Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. По умолчанию Устаревший + Произошла ошибка при работе с биометрией. Пожалуйста, попробуйте обновить биометрию на вашем устройстве или обратитесь в службу поддержки. + Ошибка аутентификации Как сканировать Обратиться в поддержку Попробовать снова @@ -476,6 +478,9 @@ Отправка средств в другой сети может повлечь потерю средств. %s сеть Отправляйте средства, используя только + Все сети + Все типы + Часто используемые Привет, команда поддержки, у меня возникла ошибка с кодом: %s Ошибка WalletConnect Вы использовали карту или кольцо от другого кошелька. Приложите карту или кольцо, связанную с этим кошельком. @@ -850,7 +855,7 @@ %dмин назад Резюме - Связанные новости + Новости Связанные токены Связанные новости Будьте в курсе diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 67360c1ce3..4bb916c300 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -86,6 +86,8 @@ Надсилайте тільки %1$s (%2$s) в мережі %3$s на цю адресу. Використання іншої мережі може призвести до втрати коштів. За замовчуванням Застарілий + Сталася помилка під час роботи з біометрією. Будь ласка, спробуйте оновити біометрію на вашому пристрої або зверніться до служби підтримки. + Помилка автентифікації Як сканувати Звернутися в підтримку Спробуйте ще раз diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index ed8c132108..8916a5a857 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -86,7 +86,7 @@ Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. Default Legacy - Try to reset biometrics on your device or contact support + Something went wrong with biometrics. Please try resetting biometrics on your device or contact support. Authentication error How to scan Request support @@ -468,9 +468,13 @@ %s network Send funds using only Best opportunities + Clear filter All networks All types + Filter by + Networks Mostly used + No results Earn Hi support team, I\'ve encountered an error with code: %s WalletConnect error @@ -839,7 +843,7 @@ %d minutes ago Quick recap - Related News + News Related tokens Related news Stay in the loop diff --git a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 520f49e395..e52c876c09 100644 --- a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -56,6 +56,10 @@ sealed interface FeedbackEmailType { override val walletMetaInfo: WalletMetaInfo? = null } + data object BiometricsAuthenticationFailed : FeedbackEmailType { + override val walletMetaInfo: WalletMetaInfo? = null + } + sealed class Visa : FeedbackEmailType { data class DirectUserRequest(override val walletMetaInfo: WalletMetaInfo) : Visa() diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt index 07e0eb137e..3289cba524 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/SendFeedbackEmailUseCase.kt @@ -92,6 +92,7 @@ class SendFeedbackEmailUseCase( is FeedbackEmailType.CurrencyDescriptionError, is FeedbackEmailType.PreActivatedWallet, is FeedbackEmailType.CardAttestationFailed, + is FeedbackEmailType.BiometricsAuthenticationFailed, is FeedbackEmailType.Visa.Dispute, is FeedbackEmailType.Visa.DisputeV2, is FeedbackEmailType.Visa.FeatureIsBeta, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index 1e4ca44bd2..4533c06c96 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -30,6 +30,7 @@ internal class EmailMessageBodyResolver( is FeedbackEmailType.PreActivatedWallet -> addUserRequestBody(type.walletMetaInfo) is FeedbackEmailType.ScanningProblem, is FeedbackEmailType.CardAttestationFailed, + is FeedbackEmailType.BiometricsAuthenticationFailed, -> addPhoneInfoBody() is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.walletMetaInfo) is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt index 36940c948e..852e933644 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageTitleResolver.kt @@ -34,6 +34,7 @@ internal class EmailMessageTitleResolver(private val resources: Resources) { is FeedbackEmailType.TransactionSendingProblem, is FeedbackEmailType.StakingProblem, is FeedbackEmailType.SwapProblem, + is FeedbackEmailType.BiometricsAuthenticationFailed, -> R.string.feedback_preface_tx_failed } diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt index 4b1e4f9799..39f92cf852 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailSubjectResolver.kt @@ -38,6 +38,7 @@ internal class EmailSubjectResolver(private val resources: Resources) { resources.getStringSafe(R.string.feedback_token_description_error) } FeedbackEmailType.CardAttestationFailed -> "Card attestation failed" + FeedbackEmailType.BiometricsAuthenticationFailed -> "Biometrics authentication failed" is FeedbackEmailType.Visa.Activation -> "[Visa] [Activation] {auto-filled subject}" is FeedbackEmailType.Visa.DirectUserRequest -> "[Visa] {auto-filled subject}" is FeedbackEmailType.Visa.FailedIssueCard -> "[Visa] {auto-filled subject}" From 2bba23e1a9fa085213e83d85f60eb4b2296e169f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 15:19:27 +0300 Subject: [PATCH 06/33] Updated on 2026-08-14 --- .../wallets/usecase/GetSavedWalletsCountUseCase.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt index 2784698845..eb6257bd8c 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetSavedWalletsCountUseCase.kt @@ -5,9 +5,12 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.legacy.isLockedSync +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map class GetSavedWalletsCountUseCase( @@ -16,9 +19,14 @@ class GetSavedWalletsCountUseCase( private val useNewRepository: Boolean, ) { + @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke(): Flow> { if (useNewRepository) { - return userWalletsListRepository.userWallets.map { requireNotNull(it) } + return flowOf(Unit) + .flatMapLatest { + userWalletsListRepository.load() + userWalletsListRepository.userWallets.map { wallets -> requireNotNull(wallets) } + } } return userWalletsListManager.savedWalletsCount From 295d82db8de7b26c5bd5a94502e9c6bfee42ea44 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 15:23:30 +0300 Subject: [PATCH 07/33] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 098662beb5..a5d1a89425 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 098662beb5b0123b11f5ee4873d4bd667ac93c73 +Subproject commit a5d1a89425a95bc9c90c7a6fed3c578b0d324994 From 51e6f1d5a35a191858b0e0e1bf202a3317275254 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 14:07:57 +0100 Subject: [PATCH 08/33] Updated on 2026-08-14 --- .../main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt | 2 +- core/res/src/main/res/values-es/strings.xml | 2 +- core/res/src/main/res/values-ru/strings.xml | 2 +- core/res/src/main/res/values-uk-rUA/strings.xml | 2 ++ core/res/src/main/res/values/strings.xml | 2 +- .../main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt | 2 +- 6 files changed, 7 insertions(+), 5 deletions(-) diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt index f2931ff828..3215446426 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/MarketListItem.kt @@ -181,7 +181,7 @@ private fun RowScope.TokenRatingPlace(ratingPosition: String?) { .alignByBaseline() .heightIn(min = TangemTheme.dimens.size16) .background( - color = TangemTheme.colors.field.primary, + color = TangemTheme.colors.button.secondary, shape = TangemTheme.shapes.roundedCornersSmall2, ) .padding(horizontal = TangemTheme.dimens.spacing5), diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index c3b8bb8a6a..2a986bbb39 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -836,7 +836,7 @@ Hace %d minutos Resumen rápido - Noticias relacionadas + Noticias Tokens relacionados Fuentes Manténgase informado diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index b0e42bdf47..f89de43b01 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -850,7 +850,7 @@ %dмин назад Резюме - Связанные новости + Новости Связанные токены Связанные новости Будьте в курсе diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 67360c1ce3..4bb916c300 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -86,6 +86,8 @@ Надсилайте тільки %1$s (%2$s) в мережі %3$s на цю адресу. Використання іншої мережі може призвести до втрати коштів. За замовчуванням Застарілий + Сталася помилка під час роботи з біометрією. Будь ласка, спробуйте оновити біометрію на вашому пристрої або зверніться до служби підтримки. + Помилка автентифікації Як сканувати Звернутися в підтримку Спробуйте ще раз diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index ed8c132108..3477c93f5a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -839,7 +839,7 @@ %d minutes ago Quick recap - Related News + News Related tokens Related news Stay in the loop diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index 43dcd4a50e..88ebc8dc1f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -423,7 +423,7 @@ private fun Charts( ) { BlockCard( modifier = modifier, - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.primary), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), ) { Column(modifier = Modifier.fillMaxWidth()) { when (marketChart) { From 92b2c145853085246f0e297dde85483d2100c212 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 18:03:32 +0400 Subject: [PATCH 09/33] Updated on 2026-08-14 --- .../java/com/tangem/core/analytics/models/AnalyticsParam.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index ff196b2f0e..287b8a9170 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -278,5 +278,5 @@ sealed class AnalyticsParam { fun getReferralParams(referralId: String?): List> = listOf( REFERRAL to (!referralId.isNullOrBlank()).toString().replaceFirstChar(Char::titlecase), - REFERRAL_ID to (referralId ?: "None"), + REFERRAL_ID to (referralId ?: "Empty"), ) \ No newline at end of file From 3d9b107be6f66436c91ff1d909fead95b9334d6a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 18:11:14 +0300 Subject: [PATCH 10/33] Updated on 2026-08-14 --- .../tangem/features/send/v2/api/params/FeeSelectorParams.kt | 3 +++ .../send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt | 1 + .../send/v2/feeselector/model/FeeSelectorBlockModel.kt | 2 ++ .../features/send/v2/feeselector/model/FeeSelectorLogic.kt | 6 ++++++ .../features/send/v2/feeselector/model/FeeSelectorModel.kt | 1 + .../main/java/com/tangem/feature/swap/model/SwapModel.kt | 4 ++++ 6 files changed, 17 insertions(+) diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt index 77877cf76e..75cd869eb6 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/params/FeeSelectorParams.kt @@ -23,6 +23,7 @@ sealed class FeeSelectorParams { abstract val feeDisplaySource: FeeDisplaySource abstract val analyticsCategoryName: String abstract val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource + abstract val shouldShowOnlySpeedOption: Boolean data class FeeSelectorBlockParams( override val state: FeeSelectorUM, @@ -37,6 +38,7 @@ sealed class FeeSelectorParams { override val feeDisplaySource: FeeDisplaySource, override val analyticsCategoryName: String, override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, + override val shouldShowOnlySpeedOption: Boolean = false, val bottomSheetShown: (Boolean) -> Unit = {}, ) : FeeSelectorParams() @@ -53,6 +55,7 @@ sealed class FeeSelectorParams { override val feeDisplaySource: FeeDisplaySource, override val analyticsCategoryName: String, override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource, + override val shouldShowOnlySpeedOption: Boolean = false, val callback: FeeSelectorModelCallback, ) : FeeSelectorParams() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index baf95a222c..ca35f59a98 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -56,6 +56,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( analyticsCategoryName = params.analyticsCategoryName, analyticsSendSource = params.analyticsSendSource, userWalletId = params.userWalletId, + shouldShowOnlySpeedOption = model.shouldShowOnlySpeedOption, ), onDismiss = { model.feeSelectorBottomSheet.dismiss() diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorBlockModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorBlockModel.kt index a4890d88c5..05abb78df9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorBlockModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorBlockModel.kt @@ -38,6 +38,8 @@ internal class FeeSelectorBlockModel @Inject constructor( modelScope = modelScope, ) + val shouldShowOnlySpeedOption: Boolean + get() = feeSelectorLogic.shouldShowOnlySpeedOption.value val feeSelectorBottomSheet = SlotNavigation() val uiState: StateFlow field = feeSelectorLogic.uiState diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt index b9f62b2008..b39a4b4717 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt @@ -37,6 +37,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -68,6 +69,9 @@ internal class FeeSelectorLogic @AssistedInject constructor( isGaslessFeeSupportedForNetwork(params.feeCryptoCurrencyStatus.currency.network) && params.cryptoCurrencyStatus.currency is CryptoCurrency.Token + val shouldShowOnlySpeedOption: StateFlow + field = MutableStateFlow(params.shouldShowOnlySpeedOption) + init { initAppCurrency() subscribeOnFeeReloadTriggerUpdates() @@ -253,12 +257,14 @@ internal class FeeSelectorLogic @AssistedInject constructor( is GetFeeError.GaslessError.NotEnoughFunds -> error.left() is GetFeeError.GaslessError -> { // Something wrong with gasless fee, fallback to basic fee + shouldShowOnlySpeedOption.value = true params.onLoadFee().map { LoadedFeeResult.Basic(it) } } else -> error.left() } }, ifRight = { fee -> + shouldShowOnlySpeedOption.value = false populateExtendedFee(fee) }, ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt index 52d5766642..91861c714a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorModel.kt @@ -130,6 +130,7 @@ internal class FeeSelectorModel @Inject constructor( fun getInitialRoute(): FeeSelectorRoute { return when { + params.shouldShowOnlySpeedOption -> FeeSelectorRoute.ChooseSpeed feeSelectorLogic.isGaslessEnabled -> FeeSelectorRoute.NetworkFee else -> FeeSelectorRoute.ChooseSpeed } 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 fd342433db..496279474f 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 @@ -2161,6 +2161,10 @@ internal class SwapModel @Inject constructor( val toToken = dataState.toCryptoCurrency ?: return Either.Left(GetFeeError.UnknownError) val selectedProvider = dataStateStateFlow.first { it.selectedProvider != null }.selectedProvider!! + if (selectedProvider.type != ExchangeProviderType.CEX) { + return Either.Left(GetFeeError.GaslessError.NetworkIsNotSupported) + } + if (dataState.lastLoadedSwapStates[selectedProvider] !is SwapState.QuotesLoadedState) { return Either.Left(GetFeeError.UnknownError) } From 13a96189b0f2638d1a13c68105c2decf54cf8327 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Feb 2026 18:32:27 +0300 Subject: [PATCH 11/33] Updated on 2026-08-14 --- .../com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 9 ++++++++- .../main/java/com/tangem/feature/swap/model/SwapModel.kt | 9 +++++++-- .../feature/swap/model/SwapNotificationsFactory.kt | 5 ++++- 3 files changed, 19 insertions(+), 4 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 a8b55e5af2..8b1c5ff4a5 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 @@ -1648,7 +1648,14 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } } else { - IncludeFeeInAmount.Excluded + val fee = txFeeSealedState.txFee.fee.amount.value ?: BigDecimal.ZERO + getIncludeFeeInAmountForNative( + networkId = networkId, + amount = amount, + reduceBalanceBy = reduceBalanceBy, + fromToken = fromToken.currency, + feeValue = fee, + ) } } is TxFeeSealedState.Legacy -> { 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 496279474f..b51473e5c1 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 @@ -2194,9 +2194,14 @@ internal class SwapModel @Inject constructor( state.value = newState // If fee currency is same as from currency, we need to reload quotes to update fee info - if (newState is FeeSelectorUM.Content && + val isFeeCurrencySameAsFromCurrency = newState is FeeSelectorUM.Content && dataState.fromCryptoCurrency?.currency?.id == newState.feeExtraInfo.feeCryptoCurrencyStatus.currency.id - ) { + + // If fee currency is coin, we need to reload quotes to update fee related warnings (e.g. insufficient funds) + val isCoinFeeSelected = newState is FeeSelectorUM.Content && + newState.feeExtraInfo.feeCryptoCurrencyStatus.currency is CryptoCurrency.Coin + + if (isFeeCurrencySameAsFromCurrency || isCoinFeeSelected) { // block swap button until fee is loaded uiState = uiState.copy( swapButton = uiState.swapButton.copy( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index f7a1817cb3..6c9bd0a484 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -298,9 +298,12 @@ internal class SwapNotificationsFactory( quoteModel.permissionState !is PermissionDataState.PermissionLoading && feeEnoughState.feeCurrency != fromToken + val isNotEnoughFee = + quoteModel.preparedSwapConfigState.includeFeeInAmount is IncludeFeeInAmount.BalanceNotEnough + val isGaslessAvailable = iGaslessFeeSupportedForNetwork(fromToken.network) && quoteModel.swapProvider.type == ExchangeProviderType.CEX - if (shouldShowCoverWarning && !isGaslessAvailable) { + if (shouldShowCoverWarning && !isGaslessAvailable || isNotEnoughFee) { add( SwapNotificationUM.Error.UnableToCoverFeeWarning( fromToken = fromToken, From 45688033c699115c41bf721dbb2fa4e09c729638 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Feb 2026 07:54:30 +0300 Subject: [PATCH 12/33] Updated on 2026-08-14 --- .../models/event/OnboardingAnalyticsEvent.kt | 3 +++ .../domain/card/analytics/IntroductionProcess.kt | 10 +++++++++- features/create-wallet-start/impl/build.gradle.kts | 1 + .../createwalletstart/CreateWalletStartModel.kt | 12 +++++++++--- .../im/port/model/AddExistingWalletImportModel.kt | 3 +++ .../createmobilewallet/CreateMobileWalletModel.kt | 3 +++ .../v2/common/analytics/OnboardingEvent.kt | 4 ++++ .../model/MultiWalletCreateWalletModel.kt | 5 ++++- .../seedphrase/model/MultiWalletSeedPhraseModel.kt | 3 +++ .../create/model/OnboardingNoteCreateWalletModel.kt | 3 +++ .../v2/twin/impl/model/OnboardingTwinModel.kt | 3 +++ 11 files changed, 45 insertions(+), 5 deletions(-) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt index e449459d0e..6743eccfea 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OnboardingAnalyticsEvent.kt @@ -3,6 +3,7 @@ package com.tangem.core.analytics.models.event import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AppsFlyerIncludedEvent +import com.tangem.core.analytics.models.getReferralParams sealed class OnboardingAnalyticsEvent( category: String, @@ -55,6 +56,7 @@ sealed class OnboardingAnalyticsEvent( creationType: WalletCreationType = WalletCreationType.NewSeed, seedPhraseLength: Int? = null, passPhraseState: AnalyticsParam.EmptyFull, + referralId: String?, ) : CreateWallet( event = "Wallet Created Successfully", params = buildMap { @@ -64,6 +66,7 @@ sealed class OnboardingAnalyticsEvent( if (seedPhraseLength != null) { put("Seed Phrase Length", seedPhraseLength.toString()) } + putAll(getReferralParams(referralId)) }, ), AppsFlyerIncludedEvent diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt index b7151afd3d..98114357c7 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/analytics/IntroductionProcess.kt @@ -2,6 +2,7 @@ package com.tangem.domain.card.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.getReferralParams sealed class IntroductionProcess( event: String, @@ -13,7 +14,14 @@ sealed class IntroductionProcess( class ButtonBuyCards : IntroductionProcess("Button - Buy Cards") class ButtonScanCardLegacy : IntroductionProcess("Button - Scan Card") - class CreateWalletIntroScreenOpened : IntroductionProcess("Create Wallet Intro Screen Opened") + class CreateWalletIntroScreenOpened( + referralId: String?, + ) : IntroductionProcess( + event = "Create Wallet Intro Screen Opened", + params = buildMap { + putAll(getReferralParams(referralId)) + }, + ) class ButtonScanCard( val source: AnalyticsParam.ScreensSources, diff --git a/features/create-wallet-start/impl/build.gradle.kts b/features/create-wallet-start/impl/build.gradle.kts index 987ab1a58b..1578c1916f 100644 --- a/features/create-wallet-start/impl/build.gradle.kts +++ b/features/create-wallet-start/impl/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.models) implementation(projects.domain.hotWallet) + implementation(projects.domain.wallets.models) /** Core modules */ implementation(projects.core.configToggles) diff --git a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt index 0760ef5917..9596dcee32 100644 --- a/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt +++ b/features/create-wallet-start/impl/src/main/kotlin/com/tangem/features/createwalletstart/CreateWalletStartModel.kt @@ -20,6 +20,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog +import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.card.ScanCardProcessor import com.tangem.domain.card.analytics.IntroductionProcess import com.tangem.domain.card.repository.CardSdkConfigRepository @@ -65,6 +66,7 @@ internal class CreateWalletStartModel @Inject constructor( private val trackingContextProxy: TrackingContextProxy, private val analyticsEventHandler: AnalyticsEventHandler, private val hotWalletFeatureToggles: HotWalletFeatureToggles, + private val appsFlyerStore: AppsFlyerStore, ) : Model() { private val params = paramsContainer.require() @@ -133,9 +135,13 @@ internal class CreateWalletStartModel @Inject constructor( ) init { - analyticsEventHandler.send( - event = IntroductionProcess.CreateWalletIntroScreenOpened(), - ) + modelScope.launch { + analyticsEventHandler.send( + event = IntroductionProcess.CreateWalletIntroScreenOpened( + referralId = appsFlyerStore.get()?.refcode, + ), + ) + } } private fun onScanClick() { diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index 4245e9f79d..6b0489cab4 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -14,6 +14,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.crypto.bip39.Mnemonic +import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.common.wallets.error.SaveWalletError import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase @@ -41,6 +42,7 @@ internal class AddExistingWalletImportModel @Inject constructor( private val saveUserWalletUseCase: SaveWalletUseCase, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, + private val appsFlyerStore: AppsFlyerStore, ) : Model() { private val params: AddExistingWalletImportComponent.Params = paramsContainer.require() @@ -122,6 +124,7 @@ internal class AddExistingWalletImportModel @Inject constructor( } else { AnalyticsParam.EmptyFull.Full }, + referralId = appsFlyerStore.get()?.refcode, ), ) params.callbacks.onWalletImported(userWallet.walletId) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt index d268abddb8..dad097a1fb 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/createmobilewallet/CreateMobileWalletModel.kt @@ -11,6 +11,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.message.dialog.Dialogs.hotWalletCreationNotSupportedDialog +import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase @@ -44,6 +45,7 @@ internal class CreateMobileWalletModel @Inject constructor( private val isHotWalletCreationSupported: IsHotWalletCreationSupported, private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, + private val appsFlyerStore: AppsFlyerStore, ) : Model() { private val params: CreateMobileWalletComponent.Params = paramsContainer.require() @@ -100,6 +102,7 @@ internal class CreateMobileWalletModel @Inject constructor( creationType = OnboardingAnalyticsEvent.CreateWallet.WalletCreationType.NewSeed, seedPhraseLength = SEED_PHRASE_LENGTH, passPhraseState = AnalyticsParam.EmptyFull.Empty, + referralId = appsFlyerStore.get()?.refcode, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt index b8ed2c5101..07da257630 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/analytics/OnboardingEvent.kt @@ -3,6 +3,8 @@ package com.tangem.features.onboarding.v2.common.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AppsFlyerIncludedEvent +import com.tangem.core.analytics.models.getReferralParams +import kotlin.collections.putAll sealed class OnboardingEvent( category: String, @@ -25,6 +27,7 @@ sealed class OnboardingEvent( creationType: WalletCreationType = WalletCreationType.PrivateKey, seedPhraseLength: Int? = null, passPhraseState: AnalyticsParam.EmptyFull, + referralId: String?, ) : CreateWallet( event = "Wallet Created Successfully", params = buildMap { @@ -33,6 +36,7 @@ sealed class OnboardingEvent( if (seedPhraseLength != null) { put("Seed Phrase Length", seedPhraseLength.toString()) } + putAll(getReferralParams(referralId)) }, ), AppsFlyerIncludedEvent diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt index 42d2db0372..d777faa37b 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/createwallet/model/MultiWalletCreateWalletModel.kt @@ -9,13 +9,14 @@ 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.resourceReference +import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.builder.ColdUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent import com.tangem.features.onboarding.v2.impl.R @@ -45,6 +46,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( private val analyticsHandler: AnalyticsEventHandler, private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val saveWalletUseCase: SaveWalletUseCase, + private val appsFlyerStore: AppsFlyerStore, ) : Model() { private val params = paramsContainer.require() @@ -109,6 +111,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor( analyticsHandler.send( event = OnboardingEvent.CreateWallet.WalletCreatedSuccessfully( passPhraseState = AnalyticsParam.EmptyFull.Empty, + referralId = appsFlyerStore.get()?.refcode, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt index 3468f7a11c..fc3e5ad044 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/model/MultiWalletSeedPhraseModel.kt @@ -18,6 +18,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.SnackbarMessage import com.tangem.crypto.bip39.Mnemonic +import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -64,6 +65,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, + private val appsFlyerStore: AppsFlyerStore, ) : Model() { private val params = paramsContainer.require() @@ -248,6 +250,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor( } else { AnalyticsParam.EmptyFull.Full }, + referralId = appsFlyerStore.get()?.refcode, ), ) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt index b286fead4f..f083fdec7e 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/note/impl/child/create/model/OnboardingNoteCreateWalletModel.kt @@ -7,6 +7,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.artwork.ArtworkUM +import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.builder.ColdUserWalletBuilder @@ -33,6 +34,7 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor( private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory, private val saveWalletUseCase: SaveWalletUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val appsFlyerStore: AppsFlyerStore, ) : Model() { private val params = paramsContainer.require() @@ -68,6 +70,7 @@ internal class OnboardingNoteCreateWalletModel @Inject constructor( analyticsEventHandler.send( event = OnboardingEvent.CreateWallet.WalletCreatedSuccessfully( passPhraseState = AnalyticsParam.EmptyFull.Empty, + referralId = appsFlyerStore.get()?.refcode, ), ) createWalletAndNavigateBackWithDone(scanResponse.copy(card = result.data.card)) diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt index 79c8741734..2dbc11a45c 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/twin/impl/model/OnboardingTwinModel.kt @@ -15,6 +15,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList +import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.domain.card.common.TwinCardNumber import com.tangem.domain.card.common.getTwinCardNumber @@ -63,6 +64,7 @@ internal class OnboardingTwinModel @Inject constructor( private val cardRepository: CardRepository, private val uiMessageSender: UiMessageSender, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val appsFlyerStore: AppsFlyerStore, ) : Model() { private val params = paramsContainer.require() @@ -174,6 +176,7 @@ internal class OnboardingTwinModel @Inject constructor( analyticsEventHandler.send( event = OnboardingEvent.CreateWallet.WalletCreatedSuccessfully( passPhraseState = AnalyticsParam.EmptyFull.Empty, + referralId = appsFlyerStore.get()?.refcode, ), ) From 184c94d9d3ba1566d7391b7f187332a7d3030b52 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Feb 2026 13:34:48 +0500 Subject: [PATCH 13/33] Updated on 2026-08-14 --- .../ui/components/pager/PagerIndicator.kt | 447 ++++++++++++------ 1 file changed, 292 insertions(+), 155 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt index 418cc7344d..39cdb7d9ea 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/pager/PagerIndicator.kt @@ -1,201 +1,284 @@ package com.tangem.core.ui.components.pager +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.* -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import kotlin.math.abs import kotlin.math.min +import kotlin.math.roundToInt -// six - cause the central indicator has width multiplied twice -private const val TOTAL_MAX_INDICATORS = 6 -private const val SPACER_COUNT_BETWEEN_INDICATORS = 4 +private const val ANIMATION_DURATION = 300 +private const val MAX_VISIBLE_DOTS = 5 +private const val MIN_HIDDEN_FOR_SMALL_DOT = 2 +private const val MIN_DISTANCE_FOR_SMALL_DOT = 3 +private const val MIN_DISTANCE_FOR_HINT_DOT = 2 -/** - * Horizontal pager indicator - * - * @param pagerState state of pager - * @param indicatorCount counter of visible indicator items - */ +private val SPACING = 4.dp +private val BACKGROUND_SIZE = DpSize(92.dp, 32.dp) + +private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp) +private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp) +private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp) +private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp) + +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable -fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier, indicatorCount: Int = 5) { - if (pagerState.pageCount == 0) return +fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier) { + val totalPages = pagerState.pageCount + val currentIndex = pagerState.currentPage - val listState = rememberLazyListState() + if (totalPages == 0) return val indicatorColor = TangemTheme.colors.control.key val overlayColor = TangemTheme.colors.overlay.secondary + val inactiveIndicatorColor = TangemTheme.colors.text.tertiary - val inactiveIndicatorColor = remember(indicatorColor) { - indicatorColor.copy(alpha = 0.5f) - } + val density = LocalDensity.current - val baseIndicatorSize = 8.dp - val spacing = 4.dp + val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex) - val indicatorState by remember(pagerState, indicatorCount) { - derivedStateOf { - val count = pagerState.pageCount - val current = pagerState.currentPage + var displayLower by remember { mutableIntStateOf(targetLower) } + var displayUpper by remember { mutableIntStateOf(targetUpper) } + var prevTargetLower by remember { mutableIntStateOf(targetLower) } - val winSize = min(indicatorCount, count) - val centerPosition = winSize / 2 + val slideOffset = remember { Animatable(0f) } + var isSliding by remember { mutableStateOf(false) } + var slideDirection by remember { mutableIntStateOf(0) } + val fadeProgress = remember { Animatable(0f) } + var fadeJob by remember { mutableStateOf(null) } - val start = when { - count <= winSize -> 0 - current <= centerPosition -> 0 - current >= count - centerPosition - 1 -> count - winSize - else -> current - centerPosition + LaunchedEffect(targetLower) { + if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) { + fadeJob?.cancel() + slideOffset.stop() + fadeProgress.stop() + + val dir = if (targetLower > prevTargetLower) 1 else -1 + val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() } + val halfEdge = edgeDotSize / 2 + + isSliding = true + slideDirection = dir + fadeProgress.snapTo(0f) + + if (dir > 0) { + displayLower = prevTargetLower + displayUpper = targetUpper + slideOffset.snapTo(halfEdge) + } else { + displayLower = targetLower + displayUpper = prevTargetLower + MAX_VISIBLE_DOTS + slideOffset.snapTo(-halfEdge) } - Triple(count, winSize, start) + + prevTargetLower = targetLower + + fadeJob = launch { + fadeProgress.animateTo(1f, tween(ANIMATION_DURATION)) + } + slideOffset.animateTo( + if (dir > 0) -halfEdge else halfEdge, + tween(ANIMATION_DURATION), + ) + + displayLower = targetLower + displayUpper = targetUpper + slideOffset.snapTo(0f) + isSliding = false + slideDirection = 0 } } - - val (itemCount, windowSize, windowStart) = indicatorState - val currentItem by remember { derivedStateOf { pagerState.currentPage } } - - LaunchedEffect(currentItem, windowStart) { - if (itemCount > windowSize) { - listState.animateScrollToItem(windowStart.coerceIn(0, itemCount - 1)) - } - } - - val maxContainerWidth = remember(baseIndicatorSize, spacing) { - baseIndicatorSize * TOTAL_MAX_INDICATORS + spacing * SPACER_COUNT_BETWEEN_INDICATORS - } + val visibleIndices = (displayLower until displayUpper).toList() Box( modifier = modifier - .height(32.dp) - .width(maxContainerWidth + 32.dp) + .width(BACKGROUND_SIZE.width) + .height(BACKGROUND_SIZE.height) .background( color = overlayColor, shape = CircleShape, ) - .padding(horizontal = 16.dp, vertical = 12.dp) .clip(CircleShape), contentAlignment = Alignment.Center, ) { - LazyRow( - modifier = Modifier.wrapContentWidth(), - state = listState, + Row( + modifier = Modifier.offset { + IntOffset(slideOffset.value.roundToInt(), 0) + }, + horizontalArrangement = Arrangement.spacedBy(SPACING), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(spacing), - userScrollEnabled = false, ) { - indicatorItems( - itemCount = itemCount, - currentItem = currentItem, - activeColor = indicatorColor, - inActiveColor = inactiveIndicatorColor, - baseSize = baseIndicatorSize, - windowSize = windowSize, - windowStart = windowStart, - ) + visibleIndices.forEach { index -> + val dotAlpha = when { + !isSliding -> 1f + slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value + slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value + slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value + slideDirection < 0 && index == displayLower -> fadeProgress.value + else -> 1f + } + + key(index) { + Dot( + index = index, + currentIndex = currentIndex, + totalPages = totalPages, + activeColor = indicatorColor, + inactiveColor = inactiveIndicatorColor, + modifier = Modifier.graphicsLayer { alpha = dotAlpha }, + ) + } + } } } } -@Suppress("MagicNumber", "CyclomaticComplexMethod") -private fun calculateIndicatorHeight(position: Int, currentPosition: Int, baseSize: Dp, windowSize: Int): Dp { - val distance = abs(position - currentPosition) - val mediumSize = 6.dp - val smallSize = 4.dp - - if (windowSize < 5) { - return when { - distance <= 1 -> baseSize - distance == 2 -> mediumSize - else -> smallSize - } +private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair { + if (totalPages <= MAX_VISIBLE_DOTS) { + return 0 to totalPages } - - val isEdgeFocus = currentPosition == 0 || currentPosition == windowSize - 1 - val isNearEdgeFocus = currentPosition == 1 || currentPosition == windowSize - 2 - return when { - isEdgeFocus -> when { - distance <= 2 -> baseSize - distance == 3 -> mediumSize - else -> smallSize - } - isNearEdgeFocus -> when { - distance <= 1 -> baseSize - distance == 2 -> mediumSize - else -> smallSize - } - else -> when { - distance <= 1 -> baseSize - else -> mediumSize - } + val lowerBound = when { + currentIndex <= 1 -> 0 + currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS + else -> currentIndex - 2 } + val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages) + return lowerBound to upperBound } -@Suppress("LongParameterList") -private fun LazyListScope.indicatorItems( - itemCount: Int, - currentItem: Int, - activeColor: Color, - inActiveColor: Color, - baseSize: Dp, - windowSize: Int, - windowStart: Int, +private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize { + if (index == currentIndex) { + return CURRENT_DOT_SIZE + } + if (totalPages <= MAX_VISIBLE_DOTS) { + return NORMAL_DOT_SIZE + } + val params = DotSizeParams.create(index, currentIndex, totalPages) + return params.calculateSize() +} + +private class DotSizeParams private constructor( + val posInWindow: Int, + val currentPosInWindow: Int, + val hiddenLeft: Int, + val hiddenRight: Int, + val distanceFromCurrent: Int, ) { - val safeWindowSize = min(windowSize, itemCount) - if (safeWindowSize <= 0) return + private val lastPos = MAX_VISIBLE_DOTS - 1 + private val isCentered = currentPosInWindow == 2 && hiddenLeft >= 1 && hiddenRight >= 1 - val windowEnd = windowStart + safeWindowSize - val currentPosInWindow = (currentItem - windowStart).coerceIn(0, safeWindowSize - 1) - - items(itemCount) { pageIndex -> - val isInWindow = pageIndex in windowStart until windowEnd - val positionInWindow = (pageIndex - windowStart).coerceIn(0, safeWindowSize - 1) - - val isSelected = pageIndex == currentItem - - val refinedHeight = if (isInWindow) { - calculateIndicatorHeight( - position = positionInWindow, - currentPosition = currentPosInWindow, - baseSize = baseSize, - windowSize = safeWindowSize, - ) - } else { - 0.dp - } - val targetWidth = if (isSelected) refinedHeight * 2 else refinedHeight - val targetShape = if (isSelected) RoundedCornerShape(16.dp) else CircleShape - val animatedWidth by animateDpAsState(targetValue = targetWidth, label = "width") - val animatedHeight by animateDpAsState(targetValue = refinedHeight, label = "height") - - Box( - modifier = Modifier - .padding(vertical = (baseSize - animatedHeight) / 2) - .clip(targetShape) - .width(animatedWidth) - .height(animatedHeight) - .background( - if (isSelected) activeColor else inActiveColor, - targetShape, - ), - ) + fun calculateSize(): DpSize = when { + isCentered -> getCenteredSize() + hiddenRight >= 1 -> getRightEdgeSize() + hiddenLeft >= 1 -> getLeftEdgeSize() + else -> NORMAL_DOT_SIZE } + + private fun getCenteredSize(): DpSize = when (posInWindow) { + 0, lastPos -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + + private fun getRightEdgeSize(): DpSize { + val isLastPos = posInWindow == lastPos + val isSecondToLast = posInWindow == lastPos - 1 + val hasExtraHidden = hiddenRight >= MIN_HIDDEN_FOR_SMALL_DOT + val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT + val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT + + return when { + isLastPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE + isLastPos && isModerateDistance -> HINT_DOT_SIZE + isSecondToLast && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + } + + private fun getLeftEdgeSize(): DpSize { + val isFirstPos = posInWindow == 0 + val isSecondPos = posInWindow == 1 + val hasExtraHidden = hiddenLeft >= MIN_HIDDEN_FOR_SMALL_DOT + val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT + val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT + + return when { + isFirstPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE + isFirstPos && isModerateDistance -> HINT_DOT_SIZE + isSecondPos && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + } + + companion object { + fun create(index: Int, currentIndex: Int, totalPages: Int): DotSizeParams { + val (windowStart, windowEnd) = getWindowBounds(totalPages, currentIndex) + val posInWindow = index - windowStart + val currentPosInWindow = currentIndex - windowStart + return DotSizeParams( + posInWindow = posInWindow, + currentPosInWindow = currentPosInWindow, + hiddenLeft = windowStart, + hiddenRight = totalPages - windowEnd, + distanceFromCurrent = abs(posInWindow - currentPosInWindow), + ) + } + } +} + +@Composable +private fun Dot( + index: Int, + currentIndex: Int, + totalPages: Int, + activeColor: Color, + inactiveColor: Color, + modifier: Modifier = Modifier, +) { + val isActive = index == currentIndex + val size = getDotSize(index, currentIndex, totalPages) + + val animSpec = tween(ANIMATION_DURATION) + val colorSpec = tween(ANIMATION_DURATION) + + val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index") + val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index") + val animatedColor by animateColorAsState( + targetValue = if (isActive) activeColor else inactiveColor, + animationSpec = colorSpec, + label = "c$index", + ) + + val shape = RoundedCornerShape(animatedHeight / 2) + + Box( + modifier = modifier + .width(animatedWidth) + .height(animatedHeight) + .background(animatedColor, shape), + ) } @Preview(showBackground = true) @@ -203,28 +286,82 @@ private fun LazyListScope.indicatorItems( private fun PagerIndicatorPreview() { TangemThemePreview { Column( - modifier = Modifier + Modifier .background(TangemTheme.colors.background.primary) .padding(20.dp), verticalArrangement = Arrangement.spacedBy(10.dp), ) { - val pagerState = rememberPagerState( - initialPage = 2, - pageCount = { 10 }, - ) - PagerIndicator(pagerState = pagerState) + listOf(0, 1, 2, 3, 4).forEach { page -> + PagerIndicator(rememberPagerState(page) { 5 }) + } + } + } +} - val pagerState1 = rememberPagerState( - initialPage = 0, - pageCount = { 3 }, - ) - PagerIndicator(pagerState = pagerState1) +@Preview(showBackground = true) +@Composable +private fun PagerIndicator6ItemsPreview() { + TangemThemePreview { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5).forEach { page -> + PagerIndicator(rememberPagerState(page) { 6 }) + } + } + } +} - val pagerState2 = rememberPagerState( - initialPage = 0, - pageCount = { 1 }, - ) - PagerIndicator(pagerState = pagerState2) +@Preview(showBackground = true) +@Composable +private fun PagerIndicator7ItemsPreview() { + TangemThemePreview { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5, 6).forEach { page -> + PagerIndicator(rememberPagerState(page) { 7 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator10ItemsPreview() { + TangemThemePreview { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).forEach { page -> + PagerIndicator(rememberPagerState(page) { 10 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicatorSmallCountsPreview() { + TangemThemePreview { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + PagerIndicator(rememberPagerState(0) { 1 }) + PagerIndicator(rememberPagerState(1) { 2 }) + PagerIndicator(rememberPagerState(1) { 3 }) } } } \ No newline at end of file From 2938fda36e7989478863f28ab732ab8acb138b43 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Feb 2026 12:09:36 +0300 Subject: [PATCH 14/33] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 1 + .../sign/WcSignUseCaseDelegate.kt | 2 + .../tangem/domain/models/account/Account.kt | 8 ++- .../domain/walletconnect/WcAnalyticEvents.kt | 70 +++++++++---------- .../analytics/CustomTokenAnalyticsEvent.kt | 2 +- .../analytics/SendWithSwapAnalyticEvents.kt | 18 ++--- .../confirm/model/SendWithSwapConfirmModel.kt | 2 + .../feature/swap/analytics/SwapEvents.kt | 21 +++--- .../connections/model/WcPairModel.kt | 17 ++--- .../transaction/model/WcAddNetworkModel.kt | 3 +- .../model/WcSendTransactionModel.kt | 3 + .../model/WcSignTransactionModel.kt | 3 + 12 files changed, 83 insertions(+), 67 deletions(-) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 8fead6f8ae..986d808190 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -285,5 +285,6 @@ sealed class AnalyticsParam { const val ENS_ADDRESS = "ENS Address" const val ACCOUNT_DERIVATION_FROM = "Account Derivation (from)" const val FEE_TOKEN = "Fee Token" + const val ACCOUNT_DERIVATION = "Account Derivation" } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt index c360f8af34..346ba94376 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt @@ -5,6 +5,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.sign.SignStateConverter.toPreSign import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.WcRequestError.Companion.code @@ -71,6 +72,7 @@ internal class WcSignUseCaseDelegate( network = context.network, errorCode = error.code(), errorMessage = errorMessage, + accountDerivation = context.session.account?.derivationIndex?.value, ) analytics.send(event) } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt index af4f1fd430..851c25e463 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/Account.kt @@ -185,4 +185,10 @@ sealed interface Account { error("Not yet implemented") } } -} \ No newline at end of file +} + +val Account.derivationIndex: DerivationIndex? + get() = when (this) { + is Account.CryptoPortfolio -> derivationIndex + is Account.Payment -> TODO("[REDACTED_JIRA]") + } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt index 0824032547..d9f540cefb 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcAnalyticEvents.kt @@ -34,8 +34,17 @@ sealed class WcAnalyticEvents( ), ) - class PairButtonConnect : WcAnalyticEvents( + class PairButtonConnect( + dAppName: String, + accountDerivation: Int?, + ) : WcAnalyticEvents( event = "Button - Connect", + params = buildMap { + put(AnalyticsParam.DAPP_NAME, dAppName) + accountDerivation?.let { + put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) + } + }, ) class PairRequested( @@ -114,6 +123,7 @@ sealed class WcAnalyticEvents( rawRequest: WcSdkSessionRequest, network: Network, emulationStatus: EmulationStatus?, + accountDerivation: Int?, securityStatus: CheckDAppResult, ) : WcAnalyticEvents( event = "Signature Request Received", @@ -124,6 +134,7 @@ sealed class WcAnalyticEvents( AnalyticsParam.BLOCKCHAIN to network.name, AnalyticsParam.EMULATION_STATUS to emulationStatus?.status, AnalyticsParam.TYPE to securityStatus.toAnalyticVerificationStatus(), + AnalyticsParam.ACCOUNT_DERIVATION to accountDerivation?.toString(), ).mapNotNullValues { it.value }, ) { enum class EmulationStatus(val status: String) { @@ -137,15 +148,19 @@ sealed class WcAnalyticEvents( rawRequest: WcSdkSessionRequest, network: Network, securityStatus: CheckDAppResult, + accountDerivation: Int?, ) : WcAnalyticEvents( event = "Signature Request Handled", - params = mapOf( - AnalyticsParam.DAPP_NAME to rawRequest.dAppMetaData.name, - AnalyticsParam.DAPP_URL to rawRequest.dAppMetaData.url, - AnalyticsParam.METHOD_NAME to rawRequest.request.method, - AnalyticsParam.BLOCKCHAIN to network.name, - AnalyticsParam.TYPE to securityStatus.toAnalyticVerificationStatus(), - ), + params = buildMap { + put(AnalyticsParam.DAPP_NAME, rawRequest.dAppMetaData.name) + put(AnalyticsParam.DAPP_URL, rawRequest.dAppMetaData.url) + put(AnalyticsParam.METHOD_NAME, rawRequest.request.method) + put(AnalyticsParam.BLOCKCHAIN, network.name) + put(AnalyticsParam.TYPE, securityStatus.toAnalyticVerificationStatus()) + accountDerivation?.let { + put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) + } + }, ), AppsFlyerIncludedEvent class SignatureRequestFailed( @@ -153,16 +168,18 @@ sealed class WcAnalyticEvents( network: Network, errorCode: String, errorMessage: String, + accountDerivation: Int?, ) : WcAnalyticEvents( event = "Signature Request Failed", - params = mapOf( - AnalyticsParam.DAPP_NAME to rawRequest.dAppMetaData.name, - AnalyticsParam.DAPP_URL to rawRequest.dAppMetaData.url, - AnalyticsParam.METHOD_NAME to rawRequest.request.method, - AnalyticsParam.BLOCKCHAIN to network.name, - AnalyticsParam.ERROR_CODE to errorCode, - AnalyticsParam.ERROR_DESCRIPTION to errorMessage, - ), + params = buildMap { + put(AnalyticsParam.DAPP_NAME, rawRequest.dAppMetaData.name) + put(AnalyticsParam.DAPP_URL, rawRequest.dAppMetaData.url) + put(AnalyticsParam.METHOD_NAME, rawRequest.request.method) + put(AnalyticsParam.BLOCKCHAIN, network.name) + put(AnalyticsParam.ERROR_CODE, errorCode) + put(AnalyticsParam.ERROR_DESCRIPTION, errorMessage) + accountDerivation?.let { put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) } + }, ) class SignatureRequestReceivedFailed( @@ -279,23 +296,4 @@ fun CheckDAppResult.toAnalyticVerificationStatus(): String = when (this) { SAFE -> DAppVerificationStatus.Verified UNSAFE -> DAppVerificationStatus.Risky FAILED_TO_VERIFY -> DAppVerificationStatus.Unknown -}.status - -sealed class WcAnalyticAccountEvents( - event: String, - params: Map = emptyMap(), -) : AnalyticsEvent(category = WC_CATEGORY_ACCOUNT_NAME, event = event, params = params) { - - data class PairButtonConnect( - private val accountDerivation: Int, - ) : WcAnalyticAccountEvents( - event = "Button - Connect", - params = mapOf( - "Account Derivation" to accountDerivation.toString(), - ), - ) - - companion object { - const val WC_CATEGORY_ACCOUNT_NAME = "WalletConnect / Account" - } -} \ No newline at end of file +}.status \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt index c3a41d047c..03cab5fb6d 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/analytics/CustomTokenAnalyticsEvent.kt @@ -8,7 +8,7 @@ internal sealed class CustomTokenAnalyticsEvent( event: String, params: Map = mapOf(), ) : AnalyticsEvent( - category = "Manage Tokens / Custom", + category = "Manage Tokens / Custom Token", event = event, params = params, ) { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt index 8f48f4568b..8aca31698c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -22,16 +22,18 @@ internal sealed class SendWithSwapAnalyticEvents( val feeType: AnalyticsParam.FeeType, val fromToken: CryptoCurrency, val toToken: CryptoCurrency, + val fromDerivationIndex: Int?, ) : SendWithSwapAnalyticEvents( event = "Send With Swap In Progress Screen Opened", - params = mapOf( - PROVIDER to providerName, - FEE_TYPE to if (feeType is AnalyticsParam.FeeType.Normal) "Market" else "Fast", - SEND_TOKEN to fromToken.symbol, - RECEIVE_TOKEN to toToken.symbol, - SEND_BLOCKCHAIN to fromToken.network.name, - RECEIVE_BLOCKCHAIN to toToken.network.name, - ), + params = buildMap { + put(PROVIDER, providerName) + put(FEE_TYPE, if (feeType is AnalyticsParam.FeeType.Normal) "Market" else "Fast") + put(SEND_TOKEN, fromToken.symbol) + put(RECEIVE_TOKEN, toToken.symbol) + put(SEND_BLOCKCHAIN, fromToken.network.name) + put(RECEIVE_BLOCKCHAIN, toToken.network.name) + if (fromDerivationIndex != null) put("Account Derivation (from)", fromDerivationIndex.toString()) + }, ), AppsFlyerIncludedEvent data class NoticeCanNotSwapToken( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 97a3d13b93..2923283a79 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -448,6 +448,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( val toCurrency = confirmData.toCryptoCurrencyStatus?.currency ?: return val feeSelectorUM = uiState.value.feeSelectorUM as? FeeSelectorUM.Content ?: return val feeType = feeSelectorUM.toAnalyticType() + val fromDerivationIndex = confirmData.fromAccount?.derivationIndex?.value analyticsEventHandler.send( SendWithSwapAnalyticEvents.TransactionScreenOpened( @@ -455,6 +456,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( feeType = feeType, fromToken = fromCurrency, toToken = toCurrency, + fromDerivationIndex = fromDerivationIndex, ), ) analyticsEventHandler.send( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index a2b2f3ea67..408988e88e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -98,16 +98,17 @@ sealed class SwapEvents( val toDerivationIndex: Int?, ) : SwapEvents( event = "Swap in Progress Screen Opened", - params = mapOf( - "Provider" to provider.name, - "Commission" to if (commission == FeeType.NORMAL) "Market" else "Fast", - "Send Token" to sendToken, - "Receive Token" to receiveToken, - "Send Blockchain" to sendBlockchain, - "Receive Blockchain" to receiveBlockchain, - "Account Derivation From or To (optional)" to "$fromDerivationIndex, $toDerivationIndex", - FEE_TOKEN to feeToken, - ), + params = buildMap { + put("Provider", provider.name) + put("Commission", if (commission == FeeType.NORMAL) "Market" else "Fast") + put("Send Token", sendToken) + put("Receive Token", receiveToken) + put("Send Blockchain", sendBlockchain) + put("Receive Blockchain", receiveBlockchain) + if (fromDerivationIndex != null) put("Account Derivation (from)", fromDerivationIndex.toString()) + if (toDerivationIndex != null) put("Account Derivation (to)", toDerivationIndex.toString()) + put(FEE_TOKEN, feeToken) + }, ), AppsFlyerIncludedEvent class ProviderClicked : SwapEvents("Provider Clicked") diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index 285229cc87..f9fefde39c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -27,12 +27,12 @@ import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.walletconnect.WcAnalyticAccountEvents import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairError.Unknown @@ -324,15 +324,12 @@ internal class WcPairModel @Inject constructor( val account = selectedPortfolio?.second?.account modelScope.launch { - if (selectorController.isAccountModeSync() && account != null) { - val derivationIndex = when (account) { - is Account.CryptoPortfolio -> account.derivationIndex.value - is Account.Payment -> TODO("[REDACTED_JIRA]") - } - analytics.send(WcAnalyticAccountEvents.PairButtonConnect(derivationIndex)) - } else { - analytics.send(WcAnalyticEvents.PairButtonConnect()) - } + analytics.send( + WcAnalyticEvents.PairButtonConnect( + dAppName = sessionProposal.dAppMetaData.name, + accountDerivation = account?.derivationIndex?.value, + ), + ) } wcPairUseCase.approve( WcSessionApprove( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt index 8ed61a1b5d..7f74dc7dca 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt @@ -11,6 +11,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcRequestUseCaseFactory import com.tangem.domain.walletconnect.usecase.method.WcAddNetworkUseCase @@ -131,13 +132,13 @@ internal class WcAddNetworkModel @Inject constructor( private fun sendSignatureReceivedAnalytics(useCase: WcAddNetworkUseCase) { if (signatureReceivedAnalyticsSendState.value) return - analytics.send( WcAnalyticEvents.SignatureRequestReceived( rawRequest = useCase.rawSdkRequest, network = useCase.network, emulationStatus = null, securityStatus = CheckDAppResult.FAILED_TO_VERIFY, + accountDerivation = useCase.session.account?.derivationIndex?.value, ), ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index f02eb11f51..9eaceb4978 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -23,6 +23,7 @@ import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.core.lce.Lce +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet @@ -426,6 +427,7 @@ internal class WcSendTransactionModel @Inject constructor( rawRequest = useCase.rawSdkRequest, network = useCase.network, securityStatus = securityStatusState.value.toCheckDAppResult(), + accountDerivation = useCase.session.account?.derivationIndex?.value, ) analytics.send(event) showSuccessSignMessage() @@ -465,6 +467,7 @@ internal class WcSendTransactionModel @Inject constructor( network = useCase.network, emulationStatus = emulationStatus, securityStatus = securityCheck.toCheckDAppResult(), + accountDerivation = useCase.session.account?.derivationIndex?.value, ), ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt index 88852cdbbc..1e97be37f4 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt @@ -12,6 +12,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.clipboard.ClipboardManager +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcRequestUseCaseFactory import com.tangem.domain.walletconnect.model.WcEthMethod @@ -143,6 +144,7 @@ internal class WcSignTransactionModel @Inject constructor( rawRequest = useCase.rawSdkRequest, network = useCase.network, securityStatus = CheckDAppResult.FAILED_TO_VERIFY, + accountDerivation = useCase.session.account?.derivationIndex?.value, ) analytics.send(event) showSuccessSignMessage() @@ -171,6 +173,7 @@ internal class WcSignTransactionModel @Inject constructor( network = useCase.network, emulationStatus = null, securityStatus = CheckDAppResult.FAILED_TO_VERIFY, + accountDerivation = useCase.session.account?.derivationIndex?.value, ), ) From 1554090a9dbbe70ebaf7dd0f8bacccebf97c25e7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Feb 2026 13:33:17 +0300 Subject: [PATCH 15/33] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index b55ca9ec9b..a5d1a89425 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit b55ca9ec9bc7ff88a8764f00656ee7a884df9125 +Subproject commit a5d1a89425a95bc9c90c7a6fed3c578b0d324994 From 775a03b0389e0248f39c816d6740b811dcb8f896 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Feb 2026 17:51:20 +0400 Subject: [PATCH 16/33] Updated on 2026-08-14 --- .../tangem/data/pay/DefaultTangemPayEligibilityManager.kt | 4 ++++ .../com/tangem/domain/pay/TangemPayEligibilityManager.kt | 1 + .../kotlin/com/tangem/domain/pay/model/CustomerInfo.kt | 1 + .../pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt | 8 ++++++-- .../wallet/subscribers/TangemPayMainSubscriber.kt | 3 +++ 5 files changed, 15 insertions(+), 2 deletions(-) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt index 7225c10f60..106cda734f 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt @@ -45,6 +45,10 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( .also { isEligible -> if (!isEligible) reset() } } + override suspend fun isPaeraCustomerForAnyWallet(): Boolean { + return getUserWalletsData().any { it.isPaeraCustomer } + } + private suspend fun getUserWalletsData(): List { cachedEligibleWallets?.let { return it } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt index fcb41b6984..462dbd6e6f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt @@ -6,5 +6,6 @@ interface TangemPayEligibilityManager { suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List suspend fun getTangemPayAvailability(): Boolean + suspend fun isPaeraCustomerForAnyWallet(): Boolean fun reset() } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index baac6c4f75..501df2011b 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -7,6 +7,7 @@ sealed class MainCustomerInfoContentState { object Loading : MainCustomerInfoContentState() object OnboardingBanner : MainCustomerInfoContentState() data class Content(val info: MainScreenCustomerInfo) : MainCustomerInfoContentState() + object Empty : MainCustomerInfoContentState() } data class MainScreenCustomerInfo( diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index dfe593979e..7ff408d1c1 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -68,17 +68,21 @@ class TangemPayMainScreenCustomerInfoUseCase( } private suspend fun showOnboardingBannerIfEligible(userWalletId: UserWalletId) { + if (eligibilityManager.isPaeraCustomerForAnyWallet()) { + updateState(userWalletId, MainCustomerInfoContentState.Empty.right()) + return + } val isEligible = eligibilityManager .getEligibleWallets(shouldExcludePaeraCustomers = false) .any { it.walletId == userWalletId } if (isEligible) { if (onboardingRepository.getHideMainOnboardingBanner(userWalletId)) { - updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + updateState(userWalletId, MainCustomerInfoContentState.Empty.right()) } else { updateState(userWalletId, MainCustomerInfoContentState.OnboardingBanner.right()) } } else { - updateState(userWalletId, TangemPayCustomerInfoError.UnknownError.left()) + updateState(userWalletId, MainCustomerInfoContentState.Empty.right()) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt index e6f5955bb8..1dcc11ff2a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TangemPayMainSubscriber.kt @@ -88,6 +88,9 @@ internal class TangemPayMainSubscriber @AssistedInject constructor( closeOnClick = clickIntents::onOnboardingBannerCloseClick, ), ) + is MainCustomerInfoContentState.Empty -> stateController.update( + transformer = TangemPayHiddenStateTransformer(userWalletId), + ) } } From 1bb825744eaa708bb9b97f4f4c363094548ba674 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Feb 2026 21:38:15 +0400 Subject: [PATCH 17/33] Updated on 2026-08-14 --- .../pay/DefaultTangemPayEligibilityManager.kt | 23 +++++---- .../domain/pay/TangemPayEligibilityManager.kt | 10 ++++ .../model/TangemPayOnboardingModel.kt | 48 ++++++++++--------- 3 files changed, 51 insertions(+), 30 deletions(-) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt index 106cda734f..c9171fd5f3 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt @@ -3,6 +3,7 @@ package com.tangem.data.pay import com.tangem.common.card.FirmwareVersion import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.pay.TangemPayEligibilityManager @@ -40,6 +41,12 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( } } + override suspend fun getPossibleWalletsIds(shouldExcludePaeraCustomers: Boolean): List { + return getPossibleWalletsForTangemPay().addPaeraCustomersData().mapNotNull { + if (!it.isPaeraCustomer || !shouldExcludePaeraCustomers) it.userWallet.walletId else null + } + } + override suspend fun getTangemPayAvailability(): Boolean { return onboardingRepository.checkCustomerEligibility() .also { isEligible -> if (!isEligible) reset() } @@ -58,9 +65,13 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( coroutineScope { val deferred = async { - getPossibleWalletsForTangemPay() - .addPaeraCustomersData() - .also { cachedEligibleWallets = it } + if (!checkTangemPayEligibility()) { + emptyList() + } else { + getPossibleWalletsForTangemPay() + .addPaeraCustomersData() + .also { cachedEligibleWallets = it } + } } eligibleWalletsDeferred = deferred try { @@ -72,11 +83,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( } } - private suspend fun getPossibleWalletsForTangemPay(): List { - if (!checkTangemPayEligibility()) { - return emptyList() - } - + private fun getPossibleWalletsForTangemPay(): List { val wallets = if (hotWalletFeatureToggles.isHotWalletEnabled) { userWalletsListRepository.userWallets.value } else { diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt index 462dbd6e6f..b028029e06 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayEligibilityManager.kt @@ -1,11 +1,21 @@ package com.tangem.domain.pay import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId interface TangemPayEligibilityManager { suspend fun getEligibleWallets(shouldExcludePaeraCustomers: Boolean): List + + /** + * Returns all compatible user wallets without checking Tangem Pay eligibility, only used when opening deeplink + * Remove after removing [TangemPayOnboardingComponent.Params.Deeplink] + * */ + suspend fun getPossibleWalletsIds(shouldExcludePaeraCustomers: Boolean): List + suspend fun getTangemPayAvailability(): Boolean + suspend fun isPaeraCustomerForAnyWallet(): Boolean + fun reset() } \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index b8ce2a6fac..d2b0d915e1 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -116,37 +116,41 @@ internal class TangemPayOnboardingModel @Inject constructor( private fun onGetCardClick() { analytics.send(TangemPayAnalyticsEvents.GetCardClicked()) - // if user came from deeplink or banner in settings and already is a paera customer -> exclude this wallet - val shouldExcludePaeraCustomers = params is TangemPayOnboardingComponent.Params.FromBannerInSettings || - params is TangemPayOnboardingComponent.Params.Deeplink - modelScope.launch { - val eligibleWalletsIds = eligibilityManager - .getEligibleWallets(shouldExcludePaeraCustomers = shouldExcludePaeraCustomers) - .map { it.walletId } - if (eligibleWalletsIds.isEmpty()) { - back() - return@launch + if (params is TangemPayOnboardingComponent.Params.Deeplink) { + modelScope.launch { + openWalletSelectorIfNeeds( + walletsIds = eligibilityManager.getPossibleWalletsIds(shouldExcludePaeraCustomers = true), + ) } - when (params) { - is TangemPayOnboardingComponent.Params.ContinueOnboarding -> { - checkCustomerInfo(params.userWalletId) + } else { + // if user came from banner in settings and already is a paera customer -> exclude this wallet + val shouldExcludePaeraCustomers = params is TangemPayOnboardingComponent.Params.FromBannerInSettings + modelScope.launch { + val eligibleWalletsIds = eligibilityManager + .getEligibleWallets(shouldExcludePaeraCustomers = shouldExcludePaeraCustomers) + .map { it.walletId } + if (eligibleWalletsIds.isEmpty()) { + back() + return@launch } - is TangemPayOnboardingComponent.Params.FromBannerOnMain, - is TangemPayOnboardingComponent.Params.Deeplink, - is TangemPayOnboardingComponent.Params.FromBannerInSettings, - -> { - openWalletSelectorIfNeeds(eligibleWalletsIds) + when (params) { + is TangemPayOnboardingComponent.Params.ContinueOnboarding -> { + checkCustomerInfo(params.userWalletId) + } + else -> { + openWalletSelectorIfNeeds(eligibleWalletsIds) + } } } } } - private fun openWalletSelectorIfNeeds(eligibleWalletsIds: List) { - if (eligibleWalletsIds.size == 1) { - checkCustomerInfo(userWalletId = eligibleWalletsIds[0]) + private fun openWalletSelectorIfNeeds(walletsIds: List) { + if (walletsIds.size == 1) { + checkCustomerInfo(userWalletId = walletsIds[0]) } else { analytics.send(TangemPayAnalyticsEvents.ChooseWalletPopup()) - bottomSheetNavigation.activate(TangemPayOnboardingNavigation.WalletSelector(eligibleWalletsIds)) + bottomSheetNavigation.activate(TangemPayOnboardingNavigation.WalletSelector(walletsIds)) } } From 92e21cc79cbfdd31f79afa454b8555500a442d89 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Feb 2026 19:32:02 +0100 Subject: [PATCH 18/33] Updated on 2026-08-14 --- .../models/OneTimePerSessionEvent.kt | 28 +++++++++++++------ .../com/tangem/core/analytics/Analytics.kt | 22 ++++++++++++--- .../model/news/details/NewsDetailsModel.kt | 7 ++++- .../analytics/NewsDetailsAnalyticsEvent.kt | 16 +++++++++++ .../factory/NewsDetailsStateFactory.kt | 15 ++-------- 5 files changed, 62 insertions(+), 26 deletions(-) diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt index a94df878eb..85f22ce2df 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/OneTimePerSessionEvent.kt @@ -1,22 +1,32 @@ package com.tangem.core.analytics.models /** - * Marker interface for analytics events that should be sent only once per session. + * Marker interface for analytics events that can be throttled by session and/or time. * - * Events implementing this interface will be tracked by their [oneTimeEventId] to ensure - * they are not sent multiple times during the same application session. Once an event - * with a specific [oneTimeEventId] has been sent, subsequent attempts to send an event - * with the same ID will be ignored. + * Events implementing this interface will be tracked by their [oneTimeEventId]. + * + * Behavior depends on [throttleSeconds]: + * - **null** (default): One time per session — event is sent only once during the application session. + * - **non-null**: Time-based throttling — event is not sent if less than [throttleSeconds] seconds + * have passed since the last send for this [oneTimeEventId]. * * @see Analytics.send */ interface OneTimePerSessionEvent { /** - * Unique identifier for the one-time event. + * Unique identifier for the throttled event. * - * This ID is used to track whether the event has already been sent in the current session. - * Events with the same [oneTimeEventId] will only be sent once, even if they are - * different instances of the same event class. + * This ID is used to track whether and when the event was last sent. + * Events with the same [oneTimeEventId] share the same throttling state. */ val oneTimeEventId: String + + /** + * Minimum interval in seconds between sends for this event. + * + * - **null**: One time per session only. Event is sent at most once per session. + * - **non-null**: Don't send if less than this many seconds have passed since the last send. + */ + val throttleSeconds: Long? + get() = null } \ No newline at end of file diff --git a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt index c5630c4906..d6571cb78c 100644 --- a/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt +++ b/core/analytics/src/main/java/com/tangem/core/analytics/Analytics.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit /** [REDACTED_AUTHOR] @@ -32,7 +33,7 @@ object Analytics : GlobalAnalyticsEventHandler { private val handlers = mutableMapOf() private val paramsInterceptors = ConcurrentHashMap() - private val oneEventsPerSession = ConcurrentHashMap() + private val throttledEventsState = ConcurrentHashMap() private val analyticsFilters = mutableSetOf() private val analyticsMutex = Mutex() @@ -87,9 +88,7 @@ object Analytics : GlobalAnalyticsEventHandler { override fun send(event: AnalyticsEvent) { analyticsScope.launch { - if (event is OneTimePerSessionEvent && - oneEventsPerSession.putIfAbsent(event.oneTimeEventId, true) != null - ) { + if (event is OneTimePerSessionEvent && !shouldSendThrottledEvent(event)) { return@launch } event.params = applyParamsInterceptors(event) @@ -137,6 +136,21 @@ object Analytics : GlobalAnalyticsEventHandler { return interceptedParams } + private fun shouldSendThrottledEvent(event: OneTimePerSessionEvent): Boolean { + val now = System.currentTimeMillis() + val id = event.oneTimeEventId + return when (val throttleMs = event.throttleSeconds?.let(TimeUnit.SECONDS::toMillis)) { + null -> throttledEventsState.putIfAbsent(id, now) == null + else -> throttledEventsState.compute(id) { _, lastSendTime -> + when { + lastSendTime == null -> now + now - lastSendTime >= throttleMs -> now + else -> lastSendTime + } + } == now + } + } + private fun createScope(): CoroutineScope { val name = "Analytics" val dispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt index 5752717896..63cc26391a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/NewsDetailsModel.kt @@ -108,9 +108,9 @@ internal class NewsDetailsModel @Inject constructor( private val stateFactory by lazy(LazyThreadSafetyMode.NONE) { NewsDetailsStateFactory( currentStateProvider = Provider { _state.value }, - shareManager = shareManager, onStateUpdate = { newState -> _state.update { newState } }, onRetryClick = ::onRetryClicked, + onShareClick = ::onShareClick, ) } @@ -149,6 +149,11 @@ internal class NewsDetailsModel @Inject constructor( urlOpener.openUrl(relatedArticle.url) } + private fun onShareClick(article: ArticleUM) { + shareManager.shareText(article.newsUrl) + analyticsEventHandler.send(NewsDetailsAnalyticsEvent.NewsShareButtonClick(article.id)) + } + private fun onArticleIndexChanged(newIndex: Int) { stateFactory.updateSelectedArticleIndex(newIndex) val currentArticle = when (state.value.articlesStateUM) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/analytics/NewsDetailsAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/analytics/NewsDetailsAnalyticsEvent.kt index 6e43d74467..a3e9a20b25 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/analytics/NewsDetailsAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/analytics/NewsDetailsAnalyticsEvent.kt @@ -70,4 +70,20 @@ internal sealed class NewsDetailsAnalyticsEvent( ERROR_MESSAGE to message, ), ) + + data class NewsShareButtonClick( + private val newsId: Int, + ) : NewsDetailsAnalyticsEvent( + event = "News Share Button Clicked", + params = mapOf( + "News Id" to newsId.toString(), + ), + ), OneTimePerSessionEvent { + override val oneTimeEventId: String = event + override val throttleSeconds: Long = THROTTLE_SECONDS + } + + private companion object { + const val THROTTLE_SECONDS = 10L + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/factory/NewsDetailsStateFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/factory/NewsDetailsStateFactory.kt index d52db614b1..43adcb7ab7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/factory/NewsDetailsStateFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/details/factory/NewsDetailsStateFactory.kt @@ -1,6 +1,5 @@ package com.tangem.features.feed.model.news.details.factory -import com.tangem.core.navigation.share.ShareManager import com.tangem.features.feed.ui.news.details.state.ArticleUM import com.tangem.features.feed.ui.news.details.state.ArticlesStateUM import com.tangem.features.feed.ui.news.details.state.NewsDetailsUM @@ -10,7 +9,7 @@ import kotlinx.collections.immutable.toImmutableList internal class NewsDetailsStateFactory( private val currentStateProvider: Provider, - private val shareManager: ShareManager, + private val onShareClick: (ArticleUM) -> Unit, private val onStateUpdate: (NewsDetailsUM) -> Unit, private val onRetryClick: () -> Unit, ) { @@ -23,11 +22,7 @@ internal class NewsDetailsStateFactory( articles = articles.toImmutableList(), articlesStateUM = ArticlesStateUM.Content, selectedArticleIndex = selectedIndex, - onShareClick = { - currentArticle?.let { - shareManager.shareText(it.newsUrl) - } - }, + onShareClick = { currentArticle?.let(onShareClick) }, ), ) } @@ -38,11 +33,7 @@ internal class NewsDetailsStateFactory( onStateUpdate( currentState.copy( selectedArticleIndex = newIndex, - onShareClick = { - currentArticle?.let { - shareManager.shareText(it.newsUrl) - } - }, + onShareClick = { currentArticle?.let(onShareClick) }, ), ) } From a40e6e7e28d7b74f506c5389fc4e9037df8e4260 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Feb 2026 14:27:37 +0500 Subject: [PATCH 19/33] Updated on 2026-08-14 --- .../AppsFlyerReferralParamsHandler.kt | 4 +++ .../AppsFlyerReferralParamsHandlerTest.kt | 8 ++++++ features/home/impl/build.gradle.kts | 5 ++++ .../features/home/impl/model/HomeModel.kt | 16 +++++++++++- features/referral/data/build.gradle.kts | 2 ++ .../DefaultMobileWalletPromoRepository.kt | 26 +++++++++++++++++++ .../referral/di/ReferralRepositoryModule.kt | 10 +++++++ features/referral/domain/build.gradle.kts | 1 + .../domain/MobileWalletPromoRepository.kt | 8 ++++++ .../SetShouldShowMobileWalletPromoUseCase.kt | 18 +++++++++++++ .../ShouldShowMobileWalletPromoUseCase.kt | 12 +++++++++ 11 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 features/referral/data/src/main/java/com/tangem/feature/referral/data/DefaultMobileWalletPromoRepository.kt create mode 100644 features/referral/domain/src/main/java/com/tangem/feature/referral/domain/MobileWalletPromoRepository.kt create mode 100644 features/referral/domain/src/main/java/com/tangem/feature/referral/domain/SetShouldShowMobileWalletPromoUseCase.kt create mode 100644 features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ShouldShowMobileWalletPromoUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt index ed1ca3be0a..68a63f0c00 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandler.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common.analytics.appsflyer import com.appsflyer.deeplink.DeepLink import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.wallets.models.AppsFlyerConversionData +import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -18,6 +19,7 @@ import kotlin.contracts.contract @Singleton class AppsFlyerReferralParamsHandler @Inject constructor( private val appsFlyerStore: AppsFlyerStore, + private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase, dispatchers: CoroutineDispatcherProvider, ) { @@ -69,6 +71,8 @@ class AppsFlyerReferralParamsHandler @Inject constructor( private fun storeConversionData(refcode: String, campaign: String?) { coroutineScope.launch { mutex.withLock { + setShouldShowMobileWalletPromoUseCase() + .onLeft { Timber.e(it) } appsFlyerStore.storeIfAbsent( value = AppsFlyerConversionData(refcode = refcode, campaign = campaign), ) diff --git a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt index 5b7dfd4403..c804791992 100644 --- a/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/common/analytics/appsflyer/AppsFlyerReferralParamsHandlerTest.kt @@ -3,9 +3,12 @@ package com.tangem.tap.common.analytics.appsflyer import com.appsflyer.deeplink.DeepLink import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.wallets.models.AppsFlyerConversionData +import com.tangem.feature.referral.domain.SetShouldShowMobileWalletPromoUseCase import com.tangem.test.core.ProvideTestModels import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import arrow.core.right import io.mockk.clearMocks +import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk @@ -22,9 +25,13 @@ import org.junit.jupiter.params.ParameterizedTest class AppsFlyerReferralParamsHandlerTest { private val appsFlyerStore: AppsFlyerStore = mockk(relaxUnitFun = true) + private val setShouldShowMobileWalletPromoUseCase: SetShouldShowMobileWalletPromoUseCase = mockk { + coEvery { this@mockk.invoke() } returns Unit.right() + } private val handler = AppsFlyerReferralParamsHandler( appsFlyerStore = appsFlyerStore, dispatchers = TestingCoroutineDispatcherProvider(), + setShouldShowMobileWalletPromoUseCase = setShouldShowMobileWalletPromoUseCase, ) @AfterEach @@ -43,6 +50,7 @@ class AppsFlyerReferralParamsHandlerTest { if (model.shouldStore) { val value = AppsFlyerConversionData(refcode = SUCCESS_REFCODE, campaign = SUCCESS_CAMPAIGN) + coVerify { appsFlyerStore.storeIfAbsent(value = value) } } else { coVerify(inverse = true) { appsFlyerStore.storeIfAbsent(value = any()) } diff --git a/features/home/impl/build.gradle.kts b/features/home/impl/build.gradle.kts index d06bedde20..bd620457d9 100644 --- a/features/home/impl/build.gradle.kts +++ b/features/home/impl/build.gradle.kts @@ -27,6 +27,7 @@ dependencies { implementation(projects.common.routing) /** Domain */ + implementation(projects.domain.common) implementation(projects.domain.models) implementation(projects.domain.core) implementation(projects.domain.card) @@ -37,6 +38,10 @@ dependencies { implementation(projects.domain.legacy) implementation(projects.domain.feedback) implementation(projects.domain.feedback.models) + implementation(projects.domain.referral) + + /** Referral */ + implementation(projects.features.referral.domain) /** AndroidX libraries */ implementation(deps.androidx.activity.compose) diff --git a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt index b261bf588d..91d539dcc8 100644 --- a/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt +++ b/features/home/impl/src/main/kotlin/com/tangem/features/home/impl/model/HomeModel.kt @@ -42,8 +42,10 @@ import com.tangem.features.home.api.HomeComponent import com.tangem.features.home.impl.ui.state.HomeUM import com.tangem.features.home.impl.ui.state.Stories import com.tangem.features.home.impl.ui.state.getRestrictedStories +import com.tangem.feature.referral.domain.ShouldShowMobileWalletPromoUseCase import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.Debouncer import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -74,9 +76,12 @@ internal class HomeModel @Inject constructor( private val hotWalletFeatureToggles: HotWalletFeatureToggles, private val userWalletsListRepository: UserWalletsListRepository, private val reduxStateHolder: ReduxStateHolder, + private val shouldShowMobileWalletPromoUseCase: ShouldShowMobileWalletPromoUseCase, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, ) : Model() { + private val debouncer = Debouncer() + val params = paramsContainer.require() private val _uiState = MutableStateFlow( @@ -145,7 +150,16 @@ internal class HomeModel @Inject constructor( } private fun onGetStartedClick() { - router.push(AppRoute.CreateWalletStart(mode = AppRoute.CreateWalletStart.Mode.ColdWallet)) + debouncer.debounce(modelScope) { + modelScope.launch { + val mode = if (shouldShowMobileWalletPromoUseCase()) { + AppRoute.CreateWalletStart.Mode.HotWallet + } else { + AppRoute.CreateWalletStart.Mode.ColdWallet + } + router.push(AppRoute.CreateWalletStart(mode = mode)) + } + } } private fun scanCard() { diff --git a/features/referral/data/build.gradle.kts b/features/referral/data/build.gradle.kts index 85cd526d9f..ac60688bc4 100644 --- a/features/referral/data/build.gradle.kts +++ b/features/referral/data/build.gradle.kts @@ -18,8 +18,10 @@ dependencies { /** Data modules */ implementation(projects.data.common) + implementation(deps.androidx.datastore) /** Domain modules */ + implementation(projects.domain.common) implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) implementation(projects.domain.models) diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/data/DefaultMobileWalletPromoRepository.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/data/DefaultMobileWalletPromoRepository.kt new file mode 100644 index 0000000000..56a482e96d --- /dev/null +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/data/DefaultMobileWalletPromoRepository.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.referral.data + +import androidx.datastore.preferences.core.booleanPreferencesKey +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.feature.referral.domain.MobileWalletPromoRepository +import javax.inject.Inject + +internal class DefaultMobileWalletPromoRepository @Inject constructor( + private val appPreferencesStore: AppPreferencesStore, +) : MobileWalletPromoRepository { + + override suspend fun shouldShowMobileWalletPromo(): Boolean { + return appPreferencesStore.getSyncOrDefault(key = SHOULD_SHOW_MOBILE_WALLET_PROMO_KEY, default = false) + } + + override suspend fun setShouldShowMobileWalletPromo(value: Boolean) { + appPreferencesStore.editData { preferences -> + preferences[SHOULD_SHOW_MOBILE_WALLET_PROMO_KEY] = value + } + } + + private companion object { + val SHOULD_SHOW_MOBILE_WALLET_PROMO_KEY = booleanPreferencesKey("should_show_mobile_wallet_promo") + } +} \ No newline at end of file diff --git a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt index 8d2c4cdfd4..2170e1651d 100644 --- a/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt +++ b/features/referral/data/src/main/java/com/tangem/feature/referral/di/ReferralRepositoryModule.kt @@ -2,10 +2,13 @@ package com.tangem.feature.referral.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.feature.referral.converters.ReferralConverter +import com.tangem.feature.referral.data.DefaultMobileWalletPromoRepository import com.tangem.feature.referral.data.ExternalReferralRepository import com.tangem.feature.referral.data.ReferralRepositoryImpl +import com.tangem.feature.referral.domain.MobileWalletPromoRepository import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -53,4 +56,11 @@ class ReferralRepositoryModule { excludedBlockchains = excludedBlockchains, ) } + + @Provides + @Singleton + fun provideMobileWalletPromoRepository(appPreferencesStore: AppPreferencesStore): MobileWalletPromoRepository = + DefaultMobileWalletPromoRepository( + appPreferencesStore = appPreferencesStore, + ) } \ No newline at end of file diff --git a/features/referral/domain/build.gradle.kts b/features/referral/domain/build.gradle.kts index b9bec0d9f3..3604d3a6be 100644 --- a/features/referral/domain/build.gradle.kts +++ b/features/referral/domain/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { /** Domain modules */ implementation(projects.domain.account.status) implementation(projects.domain.card) + implementation(projects.domain.common) implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/MobileWalletPromoRepository.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/MobileWalletPromoRepository.kt new file mode 100644 index 0000000000..642ab878c7 --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/MobileWalletPromoRepository.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.referral.domain + +interface MobileWalletPromoRepository { + + suspend fun shouldShowMobileWalletPromo(): Boolean + + suspend fun setShouldShowMobileWalletPromo(value: Boolean) +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/SetShouldShowMobileWalletPromoUseCase.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/SetShouldShowMobileWalletPromoUseCase.kt new file mode 100644 index 0000000000..9049cc72bb --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/SetShouldShowMobileWalletPromoUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.referral.domain + +import arrow.core.Either +import com.tangem.domain.common.wallets.UserWalletsListRepository +import javax.inject.Inject + +class SetShouldShowMobileWalletPromoUseCase @Inject constructor( + private val mobileWalletPromoRepository: MobileWalletPromoRepository, + private val userWalletsListRepository: UserWalletsListRepository, +) { + + suspend operator fun invoke(): Either = Either.catch { + val wallets = userWalletsListRepository.userWallets.value + if (wallets.isNullOrEmpty()) { + mobileWalletPromoRepository.setShouldShowMobileWalletPromo(true) + } + } +} \ No newline at end of file diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ShouldShowMobileWalletPromoUseCase.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ShouldShowMobileWalletPromoUseCase.kt new file mode 100644 index 0000000000..09a3b358cc --- /dev/null +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ShouldShowMobileWalletPromoUseCase.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.referral.domain + +import javax.inject.Inject + +class ShouldShowMobileWalletPromoUseCase @Inject constructor( + private val mobileWalletPromoRepository: MobileWalletPromoRepository, +) { + + suspend operator fun invoke(): Boolean { + return mobileWalletPromoRepository.shouldShowMobileWalletPromo() + } +} \ No newline at end of file From fa86588528ae7165e91330018c2f006323e9d5f5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Feb 2026 16:27:54 +0700 Subject: [PATCH 20/33] Updated on 2026-08-14 --- .../models/PromocodeActivationBody.kt | 1 + .../data/wallets/DefaultWalletsRepository.kt | 2 ++ .../wallets/DefaultWalletsRepositoryTest.kt | 13 +++++++--- .../wallets/repository/WalletsRepository.kt | 6 ++++- .../ActivateBitcoinPromocodeUseCase.kt | 12 +++++++-- .../deeplink/DefaultPromoDeeplinkHandler.kt | 14 +++++++--- .../DefaultPromoDeeplinkHandlerTest.kt | 26 +++++++++---------- 7 files changed, 52 insertions(+), 22 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationBody.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationBody.kt index 9d28f527e7..9651ae0c5c 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationBody.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/PromocodeActivationBody.kt @@ -7,4 +7,5 @@ import com.squareup.moshi.JsonClass data class PromocodeActivationBody( @Json(name = "promoCode") val promoCode: String, @Json(name = "address") val address: String, + @Json(name = "walletId") val walletId: String, ) \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index d2e807c047..ea7ba4b3b1 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -435,6 +435,7 @@ internal class DefaultWalletsRepository( } override suspend fun activatePromoCode( + userWalletId: UserWalletId, promoCode: String, bitcoinAddress: String, ): Either = withContext(dispatchers.io) { @@ -442,6 +443,7 @@ internal class DefaultWalletsRepository( body = PromocodeActivationBody( promoCode = promoCode, address = bitcoinAddress, + walletId = userWalletId.stringValue, ), ).fold( onSuccess = { it.status.right() }, diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index 9fbe71c7a6..30dd508d5b 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -268,6 +268,7 @@ class DefaultWalletsRepositoryTest { @Test fun `GIVEN valid data WHEN activatePromoCode THEN returns Right with status and calls API`() = runTest { // GIVEN + val walletId = UserWalletId("1234567890abcdef") val promoCode = "PROMO123" val address = "bc1qexampleaddress" coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Success( @@ -275,7 +276,11 @@ class DefaultWalletsRepositoryTest { ) // WHEN - val result = repository.activatePromoCode(promoCode = promoCode, bitcoinAddress = address) + val result = repository.activatePromoCode( + userWalletId = walletId, + promoCode = promoCode, + bitcoinAddress = address + ) // THEN var right: String? = null @@ -294,13 +299,14 @@ class DefaultWalletsRepositoryTest { @Test fun `GIVEN NOT_FOUND error WHEN activatePromoCode THEN returns Left InvalidPromoCode`() = runTest { // GIVEN + val walletId = UserWalletId("1234567890abcdef") coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Error( HttpException(code = HttpException.Code.NOT_FOUND, message = null, errorBody = null), ) as ApiResponse // WHEN - val result = repository.activatePromoCode(promoCode = "PROMO", bitcoinAddress = "addr") + val result = repository.activatePromoCode(userWalletId = walletId, promoCode = "PROMO", bitcoinAddress = "addr") // THEN var error: ActivatePromoCodeError? = null @@ -311,13 +317,14 @@ class DefaultWalletsRepositoryTest { @Test fun `GIVEN CONFLICT error WHEN activatePromoCode THEN returns Left PromocodeAlreadyUsed`() = runTest { // GIVEN + val walletId = UserWalletId("1234567890abcdef") coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Error( HttpException(code = HttpException.Code.CONFLICT, message = null, errorBody = null), ) as ApiResponse // WHEN - val result = repository.activatePromoCode(promoCode = "PROMO", bitcoinAddress = "addr") + val result = repository.activatePromoCode(userWalletId = walletId, promoCode = "PROMO", bitcoinAddress = "addr") // THEN var error: ActivatePromoCodeError? = null diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 1431dc5e6f..3757a72ea5 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -75,5 +75,9 @@ interface WalletsRepository { @Throws suspend fun associateWallets(applicationId: String, wallets: List) - suspend fun activatePromoCode(promoCode: String, bitcoinAddress: String): Either + suspend fun activatePromoCode( + userWalletId: UserWalletId, + promoCode: String, + bitcoinAddress: String, + ): Either } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ActivateBitcoinPromocodeUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ActivateBitcoinPromocodeUseCase.kt index 0acd401fcb..638cfa438a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ActivateBitcoinPromocodeUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/ActivateBitcoinPromocodeUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError import com.tangem.domain.wallets.repository.WalletsRepository import javax.inject.Inject @@ -9,6 +10,13 @@ class ActivateBitcoinPromocodeUseCase @Inject constructor( private val walletsRepository: WalletsRepository, ) { - suspend operator fun invoke(address: String, promoCode: String): Either = - walletsRepository.activatePromoCode(promoCode = promoCode, bitcoinAddress = address) + suspend operator fun invoke( + userWalletId: UserWalletId, + address: String, + promoCode: String, + ): Either = walletsRepository.activatePromoCode( + userWalletId = userWalletId, + promoCode = promoCode, + bitcoinAddress = address, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt index 0cb63b07c9..e2c99b4aa3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/deeplink/DefaultPromoDeeplinkHandler.kt @@ -130,7 +130,11 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( "Start activation promoCode ${promoCode.mask()} address ${bitcoinAddress.mask()}", ) - activatePromoCode(bitcoinAddress = bitcoinAddress, promoCode = promoCode) + activatePromoCode( + userWallet = userWallet, + bitcoinAddress = bitcoinAddress, + promoCode = promoCode, + ) } else { uiMessageSender.send(GlobalLoadingMessage(false)) delay(DEFAULT_MESSAGE_SENDER_DELAY) @@ -141,9 +145,13 @@ internal class DefaultPromoDeeplinkHandler @AssistedInject constructor( } } - private suspend fun activatePromoCode(bitcoinAddress: String, promoCode: String) { + private suspend fun activatePromoCode(userWallet: UserWallet, bitcoinAddress: String, promoCode: String) { uiMessageSender.send(GlobalLoadingMessage(true)) - activateBitcoinPromocodeUseCase(bitcoinAddress, promoCode).onRight { + activateBitcoinPromocodeUseCase( + userWalletId = userWallet.walletId, + address = bitcoinAddress, + promoCode = promoCode, + ).onRight { delay(DEFAULT_MESSAGE_SENDER_DELAY) uiMessageSender.send(GlobalLoadingMessage(false)) delay(DEFAULT_MESSAGE_SENDER_DELAY) diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt index 602c3cb5be..28fd3f1aec 100644 --- a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt @@ -102,7 +102,7 @@ class DefaultPromoDeeplinkHandlerTest { } val btcCoin = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id) coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoin) - coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qxyz", promoCode) } returns Either.Right("ok") + coEvery { activateBitcoinPromocodeUseCase.invoke(any(), "bc1qxyz", promoCode) } returns Either.Right("ok") val dispatcherProvider = testDispatcherProvider(testScheduler) DefaultPromoDeeplinkHandler( @@ -251,7 +251,7 @@ class DefaultPromoDeeplinkHandlerTest { coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatus)) val btcCoin = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id) coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoin) - coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qxyz", promoCode) } returns Either.Right("ok") + coEvery { activateBitcoinPromocodeUseCase.invoke(any(), "bc1qxyz", promoCode) } returns Either.Right("ok") val dispatcherProvider = testDispatcherProvider(testScheduler) DefaultPromoDeeplinkHandler( @@ -369,7 +369,7 @@ class DefaultPromoDeeplinkHandlerTest { coEvery { multiNetworkStatusSupplier.invoke(any()) } returns flowOf(setOf(btcStatus)) val btcCurrency = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id) coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCurrency) - coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qxyz", promoCode) } returns Either.Left(error) + coEvery { activateBitcoinPromocodeUseCase.invoke(any(), "bc1qxyz", promoCode) } returns Either.Left(error) val dispatcherProvider = testDispatcherProvider(testScheduler) DefaultPromoDeeplinkHandler( @@ -506,7 +506,7 @@ class DefaultPromoDeeplinkHandlerTest { btcCoinCustom, btcCoinCard, ) - coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qcustom", promoCode) } returns Either.Right("ok") + coEvery { activateBitcoinPromocodeUseCase.invoke(any(), "bc1qcustom", promoCode) } returns Either.Right("ok") val dispatcherProvider = testDispatcherProvider(testScheduler) @@ -530,8 +530,8 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success)) - coVerify(exactly = 1) { activateBitcoinPromocodeUseCase.invoke("bc1qcustom", promoCode) } - coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) } + coVerify(exactly = 1) { activateBitcoinPromocodeUseCase.invoke(any(), "bc1qcustom", promoCode) } + coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), "bc1qcard", promoCode) } } @Test @@ -560,7 +560,7 @@ class DefaultPromoDeeplinkHandlerTest { val btcCoinCard = buildCryptoCurrency(rawNetworkId = Blockchain.Bitcoin.id, derivationPath = dpCard) coEvery { multiWalletCryptoCurrenciesSupplier.getSyncOrNull(any()) } returns setOf(btcCoinCard) - coEvery { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) } returns Either.Right("ok") + coEvery { activateBitcoinPromocodeUseCase.invoke(any(), "bc1qcard", promoCode) } returns Either.Right("ok") val dispatcherProvider = testDispatcherProvider(testScheduler) @@ -584,8 +584,8 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_activation_success)) - coVerify(exactly = 1) { activateBitcoinPromocodeUseCase.invoke("bc1qcard", promoCode) } - coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke("bc1qcustom", promoCode) } + coVerify(exactly = 1) { activateBitcoinPromocodeUseCase.invoke(any(), "bc1qcard", promoCode) } + coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), "bc1qcustom", promoCode) } verify( exactly = 1, @@ -644,7 +644,7 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address)) - coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) } + coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(),any(), any()) } verify( exactly = 1, @@ -703,7 +703,7 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address)) - coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) } + coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any(), any()) } verify( exactly = 1, @@ -758,7 +758,7 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address)) - coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) } + coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any(), any()) } verify( exactly = 1, @@ -813,7 +813,7 @@ class DefaultPromoDeeplinkHandlerTest { Truth.assertThat(sent.title).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address_title)) Truth.assertThat(sent.message).isEqualTo(resourceReference(R.string.bitcoin_promo_no_address)) - coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any()) } + coVerify(exactly = 0) { activateBitcoinPromocodeUseCase.invoke(any(), any(), any()) } verify( exactly = 1, From bb1ff81252c7b19ff31b0761de21be26a016c115 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Feb 2026 14:28:48 +0500 Subject: [PATCH 21/33] Updated on 2026-08-14 --- .../entity/WalletSettingsItemUM.kt | 2 +- .../walletsettings/ui/WalletSettingsScreen.kt | 17 +++++++++++------ .../walletsettings/utils/ItemsBuilder.kt | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt index d9ff609d19..a0598dcab9 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/entity/WalletSettingsItemUM.kt @@ -14,7 +14,7 @@ internal sealed class WalletSettingsItemUM { data class WithItems( override val id: String, - val description: TextReference, + val description: TextReference?, val blocks: ImmutableList, ) : WalletSettingsItemUM() diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt index e3efc898cd..0b600daa13 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/ui/WalletSettingsScreen.kt @@ -167,6 +167,7 @@ private fun Content(state: WalletSettingsUM, modifier: Modifier = Modifier) { @Composable private fun ItemsBlock(model: WalletSettingsItemUM.WithItems, modifier: Modifier = Modifier) { + val description = model.description Column( modifier = modifier, horizontalAlignment = Alignment.Start, @@ -190,12 +191,16 @@ private fun ItemsBlock(model: WalletSettingsItemUM.WithItems, modifier: Modifier } } - Text( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), - text = model.description.resolveReference(), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.caption2, - ) + AnimatedVisibility(visible = description != null) { + if (description != null) { + Text( + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), + text = description.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + ) + } + } } } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index a103dee2de..8bde456990 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -144,7 +144,7 @@ internal class ItemsBuilder @Inject constructor() { onCardSettingsClick: () -> Unit, ) = WalletSettingsItemUM.WithItems( id = "card", - description = resourceReference(R.string.settings_card_settings_footer), + description = null, blocks = buildList { val isHotWallet = userWallet is UserWallet.Hot if (isHotWallet) { From 6edf2d043be0eb9ada7b46813b400cba0cd9b92c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Feb 2026 12:29:16 +0300 Subject: [PATCH 22/33] Updated on 2026-08-14 --- .../swap/ui/ChooseProviderBottomSheet.kt | 50 +++++++++++-------- .../tangem/feature/swap/ui/ProviderItem.kt | 21 +++++--- 2 files changed, 44 insertions(+), 27 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 996b41aa9e..181996f1c4 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -3,8 +3,10 @@ package com.tangem.feature.swap.ui import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -16,10 +18,13 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -32,13 +37,20 @@ import kotlinx.collections.immutable.persistentListOf @Composable fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( + TangemModalBottomSheet( config = config, containerColor = TangemTheme.colors.background.tertiary, - titleText = resourceReference(R.string.express_choose_providers_title), - ) { content: ChooseProviderBottomSheetConfig -> - ChooseProviderBottomSheetContent(content = content) - } + title = { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.express_choose_providers_title), + endIconRes = R.drawable.ic_close_24, + onEndClick = config.onDismissRequest, + ) + }, + content = { content -> + ChooseProviderBottomSheetContent(content = content) + }, + ) } @Suppress("LongMethod") @@ -50,10 +62,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, modifier = Modifier - .padding( - top = 10.dp, - bottom = 16.dp, - ) + .padding(bottom = 14.dp) .padding(horizontal = TangemTheme.dimens.spacing56), textAlign = TextAlign.Center, ) @@ -76,31 +85,30 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC start = 16.dp, end = 16.dp, bottom = 14.dp, - ) - .background( - color = TangemTheme.colors.background.action, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .clip(shape = TangemTheme.shapes.roundedCornersXMedium), + ), + verticalArrangement = Arrangement.spacedBy(6.dp), ) { content.providers.forEach { provider -> val isSelected = provider.id == content.selectedProviderId ProviderItem( state = provider, - isSelected = isSelected, + isSelected = false, modifier = Modifier + .selectedBorder(isSelected = isSelected) + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) .clickable( enabled = provider.onProviderClick != null, onClick = { provider.onProviderClick?.invoke(provider.id) }, ) .padding( - top = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, + vertical = 16.dp, + horizontal = 2.dp, ), ) } } + SpacerH(6.dp) Icon( painterResource(id = R.drawable.ic_lightning_16), contentDescription = null, @@ -111,7 +119,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC style = TangemTheme.typography.caption2, color = TangemTheme.colors.icon.informative, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing6, bottom = TangemTheme.dimens.spacing16) + .padding(top = 4.dp, bottom = 32.dp) .padding(horizontal = TangemTheme.dimens.spacing56), textAlign = TextAlign.Center, ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index a610eaac34..4c8aca99fe 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -99,7 +100,7 @@ private fun ProviderContentState( isSelected: Boolean = false, ) { Box(modifier = modifier.fillMaxWidth()) { - Row { + Row(verticalAlignment = Alignment.CenterVertically) { SubcomposeAsyncImage( modifier = Modifier .padding(start = TangemTheme.dimens.spacing12) @@ -120,8 +121,10 @@ private fun ProviderContentState( Column( modifier = Modifier + .heightIn(min = TangemTheme.dimens.size40) .padding(start = TangemTheme.dimens.spacing12) .testTag(SwapTokenScreenTestTags.PROVIDERS_BLOCK), + verticalArrangement = Arrangement.SpaceBetween, ) { Row { if (state.namePrefix == ProviderState.PrefixType.PROVIDED_BY) { @@ -158,7 +161,6 @@ private fun ProviderContentState( } Row( modifier = Modifier.padding( - top = TangemTheme.dimens.spacing6, end = TangemTheme.dimens.spacing56, ), ) { @@ -205,7 +207,7 @@ private fun ProviderUnavailableState( modifier: Modifier = Modifier, ) { Box(modifier = modifier.fillMaxWidth()) { - Row { + Row(verticalAlignment = Alignment.CenterVertically) { val (alpha, colorFilter) = GRAY_SCALE_ALPHA to GrayscaleColorFilter SubcomposeAsyncImage( modifier = Modifier @@ -228,7 +230,10 @@ private fun ProviderUnavailableState( ) Column( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), + modifier = Modifier + .heightIn(min = TangemTheme.dimens.size40) + .padding(start = TangemTheme.dimens.spacing12), + verticalArrangement = Arrangement.SpaceBetween, ) { Row { AnimatedContent(targetState = state.name, label = "") { name -> @@ -247,12 +252,16 @@ private fun ProviderUnavailableState( ) } } - AnimatedContent(targetState = state.alertText, label = "") { alertText -> + AnimatedContent( + targetState = state.alertText, + contentAlignment = Alignment.BottomStart, + label = "", + ) { alertText -> Text( text = alertText.resolveReference(), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing6), + modifier = Modifier, ) } } From a5389365f4c5dc03c14a3acdf2b139c3730b40dc Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Feb 2026 11:54:17 +0200 Subject: [PATCH 23/33] Updated on 2026-08-14 --- .../market/details/portfolio/add/impl/model/AddTokenModel.kt | 4 ++-- .../markets/portfolio/add/impl/model/AddTokenModel.kt | 4 ++-- .../onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt index 2e8b332e31..1d6e6d57be 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt @@ -96,11 +96,11 @@ internal class AddTokenModel @Inject constructor( return@launch } - val status = getAccountCurrencyStatusUseCase.invokeSync( + val status = getAccountCurrencyStatusUseCase( userWalletId = accountId.userWalletId, currencyId = cryptoCurrency.id, network = cryptoCurrency.network, - ).getOrNull() + ).firstOrNull() if (status == null) { processError(error = null) } else { diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt index f6ac24e4dd..416154863e 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt @@ -96,11 +96,11 @@ internal class AddTokenModel @Inject constructor( return@launch } - val status = getAccountCurrencyStatusUseCase.invokeSync( + val status = getAccountCurrencyStatusUseCase( userWalletId = accountId.userWalletId, currencyId = cryptoCurrency.id, network = cryptoCurrency.network, - ).getOrNull() + ).firstOrNull() if (status == null) { processError(error = null) } else { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt index 8a895a5fb9..d786999cad 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt @@ -75,11 +75,11 @@ internal class OnrampAddTokenModel @Inject constructor( return@launch } - val status = getAccountCurrencyStatusUseCase.invokeSync( + val status = getAccountCurrencyStatusUseCase( userWalletId = accountId.userWalletId, currencyId = cryptoCurrency.id, network = cryptoCurrency.network, - ).getOrNull() + ).firstOrNull() if (status == null) { processError(error = null) } else { From 5346881a57ad7d349927892e011febec563a17cb Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Feb 2026 16:49:02 +0400 Subject: [PATCH 24/33] Updated on 2026-08-14 --- .../tangem/data/pay/di/TangemPayDataModule.kt | 6 ++ .../DefaultGetTangemPayCustomerIdUseCase.kt | 25 +++++++++ .../feedback/models/FeedbackEmailType.kt | 29 ++++++++-- .../domain/feedback/FeedbackDataBuilder.kt | 2 +- .../utils/EmailMessageBodyResolver.kt | 12 +++- .../domain/pay/TangemPayDetailsConfig.kt | 1 + .../com/tangem/domain/visa/error/VisaError.kt | 1 + .../GetTangemPayCustomerIdUseCase.kt | 10 ++++ .../features/details/model/DetailsModel.kt | 33 +++++++---- .../impl/DefaultOnboardingStepperComponent.kt | 10 +++- .../tangem/feature/swap/model/SwapModel.kt | 10 +++- .../components/TangemPayDetailsComponent.kt | 1 + .../TangemPayTxHistoryDetailsComponent.kt | 1 + .../tangempay/model/TangemPayDetailsModel.kt | 1 + .../model/TangemPayTxHistoryDetailsModel.kt | 5 +- .../model/intents/TangemPayClickIntents.kt | 11 ++-- .../TangemPayUpdateInfoStateTransformer.kt | 55 ++++++++++--------- 17 files changed, 158 insertions(+), 55 deletions(-) create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultGetTangemPayCustomerIdUseCase.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/tangempay/GetTangemPayCustomerIdUseCase.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 719542728a..500e5fa50d 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -4,6 +4,7 @@ import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory import com.tangem.data.pay.DefaultTangemPayEligibilityManager import com.tangem.data.pay.repository.* import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase +import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.TangemPayEligibilityManager @@ -11,6 +12,7 @@ import com.tangem.domain.pay.repository.* import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase +import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository import com.tangem.security.DeviceSecurityInfoProvider @@ -65,6 +67,10 @@ internal interface TangemPayDataModule { @Singleton fun bindTangemPayWithdrawUseCase(impl: DefaultTangemPayWithdrawUseCase): TangemPayWithdrawUseCase + @Binds + @Singleton + fun bindGetTangemPayCustomerIdUseCase(impl: DefaultGetTangemPayCustomerIdUseCase): GetTangemPayCustomerIdUseCase + @Binds @Singleton fun bindTangemPayEligibilityManager(impl: DefaultTangemPayEligibilityManager): TangemPayEligibilityManager diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultGetTangemPayCustomerIdUseCase.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultGetTangemPayCustomerIdUseCase.kt new file mode 100644 index 0000000000..b4da25e73f --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/usecase/DefaultGetTangemPayCustomerIdUseCase.kt @@ -0,0 +1,25 @@ +package com.tangem.data.pay.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.core.error.UniversalError +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase +import com.tangem.domain.visa.error.VisaApiError +import javax.inject.Inject + +internal class DefaultGetTangemPayCustomerIdUseCase @Inject constructor( + private val tangemPayOnboardingRepository: OnboardingRepository, +) : GetTangemPayCustomerIdUseCase { + + override fun invoke(userWalletId: UserWalletId): Either { + val customerId = tangemPayOnboardingRepository.getSavedCustomerInfo(userWalletId)?.customerId + return if (customerId.isNullOrEmpty()) { + VisaApiError.CustomerIdUnavailable.left() + } else { + customerId.right() + } + } +} \ No newline at end of file diff --git a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt index 1dae5fb656..7b74739db5 100644 --- a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt +++ b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/FeedbackEmailType.kt @@ -60,31 +60,50 @@ sealed interface FeedbackEmailType { } sealed class Visa : FeedbackEmailType { + abstract val customerId: String - data class DirectUserRequest(override val walletMetaInfo: WalletMetaInfo) : Visa() + data class DirectUserRequest( + override val walletMetaInfo: WalletMetaInfo, + override val customerId: String, + ) : Visa() - data class Activation(override val walletMetaInfo: WalletMetaInfo) : Visa() + data class Activation( + override val walletMetaInfo: WalletMetaInfo, + override val customerId: String, + ) : Visa() data class Dispute( val visaTxDetails: VisaTxDetails, override val walletMetaInfo: WalletMetaInfo, + override val customerId: String, ) : Visa() - data class FailedIssueCard(override val walletMetaInfo: WalletMetaInfo) : Visa() + data class FailedIssueCard( + override val walletMetaInfo: WalletMetaInfo, + override val customerId: String, + ) : Visa() data class DisputeV2( val item: TangemPayTxHistoryItem, override val walletMetaInfo: WalletMetaInfo, + override val customerId: String, ) : Visa() data class Withdrawal( override val walletMetaInfo: WalletMetaInfo, + override val customerId: String, val providerName: String, val txId: String, ) : Visa() - data class FeatureIsBeta(override val walletMetaInfo: WalletMetaInfo) : Visa() + data class FeatureIsBeta( + override val walletMetaInfo: WalletMetaInfo, + override val customerId: String, + ) : Visa() - data class KycRejected(override val walletMetaInfo: WalletMetaInfo, val customerId: String) : Visa() + data class KycRejected( + override val walletMetaInfo: WalletMetaInfo, + override val customerId: String, + ) : Visa() } } \ No newline at end of file diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt index d10954f517..b44422a99a 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/FeedbackDataBuilder.kt @@ -55,7 +55,7 @@ internal class FeedbackDataBuilder { } fun addCustomerId(customerId: String) { - builder.appendKeyValue("ID: ", customerId) + builder.appendKeyValue("Tangem Pay Customer ID", customerId) } fun addUserWalletMetaInfo(walletMetaInfo: WalletMetaInfo) { diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index f9307e7914..04228907bc 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -34,7 +34,11 @@ class EmailMessageBodyResolver( -> addPhoneInfoBody() is FeedbackEmailType.Visa.Activation -> addUserRequestBody(type.walletMetaInfo) is FeedbackEmailType.Visa.DirectUserRequest -> addUserRequestBody(type.walletMetaInfo) - is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody(type.walletMetaInfo, type.visaTxDetails) + is FeedbackEmailType.Visa.Dispute -> addVisaRequestBody( + walletMetaInfo = type.walletMetaInfo, + customerId = type.customerId, + visaTxDetails = type.visaTxDetails, + ) is FeedbackEmailType.Visa.FailedIssueCard -> addTangemPayFailedIssuingCardBody(type) is FeedbackEmailType.Visa.DisputeV2 -> addTangemPayDisputeRequestBody(type) is FeedbackEmailType.Visa.Withdrawal -> addTangemPayWithdrawalRequestBody(type) @@ -48,6 +52,7 @@ class EmailMessageBodyResolver( private fun FeedbackDataBuilder.addTangemPayFailedIssuingCardBody(type: FeedbackEmailType.Visa.FailedIssueCard) { addTangemPayPhoneInfoBody(type) addDelimiter() + addCustomerId(customerId = type.customerId) type.walletMetaInfo.userWalletId?.let { userWalletId -> addUserWalletId(userWalletId = userWalletId.stringValue) } @@ -58,6 +63,7 @@ class EmailMessageBodyResolver( addDelimiter() addTangemPayTxInfo(type.item) addDelimiter() + addCustomerId(customerId = type.customerId) type.walletMetaInfo.userWalletId?.let { userWalletId -> addUserWalletId(userWalletId = userWalletId.stringValue) } @@ -66,6 +72,7 @@ class EmailMessageBodyResolver( private fun FeedbackDataBuilder.addTangemPayBetaRequestBody(type: FeedbackEmailType.Visa) { addTangemPayPhoneInfoBody(type) addDelimiter() + addCustomerId(customerId = type.customerId) type.walletMetaInfo?.userWalletId?.let { userWalletId -> addUserWalletId(userWalletId = userWalletId.stringValue) } @@ -82,6 +89,7 @@ class EmailMessageBodyResolver( ) { addUserWalletMetaInfo(type.walletMetaInfo) addDelimiter() + addCustomerId(customerId = type.customerId) val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" } val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) @@ -107,9 +115,11 @@ class EmailMessageBodyResolver( private suspend fun FeedbackDataBuilder.addVisaRequestBody( walletMetaInfo: WalletMetaInfo, visaTxDetails: VisaTxDetails, + customerId: String, ) { addUserRequestBody(walletMetaInfo) addDelimiter() + addCustomerId(customerId = customerId) addVisaTxInfo(visaTxDetails) } diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt index 88f7b5556c..14edae3667 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt @@ -5,6 +5,7 @@ import kotlinx.serialization.Serializable @Serializable data class TangemPayDetailsConfig( + val customerId: String, val cardId: String, val isPinSet: Boolean, val cardFrozenState: TangemPayCardFrozenState, diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt index 412045caff..be321d6b33 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt @@ -66,6 +66,7 @@ sealed class VisaApiError( data object SignWithdrawError : VisaApiError(104004004) data object WithdrawError : VisaApiError(104004005) data object ServerUnavailable : VisaApiError(104004006) + data object CustomerIdUnavailable : VisaApiError(104004007) companion object { fun fromBackendError(backendErrorCode: Int): VisaApiError { diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/GetTangemPayCustomerIdUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/GetTangemPayCustomerIdUseCase.kt new file mode 100644 index 0000000000..af30a64af0 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/GetTangemPayCustomerIdUseCase.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.tangempay + +import arrow.core.Either +import com.tangem.core.error.UniversalError +import com.tangem.domain.models.wallet.UserWalletId + +interface GetTangemPayCustomerIdUseCase { + + operator fun invoke(userWalletId: UserWalletId): Either +} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index 4abde012d5..f31f23da03 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -23,6 +23,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.redux.LegacyAction import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase import com.tangem.domain.wallets.usecase.GenerateBuyTangemCardLinkUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -63,6 +64,7 @@ internal class DetailsModel @Inject constructor( private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val appStateHolder: ReduxStateHolder, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, + private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getWalletsUseCase: GetWalletsUseCase, override val dispatchers: CoroutineDispatcherProvider, @@ -134,14 +136,20 @@ internal class DetailsModel @Inject constructor( ?: error("Selected wallet is null") val metaInfo = getWalletMetaInfoUseCase(selectedUserWallet.walletId).getOrNull() ?: return@launch + val visaCustomerId = getTangemPayCustomerIdUseCase(selectedUserWallet.walletId).getOrNull() val feedbackType = when { - userWallets.all { it is UserWallet.Cold && it.scanResponse.card.isVisa } -> - FeedbackEmailType.Visa.DirectUserRequest(metaInfo) + userWallets.all { + it is UserWallet.Cold && it.scanResponse.card.isVisa && !visaCustomerId.isNullOrEmpty() + } -> + FeedbackEmailType.Visa.DirectUserRequest( + walletMetaInfo = metaInfo, + customerId = requireNotNull(visaCustomerId), + ) userWallets.all { it !is UserWallet.Cold || it.scanResponse.card.isVisa.not() } -> FeedbackEmailType.DirectUserRequest(metaInfo) else -> { - showFeedbackEmailTypeOptionBS(metaInfo) + showFeedbackEmailTypeOptionBS(selectedWalletMetaInfo = metaInfo, visaCustomerId = visaCustomerId) return@launch } } @@ -158,16 +166,16 @@ internal class DetailsModel @Inject constructor( } } - private fun showFeedbackEmailTypeOptionBS(selectedWalletMetaInfo: WalletMetaInfo) { - state.update { - it.copy( + private fun showFeedbackEmailTypeOptionBS(selectedWalletMetaInfo: WalletMetaInfo, visaCustomerId: String?) { + state.update { current -> + current.copy( selectFeedbackEmailTypeBSConfig = TangemBottomSheetConfig( isShown = true, onDismissRequest = { state.update { - it.copy( + current.copy( selectFeedbackEmailTypeBSConfig = - it.selectFeedbackEmailTypeBSConfig.copy(isShown = false), + current.selectFeedbackEmailTypeBSConfig.copy(isShown = false), ) } }, @@ -176,6 +184,7 @@ internal class DetailsModel @Inject constructor( onEmailFeedbackTypeOptionSelected( selectedWalletMetaInfo = selectedWalletMetaInfo, option = option, + visaCustomerId = visaCustomerId, ) state.update { @@ -194,6 +203,7 @@ internal class DetailsModel @Inject constructor( private fun onEmailFeedbackTypeOptionSelected( selectedWalletMetaInfo: WalletMetaInfo, option: SelectEmailFeedbackTypeBS.Option, + visaCustomerId: String?, ) { modelScope.launch { val feedbackType = when (option) { @@ -211,14 +221,15 @@ internal class DetailsModel @Inject constructor( } } SelectEmailFeedbackTypeBS.Option.Visa -> { - if (selectedWalletMetaInfo.isVisa == true) { - FeedbackEmailType.Visa.DirectUserRequest(selectedWalletMetaInfo) + if (selectedWalletMetaInfo.isVisa == true && !visaCustomerId.isNullOrEmpty()) { + FeedbackEmailType.Visa.DirectUserRequest(selectedWalletMetaInfo, visaCustomerId) } else { val userWallet = getWalletsUseCase.invokeSync() .firstOrNull { it is UserWallet.Cold && it.scanResponse.card.isVisa } ?: return@launch val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch - FeedbackEmailType.Visa.DirectUserRequest(metaInfo) + val customerId = getTangemPayCustomerIdUseCase(userWallet.walletId).getOrNull() ?: return@launch + FeedbackEmailType.Visa.DirectUserRequest(metaInfo, customerId) } } } diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt index af0fadb5fb..03d7c218c2 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/stepper/impl/DefaultOnboardingStepperComponent.kt @@ -13,6 +13,7 @@ import com.tangem.domain.card.common.TapWorkarounds.isVisa import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent import com.tangem.features.onboarding.v2.stepper.impl.ui.OnboardingStepper import dagger.assisted.Assisted @@ -27,6 +28,7 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor( private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val analyticsHandler: AnalyticsEventHandler, + private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase, ) : OnboardingStepperComponent, AppComponentContext by context { override val state = instanceKeeper.getOrCreateSimple { MutableStateFlow(params.initState) } @@ -41,11 +43,13 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor( componentScope.launch { val cardInfo = getWalletMetaInfoUseCase(params.scanResponse).getOrNull() ?: return@launch + val userWalletId = cardInfo.userWalletId ?: return@launch + val visaCustomerId = getTangemPayCustomerIdUseCase(userWalletId).getOrNull() sendFeedbackEmailUseCase( - if (params.scanResponse.card.isVisa) { - FeedbackEmailType.Visa.Activation(cardInfo) + if (params.scanResponse.card.isVisa && !visaCustomerId.isNullOrEmpty()) { + FeedbackEmailType.Visa.Activation(walletMetaInfo = cardInfo, customerId = visaCustomerId) } else { - FeedbackEmailType.DirectUserRequest(cardInfo) + FeedbackEmailType.DirectUserRequest(walletMetaInfo = cardInfo) }, ) } 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 b51473e5c1..104034768e 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 @@ -56,6 +56,7 @@ import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase +import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase @@ -156,6 +157,7 @@ internal class SwapModel @Inject constructor( private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, private val excludedBlockchains: ExcludedBlockchains, private val getUserWalletsUseCase: GetWalletsUseCase, + private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase, ) : Model() { private val params = paramsContainer.require() @@ -1995,16 +1997,20 @@ internal class SwapModel @Inject constructor( uiState = uiState, error = SwapTransactionState.Error.TangemPayWithdrawalError(txId.orEmpty()), onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - onSupportClick = ::onTangemPaySupportClick, + onSupportClick = { + val customerId = getTangemPayCustomerIdUseCase(userWallet.walletId).getOrNull() ?: "Unknown" + onTangemPaySupportClick(customerId = customerId, txId = txId) + }, isReverseSwapPossible = isReverseSwapPossible(), ) } - private fun onTangemPaySupportClick(txId: String?) { + private fun onTangemPaySupportClick(customerId: String, txId: String?) { modelScope.launch { val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch val email = FeedbackEmailType.Visa.Withdrawal( walletMetaInfo = metaInfo, + customerId = customerId, providerName = dataState.selectedProvider?.name.orEmpty(), txId = txId.orEmpty(), ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt index a4e44f025e..2b5736147e 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -105,6 +105,7 @@ internal class TangemPayDetailsComponent( transaction = navigation.transaction, isBalanceHidden = navigation.isBalanceHidden, userWalletId = params.userWalletId, + customerId = params.config.customerId, onDismiss = model.bottomSheetNavigation::dismiss, ), ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryDetailsComponent.kt index eeb2b77c8f..c2792e7e33 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/TangemPayTxHistoryDetailsComponent.kt @@ -32,6 +32,7 @@ internal class TangemPayTxHistoryDetailsComponent( val transaction: TangemPayTxHistoryItem, val isBalanceHidden: Boolean, val userWalletId: UserWalletId, + val customerId: String, val onDismiss: () -> Unit, ) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index d2f9137ae1..8dfb45c420 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -371,6 +371,7 @@ internal class TangemPayDetailsModel @Inject constructor( sendFeedbackEmailUseCase.invoke( type = FeedbackEmailType.Visa.FeatureIsBeta( walletMetaInfo = WalletMetaInfo(userWalletId = params.userWalletId), + customerId = params.config.customerId, ), ) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt index 1d6f1f82fa..4e815c1f1a 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryDetailsModel.kt @@ -40,7 +40,7 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor( item = params.transaction, isBalanceHidden = params.isBalanceHidden, onExplorerClick = ::openExplorer, - onDisputeClick = ::dispute, + onDisputeClick = { dispute(customerId = params.customerId) }, onDismiss = ::dismiss, ), ), @@ -64,7 +64,7 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor( txHash?.let(urlOpener::openUrlExternalBrowser) } - private fun dispute() { + private fun dispute(customerId: String) { analytics.send(TangemPayAnalyticsEvents.SupportOnTransactionPopupClicked()) modelScope.launch { val walletMetaInfo = getWalletMetaInfoUseCase.invoke(params.userWalletId).getOrNull() ?: return@launch @@ -73,6 +73,7 @@ internal class TangemPayTxHistoryDetailsModel @Inject constructor( FeedbackEmailType.Visa.DisputeV2( item = params.transaction, walletMetaInfo = walletMetaInfo, + customerId = customerId, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index 1318303b1a..1638e3ed00 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -51,9 +51,9 @@ internal interface TangemPayIntents { fun onIssuingCardClicked() - fun onIssuingFailedClicked() + fun onIssuingFailedClicked(customerId: String) - fun onPaySupportClick() + fun onPaySupportClick(customerId: String) fun onOnboardingBannerClick(userWalletId: UserWalletId) @@ -203,7 +203,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( uiMessageSender.send(issuingBottomSheet) } - override fun onIssuingFailedClicked() { + override fun onIssuingFailedClicked(customerId: String) { val issuingBottomSheet = bottomSheetMessage { infoBlock { icon(com.tangem.core.ui.R.drawable.ic_alert_24) { @@ -216,7 +216,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( secondaryButton { text = resourceReference(R.string.tangempay_go_to_support) onClick { - onPaySupportClick() + onPaySupportClick(customerId) closeBs() } } @@ -225,7 +225,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( uiMessageSender.send(issuingBottomSheet) } - override fun onPaySupportClick() { + override fun onPaySupportClick(customerId: String) { modelScope.launch { val cardInfo = getWalletMetainfoUseCase.invoke( userWalletId = stateHolder.getSelectedWalletId(), @@ -234,6 +234,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( sendFeedbackEmailUseCase( FeedbackEmailType.Visa.FailedIssueCard( walletMetaInfo = cardInfo, + customerId = customerId, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt index 9a9ce4feee..c995e3dd44 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt @@ -45,37 +45,42 @@ internal class TangemPayUpdateInfoStateTransformer( private fun createInitialState(): TangemPayState { val cardInfo = value.info.cardInfo val productInstance = value.info.productInstance - val customerId = value.info.customerId + val customerId = value.info.customerId ?: "Unknown" // when statement copied to WalletTangemPayAnalyticsEventSender. Be careful when editing. return when { - value.orderStatus == OrderStatus.CANCELED -> createCancelledState() - value.info.kycStatus != APPROVED && !customerId.isNullOrEmpty() -> + value.orderStatus == OrderStatus.CANCELED -> createCancelledState(customerId) + value.info.kycStatus != APPROVED && !value.info.customerId.isNullOrEmpty() -> createKycInProgressState(kycStatus = value.info.kycStatus, customerId = customerId) - cardInfo != null && productInstance != null -> getCardInfoState(cardInfo, productInstance) + cardInfo != null && productInstance != null -> + getCardInfoState(customerId, cardInfo, productInstance) else -> createIssueProgressState() } } - private fun getCardInfoState(cardInfo: CardInfo, productInstance: ProductInstance): TangemPayState = - TangemPayState.Card( - lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"), - balanceText = TextReference.Str(getBalanceText(cardInfo)), - balanceSymbol = stringReference("USDC"), // TODO hardcode for now - onClick = { - tangemPayClickIntents.openDetails( - userWalletId, - TangemPayDetailsConfig( - cardId = productInstance.cardId, - isPinSet = cardInfo.isPinSet, - cardFrozenState = cardFrozenState, - customerWalletAddress = cardInfo.customerWalletAddress, - cardNumberEnd = cardInfo.lastFourDigits, - chainId = POLYGON_CHAIN_ID, - ), - ) - }, - ) + private fun getCardInfoState( + customerId: String, + cardInfo: CardInfo, + productInstance: ProductInstance, + ): TangemPayState = TangemPayState.Card( + lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"), + balanceText = TextReference.Str(getBalanceText(cardInfo)), + balanceSymbol = stringReference("USDC"), // TODO hardcode for now + onClick = { + tangemPayClickIntents.openDetails( + userWalletId, + TangemPayDetailsConfig( + customerId = customerId, + cardId = productInstance.cardId, + isPinSet = cardInfo.isPinSet, + cardFrozenState = cardFrozenState, + customerWalletAddress = cardInfo.customerWalletAddress, + cardNumberEnd = cardInfo.lastFourDigits, + chainId = POLYGON_CHAIN_ID, + ), + ) + }, + ) private fun getBalanceText(cardInfo: CardInfo): String { val currency = Currency.getInstance(cardInfo.currencyCode) @@ -113,10 +118,10 @@ internal class TangemPayUpdateInfoStateTransformer( showProgress = true, ) - private fun createCancelledState(): TangemPayState = TangemPayState.FailedIssue( + private fun createCancelledState(customerId: String): TangemPayState = TangemPayState.FailedIssue( title = TextReference.Res(R.string.tangempay_payment_account), description = TextReference.Res(R.string.tangempay_failed_to_issue_card), iconRes = R.drawable.ic_alert_24, - onButtonClick = tangemPayClickIntents::onIssuingFailedClicked, + onButtonClick = { tangemPayClickIntents.onIssuingFailedClicked(customerId) }, ) } \ No newline at end of file From 6ed3317da484cf309feda33f8e4ae761c301f277 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Feb 2026 15:30:36 +0300 Subject: [PATCH 25/33] Updated on 2026-08-14 --- .../tangem/data/managetokens/DefaultCustomTokensRepository.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 3d74578599..1570f9a7c7 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -57,6 +57,8 @@ internal class DefaultCustomTokensRepository( Blockchain.Binance, Blockchain.BinanceTestnet, Blockchain.Kaspa, + Blockchain.TerraV1, + Blockchain.TerraV2, -> true Blockchain.Cardano, Blockchain.Sui, From e4b1bf4e61705f351e928f72c1613303b491eb21 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 10:58:30 +0300 Subject: [PATCH 26/33] Updated on 2026-08-14 --- .../ethereum/WcEthAddNetworkUseCase.kt | 8 ++++-- .../network/ethereum/WcEthNetwork.kt | 10 ++++++-- .../network/solana/WcSolanaNetwork.kt | 6 +++-- .../utils/WcNetworksConverter.kt | 25 +++++++++++++------ 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt index bf9f6674f9..d835bc5139 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt @@ -69,7 +69,7 @@ internal class WcEthAddNetworkUseCase @AssistedInject constructor( ?: return illegalState() // find and add all derivation val networkToAddCAIP10 = networksConverter - .allAddressForChain(networkToAddCAIP2.raw, wallet) + .allAddressForChain(networkToAddCAIP2.raw, wallet, session.account) .map { address -> CAIP10(networkToAddCAIP2, address).raw } val newNamespaces = namespaces.copy( chains = namespaces.chains.plus(networkToAddCAIP2.raw), @@ -134,7 +134,11 @@ internal class WcEthAddSwitchCommonDelegate @AssistedInject constructor( if (generalNetwork == null) { return HandleMethodError.TangemUnsupportedNetwork(caip2.raw).left() } - val addedNetwork = networksConverter.mainOrAnyWalletNetworkForRequest(caip2.raw, wallet) + val addedNetwork = networksConverter.mainOrAnyWalletNetworkForRequest( + rawChainId = caip2.raw, + wallet = wallet, + account = context.session.account, + ) if (addedNetwork == null) { return HandleMethodError.NotAddedNetwork(generalNetwork.name).left() } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index b713e9f18b..c03d165fa0 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -39,12 +39,17 @@ internal class WcEthNetwork( val name = toWcMethodName(request) ?: return error("Unknown method name") val session = sessionsManager.findSessionByTopic(request.topic) ?: return HandleMethodError.UnknownSession.left() + val account = session.account val wallet = session.wallet val chainId = request.chainId.orEmpty() val method: WcEthMethod = name.toMethod(request) .getOrElse { return error(it.message.orEmpty()) } ?: return error("Failed to parse $name") - suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet) + suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest( + rawChainId = chainId, + wallet = wallet, + account = account, + ) val accountAddress = when (method) { is WcEthMethod.MessageSign -> method.account @@ -69,12 +74,13 @@ internal class WcEthNetwork( -> anyExistNetwork() } ?: return error("Failed to find walletNetwork for accountAddress $accountAddress") + val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet, account).size val context = WcMethodUseCaseContext( session = session, rawSdkRequest = request, network = walletNetwork, accountAddress = accountAddress, - networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet).size, + networkDerivationsCount = networkDerivationsCount, ) return when (method) { is WcEthMethod.MessageSign -> factories.messageSign.create(context, method) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt index db6f83849d..bb68bd46d2 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt @@ -47,8 +47,9 @@ internal class WcSolanaNetwork( val session = sessionsManager.findSessionByTopic(request.topic) ?: return HandleMethodError.UnknownSession.left() val wallet = session.wallet + val account = session.account val chainId = request.chainId.orEmpty() - suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet) + suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet, account) suspend fun anyAddress() = anyExistNetwork() ?.let { network -> networksConverter.getAddressForWC(wallet.walletId, network).orEmpty() } .orEmpty() @@ -63,12 +64,13 @@ internal class WcSolanaNetwork( ?: anyExistNetwork() ?: return error("Failed to find walletNetwork for accountAddress $accountAddress") + val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet, account).size val context = WcMethodUseCaseContext( session = session, rawSdkRequest = request, network = walletNetwork, accountAddress = accountAddress, - networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet).size, + networkDerivationsCount = networkDerivationsCount, ) return when (method) { is WcSolanaMethod.SignMessage -> factories.messageSign.create(context, method) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index a494eb772e..82a5b0beb2 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -44,7 +44,11 @@ internal class WcNetworksConverter @Inject constructor( requestAddress: String, ): Network? { val wallet = session.wallet - val allCoinNetwork = filterWalletNetworkForRequest(request.chainId.orEmpty(), session.wallet) + val allCoinNetwork = filterWalletNetworkForRequest( + rawChainId = request.chainId.orEmpty(), + wallet = session.wallet, + account = session.account, + ) val requestNetwork = allCoinNetwork.find { network -> val address = getAddressForWC(wallet.walletId, network) @@ -56,13 +60,13 @@ internal class WcNetworksConverter @Inject constructor( /** * return network with not custom derivationPath or first custom or any */ - suspend fun mainOrAnyWalletNetworkForRequest(rawChainId: String, wallet: UserWallet): Network? { - val networks = filterWalletNetworkForRequest(rawChainId, wallet) + suspend fun mainOrAnyWalletNetworkForRequest(rawChainId: String, wallet: UserWallet, account: Account?): Network? { + val networks = filterWalletNetworkForRequest(rawChainId, wallet, account) return networks.firstOrNull { !isCustomCoin(it) } ?: networks.firstOrNull() } - suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet): List { - return filterWalletNetworkForRequest(rawChainId, wallet) + suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet, account: Account?): List { + return filterWalletNetworkForRequest(rawChainId, wallet, account) .mapNotNull { getAddressForWC(wallet.walletId, it)?.lowercase() } } @@ -80,13 +84,18 @@ internal class WcNetworksConverter @Inject constructor( /** * return all exist derivation networks */ - suspend fun filterWalletNetworkForRequest(rawChainId: String, wallet: UserWallet): List { - val walletNetworks = getWalletNetworks(wallet.walletId) + suspend fun filterWalletNetworkForRequest( + rawChainId: String, + wallet: UserWallet, + account: Account?, + ): List { + val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) } + ?: getWalletNetworks(wallet.walletId) val blockchain = namespaceConverters .firstNotNullOfOrNull { it.toBlockchain(rawChainId) } ?: return listOf() - val allCoinNetwork = walletNetworks.filter { it.rawId == blockchain.id } + val allCoinNetwork = portfolioNetworks.filter { it.rawId == blockchain.id } return allCoinNetwork } From 64ba8f017061f20e9b8a76097207b6259334fdec Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 10:57:04 +0300 Subject: [PATCH 27/33] Updated on 2026-08-14 --- .../api/subcomponents/SwapAmountUpdateTrigger.kt | 2 ++ .../impl/amount/DefaultSwapAmountUpdateTrigger.kt | 9 +++++++++ .../swap/v2/impl/amount/model/SwapAmountModel.kt | 14 ++++++++++++++ .../confirm/SendWithSwapConfirmComponent.kt | 1 + .../confirm/model/SendWithSwapConfirmModel.kt | 8 ++++++++ 5 files changed, 34 insertions(+) diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/subcomponents/SwapAmountUpdateTrigger.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/subcomponents/SwapAmountUpdateTrigger.kt index 7c9a0f4b91..de3c1fa9f2 100644 --- a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/subcomponents/SwapAmountUpdateTrigger.kt +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/subcomponents/SwapAmountUpdateTrigger.kt @@ -8,4 +8,6 @@ interface SwapAmountUpdateTrigger { suspend fun triggerUpdateAmount(amountValue: String, isEnterInFiatSelected: Boolean) suspend fun triggerQuoteReload() + + suspend fun triggerAutoUpdateEnabled(isEnabled: Boolean) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/DefaultSwapAmountUpdateTrigger.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/DefaultSwapAmountUpdateTrigger.kt index 29162606e3..168c040b92 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/DefaultSwapAmountUpdateTrigger.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/DefaultSwapAmountUpdateTrigger.kt @@ -17,6 +17,8 @@ interface SwapAmountUpdateListener { val updateAmountTriggerFlow: Flow> val reloadQuotesTriggerFlow: Flow + + val autoUpdateTriggerFlow: Flow } /** @@ -59,6 +61,9 @@ internal class DefaultSwapAmountUpdateTrigger @Inject constructor() : override val reloadQuotesTriggerFlow: Flow field = MutableSharedFlow() + override val autoUpdateTriggerFlow: Flow + field = MutableSharedFlow() + override suspend fun triggerUpdateAmount(amountValue: String, isEnterInFiatSelected: Boolean) { updateAmountTriggerFlow.emit(amountValue to isEnterInFiatSelected) } @@ -67,6 +72,10 @@ internal class DefaultSwapAmountUpdateTrigger @Inject constructor() : reloadQuotesTriggerFlow.emit(Unit) } + override suspend fun triggerAutoUpdateEnabled(isEnabled: Boolean) { + autoUpdateTriggerFlow.emit(isEnabled) + } + override suspend fun triggerReduceBy(reduceBy: ReduceByData) { reduceByTriggerFlow.emit(reduceBy) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 8546a9c6e2..b406f013fb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -132,6 +132,7 @@ internal class SwapAmountModel @Inject constructor( subscribeOnAmountIgnoreReduceTriggerUpdates() subscribeOnReloadQuotesTriggerUpdates() subscribeOnBalanceHiddenUpdates() + subscribeOnAutoupdateEnabling() } fun onStart() { @@ -482,6 +483,19 @@ internal class SwapAmountModel @Inject constructor( .launchIn(modelScope) } + private fun subscribeOnAutoupdateEnabling() { + swapAmountUpdateListener.autoUpdateTriggerFlow + .distinctUntilChanged() + .onEach { isEnabled -> + if (isEnabled) { + startLoadingQuotesTask(isSilentReload = true) + } else { + quoteTaskScheduler.cancelTask() + } + } + .launchIn(modelScope) + } + private fun observeChooseSelectToken() { swapChooseTokenNetworkListener.swapChooseTokenNetworkResultFlow .onEach { data -> diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 579a4ddafc..9bae584628 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -98,6 +98,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( analyticsCategoryName = params.analyticsCategoryName, analyticsSendSource = params.analyticsSendSource, userWalletId = params.userWallet.walletId, + bottomSheetShown = model::onFeeBottomSheetShown, ), onResult = model::onFeeResult, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 2923283a79..90526afb56 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -45,6 +45,7 @@ import com.tangem.features.send.v2.api.subcomponents.destination.entity.Destinat import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateListener import com.tangem.features.send.v2.api.subcomponents.notifications.SendNotificationsUpdateTrigger +import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.SwapAmountReduceTrigger import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM @@ -90,6 +91,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( private val sendNotificationsUpdateListener: SendNotificationsUpdateListener, private val swapNotificationsUpdateListener: SwapNotificationsUpdateListener, private val swapAmountReduceTrigger: SwapAmountReduceTrigger, + private val swapAmountUpdateTrigger: SwapAmountUpdateTrigger, private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, private val swapAlertFactory: SwapAlertFactory, private val appRouter: AppRouter, @@ -211,6 +213,12 @@ internal class SendWithSwapConfirmModel @Inject constructor( } } + fun onFeeBottomSheetShown(isShown: Boolean) { + modelScope.launch { + swapAmountUpdateTrigger.triggerAutoUpdateEnabled(isEnabled = !isShown) + } + } + fun showEditAmount() { analyticsEventHandler.send( CommonSendAnalyticEvents.ScreenReopened( From 13735f1125a58d5cbbbfaa4ea7b5980e7dda9a6f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 12:02:33 +0300 Subject: [PATCH 28/33] 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 d1e7a735d9..2318c92aa6 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.33-1407" +tangemBlockchainSdk = "releases-5.33-1413" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "releases-5.33-576" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 2957d6bdad1b8145b8719283f49a90686ff33bb7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 12:02:57 +0300 Subject: [PATCH 29/33] Updated on 2026-08-14 --- .../com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt index 1da4a1c296..3ae84d85b1 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscode/AccessCodeModel.kt @@ -248,7 +248,7 @@ internal class AccessCodeModel @Inject constructor( auth = HotAuth.Password(accessCode.toCharArray()), ) - if (walletsRepository.requireAccessCode().not()) { + if (walletsRepository.requireAccessCode().not() && canUseBiometryUseCase()) { updatedHotWalletId = tangemHotSdk.changeAuth( unlockHotWallet = unlockHotWallet, auth = HotAuth.Biometry, From fcc17181ad746c0a9d730a74fb19c1956045d8de Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 11:54:56 +0300 Subject: [PATCH 30/33] Updated on 2026-08-14 --- .../features/swap/v2/impl/amount/model/SwapAmountModel.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index b406f013fb..75b3107ff5 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -58,6 +58,7 @@ import com.tangem.utils.coroutines.SingleTaskScheduler import com.tangem.utils.extensions.orZero import com.tangem.utils.isNullOrZero import com.tangem.utils.transformer.update +import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* @@ -110,6 +111,8 @@ internal class SwapAmountModel @Inject constructor( private var isShowBestRateAnimation: Boolean = false + private var autoUpdateSubscriberJob: Job? = null + val uiState: StateFlow field = MutableStateFlow(params.amountUM) @@ -132,7 +135,6 @@ internal class SwapAmountModel @Inject constructor( subscribeOnAmountIgnoreReduceTriggerUpdates() subscribeOnReloadQuotesTriggerUpdates() subscribeOnBalanceHiddenUpdates() - subscribeOnAutoupdateEnabling() } fun onStart() { @@ -140,10 +142,12 @@ internal class SwapAmountModel @Inject constructor( scope = modelScope, task = loadQuotesTask(), ) + subscribeOnAutoupdateEnabling() } fun onStop() { quoteTaskScheduler.cancelTask() + autoUpdateSubscriberJob?.cancel() } override fun onDestroy() { @@ -484,7 +488,7 @@ internal class SwapAmountModel @Inject constructor( } private fun subscribeOnAutoupdateEnabling() { - swapAmountUpdateListener.autoUpdateTriggerFlow + autoUpdateSubscriberJob = swapAmountUpdateListener.autoUpdateTriggerFlow .distinctUntilChanged() .onEach { isEnabled -> if (isEnabled) { From 5b3b19f3ee8daf2c05577f0ee290fbaad94147f9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 5 Feb 2026 17:35:20 +0300 Subject: [PATCH 31/33] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 1 + .../com/tangem/common/routing/AppRoute.kt | 1 + .../core/analytics/models/AnalyticsParam.kt | 1 + .../AccountSettingsAnalyticEvents.kt | 60 ++++++++++++++++--- .../WalletSettingsAccountAnalyticEvents.kt | 17 +++++- .../archived/ArchivedAccountListModel.kt | 21 ++++--- .../createedit/AccountCreateEditModel.kt | 27 +++++---- .../account/details/AccountDetailsModel.kt | 20 +++++-- .../component/ManageTokensSource.kt | 3 +- .../analytics/SendWithSwapAnalyticEvents.kt | 3 +- .../feature/swap/analytics/SwapEvents.kt | 6 +- 11 files changed, 123 insertions(+), 37 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 cb500685e1..95d4585b71 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 @@ -147,6 +147,7 @@ internal class ChildFactory @Inject constructor( val source = when (route.source) { AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES + AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT } val mode = when (val portfolio = route.portfolioId) { 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 4b9bd5496d..a514a89100 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 @@ -138,6 +138,7 @@ sealed class AppRoute(val path: String) : Route { enum class Source { STORIES, SETTINGS, + ACCOUNT, } } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 986d808190..70e26afdb7 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -284,6 +284,7 @@ sealed class AnalyticsParam { const val ENS = "ENS" const val ENS_ADDRESS = "ENS Address" const val ACCOUNT_DERIVATION_FROM = "Account Derivation (from)" + const val ACCOUNT_DERIVATION_TO = "Account Derivation (to)" const val FEE_TOKEN = "Fee Token" const val ACCOUNT_DERIVATION = "Account Derivation" } diff --git a/features/account/impl/src/main/java/com/tangem/features/account/analytics/AccountSettingsAnalyticEvents.kt b/features/account/impl/src/main/java/com/tangem/features/account/analytics/AccountSettingsAnalyticEvents.kt index 02d6dd5ae5..a1873a0097 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/analytics/AccountSettingsAnalyticEvents.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/analytics/AccountSettingsAnalyticEvents.kt @@ -1,6 +1,7 @@ package com.tangem.features.account.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.features.account.AccountCreateEditComponent @@ -15,20 +16,48 @@ sealed class AccountSettingsAnalyticEvents( event = "Account Settings Screen Opened", ) - class ButtonManageTokens : AccountSettingsAnalyticEvents( + class ButtonManageTokens( + accountDerivation: Int?, + ) : AccountSettingsAnalyticEvents( event = "Button - Manage Tokens", + params = buildMap { + accountDerivation?.let { + put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) + } + }, ) - class ButtonArchiveAccount : AccountSettingsAnalyticEvents( + class ButtonArchiveAccount( + accountDerivation: Int?, + ) : AccountSettingsAnalyticEvents( event = "Button - Archive Account", + params = buildMap { + accountDerivation?.let { + put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) + } + }, ) - class ButtonArchiveAccountConfirmation : AccountSettingsAnalyticEvents( + class ButtonArchiveAccountConfirmation( + accountDerivation: Int?, + ) : AccountSettingsAnalyticEvents( event = "Button - Archive Account Confirmation", + params = buildMap { + accountDerivation?.let { + put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) + } + }, ) - class ButtonCancelAccountArchivation : AccountSettingsAnalyticEvents( + class ButtonCancelAccountArchivation( + accountDerivation: Int?, + ) : AccountSettingsAnalyticEvents( event = "Button - Cancel Account Archivation", + params = buildMap { + accountDerivation?.let { + put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) + } + }, ) class AccountArchived : AccountSettingsAnalyticEvents( @@ -39,13 +68,21 @@ sealed class AccountSettingsAnalyticEvents( event = "Button - Edit", ) - class AccountEditScreenOpened : AccountSettingsAnalyticEvents( + class AccountEditScreenOpened( + accountDerivation: Int?, + ) : AccountSettingsAnalyticEvents( event = "Account Edit Screen Opened", + params = buildMap { + accountDerivation?.let { + put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) + } + }, ) class ButtonSave( val name: AccountName, val icon: CryptoPortfolioIcon, + accountDerivation: Int?, ) : AccountSettingsAnalyticEvents( event = "Button - Save", params = buildMap { @@ -56,13 +93,16 @@ sealed class AccountSettingsAnalyticEvents( put("Name", accountName) put("Color", icon.color.name) put("Icon", icon.value.name) + accountDerivation?.let { + put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) + } }, ) class ButtonAddNewAccount( val name: AccountName, val icon: CryptoPortfolioIcon, - val derivationIndex: Int, + accountDerivation: Int?, ) : AccountSettingsAnalyticEvents( event = "Button - Add New Account", params = buildMap { @@ -73,18 +113,24 @@ sealed class AccountSettingsAnalyticEvents( put("Name", accountName) put("Color", icon.color.name) put("Icon", icon.value.name) - put("Derivation", derivationIndex.toString()) + accountDerivation?.let { + put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) + } }, ) class AccountError( val source: Source, val error: String, + accountDerivation: Int?, ) : AccountSettingsAnalyticEvents( event = "Account Error", params = buildMap { put("Error", error) put("Source", source.value) + accountDerivation?.let { + put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) + } }, ) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/analytics/WalletSettingsAccountAnalyticEvents.kt b/features/account/impl/src/main/java/com/tangem/features/account/analytics/WalletSettingsAccountAnalyticEvents.kt index 165fe01945..a2d4460b74 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/analytics/WalletSettingsAccountAnalyticEvents.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/analytics/WalletSettingsAccountAnalyticEvents.kt @@ -1,6 +1,7 @@ package com.tangem.features.account.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam sealed class WalletSettingsAccountAnalyticEvents( category: String = "Settings / Wallet Settings", @@ -16,11 +17,23 @@ sealed class WalletSettingsAccountAnalyticEvents( event = "Account Recovered", ) - class ArchivedAccountsScreenOpened : WalletSettingsAccountAnalyticEvents( + class ArchivedAccountsScreenOpened( + private val accountsCount: Int, + ) : WalletSettingsAccountAnalyticEvents( event = "Archived Accounts Screen Opened", + params = buildMap { + put("Accounts Count", accountsCount.toString()) + }, ) - class ButtonRecoverAccount : WalletSettingsAccountAnalyticEvents( + class ButtonRecoverAccount( + accountDerivation: Int?, + ) : WalletSettingsAccountAnalyticEvents( event = "Button - Recover Account", + params = buildMap { + accountDerivation?.let { + put(AnalyticsParam.ACCOUNT_DERIVATION, it.toString()) + } + }, ) } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt index 6b888f2693..cde8aabbcb 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/archived/ArchivedAccountListModel.kt @@ -13,9 +13,9 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.models.ArchivedAccount import com.tangem.domain.account.status.usecase.RecoverCryptoPortfolioUseCase import com.tangem.domain.account.usecase.GetArchivedAccountsUseCase -import com.tangem.domain.models.account.AccountId import com.tangem.features.account.ArchivedAccountListComponent import com.tangem.features.account.analytics.WalletSettingsAccountAnalyticEvents import com.tangem.features.account.archived.entity.AccountArchivedUM @@ -53,7 +53,6 @@ internal class ArchivedAccountListModel @Inject constructor( private val getArchivedAccountsJob = JobHolder() init { - analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ArchivedAccountsScreenOpened()) getArchivedAccounts() } @@ -69,14 +68,16 @@ internal class ArchivedAccountListModel @Inject constructor( umBuilder.mapContent( accounts = content, onCloseClick = onCloseClick, - onRecoverClick = { recoverCryptoPortfolio(accountId = it.accountId) }, + onRecoverClick = { recoverCryptoPortfolio(account = it) }, ) }, ifContent = { content -> + val event = WalletSettingsAccountAnalyticEvents.ArchivedAccountsScreenOpened(content.size) + analyticsEventHandler.send(event) umBuilder.mapContent( accounts = content, onCloseClick = onCloseClick, - onRecoverClick = { recoverCryptoPortfolio(accountId = it.accountId) }, + onRecoverClick = { recoverCryptoPortfolio(account = it) }, ) }, ifError = { error -> @@ -97,13 +98,15 @@ internal class ArchivedAccountListModel @Inject constructor( .saveIn(getArchivedAccountsJob) } - private fun recoverCryptoPortfolio(accountId: AccountId) = modelScope.launch { - analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.ButtonRecoverAccount()) - uiState.update { it.toggleProgress(accountId, isLoading = true) } + private fun recoverCryptoPortfolio(account: ArchivedAccount) = modelScope.launch { + analyticsEventHandler.send( + WalletSettingsAccountAnalyticEvents.ButtonRecoverAccount(account.derivationIndex.value), + ) + uiState.update { it.toggleProgress(account.accountId, isLoading = true) } val result = withContext(dispatchers.default) { - recoverCryptoPortfolioUseCase(accountId) + recoverCryptoPortfolioUseCase(account.accountId) } - uiState.update { it.toggleProgress(accountId, isLoading = false) } + uiState.update { it.toggleProgress(account.accountId, isLoading = false) } result .onLeft(::handleRecoverError) .onRight { diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index ebb0f334f3..d100998aca 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -25,6 +25,7 @@ import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents @@ -69,10 +70,13 @@ internal class AccountCreateEditModel @Inject constructor( field = MutableStateFlow(value = getInitialState()) init { - if (params is AccountCreateEditComponent.Params.Create) { - updateDerivationInfo(userWalletId = params.userWalletId) - } else { - analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountEditScreenOpened()) + when (params) { + is AccountCreateEditComponent.Params.Create -> updateDerivationInfo(userWalletId = params.userWalletId) + is AccountCreateEditComponent.Params.Edit -> { + val derivationIndex = params.account.derivationIndex?.value + val event = AccountSettingsAnalyticEvents.AccountEditScreenOpened(derivationIndex) + analyticsEventHandler.send(event) + } } } @@ -116,7 +120,7 @@ internal class AccountCreateEditModel @Inject constructor( val event = AccountSettingsAnalyticEvents.ButtonAddNewAccount( name = name, icon = icon, - derivationIndex = derivationIndex.value, + accountDerivation = derivationIndex.value, ) analyticsEventHandler.send(event) @@ -130,7 +134,7 @@ internal class AccountCreateEditModel @Inject constructor( uiState.value = uiState.value.toggleProgress(showProgress = false) result - .onLeft(::handleAddAccountError) + .onLeft { error -> handleAddAccountError(error, derivationIndex.value) } .onRight { analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountCreated()) showMessage(R.string.account_create_success_message) @@ -138,9 +142,10 @@ internal class AccountCreateEditModel @Inject constructor( } } - private fun handleAddAccountError(error: AddCryptoPortfolioUseCase.Error) { + private fun handleAddAccountError(error: AddCryptoPortfolioUseCase.Error, derivationIndex: Int) { val event = AccountSettingsAnalyticEvents.AccountError( source = params.toAnalyticSource(), + accountDerivation = derivationIndex, error = when (error) { is AddCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag is AddCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty() @@ -165,7 +170,8 @@ internal class AccountCreateEditModel @Inject constructor( val icon = CryptoPortfolioIconConverter.convertBack(state.account.portfolioIcon) val isNewName = name != params.account.accountName val isNewIcon = icon != params.account.portfolioIcon - analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon)) + val derivationIndex = params.account.derivationIndex?.value + analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon, derivationIndex)) uiState.value = uiState.value.toggleProgress(showProgress = true) val result = updateCryptoPortfolioUseCase( @@ -176,16 +182,17 @@ internal class AccountCreateEditModel @Inject constructor( uiState.value = uiState.value.toggleProgress(showProgress = false) result - .onLeft(::handleEditAccountError) + .onLeft { error -> handleEditAccountError(error, params.account.derivationIndex?.value) } .onRight { showMessage(R.string.account_edit_success_message) router.pop() } } - private fun handleEditAccountError(error: UpdateCryptoPortfolioUseCase.Error) { + private fun handleEditAccountError(error: UpdateCryptoPortfolioUseCase.Error, derivationIndex: Int?) { val event = AccountSettingsAnalyticEvents.AccountError( source = params.toAnalyticSource(), + accountDerivation = derivationIndex, error = when (error) { is UpdateCryptoPortfolioUseCase.Error.AccountListRequirementsNotMet -> error.cause.tag is UpdateCryptoPortfolioUseCase.Error.DataOperationFailed -> error.cause.message.orEmpty() diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt index c4b4297c35..47b06df25c 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt @@ -19,6 +19,7 @@ import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.account.AccountDetailsComponent @@ -65,15 +66,19 @@ internal class AccountDetailsModel @Inject constructor( private fun onManageTokensClick(account: Account) { val route = AppRoute.ManageTokens( - source = AppRoute.ManageTokens.Source.SETTINGS, + source = AppRoute.ManageTokens.Source.ACCOUNT, portfolioId = PortfolioId(account.accountId), ) - analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonManageTokens()) + analyticsEventHandler.send( + AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex?.value), + ) router.push(route) } private fun onArchiveAccountClick() { - analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccount()) + val accountDerivation = params.account.derivationIndex?.value + val event = AccountSettingsAnalyticEvents.ButtonArchiveAccount(accountDerivation) + analyticsEventHandler.send(event) confirmArchiveDialog() } @@ -81,7 +86,9 @@ internal class AccountDetailsModel @Inject constructor( val secondAction = EventMessageAction( title = resourceReference(R.string.common_cancel), onClick = { - analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation()) + val accountDerivation = params.account.derivationIndex?.value + val event = AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation(accountDerivation) + analyticsEventHandler.send(event) }, ) val firstAction = EventMessageAction( @@ -100,7 +107,9 @@ internal class AccountDetailsModel @Inject constructor( } private fun archiveCryptoPortfolio() = modelScope.launch { - analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation()) + val accountDerivation = params.account.derivationIndex?.value + val event = AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation(accountDerivation) + analyticsEventHandler.send(event) uiState.update { it.toggleProgress(true) } archiveCryptoPortfolioUseCase(accountId) .onLeft { error -> @@ -119,6 +128,7 @@ internal class AccountDetailsModel @Inject constructor( val event = AccountSettingsAnalyticEvents.AccountError( source = AccountSettingsAnalyticEvents.Source.ARCHIVE, error = error.tag, + accountDerivation = params.account.derivationIndex?.value, ) analyticsEventHandler.send(event) val titleRes: Int diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt index 3c5fa79e29..ca4d8956b2 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt @@ -6,7 +6,8 @@ import com.tangem.domain.models.wallet.UserWalletId enum class ManageTokensSource(val analyticsName: String) { STORIES(analyticsName = "Stories"), ONBOARDING(analyticsName = "Onboarding"), - SETTINGS(analyticsName = "Settings"), + SETTINGS(analyticsName = "Wallet Settings"), + ACCOUNT(analyticsName = "Account"), SEND_VIA_SWAP(analyticsName = "SendViaSwap"), } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt index 8aca31698c..213c7d7b63 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -2,6 +2,7 @@ package com.tangem.features.swap.v2.impl.sendviaswap.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE import com.tangem.core.analytics.models.AnalyticsParam.Key.PROVIDER import com.tangem.core.analytics.models.AnalyticsParam.Key.RECEIVE_BLOCKCHAIN @@ -32,7 +33,7 @@ internal sealed class SendWithSwapAnalyticEvents( put(RECEIVE_TOKEN, toToken.symbol) put(SEND_BLOCKCHAIN, fromToken.network.name) put(RECEIVE_BLOCKCHAIN, toToken.network.name) - if (fromDerivationIndex != null) put("Account Derivation (from)", fromDerivationIndex.toString()) + if (fromDerivationIndex != null) put(ACCOUNT_DERIVATION_FROM, fromDerivationIndex.toString()) }, ), AppsFlyerIncludedEvent diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 408988e88e..ebcb1465a8 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -2,6 +2,8 @@ package com.tangem.feature.swap.analytics import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_FROM +import com.tangem.core.analytics.models.AnalyticsParam.Key.ACCOUNT_DERIVATION_TO 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_MESSAGE @@ -105,8 +107,8 @@ sealed class SwapEvents( put("Receive Token", receiveToken) put("Send Blockchain", sendBlockchain) put("Receive Blockchain", receiveBlockchain) - if (fromDerivationIndex != null) put("Account Derivation (from)", fromDerivationIndex.toString()) - if (toDerivationIndex != null) put("Account Derivation (to)", toDerivationIndex.toString()) + if (fromDerivationIndex != null) put(ACCOUNT_DERIVATION_FROM, fromDerivationIndex.toString()) + if (toDerivationIndex != null) put(ACCOUNT_DERIVATION_TO, toDerivationIndex.toString()) put(FEE_TOKEN, feeToken) }, ), AppsFlyerIncludedEvent From 1bfe32c03b0930378a98633260fbf95d75da3e7d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 09:14:47 +0000 Subject: [PATCH 32/33] 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 d7281ac75f..2318c92aa6 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.32-1404" +tangemBlockchainSdk = "releases-5.33-1413" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.32-574" +tangemCardSdk = "releases-5.33-576" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 57f3adf94effefdd4a329175fb9c471245a61aba Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 09:34:17 +0000 Subject: [PATCH 33/33] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 2318c92aa6..1cc9fdda26 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,13 +5,13 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.33-1413" +tangemBlockchainSdk = "develop-1419" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.33-576" +tangemCardSdk = "develop-577" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-541" +tangemHotSdk = "develop-539" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^