Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-29 13:57:00 +05:00
parent 0de7027fbc
commit a078b7bffa
5 changed files with 74 additions and 43 deletions

View file

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

View file

@ -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<Boolean, Unit> { 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(

View file

@ -44,13 +44,14 @@ class DeepLinkFactoryTest {
every { create() } returns mockk()
}
private val tokenDetailsDeepLinkFactory = mockk<TokenDetailsDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk()
every { create(any(), any(), any()) } returns mockk()
}
private val stakingDeepLinkFactory = mockk<StakingDeepLinkHandler.Factory>(relaxed = true) {
every { create(any(), any()) } returns mockk()
}
private val mockedUri = mockk<Uri>(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=<script>"
every { mockedUri.queryParameterNames } returns setOf("unsafe")
every { mockedUri.getQueryParameter("unsafe") } returns "<script>"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
every { mockedUri.query } returns "unsafe=O'Brien"
every { mockedUri.queryParameterNames } returns setOf("unsafe")
every { mockedUri.getQueryParameter("unsafe") } returns "O'Brien"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
every { mockedUri.query } returns "unsafe=test;"
every { mockedUri.queryParameterNames } returns setOf("unsafe")
every { mockedUri.getQueryParameter("unsafe") } returns "test;"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
every { mockedUri.query } returns "unsafe=test+attack"
every { mockedUri.queryParameterNames } returns setOf("unsafe")
every { mockedUri.getQueryParameter("unsafe") } returns "test+attack"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
every { mockedUri.query } returns "unsafe=test\\path"
every { mockedUri.queryParameterNames } returns setOf("unsafe")
every { mockedUri.getQueryParameter("unsafe") } returns "test\\path"
deepLinkFactory.handleDeeplink(mockedUri, testScope)
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
advanceUntilIdle()
verify { onrampDeepLinkFactory.create(eq(testScope), eq(emptyMap())) }
}

View file

@ -5,6 +5,10 @@ import kotlinx.coroutines.CoroutineScope
interface TokenDetailsDeepLinkHandler {
interface Factory {
fun create(coroutineScope: CoroutineScope, queryParams: Map<String, String>): TokenDetailsDeepLinkHandler
fun create(
coroutineScope: CoroutineScope,
queryParams: Map<String, String>,
isFromOnNewIntent: Boolean,
): TokenDetailsDeepLinkHandler
}
}

View file

@ -16,9 +16,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import timber.log.Timber
@Suppress("LongParameterList")
internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
@Assisted private val scope: CoroutineScope,
@Assisted private val queryParams: Map<String, String>,
@Assisted private val isFromOnNewIntent: Boolean,
private val appRouter: AppRouter,
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
@ -69,11 +71,14 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
currency = cryptoCurrency,
),
)
fetchCurrencyStatusUseCase.invoke(
userWalletId = userWalletId,
id = cryptoCurrency.id,
refresh = true,
)
if (isFromOnNewIntent) {
fetchCurrencyStatusUseCase.invoke(
userWalletId = userWalletId,
id = cryptoCurrency.id,
refresh = true,
)
}
}
}
}
@ -83,6 +88,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
override fun create(
coroutineScope: CoroutineScope,
queryParams: Map<String, String>,
isFromOnNewIntent: Boolean,
): DefaultTokenDetailsDeepLinkHandler
}