Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-29 20:36:10 +07:00
parent 68efa8e30c
commit 46385fa3f0
10 changed files with 249 additions and 2 deletions

View file

@ -17,7 +17,8 @@ import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat.startActivity
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.stack.Children
import com.arkivanov.decompose.extensions.compose.stack.animation.*
import com.arkivanov.decompose.extensions.compose.stack.animation.fade
import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
import com.arkivanov.decompose.value.Value
import com.tangem.common.routing.AppRoute
@ -33,6 +34,7 @@ import com.tangem.tap.routing.component.RoutingComponent
@Composable
internal fun RootContent(
stack: Value<ChildStack<AppRoute, RoutingComponent.Child>>,
wcContent: @Composable (modifier: Modifier) -> Unit,
uiDependencies: UiDependencies,
modifier: Modifier = Modifier,
) {
@ -69,6 +71,8 @@ internal fun RootContent(
}
}
wcContent(Modifier.fillMaxSize())
TangemSnackbarHost(
modifier = Modifier
.align(Alignment.BottomCenter)

View file

@ -15,6 +15,7 @@ import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.features.walletconnect.components.WcRoutingComponent
import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.routing.RootContent
import com.tangem.tap.routing.component.RoutingComponent
@ -32,10 +33,16 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
private val childFactory: ChildFactory,
private val appRouterConfig: AppRouterConfig,
private val uiDependencies: UiDependencies,
private val wcRoutingComponentFactory: WcRoutingComponent.Factory,
) : RoutingComponent,
AppComponentContext by context,
SnackbarHandler {
private val wcRoutingComponent: WcRoutingComponent by lazy {
wcRoutingComponentFactory
.create(childByContext(componentContext = this), params = Unit)
}
private val stack: Value<ChildStack<AppRoute, Child>> = childStack(
source = navigationProvider.getOrCreateTyped(),
initialStack = { getInitialStackOrInit() },
@ -53,7 +60,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
stack.subscribe(lifecycle) { stack ->
val stackItems = stack.items.map { it.configuration }
wcRoutingComponent.onAppRouteChange(stack.active.configuration)
if (appRouterConfig.stack != stackItems) {
appRouterConfig.stack = stackItems
}
@ -66,6 +73,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor(
modifier = modifier,
stack = stack,
uiDependencies = uiDependencies,
wcContent = { wcRoutingComponent.Content(it) },
)
}

View file

@ -12,6 +12,7 @@ dependencies {
/** Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.common.routing)
/** Domain models */
implementation(projects.domain.wallets.models)

View file

@ -0,0 +1,12 @@
package com.tangem.features.walletconnect.components
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface WcRoutingComponent : ComposableContentComponent {
fun onAppRouteChange(appRoute: AppRoute)
interface Factory : ComponentFactory<Unit, WcRoutingComponent>
}

View file

@ -0,0 +1,65 @@
package com.tangem.features.walletconnect.connections.routing
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.ChildSlot
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.value.Value
import com.arkivanov.decompose.value.subscribe
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.walletconnect.components.WcRoutingComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultWcRoutingComponent @AssistedInject constructor(
@Assisted private val appComponentContext: AppComponentContext,
@Assisted params: Unit,
) : AppComponentContext by appComponentContext, WcRoutingComponent {
private val model: WcRoutingModel = getOrCreateModel()
private val slot: Value<ChildSlot<WcInnerRoute, ComposableContentComponent>> = childSlot(
source = model.innerRouter.slotNavigation,
serializer = WcInnerRoute.serializer(),
childFactory = ::childFactory,
)
init {
slot.subscribe(lifecycle) { slot -> if (slot.child == null) model.onSlotEmpty() }
}
@Composable
override fun Content(modifier: Modifier) {
val slot by slot.subscribeAsState()
slot.child?.instance?.Content(modifier)
}
override fun onAppRouteChange(appRoute: AppRoute) {
model.onAppRouteChange(appRoute)
}
private fun childFactory(config: WcInnerRoute, componentContext: ComponentContext): ComposableContentComponent {
val childContext = childByContext(
componentContext = componentContext,
router = model.innerRouter,
)
return when (config) {
is WcInnerRoute.SignMessage -> TODO()
is WcInnerRoute.Pair -> TODO()
}
}
@AssistedFactory
interface Factory : WcRoutingComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultWcRoutingComponent
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.features.walletconnect.connections.routing
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import kotlinx.serialization.Serializable
@Serializable
internal sealed interface WcInnerRoute : Route {
@Serializable
sealed interface Method : WcInnerRoute {
val rawRequest: WcSdkSessionRequest
}
@Serializable
data class SignMessage(override val rawRequest: WcSdkSessionRequest) : Method
@Serializable
data class Pair(val request: WcPairRequest) : WcInnerRoute
}

View file

@ -0,0 +1,35 @@
package com.tangem.features.walletconnect.connections.routing
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.navigate
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.decompose.navigation.Router
import kotlin.reflect.KClass
internal class WcRouter(
val slotNavigation: SlotNavigation<WcInnerRoute>,
) : Router {
override fun push(route: Route, onComplete: (Boolean) -> Unit) {
val isComplete = route is WcInnerRoute
if (isComplete) slotNavigation.activate(route)
onComplete(isComplete)
}
override fun replaceAll(vararg routes: Route, onComplete: (Boolean) -> Unit) {
/** Not allowed */
}
override fun pop(onComplete: (Boolean) -> Unit) {
slotNavigation.navigate { null }
}
override fun popTo(route: Route, onComplete: (Boolean) -> Unit) {
/** Not allowed */
}
override fun popTo(routeClass: KClass<out Route>, onComplete: (Boolean) -> Unit) {
/** Not allowed */
}
}

View file

@ -0,0 +1,89 @@
package com.tangem.features.walletconnect.connections.routing
import com.arkivanov.decompose.router.slot.SlotNavigation
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.WcRequestService
import com.tangem.domain.walletconnect.model.WcEthMethodName
import com.tangem.domain.walletconnect.model.WcMethodName
import com.tangem.domain.walletconnect.model.WcSolanaMethodName
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ModelScoped
internal class WcRoutingModel @Inject constructor(
private val requestService: WcRequestService,
private val pairService: WcPairService,
override val dispatchers: CoroutineDispatcherProvider,
private val featureToggles: WalletConnectFeatureToggles,
) : Model() {
val innerRouter = WcRouter(SlotNavigation<WcInnerRoute>())
private val isSlotEmpty = MutableStateFlow(true)
private val permittedAppRoute = MutableStateFlow(false)
init {
if (featureToggles.isRedesignedWalletConnectEnabled) {
setupQueue()
}
}
fun onSlotEmpty() {
isSlotEmpty.update { true }
}
private fun setupQueue() {
val requestFlow = requestService.wcRequest
.map { (methodName, rawRequest) ->
when (methodName) {
WcEthMethodName.EthSign,
WcEthMethodName.PersonalSign,
-> WcInnerRoute.SignMessage(rawRequest)
WcEthMethodName.SignTypeData -> TODO()
WcEthMethodName.SignTypeDataV4 -> TODO()
WcEthMethodName.SignTransaction -> TODO()
WcEthMethodName.SendTransaction -> TODO()
WcSolanaMethodName.SignMessage -> TODO()
WcSolanaMethodName.SignTransaction -> TODO()
WcSolanaMethodName.SendAllTransaction -> TODO()
is WcMethodName.Unsupported -> TODO()
}
}
val pairFlow = pairService.pairFlow
.map { request -> WcInnerRoute.Pair(request) }
merge(requestFlow, pairFlow)
.onEach { configuration ->
awaitQueueReady()
isSlotEmpty.update { false }
innerRouter.push(configuration)
}
.launchIn(modelScope)
}
private suspend fun awaitQueueReady() = combine(
isSlotEmpty,
permittedAppRoute,
) { isSlotEmpty, permittedAppRoute -> isSlotEmpty && permittedAppRoute }
.first { it }
fun onAppRouteChange(appRoute: AppRoute) {
permittedAppRoute.value = when (appRoute) {
AppRoute.Initial,
is AppRoute.Welcome,
is AppRoute.Disclaimer,
is AppRoute.Stories,
-> {
innerRouter.pop()
false
}
else -> true
}
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.features.walletconnect.di
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent
import com.tangem.features.walletconnect.components.WcRoutingComponent
import com.tangem.features.walletconnect.connections.components.DefaultWalletConnectEntryComponent
import com.tangem.features.walletconnect.connections.routing.DefaultWcRoutingComponent
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -17,4 +19,8 @@ internal interface WalletConnectFeatureModule {
fun bindWalletConnectEntryComponentFactory(
factory: DefaultWalletConnectEntryComponent.Factory,
): WalletConnectEntryComponent.Factory
@Binds
@Singleton
fun bindWcRoutingComponentFactory(factory: DefaultWcRoutingComponent.Factory): WcRoutingComponent.Factory
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.features.walletconnect.connections.model.WcAppInfoModel
import com.tangem.features.walletconnect.connections.model.WcConnectedAppInfoModel
import com.tangem.features.walletconnect.connections.model.WcConnectionsModel
import com.tangem.features.walletconnect.connections.routing.WcRoutingModel
import com.tangem.features.walletconnect.transaction.model.WcSignTransactionModel
import dagger.Binds
import dagger.Module
@ -26,6 +27,11 @@ internal interface WalletConnectModelModule {
@ClassKey(WcAppInfoModel::class)
fun bindWcAppInfoModel(model: WcAppInfoModel): Model
@Binds
@IntoMap
@ClassKey(WcRoutingModel::class)
fun bindWcRoutingModel(model: WcRoutingModel): Model
@Binds
@IntoMap
@ClassKey(WcConnectedAppInfoModel::class)