From 2415a8a37b2724f60bc08525f42429746dd35881 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jun 2026 13:07:10 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../DefaultTokenDetailsDeepLinkHandler.kt | 22 +++ .../DefaultTokenDetailsDeepLinkHandlerTest.kt | 150 ++++++++++++++++++ 2 files changed, 172 insertions(+) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt index e9ee88d2bd..42605a81a8 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt @@ -9,6 +9,7 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.models.currency.CryptoCurrency @@ -47,6 +48,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val walletBalanceFetcher: WalletBalanceFetcher, private val singleAccountListSupplier: SingleAccountListSupplier, + private val singleAccountListFetcher: SingleAccountListFetcher, ) : TokenDetailsDeepLinkHandler { init { @@ -81,6 +83,9 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( } } + // Refresh the portfolio before searching so a token just added on the backend is present locally. + refreshAccountsIfNeeded(userWallet) + val cryptoCurrency = findCryptoCurrency(userWallet = userWallet, networkId = networkId, tokenId = tokenId) if (cryptoCurrency == null) { @@ -91,6 +96,8 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( |- $TOKEN_ID_KEY: $tokenId """.trimIndent(), ) + // Token is not in the response (not indexed yet / backend error): go to main, do not add. + appRouter.popTo(AppRoute.Wallet) return@launch } @@ -123,6 +130,21 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( } } + /** + * Refreshes wallet accounts so a token just added on the backend appears in the local portfolio. + * + * Only when the app was open on push tap ([isFromOnNewIntent]) and the wallet is multi-currency: + * on cold start the fresh list is already loaded by the regular auth flow, and single-currency + * wallets have a fixed token. The fetch is best-effort — on failure we fall through and try the + * current cache, so existing tokens (e.g. swap/onramp pushes) still open without regression. + */ + private suspend fun refreshAccountsIfNeeded(userWallet: UserWallet) { + if (isFromOnNewIntent && userWallet.isMultiCurrency) { + singleAccountListFetcher(SingleAccountListFetcher.Params(userWalletId = userWallet.walletId)) + .onLeft { TangemLogger.e("Error on refreshing wallet accounts", it) } + } + } + private suspend fun fetchCurrency(userWallet: UserWallet, cryptoCurrency: CryptoCurrency) { val isMultiCurrency = userWallet.isMultiCurrency when { diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt index 079ec97e5a..deefc4ebdf 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt @@ -10,6 +10,7 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher import com.tangem.domain.account.supplier.SingleAccountListSupplier @@ -50,6 +51,7 @@ class DefaultTokenDetailsDeepLinkHandlerTest { private val getUserWalletUseCase: GetUserWalletUseCase = mockk() private val walletBalanceFetcher: WalletBalanceFetcher = mockk() private val singleAccountListSupplier: SingleAccountListSupplier = mockk() + private val singleAccountListFetcher: SingleAccountListFetcher = mockk() @BeforeEach fun setUp() { @@ -57,6 +59,8 @@ class DefaultTokenDetailsDeepLinkHandlerTest { mockkObject(TangemLogger) every { analyticsEventHandler.send(any()) } just Runs every { appRouter.push(any(), any()) } just Runs + every { appRouter.popTo(route = any(), onComplete = any()) } just Runs + coEvery { singleAccountListFetcher.invoke(any()) } returns Either.Right(Unit) val userWallet: UserWallet = mockk() every { userWallet.walletId } returns mockk() every { getSelectedWalletSync() } returns Either.Right( @@ -461,6 +465,151 @@ class DefaultTokenDetailsDeepLinkHandlerTest { } } + @Test + fun `GIVEN multicurrency wallet AND isFromOnNewIntent WHEN handle deeplink THEN refresh wallet accounts`() = + runTest { + val userWalletId = UserWalletId("011") + val cryptoCurrency = mockCryptoCurrency() + mockMultiCurrencyWallet(userWalletId) + mockSelectWallet(userWalletId) + coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = listOf(cryptoCurrency), + ) + every { + cryptoCurrencyBalanceFetcher.invoke(userWalletId = userWalletId, currency = cryptoCurrency) + } just Runs + + createHandler(scope = this, defaultQueryParams(), isFromOnNewIntent = true) + advanceUntilIdle() + + coVerify { singleAccountListFetcher.invoke(SingleAccountListFetcher.Params(userWalletId)) } + } + + @Test + fun `GIVEN multicurrency wallet AND NOT isFromOnNewIntent WHEN handle deeplink THEN do not refresh accounts`() = + runTest { + val userWalletId = UserWalletId("011") + val cryptoCurrency = mockCryptoCurrency() + mockMultiCurrencyWallet(userWalletId) + mockSelectWallet(userWalletId) + coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = listOf(cryptoCurrency), + ) + + createHandler(scope = this, defaultQueryParams(), isFromOnNewIntent = false) + advanceUntilIdle() + + coVerify(exactly = 0) { singleAccountListFetcher.invoke(any()) } + } + + @Test + fun `GIVEN single currency wallet AND isFromOnNewIntent WHEN handle deeplink THEN do not refresh accounts`() = + runTest { + val userWalletId = UserWalletId("011") + val cryptoCurrency = mockCryptoCurrency() + mockSingleCurrencyWallet(userWalletId) + mockSelectWallet(userWalletId) + coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = listOf(cryptoCurrency), + ) + every { walletDeepLinkActionTrigger.selectWallet(userWalletId) } just Runs + coEvery { + walletBalanceFetcher.invoke(WalletBalanceFetcher.Params(userWalletId = userWalletId)) + } returns mockk() + + createHandler(scope = this, defaultQueryParams(), isFromOnNewIntent = true) + advanceUntilIdle() + + coVerify(exactly = 0) { singleAccountListFetcher.invoke(any()) } + } + + @Test + fun `GIVEN crypto not found WHEN handle deeplink THEN redirect to main`() = runTest { + val userWalletId = UserWalletId("011") + mockMultiCurrencyWallet(userWalletId) + mockSelectWallet(userWalletId) + coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns null + + createHandler(scope = this, defaultQueryParams(), isFromOnNewIntent = true) + advanceUntilIdle() + + verify { appRouter.popTo(route = AppRoute.Wallet, onComplete = any()) } + } + + @Test + fun `GIVEN refresh failed AND token in cache WHEN handle deeplink THEN push new route`() = runTest { + val userWalletId = UserWalletId("011") + val cryptoCurrency = mockCryptoCurrency() + mockMultiCurrencyWallet(userWalletId) + mockSelectWallet(userWalletId) + coEvery { + singleAccountListFetcher.invoke(SingleAccountListFetcher.Params(userWalletId)) + } returns Either.Left(IllegalStateException("service unavailable")) + coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty( + userWalletId = userWalletId, + cryptoCurrencies = listOf(cryptoCurrency), + ) + every { + cryptoCurrencyBalanceFetcher.invoke(userWalletId = userWalletId, currency = cryptoCurrency) + } just Runs + val expectedRoute = AppRoute.CurrencyDetails(userWalletId = userWalletId, currency = cryptoCurrency) + + createHandler(scope = this, defaultQueryParams(), isFromOnNewIntent = true) + advanceUntilIdle() + + verify { + appRouter.push(route = expectedRoute, onComplete = any()) + } + } + + private fun defaultQueryParams() = mapOf( + WALLET_ID_KEY to "011", + NETWORK_ID_KEY to "123", + TOKEN_ID_KEY to "321", + DERIVATION_PATH_KEY to "777", + ) + + private fun mockCryptoCurrency() = mockk { + every { network } returns mockk { + every { rawId } returns "123" + every { derivationPath } returns Network.DerivationPath.Card(value = "777") + } + every { id } returns CryptoCurrency.ID( + prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, + body = CryptoCurrency.ID.Body.NetworkIdWithDerivationPath(rawId = "321", derivationPath = "777"), + suffix = CryptoCurrency.ID.Suffix.RawID("321"), + ) + } + + private fun mockMultiCurrencyWallet(userWalletId: UserWalletId) { + every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right( + value = mockk { + every { isMultiCurrency } returns true + every { walletId } returns userWalletId + every { isLocked } returns false + }, + ) + } + + private fun mockSingleCurrencyWallet(userWalletId: UserWalletId) { + every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right( + value = mockk { + every { isMultiCurrency } returns false + every { walletId } returns userWalletId + every { isLocked } returns false + }, + ) + } + + private fun mockSelectWallet(userWalletId: UserWalletId) { + coEvery { selectWalletUseCase.invoke(userWalletId) } returns Either.Right( + value = mockk { every { walletId } returns userWalletId }, + ) + } + private fun createHandler( scope: CoroutineScope, queryParams: Map, @@ -479,6 +628,7 @@ class DefaultTokenDetailsDeepLinkHandlerTest { getUserWalletUseCase = getUserWalletUseCase, walletBalanceFetcher = walletBalanceFetcher, singleAccountListSupplier = singleAccountListSupplier, + singleAccountListFetcher = singleAccountListFetcher, getSelectedWalletSyncUseCase = getSelectedWalletSync, ) }