diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index b3ad882b44..dfd119816f 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb +import androidx.core.net.toUri import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle @@ -30,6 +31,7 @@ import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.analytics.api.AnalyticsEventHandler 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.navigation.email.EmailSender import com.tangem.core.ui.UiDependencies @@ -68,6 +70,7 @@ import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.tap.routing.component.RoutingComponent 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 dagger.hilt.android.AndroidEntryPoint @@ -175,6 +178,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var routingFeatureToggle: RoutingFeatureToggle + @Inject + internal lateinit var deeplinkFactory: DeepLinkFactory + internal val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow @@ -227,13 +233,9 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { sendStakingUnsubmittedHashes() checkGoogleServicesAvailability() - if (intent != null && savedInstanceState == null) { + if (routingFeatureToggle.isDeepLinkNavigationEnabled.not() && intent != null && savedInstanceState == null) { // handle intent only on start, not on recreate - if (routingFeatureToggle.isDeepLinkNavigationEnabled) { - // todo [REDACTED_TASK_KEY] - } else { - deepLinksRegistry.launch(intent) - } + handleDeepLink(intent) } lifecycle.addObserver(WindowObscurationObserver) @@ -382,11 +384,7 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } if (intent != null) { - if (routingFeatureToggle.isDeepLinkNavigationEnabled) { - // todo [REDACTED_TASK_KEY] - } else { - deepLinksRegistry.launch(intent) - } + handleDeepLink(intent) } } @@ -467,9 +465,24 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { } } + if (routingFeatureToggle.isDeepLinkNavigationEnabled && intent != null) { + handleDeepLink(intent) + } + viewModel.checkForUnfinishedBackup() } + private fun handleDeepLink(intent: Intent) { + if (routingFeatureToggle.isDeepLinkNavigationEnabled) { + val deepLinkExtras = intent.getStringExtra(DEEPLINK_KEY)?.toUri() + val receivedDeepLink = intent.data ?: deepLinkExtras ?: return + + deeplinkFactory.handleDeeplink(deeplinkUri = receivedDeepLink, coroutineScope = lifecycleScope) + } else { + deepLinksRegistry.launch(intent) + } + } + private fun observePolkadotAccountHealthCheck() { lifecycleScope.launch { getPolkadotCheckHasResetUseCase() diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 92ac286e88..64d10c0193 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -22,6 +22,7 @@ import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent.Child import com.tangem.tap.routing.configurator.AppRouterConfig import com.tangem.tap.routing.utils.ChildFactory +import com.tangem.tap.routing.utils.DeepLinkFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -34,6 +35,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val appRouterConfig: AppRouterConfig, private val uiDependencies: UiDependencies, private val wcRoutingComponentFactory: WcRoutingComponent.Factory, + private val deeplinkFactory: DeepLinkFactory, ) : RoutingComponent, AppComponentContext by context, SnackbarHandler { @@ -60,7 +62,10 @@ internal class DefaultRoutingComponent @AssistedInject constructor( stack.subscribe(lifecycle) { stack -> val stackItems = stack.items.map { it.configuration } + wcRoutingComponent.onAppRouteChange(stack.active.configuration) + deeplinkFactory.checkRoutingReadiness(stack.active.configuration) + if (appRouterConfig.stack != stackItems) { appRouterConfig.stack = stackItems } 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 new file mode 100644 index 0000000000..ea0040261e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -0,0 +1,123 @@ +package com.tangem.tap.routing.utils + +import android.net.Uri +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.DeepLinkRoute +import com.tangem.common.routing.DeepLinkScheme +import com.tangem.features.onramp.deeplink.OnrampDeepLinkHandler +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import dagger.hilt.android.scopes.ActivityScoped +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.transformLatest +import timber.log.Timber +import javax.inject.Inject + +@ActivityScoped +internal class DeepLinkFactory @Inject constructor( + private val onrampDeepLink: OnrampDeepLinkHandler.Factory, +) { + private val permittedAppRoute = MutableStateFlow(false) + + private var lastDeepLink: Uri? = null + private val deepLinkHandlerJobHolder = JobHolder() + + @OptIn(ExperimentalCoroutinesApi::class) + fun handleDeeplink(deeplinkUri: Uri, coroutineScope: CoroutineScope) { + lastDeepLink = deeplinkUri + + Timber.i( + """ + Received deep link intent + |- Received URI: $deeplinkUri + """.trimIndent(), + ) + permittedAppRoute + .transformLatest { isPermitted -> + if (isPermitted) { + lastDeepLink?.let { + launchDeepLink(it, coroutineScope) + } + lastDeepLink = null + } + } + .launchIn(coroutineScope) + .saveIn(deepLinkHandlerJobHolder) + } + + /** + * Check if app is ready to handle deeplink + */ + fun checkRoutingReadiness(appRoute: AppRoute) { + permittedAppRoute.value = when (appRoute) { + AppRoute.Initial, + AppRoute.Home, + is AppRoute.Welcome, + is AppRoute.Disclaimer, + is AppRoute.Stories, + is AppRoute.Onboarding, + -> false + else -> true + } + } + + private fun launchDeepLink(deeplinkUri: Uri, coroutineScope: CoroutineScope) { + when (deeplinkUri.scheme) { + DeepLinkScheme.Tangem.scheme -> handleTangemDeepLinks(deeplinkUri, coroutineScope) + else -> { + Timber.i( + """ + No match found for deep link + |- Received URI: $deeplinkUri + """.trimIndent(), + ) + } + } + } + + private fun handleTangemDeepLinks(deeplinkUri: Uri, coroutineScope: CoroutineScope) { + val params = getParams(deeplinkUri) + when (deeplinkUri.host) { + DeepLinkRoute.Onramp.host -> onrampDeepLink.create(coroutineScope, params) + else -> { + Timber.i( + """ + No match found for deep link + |- Received URI: $deeplinkUri + |- With params: $params + """.trimIndent(), + ) + } + } + } + + private fun getParams(uri: Uri): Map { + val params = mutableMapOf() + + uri.queryParameterNames.forEach { paramName -> + val paramValue = uri.getQueryParameter(paramName) + + if (paramName.validate() && paramValue?.validate() == 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/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt new file mode 100644 index 0000000000..85bf976648 --- /dev/null +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/DeepLinkRoute.kt @@ -0,0 +1,15 @@ +package com.tangem.common.routing + +sealed class DeepLinkRoute { + + abstract val host: String + + data object Onramp : DeepLinkRoute() { + override val host: String = "onramp" + } +} + +enum class DeepLinkScheme(val scheme: String) { + Tangem(scheme = "tangem"), + WalletConnect(scheme = "wc"), +} \ No newline at end of file diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt index 1b2ae8c801..e590b31392 100644 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/deeplink/OnrampDeepLink.kt @@ -3,10 +3,18 @@ package com.tangem.features.onramp.deeplink import com.tangem.core.deeplink.DeepLink import kotlinx.coroutines.CoroutineScope +@Deprecated("Use OnrampDeepLinkHandler") abstract class OnrampDeepLink : DeepLink() { override val uri = "tangem://onramp" interface Factory { fun create(coroutineScope: CoroutineScope): OnrampDeepLink } +} + +interface OnrampDeepLinkHandler { + + interface Factory { + fun create(coroutineScope: CoroutineScope, params: Map): OnrampDeepLinkHandler + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt new file mode 100644 index 0000000000..c1ef286e30 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/DefaultOnrampDeepLinkHandler.kt @@ -0,0 +1,58 @@ +package com.tangem.features.onramp.deeplink + +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter +import com.tangem.features.onramp.success.OnrampSuccessScreenTrigger +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import timber.log.Timber + +class DefaultOnrampDeepLinkHandler @AssistedInject constructor( + appRouter: AppRouter, + private val onrampSuccessScreenTrigger: OnrampSuccessScreenTrigger, + @Assisted private val scope: CoroutineScope, + @Assisted params: Map, +) : OnrampDeepLinkHandler { + + init { + val txId = params[TX_ID_KEY] + val result = OnrampRedirectResult.getResult(params[RESULT_KEY]) + + when { + !txId.isNullOrEmpty() -> { + // finish current onramp flow and show onramp success screen + val replaceOnrampScreens = appRouter.stack + .filterNot { it is AppRoute.Onramp || it is AppRoute.OnrampSuccess } + .toMutableList() + AppRoute.OnrampSuccess(txId) + + appRouter.replaceAll(*replaceOnrampScreens.toTypedArray()) + } + result != OnrampRedirectResult.Unknown -> { + scope.launch { + onrampSuccessScreenTrigger.triggerOnrampSuccess(result == OnrampRedirectResult.Success) + } + } + else -> { + Timber.e( + """ + Invalid parameters for ONRAMP deeplink + |- Params: $params + """.trimIndent(), + ) + } + } + } + + @AssistedFactory + interface Factory : OnrampDeepLinkHandler.Factory { + override fun create(coroutineScope: CoroutineScope, params: Map): DefaultOnrampDeepLinkHandler + } + + private companion object { + const val TX_ID_KEY = "tx_id" + const val RESULT_KEY = "result" + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt index 6663ec80d4..fc55367fe3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/deeplink/di/OnrampDeeplinkModule.kt @@ -1,7 +1,7 @@ package com.tangem.features.onramp.deeplink.di +import com.tangem.features.onramp.deeplink.* import com.tangem.features.onramp.deeplink.DefaultOnrampDeepLink -import com.tangem.features.onramp.deeplink.OnrampDeepLink import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -15,4 +15,8 @@ internal interface OnrampDeeplinkModule { @Binds @Singleton fun bindFactory(impl: DefaultOnrampDeepLink.Factory): OnrampDeepLink.Factory + + @Binds + @Singleton + fun bindFactoryV2(impl: DefaultOnrampDeepLinkHandler.Factory): OnrampDeepLinkHandler.Factory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 436bcd0791..006073a17a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -153,7 +153,9 @@ internal class OnrampSuccessComponentModel @Inject constructor( } else { resourceReference(R.string.express_error_code, wrappedList(errorCode)) }, - onDismissRequest = router::pop, + firstActionBuilder = { + okAction(router::pop) + }, ) messageSender.send(message)