From 5f3c2c337a31cb8e82b5fef88b15228769f2818c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 May 2025 19:20:22 +0500 Subject: [PATCH 1/4] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 19 +++++++++++++++++-- .../pushes/TangemPushNotificationService.kt | 2 ++ .../tap/routing/utils/DeepLinkFactory.kt | 13 ------------- .../tangem/core/deeplink/DeepLinksRegistry.kt | 1 + .../com/tangem/utils/extensions/StringExt.kt | 12 ++++++++++++ 5 files changed, 32 insertions(+), 15 deletions(-) create mode 100644 core/utils/src/main/java/com/tangem/utils/extensions/StringExt.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 9e44bb3dc2..8a5266c470 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -33,7 +33,9 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.di.RootAppComponentContext import com.tangem.core.deeplink.DEEPLINK_KEY import com.tangem.core.deeplink.DeepLinksRegistry +import com.tangem.core.deeplink.WEBLINK_KEY import com.tangem.core.navigation.email.EmailSender +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.UiDependencies import com.tangem.data.balancehiding.DefaultDeviceFlipDetector import com.tangem.data.card.sdk.CardSdkOwner @@ -74,6 +76,7 @@ import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.utils.DeepLinkFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler +import com.tangem.utils.extensions.validate import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @@ -185,6 +188,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var walletConnectFeatureToggles: WalletConnectFeatureToggles + @Inject + internal lateinit var urlOpener: UrlOpener + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -482,9 +488,18 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { private fun handleDeepLink(intent: Intent) { if (routingFeatureToggle.isDeepLinkNavigationEnabled) { val deepLinkExtras = intent.getStringExtra(DEEPLINK_KEY)?.toUri() - val receivedDeepLink = intent.data ?: deepLinkExtras ?: return + val webLink = intent.getStringExtra(WEBLINK_KEY) - deeplinkFactory.handleDeeplink(deeplinkUri = receivedDeepLink, coroutineScope = lifecycleScope) + val receivedDeepLink = intent.data ?: deepLinkExtras + + when { + receivedDeepLink != null -> { + deeplinkFactory.handleDeeplink(deeplinkUri = receivedDeepLink, coroutineScope = lifecycleScope) + } + webLink?.validate() == true -> { + urlOpener.openUrl(webLink) + } + } } else { deepLinksRegistry.launch(intent) } diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt index 8f984c76f0..23a6725255 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -16,6 +16,7 @@ import coil.request.ImageRequest import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import com.tangem.core.deeplink.DEEPLINK_KEY +import com.tangem.core.deeplink.WEBLINK_KEY import com.tangem.domain.common.LogConfig import com.tangem.tap.MainActivity import com.tangem.tap.common.images.createCoilImageLoader @@ -39,6 +40,7 @@ internal class TangemPushNotificationService : FirebaseMessagingService() { val intent = Intent(applicationContext, MainActivity::class.java).apply { putExtra(DEEPLINK_KEY, message.data[DEEPLINK_KEY]) + putExtra(WEBLINK_KEY, message.data[WEBLINK_KEY]) putExtra(OnPushClickedIntentHandler.OPENED_FROM_GCM_PUSH, true) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 45bde26900..7c69a01577 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -126,17 +126,4 @@ internal class DeepLinkFactory @Inject constructor( return params } - - /** - * Check for malicious symbol in uri part - */ - private fun String.validate(): Boolean { - val regex = DEEPLINK_VALIDATION_REGEX.toRegex() - - return !regex.containsMatchIn(this) - } - - private companion object { - const val DEEPLINK_VALIDATION_REGEX = "['\";<>()+\\\\]" - } } \ No newline at end of file diff --git a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt index 1d3ceb8718..3fa3e28ef8 100644 --- a/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt +++ b/core/deep-links/src/main/kotlin/com/tangem/core/deeplink/DeepLinksRegistry.kt @@ -6,6 +6,7 @@ import android.content.Intent * Key to pass deeplink via intent */ const val DEEPLINK_KEY = "deeplink" +const val WEBLINK_KEY = "link" // TODO: Add tests /** diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/StringExt.kt b/core/utils/src/main/java/com/tangem/utils/extensions/StringExt.kt new file mode 100644 index 0000000000..07624b12c0 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/extensions/StringExt.kt @@ -0,0 +1,12 @@ +package com.tangem.utils.extensions + +private const val DEEPLINK_VALIDATION_REGEX = "['\";<>()+\\\\]" + +/** + * Check for malicious symbol in uri part + */ +fun String.validate(): Boolean { + val regex = DEEPLINK_VALIDATION_REGEX.toRegex() + + return !regex.containsMatchIn(this) +} \ No newline at end of file From a6bbcf1e10c5d917df9740a32cc7739ee63412ca Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 May 2025 19:20:26 +0500 Subject: [PATCH 2/4] Updated on 2026-08-14 --- .../deeplink/DefaultTokenDetailsDeepLinkHandler.kt | 7 +++++++ 1 file changed, 7 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 9a11dba923..ab3eee78a5 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 @@ -4,6 +4,7 @@ import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.domain.notifications.models.NotificationType +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase @@ -21,6 +22,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( private val appRouter: AppRouter, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, ) : TokenDetailsDeepLinkHandler { init { @@ -67,6 +69,11 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( currency = cryptoCurrency, ), ) + fetchCurrencyStatusUseCase.invoke( + userWalletId = userWalletId, + id = cryptoCurrency.id, + refresh = true, + ) } } } From 0de7027fbc3c5b98b8f1716fdcb00f8208d3b06a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 27 May 2025 19:20:31 +0500 Subject: [PATCH 3/4] Updated on 2026-08-14 --- app/src/main/AndroidManifest.xml | 10 ++ .../main/java/com/tangem/tap/MainActivity.kt | 4 +- .../tap/routing/utils/DeepLinkFactory.kt | 6 +- .../tap/routing/utils/DeepLinkFactoryTest.kt | 11 +++ .../tangem/common/routing/DeepLinkRoute.kt | 4 + .../com/tangem/utils/extensions/StringExt.kt | 2 +- .../api/deeplink/StakingDeepLinkHandler.kt | 10 ++ .../deeplink/DefaultStakingDeepLinkHandler.kt | 94 +++++++++++++++++++ .../impl/deeplink/di/StakingDeepLinkModule.kt | 17 ++++ 9 files changed, 154 insertions(+), 4 deletions(-) create mode 100644 features/staking/api/src/main/kotlin/com/tangem/features/staking/api/deeplink/StakingDeepLinkHandler.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/di/StakingDeepLinkModule.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 13f1801543..b61add5332 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -176,6 +176,16 @@ android:scheme="tangem" /> + + + + + + + + diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 8a5266c470..0a6ed669e7 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -76,7 +76,7 @@ import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.utils.DeepLinkFactory import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler -import com.tangem.utils.extensions.validate +import com.tangem.utils.extensions.uriValidate import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @@ -496,7 +496,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { receivedDeepLink != null -> { deeplinkFactory.handleDeeplink(deeplinkUri = receivedDeepLink, coroutineScope = lifecycleScope) } - webLink?.validate() == true -> { + webLink?.uriValidate() == true -> { urlOpener.openUrl(webLink) } } diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 7c69a01577..1e05612cee 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -8,11 +8,13 @@ import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler +import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.extensions.uriValidate import dagger.hilt.android.scopes.ActivityScoped import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -32,6 +34,7 @@ internal class DeepLinkFactory @Inject constructor( private val walletConnectDeepLink: WalletConnectDeepLinkHandler.Factory, private val walletDeepLink: WalletDeepLinkHandler.Factory, private val tokenDetailsDeepLink: TokenDetailsDeepLinkHandler.Factory, + private val stakingDeepLink: StakingDeepLinkHandler.Factory, ) { private val permittedAppRoute = MutableStateFlow(false) @@ -101,6 +104,7 @@ internal class DeepLinkFactory @Inject constructor( DeepLinkRoute.Referral.host -> referralDeepLink.create() DeepLinkRoute.Wallet.host -> walletDeepLink.create() DeepLinkRoute.TokenDetails.host -> tokenDetailsDeepLink.create(coroutineScope, queryParams) + DeepLinkRoute.Staking.host -> stakingDeepLink.create(coroutineScope, queryParams) else -> { Timber.i( """ @@ -119,7 +123,7 @@ internal class DeepLinkFactory @Inject constructor( uri.queryParameterNames.forEach { paramName -> val paramValue = uri.getQueryParameter(paramName) - if (paramName.validate() && paramValue?.validate() == true) { + if (paramName.uriValidate() && paramValue?.uriValidate() == true) { params[paramName] = paramValue } } diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index 32be06bd2f..f13bfa3ad8 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -6,6 +6,7 @@ import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler import com.tangem.features.send.v2.api.deeplink.SellDeepLinkHandler +import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler import com.tangem.features.tokendetails.deeplink.TokenDetailsDeepLinkHandler import com.tangem.features.wallet.deeplink.WalletDeepLinkHandler import com.tangem.features.walletconnect.components.deeplink.WalletConnectDeepLinkHandler @@ -45,6 +46,9 @@ class DeepLinkFactoryTest { private val tokenDetailsDeepLinkFactory = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() } + private val stakingDeepLinkFactory = mockk(relaxed = true) { + every { create(any(), any()) } returns mockk() + } private val mockedUri = mockk(relaxed = true) @@ -59,6 +63,7 @@ class DeepLinkFactoryTest { walletConnectDeepLinkFactory, walletDeepLinkFactory, tokenDetailsDeepLinkFactory, + stakingDeepLinkFactory, ) @OptIn(ExperimentalCoroutinesApi::class) @@ -204,6 +209,12 @@ class DeepLinkFactoryTest { advanceUntilIdle() verify { tokenDetailsDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } + // Test Staking + every { mockedUri.host } returns "staking" + deepLinkFactory.handleDeeplink(mockedUri, testScope) + advanceUntilIdle() + verify { tokenDetailsDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } + // Reset params every { mockedUri.queryParameterNames } returns emptySet() every { mockedUri.getQueryParameter(any()) } returns "" diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt index c683a4136d..3d7be7ec68 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt @@ -27,6 +27,10 @@ sealed class DeepLinkRoute { data object TokenDetails : DeepLinkRoute() { override val host: String = "token" } + + data object Staking : DeepLinkRoute() { + override val host: String = "staking" + } } enum class DeepLinkScheme(val scheme: String) { diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/StringExt.kt b/core/utils/src/main/java/com/tangem/utils/extensions/StringExt.kt index 07624b12c0..5b8f7e3ca4 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/StringExt.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/StringExt.kt @@ -5,7 +5,7 @@ private const val DEEPLINK_VALIDATION_REGEX = "['\";<>()+\\\\]" /** * Check for malicious symbol in uri part */ -fun String.validate(): Boolean { +fun String.uriValidate(): Boolean { val regex = DEEPLINK_VALIDATION_REGEX.toRegex() return !regex.containsMatchIn(this) diff --git a/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/deeplink/StakingDeepLinkHandler.kt b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/deeplink/StakingDeepLinkHandler.kt new file mode 100644 index 0000000000..0453140239 --- /dev/null +++ b/features/staking/api/src/main/kotlin/com/tangem/features/staking/api/deeplink/StakingDeepLinkHandler.kt @@ -0,0 +1,10 @@ +package com.tangem.features.staking.api.deeplink + +import kotlinx.coroutines.CoroutineScope + +interface StakingDeepLinkHandler { + + interface Factory { + fun create(coroutineScope: CoroutineScope, queryParams: Map): StakingDeepLinkHandler + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt new file mode 100644 index 0000000000..00d124d530 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt @@ -0,0 +1,94 @@ +package com.tangem.features.staking.impl.deeplink + +import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.domain.staking.GetYieldUseCase +import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( + @Assisted private val scope: CoroutineScope, + @Assisted private val queryParams: Map, + private val appRouter: AppRouter, + private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, + private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, + private val getYieldUseCase: GetYieldUseCase, +) : StakingDeepLinkHandler { + + init { + handleDeepLink() + } + + private fun handleDeepLink() { + // It is okay here, we are navigating from outside, and there is no other way to getting UserWallet + val userWalletId = queryParams[WALLET_ID_KEY]?.let(::UserWalletId) + ?: getSelectedWalletSyncUseCase().getOrNull()?.walletId + + if (userWalletId == null) { + Timber.e("Error on getting user wallet") + return + } + + val networkId = queryParams[NETWORK_ID_KEY] + val tokenId = queryParams[TOKEN_ID_KEY] + + scope.launch { + val cryptoCurrency = getCryptoCurrenciesUseCase(userWalletId = userWalletId).getOrElse { + Timber.e("Error on getting crypto currency list") + return@launch + }.firstOrNull { + it.id.rawNetworkId == networkId && it.id.rawCurrencyId?.value == tokenId + } + + if (cryptoCurrency == null) { + Timber.e( + """ + Could not get crypto currency for + |- $NETWORK_ID_KEY: $networkId + |- $TOKEN_ID_KEY: $tokenId + """.trimIndent(), + ) + return@launch + } + + val yield = getYieldUseCase.invoke( + cryptoCurrencyId = cryptoCurrency.id, + symbol = cryptoCurrency.symbol, + ).getOrElse { + error("Staking is unavailable for ${cryptoCurrency.name}") + return@launch + } + + appRouter.push( + AppRoute.Staking( + userWalletId = userWalletId, + cryptoCurrencyId = cryptoCurrency.id, + yieldId = yield.id, + ), + ) + } + } + + @AssistedFactory + interface Factory : StakingDeepLinkHandler.Factory { + override fun create( + coroutineScope: CoroutineScope, + queryParams: Map, + ): DefaultStakingDeepLinkHandler + } + + private companion object { + const val WALLET_ID_KEY = "walletId" + const val NETWORK_ID_KEY = "network_id" + const val TOKEN_ID_KEY = "token_id" + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/di/StakingDeepLinkModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/di/StakingDeepLinkModule.kt new file mode 100644 index 0000000000..b4ca9dc5b9 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/di/StakingDeepLinkModule.kt @@ -0,0 +1,17 @@ +package com.tangem.features.staking.impl.deeplink.di + +import com.tangem.features.staking.api.deeplink.StakingDeepLinkHandler +import com.tangem.features.staking.impl.deeplink.DefaultStakingDeepLinkHandler +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal interface StakingDeepLinkModule { + @Binds + @Singleton + fun bindStakingDeepLinkHandlerFactory(impl: DefaultStakingDeepLinkHandler.Factory): StakingDeepLinkHandler.Factory +} \ No newline at end of file From a078b7bffa79d12491eabc8556dbc96d775fd2e2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 29 May 2025 13:57:00 +0500 Subject: [PATCH 4/4] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 14 ++-- .../tap/routing/utils/DeepLinkFactory.kt | 16 +++-- .../tap/routing/utils/DeepLinkFactoryTest.kt | 65 +++++++++++-------- .../deeplink/TokenDetailsDeepLinkHandler.kt | 6 +- .../DefaultTokenDetailsDeepLinkHandler.kt | 16 +++-- 5 files changed, 74 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 0a6ed669e7..557f69fdd0 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -245,7 +245,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { if (routingFeatureToggle.isDeepLinkNavigationEnabled.not() && intent != null && savedInstanceState == null) { // handle intent only on start, not on recreate - handleDeepLink(intent) + handleDeepLink(intent = intent, isFromOnNewIntent = false) } lifecycle.addObserver(WindowObscurationObserver) @@ -397,7 +397,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } if (intent != null) { - handleDeepLink(intent) + handleDeepLink(intent = intent, isFromOnNewIntent = true) } } @@ -479,13 +479,13 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } if (routingFeatureToggle.isDeepLinkNavigationEnabled && intent != null) { - handleDeepLink(intent) + handleDeepLink(intent = intent, isFromOnNewIntent = false) } viewModel.checkForUnfinishedBackup() } - private fun handleDeepLink(intent: Intent) { + private fun handleDeepLink(intent: Intent, isFromOnNewIntent: Boolean) { if (routingFeatureToggle.isDeepLinkNavigationEnabled) { val deepLinkExtras = intent.getStringExtra(DEEPLINK_KEY)?.toUri() val webLink = intent.getStringExtra(WEBLINK_KEY) @@ -494,7 +494,11 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { when { receivedDeepLink != null -> { - deeplinkFactory.handleDeeplink(deeplinkUri = receivedDeepLink, coroutineScope = lifecycleScope) + deeplinkFactory.handleDeeplink( + deeplinkUri = receivedDeepLink, + coroutineScope = lifecycleScope, + isFromOnNewIntent = isFromOnNewIntent, + ) } webLink?.uriValidate() == true -> { urlOpener.openUrl(webLink) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 1e05612cee..00e016b292 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -42,7 +42,7 @@ internal class DeepLinkFactory @Inject constructor( private val deepLinkHandlerJobHolder = JobHolder() @OptIn(ExperimentalCoroutinesApi::class) - fun handleDeeplink(deeplinkUri: Uri, coroutineScope: CoroutineScope) { + fun handleDeeplink(deeplinkUri: Uri, coroutineScope: CoroutineScope, isFromOnNewIntent: Boolean) { lastDeepLink = deeplinkUri Timber.i( @@ -55,7 +55,7 @@ internal class DeepLinkFactory @Inject constructor( .transformLatest { isPermitted -> if (isPermitted) { lastDeepLink?.let { - launchDeepLink(it, coroutineScope) + launchDeepLink(it, coroutineScope, isFromOnNewIntent) } lastDeepLink = null } @@ -80,9 +80,9 @@ internal class DeepLinkFactory @Inject constructor( } } - private fun launchDeepLink(deeplinkUri: Uri, coroutineScope: CoroutineScope) { + private fun launchDeepLink(deeplinkUri: Uri, coroutineScope: CoroutineScope, isFromOnNewIntent: Boolean) { when (deeplinkUri.scheme) { - DeepLinkScheme.Tangem.scheme -> handleTangemDeepLinks(deeplinkUri, coroutineScope) + DeepLinkScheme.Tangem.scheme -> handleTangemDeepLinks(deeplinkUri, coroutineScope, isFromOnNewIntent) DeepLinkScheme.WalletConnect.scheme -> walletConnectDeepLink.create(deeplinkUri) else -> { Timber.i( @@ -95,7 +95,7 @@ internal class DeepLinkFactory @Inject constructor( } } - private fun handleTangemDeepLinks(deeplinkUri: Uri, coroutineScope: CoroutineScope) { + private fun handleTangemDeepLinks(deeplinkUri: Uri, coroutineScope: CoroutineScope, isFromOnNewIntent: Boolean) { val queryParams = getQueryParams(deeplinkUri) when (deeplinkUri.host) { DeepLinkRoute.Onramp.host -> onrampDeepLink.create(coroutineScope, queryParams) @@ -103,7 +103,11 @@ internal class DeepLinkFactory @Inject constructor( DeepLinkRoute.Buy.host -> buyDeepLink.create(coroutineScope) DeepLinkRoute.Referral.host -> referralDeepLink.create() DeepLinkRoute.Wallet.host -> walletDeepLink.create() - DeepLinkRoute.TokenDetails.host -> tokenDetailsDeepLink.create(coroutineScope, queryParams) + DeepLinkRoute.TokenDetails.host -> tokenDetailsDeepLink.create( + coroutineScope = coroutineScope, + queryParams = queryParams, + isFromOnNewIntent = isFromOnNewIntent, + ) DeepLinkRoute.Staking.host -> stakingDeepLink.create(coroutineScope, queryParams) else -> { Timber.i( diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index f13bfa3ad8..40a82fad7e 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -44,13 +44,14 @@ class DeepLinkFactoryTest { every { create() } returns mockk() } private val tokenDetailsDeepLinkFactory = mockk(relaxed = true) { - every { create(any(), any()) } returns mockk() + every { create(any(), any(), any()) } returns mockk() } private val stakingDeepLinkFactory = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() } private val mockedUri = mockk(relaxed = true) + private val isFromOnNewIntent: Boolean = false private lateinit var testDispatcher: TestDispatcher private lateinit var testScope: TestScope @@ -101,7 +102,7 @@ class DeepLinkFactoryTest { every { mockedUri.getQueryParameter("param") } returns "value" // Set permittedAppRoute to true - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) advanceUntilIdle() @@ -120,7 +121,7 @@ class DeepLinkFactoryTest { every { mockedUri.queryParameterNames } returns setOf("param") every { mockedUri.getQueryParameter("param") } returns "value" - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) deepLinkFactory.checkRoutingReadiness(AppRoute.Initial) advanceUntilIdle() @@ -138,7 +139,7 @@ class DeepLinkFactoryTest { every { mockedUri.getQueryParameter("param") } returns "value" deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() @@ -153,7 +154,7 @@ class DeepLinkFactoryTest { every { mockedUri.host } returns "" deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() @@ -167,7 +168,7 @@ class DeepLinkFactoryTest { every { mockedUri.scheme } returns "https" every { mockedUri.host } returns "example.com" - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() @@ -178,7 +179,7 @@ class DeepLinkFactoryTest { referralDeepLinkFactory.create() walletConnectDeepLinkFactory.create(any()) walletDeepLinkFactory.create() - tokenDetailsDeepLinkFactory.create(any(), any()) + tokenDetailsDeepLinkFactory.create(any(), any(), any()) } } @@ -193,27 +194,39 @@ class DeepLinkFactoryTest { // Test Onramp every { mockedUri.host } returns "onramp" - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } // Test Sell every { mockedUri.host } returns "redirect_sell" - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() verify { sellDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } // Test Token Details every { mockedUri.host } returns "token" - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() - verify { tokenDetailsDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } + verify { + tokenDetailsDeepLinkFactory.create( + eq(testScope), + eq(mapOf("param" to "value")), + eq(isFromOnNewIntent), + ) + } // Test Staking every { mockedUri.host } returns "staking" - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() - verify { tokenDetailsDeepLinkFactory.create(eq(testScope), eq(mapOf("param" to "value"))) } + verify { + tokenDetailsDeepLinkFactory.create( + eq(testScope), + eq(mapOf("param" to "value")), + eq(isFromOnNewIntent), + ) + } // Reset params every { mockedUri.queryParameterNames } returns emptySet() @@ -221,19 +234,19 @@ class DeepLinkFactoryTest { // Test Buy every { mockedUri.host } returns "redirect" - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() verify { buyDeepLinkFactory.create(eq(testScope)) } // Test Referral every { mockedUri.host } returns "referral" - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() verify { referralDeepLinkFactory.create() } // Test Wallet every { mockedUri.host } returns "main" - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() verify { walletDeepLinkFactory.create() } } @@ -244,7 +257,7 @@ class DeepLinkFactoryTest { every { mockedUri.host } returns "unknown" deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() @@ -255,7 +268,7 @@ class DeepLinkFactoryTest { referralDeepLinkFactory.create() walletConnectDeepLinkFactory.create(any()) walletDeepLinkFactory.create() - tokenDetailsDeepLinkFactory.create(any(), any()) + tokenDetailsDeepLinkFactory.create(any(), any(), any()) } } @@ -270,7 +283,7 @@ class DeepLinkFactoryTest { every { mockedUri.getQueryParameter("quote") } returns "O'Brien" deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet) - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("safe" to "ok"))) } @@ -286,49 +299,49 @@ class DeepLinkFactoryTest { every { mockedUri.query } returns "safe=ok" every { mockedUri.queryParameterNames } returns setOf("safe") every { mockedUri.getQueryParameter("safe") } returns "ok" - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("safe" to "ok"))) } every { mockedUri.query } returns "param123=ok" every { mockedUri.queryParameterNames } returns setOf("param123") every { mockedUri.getQueryParameter("param123") } returns "ok" - deepLinkFactory.handleDeeplink(mockedUri, testScope) + deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent) advanceUntilIdle() verify { onrampDeepLinkFactory.create(eq(testScope), eq(mapOf("param123" to "ok"))) } every { mockedUri.query } returns "unsafe=