From 30a9e37c940d46c84051dabfc38c24d9fc8940ef Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Apr 2026 11:43:39 +0100 Subject: [PATCH] Updated on 2026-08-14 --- features/tokendetails/impl/build.gradle.kts | 1 + .../DefaultTokenDetailsDeepLinkHandler.kt | 8 +- .../DefaultTokenDetailsDeepLinkHandlerTest.kt | 482 ++++++++++++++++++ 3 files changed, 485 insertions(+), 6 deletions(-) create mode 100644 features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index fb3369069e..c47f2c68ed 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -121,4 +121,5 @@ dependencies { testRuntimeOnly(deps.test.junit5.engine) testImplementation(deps.test.mockk) testImplementation(deps.test.truth) + testImplementation(deps.test.coroutine) } \ No newline at end of file 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 3a3054beab..5013ca11f5 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,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, 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 new file mode 100644 index 0000000000..fd2e01e58b --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandlerTest.kt @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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, + 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, + ) + } +} \ No newline at end of file