From 7231a5dd40b494c2237cda7996e9203eef50976f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Feb 2026 14:31:51 +0300 Subject: [PATCH 01/20] Updated on 2026-08-14 --- .../utils/TokenListAnalyticsSender.kt | 13 +++-- .../MultiWalletContentLoaderV2.kt | 2 + .../TokenListAnalyticsSubscriber.kt | 49 +++++++++++++++++++ 3 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListAnalyticsSubscriber.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index 5cfa978a2a..d4d08e2e70 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -48,7 +48,8 @@ internal class TokenListAnalyticsSender @Inject constructor( if (screenLifecycleProvider.isBackgroundState.value) return if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return - if (totalFiatBalance is TotalFiatBalance.Loading) { + val isFlickering = totalFiatBalance is TotalFiatBalance.Loaded && totalFiatBalance.source == StatusSource.CACHE + if (totalFiatBalance is TotalFiatBalance.Loading || isFlickering) { startLoadingTraceIfNeeded(userWallet.walletId, flattenCurrencies) return } @@ -57,12 +58,10 @@ internal class TokenListAnalyticsSender @Inject constructor( stopLoadingTraceIfNeeded(userWallet.walletId, totalFiatBalance) } - val currenciesStatuses = flattenCurrencies - - sendBalanceLoadedEventIfNeeded(totalFiatBalance, currenciesStatuses) - sendToppedUpEventIfNeeded(userWallet, totalFiatBalance, currenciesStatuses) - sendUnreachableNetworksEventIfNeeded(currenciesStatuses) - sendTokenBalancesIfNeeded(currenciesStatuses) + sendBalanceLoadedEventIfNeeded(totalFiatBalance, flattenCurrencies) + sendToppedUpEventIfNeeded(userWallet, totalFiatBalance, flattenCurrencies) + sendUnreachableNetworksEventIfNeeded(flattenCurrencies) + sendTokenBalancesIfNeeded(flattenCurrencies) } private suspend fun startLoadingTraceIfNeeded( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt index df43ae0292..bced5a1f46 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt @@ -19,6 +19,7 @@ import dagger.assisted.AssistedInject internal class MultiWalletContentLoaderV2 @AssistedInject constructor( @Assisted private val userWallet: UserWallet, private val accountListSubscriberFactory: AccountListSubscriber.Factory, + private val tokenListAnalyticsSubscriberFactory: TokenListAnalyticsSubscriber.Factory, private val walletNFTListSubscriberV2Factory: WalletNFTListSubscriberV2.Factory, private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, @@ -35,6 +36,7 @@ internal class MultiWalletContentLoaderV2 @AssistedInject constructor( override fun create(): List = listOfNotNull( accountListSubscriberFactory.create(userWallet = userWallet), + tokenListAnalyticsSubscriberFactory.create(userWallet = userWallet), walletNFTListSubscriberV2Factory.create(userWallet = userWallet), checkWalletWithFundsSubscriberFactory.create(userWallet = userWallet), MultiWalletWarningsSubscriber( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListAnalyticsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListAnalyticsSubscriber.kt new file mode 100644 index 0000000000..7fb410838e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenListAnalyticsSubscriber.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach + +/** + * Subscriber that monitors account list changes and sends token list analytics + * when the total fiat balance changes. + * +[REDACTED_AUTHOR] + */ +internal class TokenListAnalyticsSubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val stateHolder: WalletStateController, + private val tokenListAnalyticsSender: TokenListAnalyticsSender, +) : BasicWalletSubscriber() { + + override fun create(coroutineScope: CoroutineScope): Flow<*> = getAccountStatusListFlow() + .distinctUntilChanged { old, new -> old.totalFiatBalance == new.totalFiatBalance } + .onEach(::sendTokenListAnalytics) + + private suspend fun sendTokenListAnalytics(accountStatusList: AccountStatusList) { + val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId) + + val flattenCurrencies = accountStatusList.flattenCurrencies() + tokenListAnalyticsSender.send( + displayedUiState = displayedState, + userWallet = userWallet, + flattenCurrencies = flattenCurrencies, + totalFiatBalance = accountStatusList.totalFiatBalance, + ) + } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): TokenListAnalyticsSubscriber + } +} \ No newline at end of file From 405912fd546b23554cba70fd43aaca970ed0f860 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 11:16:35 +0500 Subject: [PATCH 02/20] Updated on 2026-08-14 --- .../v2/stepper/impl/DefaultOnboardingStepperComponent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 03d7c218c2..636c9f4f32 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 @@ -43,8 +43,8 @@ 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() + val userWalletId = cardInfo.userWalletId + val visaCustomerId = userWalletId?.let { id -> getTangemPayCustomerIdUseCase(id).getOrNull() } sendFeedbackEmailUseCase( if (params.scanResponse.card.isVisa && !visaCustomerId.isNullOrEmpty()) { FeedbackEmailType.Visa.Activation(walletMetaInfo = cardInfo, customerId = visaCustomerId) From 3c00bf1966a0b87a8e08f5925f117dd5bc1632f6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 13:58:57 +0500 Subject: [PATCH 03/20] Updated on 2026-08-14 --- .../java/com/tangem/data/staking/DefaultStakingRepository.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 626219cb64..bdb663a190 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.staking.store.StakeKitBalancesStore import com.tangem.domain.card.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.staking.StakingBalance import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWallet @@ -156,6 +157,10 @@ internal class DefaultStakingRepository( return false } + if (userWallet.scanResponse.productType == ProductType.Note) { + return true + } + val blockchainId = cryptoCurrency.network.rawId return when { isSolana(blockchainId) -> INVALID_BATCHES_FOR_SOLANA.contains(userWallet.scanResponse.card.batchId) From 1ed2999163acc3917457a884c1510e04645e783d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 13:19:26 +0400 Subject: [PATCH 04/20] Updated on 2026-08-14 --- .../swap/v2/impl/common/entity/ConfirmUM.kt | 6 ++ ...SendWithSwapConfirmSentStateTransformer.kt | 3 + .../success/ui/SendWithSwapSuccessContent.kt | 64 ++++++++++++++++++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt index cf27f09d02..8be7f8fc02 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/entity/ConfirmUM.kt @@ -5,6 +5,9 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.swap.models.SwapDataModel +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM +import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import kotlinx.collections.immutable.ImmutableList @Immutable @@ -37,6 +40,9 @@ internal sealed class ConfirmUM { val txUrl: String, val swapDataModel: SwapDataModel, val provider: ExpressProvider, + val amountUM: SwapAmountUM, + val destinationUM: DestinationUM, + val feeSelectorUM: FeeSelectorUM, ) : ConfirmUM() data object Empty : ConfirmUM() { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt index 970c3f1f93..d0ade6d256 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/transformers/SendWithSwapConfirmSentStateTransformer.kt @@ -20,6 +20,9 @@ internal class SendWithSwapConfirmSentStateTransformer( txUrl = txUrl, provider = provider, swapDataModel = swapDataModel, + amountUM = prevState.amountUM, + destinationUM = prevState.destinationUM, + feeSelectorUM = prevState.feeSelectorUM, ), ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt index 5657f00361..834289a971 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/success/ui/SendWithSwapSuccessContent.kt @@ -93,10 +93,10 @@ internal fun SendWithSwapSuccessContent(sendWithSwapUM: SendWithSwapUM) { @Composable private fun SuccessContent(sendWithSwapUM: SendWithSwapUM, modifier: Modifier = Modifier) { val confirmUM = sendWithSwapUM.confirmUM as? ConfirmUM.Success ?: return - val amountUM = sendWithSwapUM.amountUM as? SwapAmountUM.Content ?: return + val amountUM = confirmUM.amountUM as? SwapAmountUM.Content ?: return val quoteUM = amountUM.selectedQuote as? SwapQuoteUM.Content ?: return - val destinationUM = sendWithSwapUM.destinationUM as? DestinationUM.Content ?: return - val feeSelectorUM = sendWithSwapUM.feeSelectorUM as? FeeSelectorUM.Content ?: return + val destinationUM = confirmUM.destinationUM as? DestinationUM.Content ?: return + val feeSelectorUM = confirmUM.feeSelectorUM as? FeeSelectorUM.Content ?: return Column( modifier = modifier @@ -393,6 +393,64 @@ private fun SendWithSwapSuccessContent_Preview() { txExtraIdName = "Jeffry Blackwell", ), ), + amountUM = SwapAmountContentPreview.defaultState, + destinationUM = DestinationUM.Content( + isPrimaryButtonEnabled = false, + addressTextField = DestinationTextFieldUM.RecipientAddress( + value = "0x391316d97a07027a0702c8A002c8A0C25d8470", + keyboardOptions = KeyboardOptions(), + placeholder = TextReference.EMPTY, + label = resourceReference(R.string.send_recipient), + isError = false, + error = null, + isValuePasted = false, + blockchainAddress = "0x391316d97a07027a0702c8A002c8A0C25d8470", + ), + memoTextField = DestinationTextFieldUM.RecipientMemo( + value = "123123123", + keyboardOptions = KeyboardOptions(), + placeholder = TextReference.EMPTY, + label = resourceReference(R.string.send_recipient), + isError = false, + error = null, + isValuePasted = false, + isEnabled = true, + disabledText = TextReference.EMPTY, + ), + recent = persistentListOf(), + wallets = persistentListOf(), + networkName = "Polygon", + isValidating = false, + isInitialized = false, + isRecentHidden = false, + isAccountsMode = false, + ), + feeSelectorUM = FeeSelectorUM.Content( + fees = TransactionFee.Single( + normal = Fee.Common( + BigDecimal.ONE.convertToSdkAmount( + SwapAmountContentPreview.cryptoCurrencyStatus, + ), + ), + ), + feeItems = persistentListOf(), + selectedFeeItem = FeeItem.Market( + Fee.Common( + BigDecimal.ONE.convertToSdkAmount( + SwapAmountContentPreview.cryptoCurrencyStatus, + ), + ), + ), + feeExtraInfo = FeeExtraInfo( + isFeeApproximate = false, + isFeeConvertibleToFiat = false, + isTronToken = false, + feeCryptoCurrencyStatus = SwapAmountContentPreview.cryptoCurrencyStatus, + ), + feeFiatRateUM = null, + feeNonce = FeeNonce.None, + isPrimaryButtonEnabled = false, + ), ), navigationUM = NavigationUM.Content( source = SendWithSwapRoute.Success.javaClass.simpleName, From 6e267fc0674cbf69370434fa191074ee804fe41b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 16:49:10 +0400 Subject: [PATCH 05/20] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 2 + .../models/event/SwapAnalyticsEvent.kt | 29 +++++++++ .../add/impl/model/AddToPortfolioModel.kt | 2 + .../portfolio/add/impl/model/AddTokenModel.kt | 6 ++ .../impl/analytics/PortfolioAnalyticsEvent.kt | 62 ++++++++++++++++--- .../add/impl/model/AddToPortfolioModel.kt | 2 + .../portfolio/add/impl/model/AddTokenModel.kt | 6 ++ .../impl/analytics/PortfolioAnalyticsEvent.kt | 60 +++++++++++++++--- .../model/AvailableSwapPairsAnalyticsEvent.kt | 44 ------------- .../model/AvailableSwapPairsModel.kt | 38 +++++------- .../feature/swap/analytics/SwapEvents.kt | 19 ------ .../tangem/feature/swap/model/SwapModel.kt | 34 +++++----- 12 files changed, 182 insertions(+), 122 deletions(-) create mode 100644 core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt 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 083fa85275..ae0d6a4253 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 @@ -97,6 +97,7 @@ sealed class AnalyticsParam { data object NewsList : ScreensSources("News List") data object NewsLink : ScreensSources("News Link") data object NewsPage : ScreensSources("News Page") + data object Portfolio : ScreensSources("Portfolio") } sealed class TxSentFrom(val value: String) { @@ -292,6 +293,7 @@ sealed class AnalyticsParam { const val ACCOUNT_DERIVATION = "Account Derivation" const val REFERRAL = "Referral" const val REFERRAL_ID = "Referral_ID" + const val SEARCHED = "Searched" } } diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt new file mode 100644 index 0000000000..e6e5864f91 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/SwapAnalyticsEvent.kt @@ -0,0 +1,29 @@ +package com.tangem.core.analytics.models.event + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.SEARCHED +import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources + +/** +[REDACTED_AUTHOR] + */ +sealed class SwapAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent("Swap", event, params) { + + data class TokenSelected( + val token: String, + val source: ScreensSources, + val isSearched: Boolean, + ) : SwapAnalyticsEvent( + event = "Token Selected", + params = mapOf( + TOKEN_PARAM to token, + SOURCE to source.value, + SEARCHED to if (isSearched) "True" else "False", + ), + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt index e234d1d2f6..0d733ab09f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -157,6 +157,8 @@ internal class AddToPortfolioModel @Inject constructor( allRequireForAdd.first() // line of navigation to AddToken screen is finished; cancel the job, select a new root screen firstPartOfNavigation.cancel() + + analyticsEventHandler.send(event = eventBuilder.popupToConfirm()) navigation.replaceAll(AddToPortfolioRoutes.AddToken) var middleNavigationJob: Job? = null 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 cd16df3f04..17d27f1aad 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 @@ -88,6 +88,7 @@ internal class AddTokenModel @Inject constructor( val blockchainNames = listOf(selectedNetwork.selectedNetwork) .mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name } analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames)) + analyticsEventHandler.send(analyticsEventBuilder.addButtonClick()) manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency) .onLeft { throwable -> @@ -110,6 +111,11 @@ internal class AddTokenModel @Inject constructor( } is Account.Payment -> TODO("[REDACTED_JIRA]") } + + analyticsEventHandler.send( + event = analyticsEventBuilder.tokenAdded(status.status.currency.network.name), + ) + params.callbacks.onTokenAdded(status.status) } uiState.value = um.toggleProgress(false) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt index b75d67abf4..87ed9607b7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -15,28 +15,64 @@ internal class PortfolioAnalyticsEvent( fun addToPortfolioClicked() = PortfolioAnalyticsEvent( event = "Button - Add To Portfolio", - params = mapOf( - "Token" to tokenSymbol, - ), + params = buildMap { + put("Token", tokenSymbol) + if (source != null) put("Source", source) + }, ) fun popupToChooseAccount() = PortfolioAnalyticsEvent( event = "Choose Account Opened", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun popupToConfirm() = PortfolioAnalyticsEvent( + event = "Add Token Screen Opened", + params = buildMap { + if (source != null) put("Source", source) + }, ) fun addToNotMainAccount() = PortfolioAnalyticsEvent( event = "Button - Add To Account", + params = buildMap { + if (source != null) put("Source", source) + }, ) - fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected") + fun addButtonClick() = PortfolioAnalyticsEvent( + event = "Button - Add Token", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent( + event = "Wallet Selected", + params = buildMap { + if (source != null) put("Source", source) + }, + ) fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( event = "Token Network Selected", - params = mapOf( - "Count" to blockchainNames.size.toString(), - "Token" to tokenSymbol, - "blockchain" to blockchainNames.joinToString(separator = ", "), - ), + params = buildMap { + put("Count", blockchainNames.size.toString()) + put("Token", tokenSymbol) + put("blockchain", blockchainNames.joinToString(separator = ", ")) + if (source != null) put("Source", source) + }, + ) + + fun tokenAdded(blockchainName: String) = PortfolioAnalyticsEvent( + event = "Token Added", + params = buildMap { + put("Token", tokenSymbol) + put("Blockchain", blockchainName) + if (source != null) put("Source", source) + }, ) fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) = @@ -51,7 +87,7 @@ internal class PortfolioAnalyticsEvent( }, params = buildMap { put("Token", tokenSymbol) - source?.let { put("Source", it) } + if (source != null) put("Source", source) put("blockchain", blockchainName) }, ) @@ -64,10 +100,16 @@ internal class PortfolioAnalyticsEvent( TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake" else -> "error" }, + params = buildMap { + if (source != null) put("Source", source) + }, ) fun getTokenLater() = PortfolioAnalyticsEvent( event = "Popup Get token - Button Later", + params = buildMap { + if (source != null) put("Source", source) + }, ) } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt index 142c66cf7a..71b3634b3d 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -151,6 +151,8 @@ internal class AddToPortfolioModel @Inject constructor( allRequireForAdd.first() // line of navigation to AddToken screen is finished; cancel the job, select a new root screen firstPartOfNavigation.cancel() + + analyticsEventHandler.send(event = eventBuilder.popupToConfirm()) navigation.replaceAll(AddToPortfolioRoutes.AddToken) var middleNavigationJob: Job? = null 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 edc27eb65f..be45fedb3a 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 @@ -88,6 +88,7 @@ internal class AddTokenModel @Inject constructor( val blockchainNames = listOf(selectedNetwork.selectedNetwork) .mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name } analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames)) + analyticsEventHandler.send(analyticsEventBuilder.addButtonClick()) manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency) .onLeft { error -> @@ -110,6 +111,11 @@ internal class AddTokenModel @Inject constructor( } is Account.Payment -> TODO("[REDACTED_JIRA]") } + + analyticsEventHandler.send( + event = analyticsEventBuilder.tokenAdded(status.status.currency.network.name), + ) + params.callbacks.onTokenAdded(status.status) } uiState.value = um.toggleProgress(false) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt index ac6125d3c0..2658bb0659 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -16,28 +16,64 @@ internal class PortfolioAnalyticsEvent( fun addToPortfolioClicked() = PortfolioAnalyticsEvent( event = "Button - Add To Portfolio", - params = mapOf( - "Token" to token.symbol, - ), + params = buildMap { + put("Token", token.symbol) + if (source != null) put("Source", source) + }, ) fun popupToChooseAccount() = PortfolioAnalyticsEvent( event = "Choose Account Opened", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun popupToConfirm() = PortfolioAnalyticsEvent( + event = "Add Token Screen Opened", + params = buildMap { + if (source != null) put("Source", source) + }, ) fun addToNotMainAccount() = PortfolioAnalyticsEvent( event = "Button - Add To Account", + params = buildMap { + if (source != null) put("Source", source) + }, ) - fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected") + fun addButtonClick() = PortfolioAnalyticsEvent( + event = "Button - Add Token", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent( + event = "Wallet Selected", + params = buildMap { + if (source != null) put("Source", source) + }, + ) fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( event = "Token Network Selected", - params = mapOf( - "Count" to blockchainNames.size.toString(), - "Token" to token.symbol, - "blockchain" to blockchainNames.joinToString(separator = ", "), - ), + params = buildMap { + put("Count", blockchainNames.size.toString()) + put("Token", token.symbol) + put("blockchain", blockchainNames.joinToString(separator = ", ")) + if (source != null) put("Source", source) + }, + ) + + fun tokenAdded(blockchainName: String) = PortfolioAnalyticsEvent( + event = "Token Added", + params = buildMap { + put("Token", token.symbol) + put("Blockchain", blockchainName) + if (source != null) put("Source", source) + }, ) fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) = @@ -65,10 +101,16 @@ internal class PortfolioAnalyticsEvent( TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake" else -> "error" }, + params = buildMap { + if (source != null) put("Source", source) + }, ) fun getTokenLater() = PortfolioAnalyticsEvent( event = "Popup Get token - Button Later", + params = buildMap { + if (source != null) put("Source", source) + }, ) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt deleted file mode 100644 index f5a57df2b5..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.features.onramp.swap.availablepairs.model - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN -import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM - -private const val SWAP_CATEGORY = "Swap" - -internal sealed class AvailableSwapPairsAnalyticsEvent( - event: String, - params: Map = emptyMap(), -) : AnalyticsEvent(SWAP_CATEGORY, event, params) { - - class TokenSelected( - val token: String, - val source: String, - val isSearched: Boolean, - ) : AvailableSwapPairsAnalyticsEvent( - event = "Token Selected", - params = mapOf( - TOKEN_PARAM to token, - SOURCE to source, - SEARCHED to if (isSearched) "True" else "False", - ), - ) { - companion object { - const val SOURCE = "Source" - const val SEARCHED = "Searched" - const val SOURCE_PORTFOLIO = "Portfolio" - const val SOURCE_MARKETS = "Markets" - } - } - - class TokenAdded( - val token: String, - val blockchain: String, - ) : AvailableSwapPairsAnalyticsEvent( - event = "Token Added", - params = mapOf( - TOKEN_PARAM to token, - BLOCKCHAIN to blockchain, - ), - ) -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 62822d4108..363f0712d5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -1,11 +1,17 @@ package com.tangem.features.onramp.swap.availablepairs.model +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources +import com.tangem.core.analytics.models.event.SwapAnalyticsEvent import com.tangem.core.decompose.model.Model -import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.InputManager -import com.tangem.core.ui.R as CoreUiR +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.resourceReference @@ -43,9 +49,7 @@ import com.tangem.feature.swap.domain.models.domain.SwapPairLeast import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent -import com.tangem.features.swap.SwapFeatureToggles import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer @@ -58,24 +62,22 @@ import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMController import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import com.tangem.features.onramp.tokenlist.entity.transformer.* +import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.features.swap.SwapFeatureToggles import com.tangem.lib.crypto.BlockchainUtils -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.arkivanov.decompose.router.slot.dismiss import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.runSuspendCatching -import kotlinx.coroutines.ExperimentalCoroutinesApi import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +import com.tangem.core.ui.R as CoreUiR private typealias AvailablePairsState = Lce> @@ -567,9 +569,9 @@ internal class AvailableSwapPairsModel @Inject constructor( private fun onPortfolioTokenClick(tokenItem: TokenItemState, status: CryptoCurrencyStatus) { analyticsEventHandler.send( - AvailableSwapPairsAnalyticsEvent.TokenSelected( + SwapAnalyticsEvent.TokenSelected( token = status.currency.symbol, - source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_PORTFOLIO, + source = ScreensSources.Portfolio, isSearched = state.value.searchBarUM.query.isNotEmpty(), ), ) @@ -686,15 +688,9 @@ internal class AvailableSwapPairsModel @Inject constructor( modelScope.launch { bottomSheetNavigation.dismiss() analyticsEventHandler.send( - AvailableSwapPairsAnalyticsEvent.TokenAdded( + SwapAnalyticsEvent.TokenSelected( token = addedToken.symbol, - blockchain = addedToken.network.name, - ), - ) - analyticsEventHandler.send( - AvailableSwapPairsAnalyticsEvent.TokenSelected( - token = addedToken.symbol, - source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_MARKETS, + source = ScreensSources.Markets, isSearched = state.value.searchBarUM.query.isNotEmpty(), ), ) @@ -746,7 +742,7 @@ internal class AvailableSwapPairsModel @Inject constructor( .create( scope = modelScope, token = param, - analyticsParams = null, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), ).apply { setTokenNetworks(networks) } 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 cd44e2f95c..89a8266fd1 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 @@ -46,31 +46,12 @@ sealed class SwapEvents( class ChooseTokenScreenResult( val isTokenChosen: Boolean, val token: String? = null, - val source: String? = null, - val isSearched: Boolean? = null, ) : SwapEvents( event = "Choose Token Screen Result", params = buildMap { put("Token Chosen", if (isTokenChosen) "Yes" else "No") token?.let { put("Token", it) } - source?.let { put(TOKEN_SELECTED_SOURCE, it) } - isSearched?.let { put(SEARCHED, if (it) "True" else "False") } }, - ) { - companion object { - const val TOKEN_SELECTED_SOURCE = "Token Selected Source" - const val SEARCHED = "Searched" - const val SOURCE_PORTFOLIO = "Portfolio" - const val SOURCE_MARKETS = "Markets" - } - } - - class TokenAdded(val token: String, val blockchain: String) : SwapEvents( - event = "Token Added", - params = mapOf( - TOKEN_PARAM to token, - BLOCKCHAIN to blockchain, - ), ) class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( 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 fb4e74e1ff..0f0e4c1d4c 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 @@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue - import arrow.core.Either import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation @@ -19,12 +18,15 @@ import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources import com.tangem.core.analytics.models.Basic +import com.tangem.core.analytics.models.event.SwapAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles +import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.InputNumberFormatter @@ -90,7 +92,6 @@ import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager import com.tangem.feature.swap.models.market.state.SwapMarketState import com.tangem.feature.swap.models.states.SwapNotificationUM -import com.tangem.core.ui.R import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder @@ -107,12 +108,8 @@ import com.tangem.utils.Provider import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal import java.math.RoundingMode @@ -300,17 +297,13 @@ internal class SwapModel @Inject constructor( modelScope.launch { bottomSheetNavigation.dismiss() analyticsEventHandler.send( - SwapEvents.ChooseTokenScreenResult( - isTokenChosen = true, - token = addedToken.symbol, - source = SwapEvents.ChooseTokenScreenResult.SOURCE_MARKETS, - isSearched = searchQueryState.value.isNotEmpty(), - ), + SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = addedToken.symbol), ) analyticsEventHandler.send( - SwapEvents.TokenAdded( + SwapAnalyticsEvent.TokenSelected( token = addedToken.symbol, - blockchain = addedToken.network.name, + source = ScreensSources.Markets, + isSearched = searchQueryState.value.isNotEmpty(), ), ) searchQueryState.value = "" @@ -1339,10 +1332,13 @@ internal class SwapModel @Inject constructor( foundToken?.currency?.symbol?.let { symbol -> analyticsEventHandler.send( - SwapEvents.ChooseTokenScreenResult( - isTokenChosen = true, + SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol), + ) + + analyticsEventHandler.send( + SwapAnalyticsEvent.TokenSelected( token = symbol, - source = SwapEvents.ChooseTokenScreenResult.SOURCE_PORTFOLIO, + source = ScreensSources.Portfolio, isSearched = searchQueryState.value.isNotEmpty(), ), ) @@ -2241,7 +2237,7 @@ internal class SwapModel @Inject constructor( .create( scope = modelScope, token = param, - analyticsParams = null, + analyticsParams = AddToPortfolioManager.AnalyticsParams(source = ScreensSources.Swap.value), ).apply { setTokenNetworks(networks) } From 8f4a8b18ef9f6c3a91f6e2600a43a45812b1b98c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 09:59:10 +0400 Subject: [PATCH 06/20] Updated on 2026-08-14 --- .../api/express/models/response/SwapPair.kt | 6 +++++- .../tangem/data/swap/DefaultSwapRepositoryV2.kt | 14 ++++++++------ .../swap/converters/SwapPairInfoConverter.kt | 6 +++--- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt index 9b96730278..21f255653b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/SwapPair.kt @@ -24,7 +24,11 @@ data class SwapPairProvider( @Json(name = "rateTypes") val rateTypes: List, -) +) { + fun hasOnlyFixedRateType(): Boolean { + return rateTypes.isNotEmpty() && rateTypes.all { it == RateType.FIXED } + } +} @JsonClass(generateAdapter = false) enum class RateType { diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index b229533df3..080f06bacf 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -94,9 +94,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( currencyStatus.currency.network.backendId == pair.to.network } - val mappedProviders = pair.providers.mapNotNull { - expressProviders[it.providerId] - }.filterYieldSupplyProvider(statusFrom) + val mappedProviders = pair.providers + .filterNot { it.hasOnlyFixedRateType() } + .mapNotNull { expressProviders[it.providerId] } + .filterYieldSupplyProvider(statusFrom) if (statusFrom != null && statusTo != null && mappedProviders.isNotEmpty()) { SwapPairModel( @@ -151,9 +152,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( val currencyStatusFrom = createSendWithSwapCryptoCurrencyStatus(statusFromDeferred.await()) val currencyStatusTo = createSendWithSwapCryptoCurrencyStatus(statusToDeferred.await()) - val mappedProvider = pair.providers.mapNotNull { - mappedProviders[it.providerId] - }.filterYieldSupplyProvider(currencyStatusFrom) + val mappedProvider = pair.providers + .filterNot { it.hasOnlyFixedRateType() } + .mapNotNull { mappedProviders[it.providerId] } + .filterYieldSupplyProvider(currencyStatusFrom) if (currencyStatusFrom != null && currencyStatusTo != null && mappedProvider.isNotEmpty()) { SwapPairModel( diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index 1080578a05..414a2649d7 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -25,9 +25,9 @@ class SwapPairInfoConverter : Converter Date: Tue, 24 Feb 2026 13:38:12 +0400 Subject: [PATCH 07/20] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 57 ++++-- .../local/preferences/PreferencesKeys.kt | 5 +- .../datasource/local/visa/TangemPayStorage.kt | 18 +- .../DefaultTangemPayWithdrawRepository.kt | 178 +++++++----------- .../repository/TangemPayWithdrawRepository.kt | 3 +- .../tangempay/model/TangemPayDetailsModel.kt | 2 +- .../state/factory/ExpressStateFactory.kt | 6 +- 7 files changed, 136 insertions(+), 133 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 5708a79d8b..42450edff8 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -1,7 +1,9 @@ package com.tangem.tap.data import android.content.Context +import com.squareup.moshi.JsonAdapter import com.squareup.moshi.Moshi +import com.squareup.moshi.Types import com.tangem.data.pay.entity.WithdrawStoreData import com.tangem.data.pay.util.WithdrawStateConverter import com.tangem.data.pay.util.WithdrawStoreDataConverter @@ -19,6 +21,7 @@ import com.tangem.domain.visa.model.TangemPayAuthTokens import com.tangem.sdk.storage.AndroidSecureStorageV2 import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.withContext import java.util.UUID import javax.inject.Inject @@ -137,28 +140,56 @@ internal class DefaultTangemPayStorage @Inject constructor( return appPreferencesStore.getSyncOrNull(PreferencesKeys.getTangemPayCheckCustomerByWalletId(userWalletId)) } - override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) { + override suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) { appPreferencesStore.editData { mutablePreferences -> - val orders = mutablePreferences.getObjectMap( - PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, - ) - .plus(createWithdrawOrderIdKey(userWalletId) to withdrawStoreDataConverter.convert(data)) + val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + .plus(createWithdrawOrderIdKey(userWalletId) to orderId) mutablePreferences.setObjectMap( - key = PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, + key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, value = orders, ) } } - override suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? { - val orders = appPreferencesStore.getObjectMapSync( - PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, - ) - val data = orders[createWithdrawOrderIdKey(userWalletId)] ?: return null - return withdrawStateConverter.convert(data) + override suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) { + val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java) + val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType) + val adapter: JsonAdapter>> = appPreferencesStore.moshi.adapter(mapType) + appPreferencesStore.editData { prefs -> + val walletKey = createWithdrawOrderIdKey(userWalletId) + val currentMap = prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY]?.let(adapter::fromJson).orEmpty() + val updatedList = currentMap[walletKey].orEmpty() + withdrawStoreDataConverter.convert(data) + prefs[PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY] = adapter + .toJson(currentMap + (walletKey to updatedList)) + } } - override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) { + override suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? { + val orders = appPreferencesStore.getObjectMapSync(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + return orders[createWithdrawOrderIdKey(userWalletId)] + } + + override suspend fun getWithdrawOrders(userWalletId: UserWalletId): List { + val listType = Types.newParameterizedType(List::class.java, WithdrawStoreData::class.java) + val mapType = Types.newParameterizedType(Map::class.java, String::class.java, listType) + val adapter: JsonAdapter>> = appPreferencesStore.moshi.adapter(mapType) + val map = appPreferencesStore.data.firstOrNull() + ?.get(PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY)?.let(adapter::fromJson).orEmpty() + return map[createWithdrawOrderIdKey(userWalletId)].orEmpty().map(withdrawStateConverter::convert) + } + + override suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) { + appPreferencesStore.editData { mutablePreferences -> + val orders = mutablePreferences.getObjectMap(PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY) + .minus(createWithdrawOrderIdKey(userWalletId)) + mutablePreferences.setObjectMap( + key = PreferencesKeys.TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY, + value = orders, + ) + } + } + + override suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) { appPreferencesStore.editData { mutablePreferences -> val orders = mutablePreferences.getObjectMap( PreferencesKeys.TANGEM_PAY_WITHDRAW_ORDERS_KEY, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index e0c785a2e8..c70f061ee1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -161,7 +161,10 @@ object PreferencesKeys { intPreferencesKey(name = "tronNetworkFeeNotificationShowCount") } - val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrders") } + val TANGEM_PAY_WITHDRAW_ORDERS_KEY by lazy { stringPreferencesKey(name = "tangemPayWithdrawOrdersKey") } + val TANGEM_PAY_ACTIVE_WITHDRAW_ORDERS_KEY by lazy { + stringPreferencesKey(name = "tangemPayActiveWithdrawOrdersKey") + } val TANGEM_PAY_ELIGIBILITY_KEY by lazy { booleanPreferencesKey(name = "tangemPayEligibility") } fun getShouldShowNotificationKey(key: String) = booleanPreferencesKey("showShowNotificationUM_$key") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index bd29637e98..25a33b8970 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -29,11 +29,25 @@ interface TangemPayStorage { suspend fun storeCheckCustomerWalletResult(userWalletId: UserWalletId, isPaeraCustomer: Boolean) suspend fun checkCustomerWalletResult(userWalletId: UserWalletId): Boolean? + /** Called after creating withdraw order, active order id */ + suspend fun storeActiveWithdrawOrderId(userWalletId: UserWalletId, orderId: String) + + /** Called after creating withdraw order, saves order data */ suspend fun storeWithdrawOrder(userWalletId: UserWalletId, data: TangemPayWithdrawState) - suspend fun getWithdrawOrder(userWalletId: UserWalletId): TangemPayWithdrawState? + /** Returns single active order id. Once the order is completed, deletes id from storage. + * Only one active order allowed for a wallet */ + suspend fun getActiveWithdrawOrderId(userWalletId: UserWalletId): String? - suspend fun deleteWithdrawOrder(userWalletId: UserWalletId) + /** Returns all withdraw orders saved. + * Once we get tx hash for an order, it gets deleted from this storage */ + suspend fun getWithdrawOrders(userWalletId: UserWalletId): List? + + /** Deletes active withdraw order. Called after order is completed */ + suspend fun deleteActiveWithdrawOrder(userWalletId: UserWalletId) + + /** Deletes withdraw order data. Called after getting its tx hash */ + suspend fun deleteWithdrawOrder(userWalletId: UserWalletId, orderId: String) suspend fun getHideMainOnboardingBanner(userWalletId: UserWalletId): Boolean diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt index cea8e61d60..922b617842 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayWithdrawRepository.kt @@ -2,7 +2,6 @@ package com.tangem.data.pay.repository import arrow.core.Either import arrow.core.left -import arrow.core.right import com.tangem.core.error.UniversalError import com.tangem.data.common.quote.QuotesFetcher import com.tangem.datasource.api.pay.TangemPayApi @@ -17,7 +16,6 @@ import com.tangem.domain.pay.TangemPayWithdrawState import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.pay.WithdrawalSignatureResult import com.tangem.domain.pay.datasource.TangemPayAuthDataSource -import com.tangem.domain.pay.model.OrderData import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.repository.CustomerOrderRepository import com.tangem.domain.pay.repository.TangemPayWithdrawRepository @@ -25,13 +23,7 @@ import com.tangem.domain.visa.error.VisaApiError import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.utils.extensions.addHexPrefix -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import timber.log.Timber @@ -40,7 +32,6 @@ import java.math.RoundingMode import java.util.Currency import java.util.Locale import javax.inject.Inject -import kotlin.collections.set import kotlin.coroutines.cancellation.CancellationException import kotlin.time.Duration.Companion.seconds @@ -113,23 +104,18 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( ) { val orderId = response.result?.orderId if (orderId != null) { - val orderData = orderRepository - .getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() - val withdrawTxHash = orderData?.withdrawTxHash + tangemPayStorage.storeActiveWithdrawOrderId(userWalletId = userWallet.walletId, orderId = orderId) + val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + val withdrawTxHash = order?.withdrawTxHash val storeData = TangemPayWithdrawState( orderId = orderId, exchangeData = exchangeData, ) - if (orderData != null && !withdrawTxHash.isNullOrEmpty()) { - finalizeWithdraw( - userWallet = userWallet, - withdrawTxHash = withdrawTxHash, - orderId = orderId, - exchangeData = exchangeData, - order = orderData, - ).onLeft { - tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) - } + if (order != null && !withdrawTxHash.isNullOrEmpty()) { + finalizeWithdraw(userWallet = userWallet, txHash = withdrawTxHash, exchangeData = exchangeData) + .onLeft { + tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) + } } else { tangemPayStorage.storeWithdrawOrder(userWalletId = userWallet.walletId, data = storeData) } @@ -138,10 +124,8 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( private suspend fun finalizeWithdraw( userWallet: UserWallet, - withdrawTxHash: String, - orderId: String, + txHash: String, exchangeData: TangemPayWithdrawExchangeState, - order: OrderData, ): Either { return swapRepository.exchangeSent( userWallet = userWallet, @@ -149,89 +133,63 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( fromNetwork = exchangeData.fromNetwork, fromAddress = exchangeData.fromAddress, payInAddress = exchangeData.payInAddress, - txHash = withdrawTxHash, + txHash = txHash, payInExtraId = exchangeData.payInExtraId, ) - .onRight { - val isActive = order.status == OrderStatus.NEW || order.status == OrderStatus.PROCESSING - if (!isActive) { - tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId) - } else { - tangemPayStorage.storeWithdrawOrder( - userWalletId = userWallet.walletId, - data = TangemPayWithdrawState(orderId = orderId, exchangeData = null), - ) - } - } .onLeft { error -> Timber.tag(TAG).e(error.toString()) } } override suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean { - val orderExchangeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) - if (orderExchangeData == null) return false - - val exchangeData = orderExchangeData.exchangeData - val orderData = orderRepository - .getOrderData(userWallet.walletId, orderId = orderExchangeData.orderId).getOrNull() - val withdrawTxHash = orderData?.withdrawTxHash - - if (exchangeData != null && orderData != null && withdrawTxHash != null) { - finalizeWithdraw( - userWallet = userWallet, - withdrawTxHash = withdrawTxHash, - orderId = orderExchangeData.orderId, - exchangeData = exchangeData, - order = orderData, - ) + val orderId = tangemPayStorage.getActiveWithdrawOrderId(userWallet.walletId) + if (orderId.isNullOrEmpty()) return false + val orderData = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + val isActive = orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING + if (!isActive) { + tangemPayStorage.deleteActiveWithdrawOrder(userWalletId = userWallet.walletId) } - - return orderData?.status == OrderStatus.NEW || orderData?.status == OrderStatus.PROCESSING + return isActive } - override suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either { - val storeData = tangemPayStorage.getWithdrawOrder(userWallet.walletId) ?: return Unit.right() - val exchangeData = storeData.exchangeData ?: return Unit.right() - - val orderId = storeData.orderId - val order = orderRepository - .getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() - ?: return Unit.right() + override suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) { + tangemPayStorage.getWithdrawOrders(userWalletId = userWallet.walletId)?.forEach { state -> + withdrawPollingScope.launch { + try { + pollWithdrawOrderIfNeeds(userWallet = userWallet, data = state) + } catch (exception: CancellationException) { + throw exception + } catch (exception: Exception) { + Timber.tag(TAG).e(exception) + } + } + } + } + private suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet, data: TangemPayWithdrawState) { + val exchangeData = data.exchangeData ?: return + val orderId = data.orderId + val order = orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId).getOrNull() + ?: return val txHash = order.withdrawTxHash if (!txHash.isNullOrEmpty()) { - finalizeWithdraw( - userWallet = userWallet, - withdrawTxHash = txHash, - orderId = storeData.orderId, - exchangeData = exchangeData, - order = order, - ).onLeft { - startWithdrawOrderPolling( - userWallet = userWallet, - orderId = orderId, - storeData = storeData, - exchangeData = exchangeData, - ) - } + finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData) + .onRight { + tangemPayStorage.deleteWithdrawOrder(userWalletId = userWallet.walletId, orderId = orderId) + } + .onLeft { + startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData) + } } else { - startWithdrawOrderPolling( - userWallet = userWallet, - orderId = orderId, - storeData = storeData, - exchangeData = exchangeData, - ) + startWithdrawOrderPolling(userWallet = userWallet, orderId = orderId, exchangeData = exchangeData) } - - return Unit.right() + return } private suspend fun startWithdrawOrderPolling( userWallet: UserWallet, orderId: String, - storeData: TangemPayWithdrawState, exchangeData: TangemPayWithdrawExchangeState, ) { withdrawPollingMutex.withLock { @@ -239,33 +197,33 @@ internal class DefaultTangemPayWithdrawRepository @Inject constructor( val pollingJob = withdrawPollingScope.launch { try { - while (isActive && withdrawPollingJobs.containsKey(orderId)) { + while (isActive) { delay(duration = 5.seconds) - val orderData = orderRepository - .getOrderData(userWalletId = userWallet.walletId, orderId = orderId) - orderData.onRight { order -> - if (order.status != OrderStatus.NEW && order.status != OrderStatus.PROCESSING) { - tangemPayStorage.deleteWithdrawOrder(userWallet.walletId) - withdrawPollingJobs.remove(key = orderId) + orderRepository.getOrderData(userWalletId = userWallet.walletId, orderId = orderId) + .onRight { order -> + val txHash = order.withdrawTxHash + if (txHash.isNullOrEmpty()) return@onRight + finalizeWithdraw(userWallet = userWallet, txHash = txHash, exchangeData = exchangeData) + .onRight { + tangemPayStorage.deleteWithdrawOrder( + userWalletId = userWallet.walletId, + orderId = orderId, + ) + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } + return@launch + } + .onLeft { error -> + Timber.tag(TAG).e("finalizeWithdraw error: $error") + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } + return@launch + } + } + .onLeft { error -> + Timber.tag(TAG).e("getOrderData error ${error.errorCode}") + withdrawPollingMutex.withLock { withdrawPollingJobs.remove(orderId) } return@launch } - val txHash = order.withdrawTxHash - if (!txHash.isNullOrEmpty()) { - finalizeWithdraw( - userWallet = userWallet, - withdrawTxHash = txHash, - orderId = storeData.orderId, - exchangeData = exchangeData, - order = order, - ).onRight { - withdrawPollingJobs.remove(key = orderId) - return@launch - } - } - }.onLeft { error -> - Timber.tag(TAG).e("error ${error.errorCode}") - } } } catch (exception: CancellationException) { throw exception diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt index 18b09d331e..6079a4e874 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayWithdrawRepository.kt @@ -6,7 +6,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayWithdrawExchangeState import com.tangem.domain.pay.WithdrawalResult -import com.tangem.domain.visa.error.VisaApiError import java.math.BigDecimal interface TangemPayWithdrawRepository { @@ -21,5 +20,5 @@ interface TangemPayWithdrawRepository { suspend fun hasWithdrawOrder(userWallet: UserWallet): Boolean - suspend fun pollWithdrawOrderIfNeeds(userWallet: UserWallet): Either + suspend fun pollWithdrawOrdersIfNeeds(userWallet: UserWallet) } \ 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 793549861d..beb2bed482 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 @@ -153,7 +153,7 @@ internal class TangemPayDetailsModel @Inject constructor( modelScope.launch { val userWallet = userWallet ?: getUserWalletUseCase(params.userWalletId).getOrNull() ?: return@launch - tangemPayWithdrawRepository.pollWithdrawOrderIfNeeds(userWallet) + tangemPayWithdrawRepository.pollWithdrawOrdersIfNeeds(userWallet) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt index e888745a6f..950bd50f41 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/ExpressStateFactory.kt @@ -25,14 +25,12 @@ internal class ExpressStateFactory( fun getStateWithClosedDialog(): ExpressTransactionsBlockState { val state = currentStateProvider() - val slot = state.dialogSlot ?: return state - return state.copy(dialogSlot = slot.copy(config = slot.config.copy(isShow = false))) + return state.copy(dialogSlot = null) } fun getStateWithClosedBottomSheet(): ExpressTransactionsBlockState { val state = currentStateProvider() - val slot = state.bottomSheetSlot ?: return state - return state.copy(bottomSheetSlot = slot.copy(config = slot.config.copy(isShown = false))) + return state.copy(bottomSheetSlot = null) } fun getStateWithConfirmHideExpressStatus(): ExpressTransactionsBlockState { From 80c1b5ba20540cb8bbc3a9fa4268d0e3f567357c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 18:24:23 +0400 Subject: [PATCH 08/20] Updated on 2026-08-14 --- .../tangem/feature/swap/model/SwapModel.kt | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 0f0e4c1d4c..4f2656df25 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 @@ -510,14 +510,7 @@ internal class SwapModel @Inject constructor( dataState.fromCryptoCurrency } - fromCryptoCurrency?.let { cryptoCurrency -> - dataState = dataState.copy( - feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = cryptoCurrency, - ).getOrNull(), - ) - } + if (fromCryptoCurrency != null) updateFeePaidCryptoCurrencyFor(fromCryptoCurrency) subscribeToCoinBalanceUpdatesIfNeeded() }.onFailure { error -> @@ -754,6 +747,15 @@ internal class SwapModel @Inject constructor( } } + private suspend fun updateFeePaidCryptoCurrencyFor(fromToken: CryptoCurrencyStatus) { + dataState = dataState.copy( + feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = fromToken, + ).getOrNull(), + ) + } + private fun loadQuotesTask( fromToken: CryptoCurrencyStatus, fromAccount: Account.CryptoPortfolio?, @@ -1402,6 +1404,9 @@ internal class SwapModel @Inject constructor( toAccount = toAccount, selectedProvider = null, ) + modelScope.launch { + updateFeePaidCryptoCurrencyFor(fromToken) + } startLoadingQuotes( fromToken = fromToken, fromAccount = fromAccount, @@ -1544,6 +1549,7 @@ internal class SwapModel @Inject constructor( toAccount = newToAccount, ) isOrderReversed = !isOrderReversed + updateFeePaidCryptoCurrencyFor(newFromToken) dataState.tokensDataState?.let { tokensDataState -> updateTokensState(tokensDataState) } From 63698bed23cfe8348ad162da9af708109bdb01a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 17:35:17 +0300 Subject: [PATCH 09/20] Updated on 2026-08-14 --- .../com/tangem/feature/swap/ui/StateBuilder.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6c955e7c86..adeed21292 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -170,10 +170,10 @@ internal class StateBuilder( amountTextFieldValue = null, amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", token = fromToken, - tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, - coinId = uiStateHolder.sendCardData.coinId, - isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, - tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, + tokenIconUrl = fromToken.currency.iconUrl, + coinId = fromToken.currency.network.backendId, + isNotNativeToken = fromToken.currency is CryptoCurrency.Token, + tokenCurrency = fromToken.currency.symbol, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = fromToken.getFormattedAmount(isNeedSymbol = false), networkIconRes = getActiveIconRes(fromToken.currency.network.rawId), @@ -213,10 +213,10 @@ internal class StateBuilder( amountTextFieldValue = null, amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", token = fromToken, - tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, - coinId = uiStateHolder.sendCardData.coinId, - isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, - tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, + tokenIconUrl = fromToken.currency.iconUrl, + coinId = fromToken.currency.network.backendId, + isNotNativeToken = fromToken.currency is CryptoCurrency.Token, + tokenCurrency = fromToken.currency.symbol, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, balance = fromToken.getFormattedAmount(isNeedSymbol = false), networkIconRes = getActiveIconRes(fromToken.currency.network.rawId), From 189cb19ff2fdcb99d7803d75995d0d5d0594171e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 19:49:20 +0400 Subject: [PATCH 10/20] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 6 +- domain/wallets/build.gradle.kts | 1 + .../usecase/SetNotificationsEnabledUseCase.kt | 6 +- .../SetNotificationsEnabledUseCaseTest.kt | 131 ++++++++++++++++++ 4 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt index fa95151e3b..5e55ebdd54 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletsDomainModule.kt @@ -1,9 +1,9 @@ package com.tangem.tap.di.domain import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.usecase.ParseSharedAddressUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase @@ -272,11 +272,11 @@ internal object WalletsDomainModule { @Singleton fun providesSetNotificationsEnabledUseCase( walletsRepository: WalletsRepository, - currenciesRepository: CurrenciesRepository, + accountsCRUDRepository: AccountsCRUDRepository, ): SetNotificationsEnabledUseCase { return SetNotificationsEnabledUseCase( walletsRepository = walletsRepository, - currenciesRepository = currenciesRepository, + accountsCRUDRepository = accountsCRUDRepository, ) } diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index b7b3798d96..f11102091a 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.domain.walletManager) implementation(projects.libs.blockchainSdk) implementation(projects.libs.tangemSdkApi) + implementation(projects.domain.account) implementation(projects.domain.models) implementation(projects.domain.tokens) implementation(projects.domain.card) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt index 5a66ceb5ab..c749d56049 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt @@ -1,13 +1,13 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository class SetNotificationsEnabledUseCase( private val walletsRepository: WalletsRepository, - private val currenciesRepository: CurrenciesRepository, + private val accountsCRUDRepository: AccountsCRUDRepository, ) { suspend operator fun invoke(userWalletId: UserWalletId, isEnabled: Boolean): Either = @@ -16,7 +16,7 @@ class SetNotificationsEnabledUseCase( userWalletId = userWalletId, isEnabled = isEnabled, ) - currenciesRepository.syncTokens(userWalletId) + accountsCRUDRepository.syncTokens(userWalletId) }.onLeft { walletsRepository.setNotificationsEnabled( userWalletId = userWalletId, diff --git a/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt new file mode 100644 index 0000000000..73d291ba22 --- /dev/null +++ b/domain/wallets/src/test/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCaseTest.kt @@ -0,0 +1,131 @@ +package com.tangem.domain.wallets.usecase + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.account.repository.AccountsCRUDRepository +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class SetNotificationsEnabledUseCaseTest { + + private lateinit var useCase: SetNotificationsEnabledUseCase + private lateinit var walletsRepository: WalletsRepository + private lateinit var accountsCRUDRepository: AccountsCRUDRepository + + @Before + fun setup() { + walletsRepository = mockk() + accountsCRUDRepository = mockk() + useCase = SetNotificationsEnabledUseCase( + walletsRepository = walletsRepository, + accountsCRUDRepository = accountsCRUDRepository, + ) + } + + @Test + fun `GIVEN notifications enabled successfully WHEN invoke THEN return Right with Unit`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = true + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isRight()).isTrue() + coVerifyOrder { + walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) + accountsCRUDRepository.syncTokens(userWalletId) + } + } + + @Test + fun `GIVEN notifications disabled successfully WHEN invoke THEN return Right with Unit`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = false + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isRight()).isTrue() + coVerifyOrder { + walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) + accountsCRUDRepository.syncTokens(userWalletId) + } + } + + @Test + fun `GIVEN setNotificationsEnabled throws exception WHEN invoke THEN return Left and revert notifications`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = true + val exception = RuntimeException("Network error") + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } throws exception + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + coVerify { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } + } + + @Test + fun `GIVEN syncTokens throws exception WHEN invoke THEN return Left and revert notifications`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = true + val exception = RuntimeException("Sync error") + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } just runs + coEvery { accountsCRUDRepository.syncTokens(userWalletId) } throws exception + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + coVerify { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } + } + + @Test + fun `GIVEN disabling notifications fails WHEN invoke THEN return Left and revert to enabled`() = runTest { + // GIVEN + val userWalletId = UserWalletId("0A0B0C0D") + val isEnabled = false + val exception = RuntimeException("Network error") + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, isEnabled) } throws exception + coEvery { walletsRepository.setNotificationsEnabled(userWalletId, !isEnabled) } just runs + + // WHEN + val result = useCase(userWalletId, isEnabled) + + // THEN + assertThat(result.isLeft()).isTrue() + result.onLeft { throwable -> + assertThat(throwable).isEqualTo(exception) + } + coVerify { walletsRepository.setNotificationsEnabled(userWalletId, true) } + } +} \ No newline at end of file From 55b6b5796375b3e2aeb8c15e9e3a977f971f230b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 19:37:46 +0300 Subject: [PATCH 11/20] Updated on 2026-08-14 --- .../swap/component/SwapFeeSelectorBlockComponent.kt | 10 +++++++++- .../java/com/tangem/feature/swap/model/SwapModel.kt | 13 +++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt index 19e1760238..96a5d81ee3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/component/SwapFeeSelectorBlockComponent.kt @@ -20,6 +20,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -55,7 +56,11 @@ class SwapFeeSelectorBlockComponent @AssistedInject constructor( init { params.repository.state - .onEach(feeSelectorBlockComponent::updateState) + .onEach { feeSelectorBlockComponent.updateState(it) } + .launchIn(componentScope) + + params.repository.forceUpdateState + .onEach { feeSelectorBlockComponent.updateState(it) } .launchIn(componentScope) } @@ -68,6 +73,9 @@ class SwapFeeSelectorBlockComponent @AssistedInject constructor( val state: StateFlow get() = MutableStateFlow(FeeSelectorUM.Loading) + val forceUpdateState: SharedFlow + get() = MutableStateFlow(FeeSelectorUM.Loading) + fun onResult(newState: FeeSelectorUM) suspend fun loadFee(): Either 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 4f2656df25..38be62641a 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 @@ -2325,6 +2325,8 @@ internal class SwapModel @Inject constructor( FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true), ) + override val forceUpdateState = MutableSharedFlow() + override suspend fun loadFeeExtended( selectedToken: CryptoCurrencyStatus?, ): Either { @@ -2357,18 +2359,13 @@ internal class SwapModel @Inject constructor( } override fun onResult(newState: FeeSelectorUM) { - if (isPermissionNotificationShown()) { - state.value = FeeSelectorUM.Error(GetFeeError.UnknownError, isHidden = true) - return - } - if (newState is FeeSelectorUM.Error) { - state.value = newState.copy(isHidden = true) + modelScope.launch { + forceUpdateState.emit(newState.copy(isHidden = true)) + } return } - state.value = newState - // If fee currency is same as from currency, we need to reload quotes to update fee info val isFeeCurrencySameAsFromCurrency = newState is FeeSelectorUM.Content && dataState.fromCryptoCurrency?.currency?.id == newState.feeExtraInfo.feeCryptoCurrencyStatus.currency.id From 60e697282b5e9b1bb923b9ce03388578dfe322ed Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 21:40:36 +0500 Subject: [PATCH 12/20] Updated on 2026-08-14 --- .../usecase/ManageCryptoCurrenciesUseCase.kt | 15 +++++++++++++-- .../referral/domain/ReferralInteractorImpl.kt | 12 +++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index d41defe9be..2bef64e9b1 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -59,14 +59,21 @@ class ManageCryptoCurrenciesUseCase( accountId: AccountId, add: CryptoCurrency? = null, remove: CryptoCurrency? = null, + skipDerivationErrors: Boolean = true, ): Either { - return invoke(accountId = accountId, add = listOfNotNull(add), remove = listOfNotNull(remove)) + return invoke( + accountId = accountId, + add = listOfNotNull(add), + remove = listOfNotNull(remove), + skipDerivationErrors = skipDerivationErrors, + ) } suspend operator fun invoke( accountId: AccountId, add: List = emptyList(), remove: List = emptyList(), + skipDerivationErrors: Boolean = true, ): Either = eitherOn(dispatchers.default) { if (add.isEmpty() && remove.isEmpty()) { Timber.d("No currencies to add or remove, skipping") @@ -89,7 +96,7 @@ class ManageCryptoCurrenciesUseCase( account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total), ) - derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) + val result = derivePublicKeys(userWalletId = userWalletId, currencies = modifiedCurrencyList.added) parallelUpdatingScope.launch { syncTokens(userWalletId, modifiedCurrencyList) @@ -98,6 +105,10 @@ class ManageCryptoCurrenciesUseCase( refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total) clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed) } + + if (!skipDerivationErrors) { + result.bind() + } } } diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index cfbdeea741..930418c920 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -73,7 +73,17 @@ internal class ReferralInteractorImpl( when (portfolioId) { is PortfolioId.Account -> { - manageCryptoCurrenciesUseCase(accountId = portfolioId.accountId, add = cryptoCurrency) + manageCryptoCurrenciesUseCase( + accountId = portfolioId.accountId, + add = cryptoCurrency, + skipDerivationErrors = false, + ).mapLeft { + it.mapToDomainError() + }.onLeft { error -> + if (error is ReferralError.UserCancelledException) { + throw error + } + } } is PortfolioId.Wallet -> { derivePublicKeysUseCase(userWallet.walletId, listOf(cryptoCurrency)).getOrElse { throwable -> From caa14291752daa6c99938780ebf958d07d852b70 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 23:46:56 +0700 Subject: [PATCH 13/20] Updated on 2026-08-14 --- .../domain/scanCard/LegacyScanProcessor.kt | 99 +++++-------------- .../sdk/impl/DefaultTangemSdkManager.kt | 1 + .../tasks/product/ResetBackupCardTask.kt | 1 + .../domain/tasks/product/ScanProductTask.kt | 10 ++ .../tap/domain/twins/FinalizeTwinTask.kt | 6 +- gradle/tangem_dependencies.toml | 2 +- 6 files changed, 41 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 95d2b8fe04..ad63237780 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -19,15 +19,15 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.toWrappedList import com.tangem.core.ui.message.dialog.Dialogs -import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.card.common.util.twinsIsTwinned +import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.sdk.extensions.localizedDescriptionRes import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.redux.AppDialog +import com.tangem.tap.common.extensions.dispatchNavigationAction +import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.onboarding.OnboardingHelper @@ -113,7 +113,6 @@ internal class LegacyScanProcessor @Inject constructor( onProgressStateChange = onProgressStateChange, onSuccess = onSuccess, onWalletNotCreated = onWalletNotCreated, - onCancel = onCancel, ) }, ) @@ -198,90 +197,38 @@ internal class LegacyScanProcessor @Inject constructor( scanResponse: ScanResponse, crossinline onProgressStateChange: suspend (showProgress: Boolean) -> Unit, crossinline onWalletNotCreated: suspend () -> Unit, - crossinline onCancel: suspend () -> Unit, crossinline onSuccess: suspend (ScanResponse) -> Unit, ) { - checkCardWasUsedInApp( - scanResponse = scanResponse, - onCancel = { - mainScope.launch { - onProgressStateChange.invoke(false) - onCancel() - } - }, - ) { - if (OnboardingHelper.isOnboardingCase(scanResponse)) { - trackingContextProxy.addContext(scanResponse) + if (OnboardingHelper.isOnboardingCase(scanResponse)) { + trackingContextProxy.addContext(scanResponse) + onWalletNotCreated() + navigateTo( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.Onboarding, + ), + ) { onProgressStateChange(it) } + } else { + trackingContextProxy.setContext(scanResponse) + + val wasTwinsOnboardingShown = + store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() + + if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { onWalletNotCreated() navigateTo( AppRoute.Onboarding( scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.Onboarding, + mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, ), ) { onProgressStateChange(it) } } else { - trackingContextProxy.setContext(scanResponse) - - val wasTwinsOnboardingShown = - store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() - - if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { - onWalletNotCreated() - navigateTo( - AppRoute.Onboarding( - scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, - ), - ) { onProgressStateChange(it) } - } else { - delay(DELAY_SDK_DIALOG_CLOSE) - onSuccess(scanResponse) - } + delay(DELAY_SDK_DIALOG_CLOSE) + onSuccess(scanResponse) } } } - /** - * Checks if card has password and never login at this app - * Show alert in this case - */ - private suspend fun checkCardWasUsedInApp( - scanResponse: ScanResponse, - onCancel: () -> Unit, - onSuccess: suspend () -> Unit, - ) { - val userWalletId = runCatching { UserWalletIdBuilder.card(scanResponse.card).build() }.getOrNull() - if (userWalletId == null) { - onSuccess() - return - } - - val userTokensResponseStore = store.inject(DaggerGraphState::userTokensResponseStore) - val tokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - - if (scanResponse.card.isAccessCodeSet && tokens == null) { - store.dispatchDialogShow( - AppDialog.WalletAlreadyWasUsedDialog( - onOk = { mainScope.launch { onSuccess() } }, - onSupportClick = { - val cardInfo = - store.inject(DaggerGraphState::getWalletMetaInfoUseCase).invoke(scanResponse).getOrNull() - ?: error("CardInfo must be not null") - - scope.launch { - store.inject(DaggerGraphState::sendFeedbackEmailUseCase) - .invoke(type = FeedbackEmailType.PreActivatedWallet(cardInfo)) - } - onCancel() - }, - onCancel = { onCancel() }, - ), - ) - } else { - onSuccess() - } - } - private suspend inline fun navigateTo(route: AppRoute, onProgressStateChange: (showProgress: Boolean) -> Unit) { delay(DELAY_SDK_DIALOG_CLOSE) store.dispatchNavigationAction { push(route) } 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 11a3ffb59b..a9c061c5f7 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 @@ -176,6 +176,7 @@ internal class DefaultTangemSdkManager( allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, visaCardScanHandler = visaCardScanHandler, visaCoroutineScope = this, + shouldCheckIsAlreadyActivated = true, onboardingV2FeatureToggles = onboardingV2FeatureToggles, ), cardId = cardId, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt index bb0fafa039..f1a1683517 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ResetBackupCardTask.kt @@ -27,6 +27,7 @@ internal class ResetBackupCardTask( PreflightReadTask( readMode = PreflightReadMode.FullCardRead, filter = UserWalletIdPreflightReadFilter(expectedUserWalletId = userWalletId), + secureStorage = session.environment.secureStorage, ).run(session) { result -> when (result) { is CompletionResult.Success -> resetCard(session, callback) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 93a1c1f2be..4c334fd027 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -29,6 +29,7 @@ import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles +import com.tangem.operations.PreflightReadMode import com.tangem.operations.ScanTask import com.tangem.operations.backup.PrimaryCard import com.tangem.operations.backup.StartPrimaryCardLinkingTask @@ -52,6 +53,7 @@ internal class ScanProductTask( private val visaCardScanHandler: VisaCardScanHandler?, private val visaCoroutineScope: CoroutineScope?, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?, + private val shouldCheckIsAlreadyActivated: Boolean, override val allowsRequestAccessCodeFromRepository: Boolean = false, ) : CardSessionRunnable { @@ -109,6 +111,14 @@ internal class ScanProductTask( } } + override fun preflightReadMode(): PreflightReadMode { + return if (shouldCheckIsAlreadyActivated) { + PreflightReadMode.FullCardReadWithAccessCodeCheck + } else { + return super.preflightReadMode() + } + } + private fun getErrorIfExcludedCard(cardDto: CardDTO, card: Card): TangemError? { if (cardDto.isExcluded) return TapSdkError.CardForDifferentApp() if (cardDto.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease() diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 2b20696ee9..0f7ba29f04 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -20,7 +20,10 @@ class FinalizeTwinTask( WriteProtectedIssuerDataTask(twinPublicKey, issuerKeys).run(session) { result -> when (result) { is CompletionResult.Success -> - PreflightReadTask(PreflightReadMode.FullCardRead).run(session) { readResult -> + PreflightReadTask( + readMode = PreflightReadMode.FullCardRead, + secureStorage = session.environment.secureStorage, + ).run(session) { readResult -> when (readResult) { is CompletionResult.Success -> ScanProductTask( @@ -28,6 +31,7 @@ class FinalizeTwinTask( derivationsFinder = null, visaCardScanHandler = null, visaCoroutineScope = null, + shouldCheckIsAlreadyActivated = false, onboardingV2FeatureToggles = null, ).run(session, callback) is CompletionResult.Failure -> diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index a0d719fc6b..283bf15238 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-579" +tangemCardSdk = "releases-5.34-580" #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 474d4b4fb4fad52ee447d60c566acb067b51cb6a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 19:51:42 +0300 Subject: [PATCH 14/20] 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 2d18650f6d..e4fac168bd 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 2d18650f6d4286353046ccb841745cef107c4fa6 +Subproject commit e4fac168bd941fe90b0f081dfa70ea1b4148b49f From cd504ed349a3d8c31a6aa773fd374bd229bac429 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 22:14:43 +0000 Subject: [PATCH 15/20] 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 4372af7f4b..283bf15238 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-1423" +tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.33-576" +tangemCardSdk = "releases-5.34-580" #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-545" +tangemHotSdk = "develop-539" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^ From dcd728503ef1478de5a59ad15e3e897770aa090c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 11:17:10 +0200 Subject: [PATCH 16/20] Updated on 2026-08-14 --- tangem-android-tools | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tangem-android-tools b/tangem-android-tools index 35755ad66f..43fab6f690 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit 35755ad66f5f8fa6bcb1c84b443826cd315495d0 +Subproject commit 43fab6f690538391cae17e046ffb2ec9fe08b0c7 From 6e2ad61589872a2f136b0813fcc78c4b2885d210 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 16:36:14 +0500 Subject: [PATCH 17/20] Updated on 2026-08-14 --- .../presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f92476350d..11aaab2b47 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 @@ -33,7 +33,7 @@ sealed class WalletScreenAnalyticsEvent { put(AnalyticsParam.BALANCE, balance.value) tokensCount?.let { put(AnalyticsParam.TOKENS_COUNT, it.toString()) } }, - ) + ), AppsFlyerIncludedEvent class TokenBalance(balance: AnalyticsParam.EmptyFull, token: String) : Basic( event = "Token Balance", From 84153dd63b7c27e55d2ba314119e3b37602431fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 16:38:32 +0500 Subject: [PATCH 18/20] Updated on 2026-08-14 --- .../tap/di/domain/HotWalletDomainModule.kt | 9 ++++++ .../hotwallet/DefaultHotWalletRepository.kt | 7 ++--- .../CheckHotWalletUpgradeBannerUseCase.kt | 2 +- ...GetUpgradeBannerClosureTimestampUseCase.kt | 13 +++++++++ .../repository/HotWalletRepository.kt | 2 +- .../CheckHotWalletUpgradeBannerUseCaseTest.kt | 28 ++++++++++--------- .../domain/GetMultiWalletWarningsFactory.kt | 12 ++++++-- 7 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt index c99860711a..f3ee5bef10 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.GetUpgradeBannerClosureTimestampUseCase import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase @@ -67,4 +68,12 @@ internal object HotWalletDomainModule { ): ShouldShowUpgradeHotWalletBannerUseCase { return ShouldShowUpgradeHotWalletBannerUseCase(hotWalletRepository) } + + @Provides + @Singleton + fun provideGetUpgradeBannerClosureTimestampUseCase( + hotWalletRepository: HotWalletRepository, + ): GetUpgradeBannerClosureTimestampUseCase { + return GetUpgradeBannerClosureTimestampUseCase(hotWalletRepository) + } } \ No newline at end of file diff --git a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt index dbbe66db29..c112597c17 100644 --- a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt +++ b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt @@ -53,10 +53,9 @@ internal class DefaultHotWalletRepository( } } - override suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? { - return appPreferencesStore - .getObjectMapSync(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY)[userWalletId.stringValue] - } + override fun upgradeBannerClosureTimestamp(userWalletId: UserWalletId): Flow = appPreferencesStore + .getObjectMap(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY) + .map { it[userWalletId.stringValue] } override suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) { appPreferencesStore.editData { mutablePreferences -> diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt index 1038773b7c..28f7be6bd3 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt @@ -14,6 +14,7 @@ class CheckHotWalletUpgradeBannerUseCase( walletId: UserWalletId, hasBalance: Boolean, shouldShowUpgradeBanner: Boolean, + closureTimestamp: Long?, ): Either = try { val currentTime = System.currentTimeMillis() val creationTimestamp = hotWalletRepository.getWalletCreationTimestamp(walletId) @@ -27,7 +28,6 @@ class CheckHotWalletUpgradeBannerUseCase( creationTimestamp } - val closureTimestamp = hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(walletId) val daysSinceCreation = TimeUnit.MILLISECONDS.toDays(currentTime - creationTimestampActual) diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt new file mode 100644 index 0000000000..dbe9d2f2af --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.hotwallet + +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +class GetUpgradeBannerClosureTimestampUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow { + return hotWalletRepository.upgradeBannerClosureTimestamp(userWalletId) + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt index 01229d8d3a..2d63b01cde 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt @@ -17,7 +17,7 @@ interface HotWalletRepository { suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) - suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? + fun upgradeBannerClosureTimestamp(userWalletId: UserWalletId): Flow suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) diff --git a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt index 6e415efea4..447864f896 100644 --- a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt +++ b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt @@ -23,7 +23,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { @Test fun `GIVEN creation timestamp is null WHEN invoke THEN set timestamp and return false`() = runTest { coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns null - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -31,6 +30,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -42,7 +42,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN shouldShowUpgradeBanner is true and hasBalance WHEN invoke THEN return true`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -50,6 +49,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = true, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -60,7 +60,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN shouldShowUpgradeBanner is true WHEN invoke THEN return true regardless of balance`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -68,6 +67,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = true, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -79,7 +79,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -87,6 +86,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = false, + closureTimestamp = closureTimestamp, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -99,7 +99,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -107,7 +106,8 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = false, - ) + closureTimestamp = closureTimestamp, + ) assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isFalse() @@ -117,7 +117,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN no flags set and no closure and 30 days since creation WHEN invoke THEN return true`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -125,6 +124,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -135,7 +135,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN no flags set but less than 30 days since creation WHEN invoke THEN return false`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -143,6 +142,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -154,7 +154,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -162,6 +161,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = closureTimestamp, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -172,7 +172,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN first top-up detected WHEN invoke THEN return false and mark session`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -180,6 +179,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -194,7 +194,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN first top-up detected this session WHEN invoke THEN return false`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns true @@ -202,6 +201,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = true, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -212,7 +212,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN already had first top-up WHEN invoke with balance THEN do not set flags again`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -220,6 +219,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = true, + closureTimestamp = null, ) coVerify(exactly = 0) { hotWalletRepository.setHasHadFirstTopUp(any(), any()) } @@ -230,7 +230,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN multiple re-emissions with same state WHEN invoke THEN return same result`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -238,16 +237,19 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) val result2 = useCase( walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) val result3 = useCase( walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat((result1 as Either.Right).value).isTrue() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 6218c6257a..a243ddef48 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -15,6 +15,7 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.GetUpgradeBannerClosureTimestampUseCase import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance @@ -62,10 +63,11 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val accountDependencies: AccountDependencies, private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, private val shouldShowUpgradeHotWalletBannerUseCase: ShouldShowUpgradeHotWalletBannerUseCase, + private val getUpgradeBannerClosureTimestampUseCase: GetUpgradeBannerClosureTimestampUseCase, private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase, ) { - @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod") + @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType") fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver @@ -105,8 +107,10 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( .distinctUntilChanged(), shouldShowUpgradeHotWalletBannerUseCase.invoke(userWallet.walletId) .distinctUntilChanged(), + getUpgradeBannerClosureTimestampUseCase(userWallet.walletId) + .distinctUntilChanged(), ) { array -> array } - .combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) } + .combine(tokenListFlow()) { array, any: Any? -> arrayOf(any).plus(elements = array) } .map { array -> val lceTokens = array[0] as Lce>> val totalFiatBalance = lceTokens.map { it.first } @@ -119,6 +123,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldAccessCodeSkipped = array[6] as Boolean val shouldShowYieldPromo = array[7] as Boolean val shouldShowUpgradeBanner = array[8] as Boolean + val closureTimestamp = array[9] as? Long buildList { addUsedOutdatedDataNotification(totalFiatBalance) @@ -130,6 +135,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( flattenCurrencies = flattenCurrencies, clickIntents = clickIntents, shouldShowUpgradeBanner = shouldShowUpgradeBanner, + closureTimestamp = closureTimestamp, ) addFinishWalletActivationNotification( @@ -469,6 +475,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( flattenCurrencies: Lce>, clickIntents: WalletClickIntents, shouldShowUpgradeBanner: Boolean, + closureTimestamp: Long?, ) { if (userWallet !is UserWallet.Hot) return @@ -479,6 +486,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( walletId = userWallet.walletId, hasBalance = hasBalance, shouldShowUpgradeBanner = shouldShowUpgradeBanner, + closureTimestamp = closureTimestamp, ).getOrNull() ?: return addIf( From 8b02a0825abe5285b43beae86417d50ad612d868 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 14:40:16 +0300 Subject: [PATCH 19/20] 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 283bf15238..423909e1bc 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-580" +tangemCardSdk = "releases-5.34-581" #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 3b86b97c12e39068946d3349cf1212923a8096ac Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 11:40:46 +0000 Subject: [PATCH 20/20] 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 423909e1bc..ae62f1e18f 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-581" +tangemCardSdk = "develop-578" #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 ^