Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-01 13:07:10 +05:00
parent 2b000c1c72
commit 2415a8a37b
2 changed files with 172 additions and 0 deletions

View file

@ -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 {

View file

@ -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<CryptoCurrency> {
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<String, String>,
@ -479,6 +628,7 @@ class DefaultTokenDetailsDeepLinkHandlerTest {
getUserWalletUseCase = getUserWalletUseCase,
walletBalanceFetcher = walletBalanceFetcher,
singleAccountListSupplier = singleAccountListSupplier,
singleAccountListFetcher = singleAccountListFetcher,
getSelectedWalletSyncUseCase = getSelectedWalletSync,
)
}