Updated on 2026-08-14
This commit is contained in:
commit
e33bd4d849
13 changed files with 255 additions and 50 deletions
|
|
@ -176,6 +176,16 @@
|
|||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:host="staking"
|
||||
android:scheme="tangem" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- Disable android.startup completely. Used for Worker according doc -->
|
||||
|
|
|
|||
|
|
@ -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.uriValidate
|
||||
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<AppThemeMode>
|
||||
|
|
@ -239,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)
|
||||
|
|
@ -391,7 +397,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder {
|
|||
}
|
||||
|
||||
if (intent != null) {
|
||||
handleDeepLink(intent)
|
||||
handleDeepLink(intent = intent, isFromOnNewIntent = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -473,18 +479,31 @@ 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 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,
|
||||
isFromOnNewIntent = isFromOnNewIntent,
|
||||
)
|
||||
}
|
||||
webLink?.uriValidate() == true -> {
|
||||
urlOpener.openUrl(webLink)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deepLinksRegistry.launch(intent)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
@ -39,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(
|
||||
|
|
@ -52,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
|
||||
}
|
||||
|
|
@ -77,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(
|
||||
|
|
@ -92,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)
|
||||
|
|
@ -100,7 +103,12 @@ 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(
|
||||
"""
|
||||
|
|
@ -119,24 +127,11 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
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 = "['\";<>()+\\\\]"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -43,10 +44,14 @@ class DeepLinkFactoryTest {
|
|||
every { create() } returns mockk()
|
||||
}
|
||||
private val tokenDetailsDeepLinkFactory = mockk<TokenDetailsDeepLinkHandler.Factory>(relaxed = true) {
|
||||
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
|
||||
|
|
@ -59,6 +64,7 @@ class DeepLinkFactoryTest {
|
|||
walletConnectDeepLinkFactory,
|
||||
walletDeepLinkFactory,
|
||||
tokenDetailsDeepLinkFactory,
|
||||
stakingDeepLinkFactory,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
|
|
@ -96,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()
|
||||
|
|
@ -115,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()
|
||||
|
|
@ -133,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()
|
||||
|
||||
|
|
@ -148,7 +154,7 @@ class DeepLinkFactoryTest {
|
|||
every { mockedUri.host } returns ""
|
||||
|
||||
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope)
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
|
|
@ -162,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()
|
||||
|
||||
|
|
@ -173,7 +179,7 @@ class DeepLinkFactoryTest {
|
|||
referralDeepLinkFactory.create()
|
||||
walletConnectDeepLinkFactory.create(any())
|
||||
walletDeepLinkFactory.create()
|
||||
tokenDetailsDeepLinkFactory.create(any(), any())
|
||||
tokenDetailsDeepLinkFactory.create(any(), any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -188,21 +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, isFromOnNewIntent)
|
||||
advanceUntilIdle()
|
||||
verify {
|
||||
tokenDetailsDeepLinkFactory.create(
|
||||
eq(testScope),
|
||||
eq(mapOf("param" to "value")),
|
||||
eq(isFromOnNewIntent),
|
||||
)
|
||||
}
|
||||
|
||||
// Reset params
|
||||
every { mockedUri.queryParameterNames } returns emptySet()
|
||||
|
|
@ -210,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() }
|
||||
}
|
||||
|
|
@ -233,7 +257,7 @@ class DeepLinkFactoryTest {
|
|||
every { mockedUri.host } returns "unknown"
|
||||
|
||||
deepLinkFactory.checkRoutingReadiness(AppRoute.Wallet)
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope)
|
||||
deepLinkFactory.handleDeeplink(mockedUri, testScope, isFromOnNewIntent)
|
||||
|
||||
advanceUntilIdle()
|
||||
|
||||
|
|
@ -244,7 +268,7 @@ class DeepLinkFactoryTest {
|
|||
referralDeepLinkFactory.create()
|
||||
walletConnectDeepLinkFactory.create(any())
|
||||
walletDeepLinkFactory.create()
|
||||
tokenDetailsDeepLinkFactory.create(any(), any())
|
||||
tokenDetailsDeepLinkFactory.create(any(), any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -259,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"))) }
|
||||
|
|
@ -275,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())) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.utils.extensions
|
||||
|
||||
private const val DEEPLINK_VALIDATION_REGEX = "['\";<>()+\\\\]"
|
||||
|
||||
/**
|
||||
* Check for malicious symbol in uri part
|
||||
*/
|
||||
fun String.uriValidate(): Boolean {
|
||||
val regex = DEEPLINK_VALIDATION_REGEX.toRegex()
|
||||
|
||||
return !regex.containsMatchIn(this)
|
||||
}
|
||||
|
|
@ -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<String, String>): StakingDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>,
|
||||
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<String, String>,
|
||||
): DefaultStakingDeepLinkHandler
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val WALLET_ID_KEY = "walletId"
|
||||
const val NETWORK_ID_KEY = "network_id"
|
||||
const val TOKEN_ID_KEY = "token_id"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -15,12 +16,15 @@ 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,
|
||||
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
|
||||
) : TokenDetailsDeepLinkHandler {
|
||||
|
||||
init {
|
||||
|
|
@ -67,6 +71,14 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
|
|||
currency = cryptoCurrency,
|
||||
),
|
||||
)
|
||||
|
||||
if (isFromOnNewIntent) {
|
||||
fetchCurrencyStatusUseCase.invoke(
|
||||
userWalletId = userWalletId,
|
||||
id = cryptoCurrency.id,
|
||||
refresh = true,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -76,6 +88,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor(
|
|||
override fun create(
|
||||
coroutineScope: CoroutineScope,
|
||||
queryParams: Map<String, String>,
|
||||
isFromOnNewIntent: Boolean,
|
||||
): DefaultTokenDetailsDeepLinkHandler
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue