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 9e44bb3dc2..557f69fdd0 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.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
@@ -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)
}
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..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
@@ -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 { 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 = "['\";<>()+\\\\]"
- }
}
\ No newline at end of file
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..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
@@ -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(relaxed = true) {
+ 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
@@ -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=