Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-14 12:03:05 +01:00
commit da66fd41ac
3 changed files with 485 additions and 6 deletions

View file

@ -121,4 +121,5 @@ dependencies {
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
testImplementation(deps.test.coroutine)
}

View file

@ -9,8 +9,8 @@ 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.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
@ -18,7 +18,6 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.notifications.models.NotificationType
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
@ -26,12 +25,12 @@ import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnaly
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionTrigger
import com.tangem.utils.logging.TangemLogger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import com.tangem.utils.logging.TangemLogger
@Suppress("LongParameterList")
internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
@ -118,9 +117,6 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
private suspend fun fetchCurrency(userWallet: UserWallet, cryptoCurrency: CryptoCurrency) {
val isMultiCurrency = userWallet.isMultiCurrency
// single-currency wallet with token (NODL)
userWallet is UserWallet.Cold &&
userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
when {
isMultiCurrency -> cryptoCurrencyBalanceFetcher(
userWalletId = userWallet.walletId,

View file

@ -0,0 +1,482 @@
package com.tangem.feature.tokendetails.deeplink
import arrow.core.Either
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.deeplink.DeeplinkConst.DERIVATION_PATH_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.NETWORK_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
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.models.AccountList
import com.tangem.domain.account.status.utils.CryptoCurrencyBalanceFetcher
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.common.wallets.error.SelectWalletError
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.wallets.models.GetUserWalletError
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SelectWalletUseCase
import com.tangem.features.tangempay.TangemPayFeatureToggles
import com.tangem.features.wallet.deeplink.WalletDeepLinkActionTrigger
import com.tangem.utils.logging.TangemLogger
import io.mockk.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultTokenDetailsDeepLinkHandlerTest {
private val appRouter: AppRouter = mockk()
private val selectWalletUseCase: SelectWalletUseCase = mockk()
private val cryptoCurrencyBalanceFetcher: CryptoCurrencyBalanceFetcher = mockk()
private val tokenDetailsDeepLinkActionTrigger: TokenDetailsDeepLinkActionTrigger = mockk()
private val walletDeepLinkActionTrigger: WalletDeepLinkActionTrigger = mockk()
private val analyticsEventHandler: AnalyticsEventHandler = mockk()
private val getUserWalletUseCase: GetUserWalletUseCase = mockk()
private val walletBalanceFetcher: WalletBalanceFetcher = mockk()
private val tangemPayFeatureToggles: TangemPayFeatureToggles = mockk()
private val singleAccountListSupplier: SingleAccountListSupplier = mockk()
@BeforeEach
fun setUp() {
MockKAnnotations.init(this)
mockkObject(TangemLogger)
every { analyticsEventHandler.send(any()) } just Runs
every { appRouter.push(any(), any()) } just Runs
}
@Test
fun `GIVEN error instead of user wallet WHEN handle deeplink THEN get error`() = runTest {
val queryParams = mapOf(WALLET_ID_KEY to "011")
every {
getUserWalletUseCase.invoke(
userWalletId = UserWalletId(
"011"
)
)
} returns Either.Left(
value = GetUserWalletError.UserWalletNotFound
)
every { TangemLogger.e("Error on getting user wallet") } just Runs
createHandler(scope = this, queryParams)
advanceUntilIdle()
verify { TangemLogger.e("Error on getting user wallet") }
}
@Test
fun `GIVEN locked user wallet WHEN handle deeplink THEN get error`() = runTest {
val queryParams = mapOf(WALLET_ID_KEY to "011")
every {
getUserWalletUseCase.invoke(
userWalletId = UserWalletId(
"011"
)
)
} returns Either.Right(
value = mockk { every { isLocked } returns true }
)
every { TangemLogger.e("Error on getting user wallet") } just Runs
createHandler(scope = this, queryParams)
advanceUntilIdle()
verify { TangemLogger.e("Error on getting user wallet") }
}
@Test
fun `GIVEN error instead select wallet WHEN handle deeplink THEN get error`() = runTest {
val queryParams = mapOf(WALLET_ID_KEY to "011")
val userWalletId = UserWalletId("011")
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk { every { isLocked } returns false }
)
coEvery { selectWalletUseCase.invoke(userWalletId) } returns Either.Left(
value = SelectWalletError.UnableToSelectUserWallet
)
every { TangemLogger.e("Error on selecting user wallet") } just Runs
createHandler(scope = this, queryParams)
advanceUntilIdle()
verify { TangemLogger.e("Error on selecting user wallet") }
}
@Test
fun `GIVEN no crypto by wallet WHEN handle deeplink THEN get error`() = runTest {
val queryParams = mapOf(
WALLET_ID_KEY to "011",
NETWORK_ID_KEY to "123",
TOKEN_ID_KEY to "321",
)
val userWalletId = UserWalletId("011")
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { isMultiCurrency } returns false
every { walletId } returns userWalletId
every { isLocked } returns false
}
)
coEvery { selectWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { walletId } returns userWalletId
}
)
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns null
val expectedErrorText = """
Could not get crypto currency for
|- $NETWORK_ID_KEY: 123
|- $TOKEN_ID_KEY: 321
""".trimIndent()
every { TangemLogger.e(messageString = expectedErrorText) } just Runs
createHandler(scope = this, queryParams)
advanceUntilIdle()
verify { TangemLogger.e(messageString = expectedErrorText) }
}
@Test
fun `GIVEN multicurrency wallet WHEN handle deeplink THEN push new route`() = runTest {
val queryParams = mapOf(
WALLET_ID_KEY to "011",
NETWORK_ID_KEY to "123",
TOKEN_ID_KEY to "321",
DERIVATION_PATH_KEY to "777"
)
val userWalletId = UserWalletId("011")
val expectedCryptoCurrency = 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")
)
}
val expectedRoute = AppRoute.CurrencyDetails(
userWalletId = userWalletId,
currency = expectedCryptoCurrency,
)
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { isMultiCurrency } returns true
every { walletId } returns userWalletId
every { isLocked } returns false
}
)
coEvery { selectWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { walletId } returns userWalletId
}
)
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(expectedCryptoCurrency),
)
createHandler(scope = this, queryParams)
advanceUntilIdle()
verify {
appRouter.push(
route = expectedRoute,
onComplete = any(),
)
}
}
@Test
fun `GIVEN single currency wallet WHEN handle deeplink THEN push new route`() = runTest {
val queryParams = mapOf(
WALLET_ID_KEY to "011",
NETWORK_ID_KEY to "123",
TOKEN_ID_KEY to "321",
DERIVATION_PATH_KEY to "777"
)
val userWalletId = UserWalletId("011")
val expectedCryptoCurrency = 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")
)
}
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { isMultiCurrency } returns false
every { walletId } returns userWalletId
every { isLocked } returns false
}
)
coEvery { selectWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { walletId } returns userWalletId
}
)
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(expectedCryptoCurrency),
)
every { walletDeepLinkActionTrigger.selectWallet(userWalletId) } just Runs
createHandler(scope = this, queryParams)
advanceUntilIdle()
verify {
walletDeepLinkActionTrigger.selectWallet(userWalletId)
}
}
@ParameterizedTest
@ValueSource(strings = ["swap_status_update", "onramp_status_update"])
fun `GIVEN type WHEN handle deeplink THEN token details deeplink triggered`(type: String) = runTest {
val queryParams = mapOf(
WALLET_ID_KEY to "011",
NETWORK_ID_KEY to "123",
TOKEN_ID_KEY to "321",
DERIVATION_PATH_KEY to "777",
TRANSACTION_ID_KEY to "000",
TYPE_KEY to type,
)
val userWalletId = UserWalletId("011")
val expectedCryptoCurrency = 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")
)
}
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { isMultiCurrency } returns false
every { walletId } returns userWalletId
every { isLocked } returns false
}
)
coEvery { selectWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { walletId } returns userWalletId
}
)
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(expectedCryptoCurrency),
)
every { walletDeepLinkActionTrigger.selectWallet(userWalletId) } just Runs
coEvery { tokenDetailsDeepLinkActionTrigger.trigger("000") } just Runs
createHandler(scope = this, queryParams)
advanceUntilIdle()
coVerify {
tokenDetailsDeepLinkActionTrigger.trigger("000")
}
}
@ParameterizedTest
@ValueSource(strings = ["income_transaction", "promo", "unknown"])
fun `GIVEN type WHEN handle deeplink THEN token details deeplink not triggered`(type: String) = runTest {
val queryParams = mapOf(
WALLET_ID_KEY to "011",
NETWORK_ID_KEY to "123",
TOKEN_ID_KEY to "321",
DERIVATION_PATH_KEY to "777",
TRANSACTION_ID_KEY to "000",
TYPE_KEY to type,
)
val userWalletId = UserWalletId("011")
val expectedCryptoCurrency = 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")
)
}
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { isMultiCurrency } returns false
every { walletId } returns userWalletId
every { isLocked } returns false
}
)
coEvery { selectWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { walletId } returns userWalletId
}
)
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(expectedCryptoCurrency),
)
every { walletDeepLinkActionTrigger.selectWallet(userWalletId) } just Runs
coEvery { tokenDetailsDeepLinkActionTrigger.trigger("000") } just Runs
createHandler(scope = this, queryParams)
advanceUntilIdle()
coVerify(exactly = 0) {
tokenDetailsDeepLinkActionTrigger.trigger("000")
}
}
@Test
fun `GIVEN multicurrency wallet AND isFromOnNewIntent WHEN handle deeplink THEN fetch currency`() = runTest {
val queryParams = mapOf(
WALLET_ID_KEY to "011",
NETWORK_ID_KEY to "123",
TOKEN_ID_KEY to "321",
DERIVATION_PATH_KEY to "777"
)
val userWalletId = UserWalletId("011")
val expectedCryptoCurrency = 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")
)
}
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { isMultiCurrency } returns true
every { walletId } returns userWalletId
every { isLocked } returns false
}
)
coEvery { selectWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { walletId } returns userWalletId
}
)
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(expectedCryptoCurrency),
)
every {
cryptoCurrencyBalanceFetcher.invoke(userWalletId = userWalletId, currency = expectedCryptoCurrency)
} just Runs
createHandler(scope = this, queryParams, isFromOnNewIntent = true)
advanceUntilIdle()
verify { cryptoCurrencyBalanceFetcher.invoke(userWalletId = userWalletId, currency = expectedCryptoCurrency) }
}
@Test
fun `GIVEN single currency wallet AND isFromOnNewIntent WHEN handle deeplink THEN fetch currency`() = runTest {
val queryParams = mapOf(
WALLET_ID_KEY to "011",
NETWORK_ID_KEY to "123",
TOKEN_ID_KEY to "321",
DERIVATION_PATH_KEY to "777"
)
val userWalletId = UserWalletId("011")
val expectedCryptoCurrency = 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")
)
}
every { getUserWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { isMultiCurrency } returns false
every { walletId } returns userWalletId
every { isLocked } returns false
}
)
coEvery { selectWalletUseCase.invoke(userWalletId) } returns Either.Right(
value = mockk {
every { walletId } returns userWalletId
}
)
coEvery { singleAccountListSupplier.getSyncOrNull(userWalletId) } returns AccountList.empty(
userWalletId = userWalletId,
cryptoCurrencies = listOf(expectedCryptoCurrency),
)
every { tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled } returns true
coEvery {
walletBalanceFetcher.invoke(
WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = true
)
)
} returns mockk()
every { walletDeepLinkActionTrigger.selectWallet(userWalletId) } just Runs
createHandler(scope = this, queryParams, isFromOnNewIntent = true)
advanceUntilIdle()
coEvery {
walletBalanceFetcher.invoke(
WalletBalanceFetcher.Params(
userWalletId = userWalletId,
isPaymentAccountRefactorEnabled = true
)
)
}
}
private fun createHandler(
scope: CoroutineScope,
queryParams: Map<String, String>,
isFromOnNewIntent: Boolean = false,
) {
DefaultTokenDetailsDeepLinkHandler(
scope = scope,
queryParams = queryParams,
isFromOnNewIntent = isFromOnNewIntent,
appRouter = appRouter,
selectWalletUseCase = selectWalletUseCase,
cryptoCurrencyBalanceFetcher = cryptoCurrencyBalanceFetcher,
tokenDetailsDeepLinkActionTrigger = tokenDetailsDeepLinkActionTrigger,
walletDeepLinkActionTrigger = walletDeepLinkActionTrigger,
analyticsEventHandler = analyticsEventHandler,
getUserWalletUseCase = getUserWalletUseCase,
walletBalanceFetcher = walletBalanceFetcher,
tangemPayFeatureToggles = tangemPayFeatureToggles,
singleAccountListSupplier = singleAccountListSupplier,
)
}
}