Updated on 2026-08-14
This commit is contained in:
parent
c4603c4cbd
commit
c613890b9a
8 changed files with 241 additions and 13 deletions
|
|
@ -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<AppThemeMode>
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Boolean, Unit> { 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<String, String> {
|
||||
val params = mutableMapOf<String, String>()
|
||||
|
||||
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 = "['\";<>()+\\\\]"
|
||||
}
|
||||
}
|
||||
|
|
@ -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"),
|
||||
}
|
||||
|
|
@ -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<String, String>): OnrampDeepLinkHandler
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String>,
|
||||
) : 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<String, String>): DefaultOnrampDeepLinkHandler
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TX_ID_KEY = "tx_id"
|
||||
const val RESULT_KEY = "result"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue