Updated on 2026-08-14

This commit is contained in:
Tangem 2025-05-27 20:22:58 +04:00
parent 0782cfbd51
commit dc9341bb71
11 changed files with 327 additions and 76 deletions

View file

@ -17,6 +17,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.walletconnect.components.WcRoutingComponent
import com.tangem.features.walletconnect.connections.components.WcPairComponent
import com.tangem.features.walletconnect.connections.components.AlertsComponent
import com.tangem.features.walletconnect.transaction.components.WcAddNetworkContainerComponent
import com.tangem.features.walletconnect.transaction.components.WcSignTransactionContainerComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -60,6 +61,10 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor(
appComponentContext = childContext,
params = WcSignTransactionContainerComponent.Params(config.rawRequest),
)
is WcInnerRoute.AddNetwork -> WcAddNetworkContainerComponent(
appComponentContext = childContext,
params = WcAddNetworkContainerComponent.Params(config.rawRequest),
)
is WcInnerRoute.Pair -> WcPairComponent(
appComponentContext = childContext,
params = WcPairComponent.Params(

View file

@ -17,6 +17,9 @@ internal sealed interface WcInnerRoute : Route {
@Serializable
data class SignMessage(override val rawRequest: WcSdkSessionRequest) : Method
@Serializable
data class AddNetwork(override val rawRequest: WcSdkSessionRequest) : Method
@Serializable
data class Pair(val request: WcPairRequest) : WcInnerRoute

View file

@ -50,11 +50,12 @@ internal class WcRoutingModel @Inject constructor(
WcEthMethodName.SignTypeDataV4,
WcSolanaMethodName.SignMessage,
-> WcInnerRoute.SignMessage(rawRequest)
WcEthMethodName.AddEthereumChain,
-> WcInnerRoute.AddNetwork(rawRequest)
WcEthMethodName.SignTransaction,
WcEthMethodName.SendTransaction,
WcSolanaMethodName.SignTransaction,
WcSolanaMethodName.SendAllTransaction,
WcEthMethodName.AddEthereumChain,
is WcMethodName.Unsupported,
-> WcInnerRoute.Alert(
alertType = AlertsComponent.AlertType.UnsupportedMethod { innerRouter.pop() },

View file

@ -0,0 +1,29 @@
package com.tangem.features.walletconnect.transaction.components
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.walletconnect.transaction.model.WcAddNetworkModel
import com.tangem.features.walletconnect.transaction.ui.chain.WcAddEthereumChainModalBottomSheetContent
internal class WcAddNetworkComponent(
private val appComponentContext: AppComponentContext,
private val model: WcAddNetworkModel,
private val transactionInfoOnClick: () -> Unit,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
@Composable
override fun Content(modifier: Modifier) {
val content by model.uiState.collectAsStateWithLifecycle()
if (content != null) {
WcAddEthereumChainModalBottomSheetContent(
state = content!!.transaction,
onClickTransactionRequest = transactionInfoOnClick,
)
}
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.features.walletconnect.transaction.components
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
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.stack.Children
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pushNew
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.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.features.walletconnect.transaction.model.WcAddNetworkModel
import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes
import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionCommonBottomSheetTitle
internal class WcAddNetworkContainerComponent(
private val appComponentContext: AppComponentContext,
params: Params,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
private val model: WcAddNetworkModel = getOrCreateModel(params = params)
private val contentNavigation = StackNavigation<WcTransactionRoutes>()
private val contentStack = childStack(
source = contentNavigation,
serializer = WcTransactionRoutes.serializer(),
initialConfiguration = WcTransactionRoutes.Transaction,
childFactory = ::screenChild,
)
private fun dismiss() {
model.dismiss()
}
@Composable
override fun Content(modifier: Modifier) {
val content by contentStack.subscribeAsState()
TangemModalBottomSheet<WcTransactionRoutes>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = content.active.configuration,
),
containerColor = TangemTheme.colors.background.tertiary,
title = { config ->
WcTransactionCommonBottomSheetTitle(
route = config,
onStartClick = ::toTransaction,
onEndClick = ::dismiss,
)
},
content = {
Box(
modifier = Modifier
.fillMaxWidth()
.animateContentSize(),
) {
Children(stack = content) { child ->
child.instance.Content(modifier = Modifier)
}
}
},
)
}
private fun toTransactionRequestInfo() {
contentNavigation.pushNew(WcTransactionRoutes.TransactionRequestInfo)
}
private fun toTransaction() {
contentNavigation.pushNew(WcTransactionRoutes.Transaction)
}
private fun screenChild(
config: WcTransactionRoutes,
componentContext: ComponentContext,
): ComposableContentComponent = when (config) {
is WcTransactionRoutes.Transaction -> WcAddNetworkComponent(
appComponentContext = childByContext(componentContext),
model = model,
transactionInfoOnClick = ::toTransactionRequestInfo,
)
is WcTransactionRoutes.TransactionRequestInfo -> WcTransactionRequestInfoComponent(
appComponentContext = childByContext(componentContext),
model = model.uiState.value?.transactionRequestInfo,
)
}
data class Params(val rawRequest: WcSdkSessionRequest)
}

View file

@ -17,14 +17,12 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.model.WcSignTransactionModel
import com.tangem.features.walletconnect.transaction.routes.WcSignTransactionRoutes
import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes
import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionCommonBottomSheetTitle
internal class WcSignTransactionContainerComponent(
private val appComponentContext: AppComponentContext,
@ -32,11 +30,11 @@ internal class WcSignTransactionContainerComponent(
) : AppComponentContext by appComponentContext, ComposableContentComponent {
private val model: WcSignTransactionModel = getOrCreateModel(params = params)
private val contentNavigation = StackNavigation<WcSignTransactionRoutes>()
private val contentNavigation = StackNavigation<WcTransactionRoutes>()
private val contentStack = childStack(
source = contentNavigation,
serializer = WcSignTransactionRoutes.serializer(),
initialConfiguration = WcSignTransactionRoutes.Transaction,
serializer = WcTransactionRoutes.serializer(),
initialConfiguration = WcTransactionRoutes.Transaction,
childFactory = ::screenChild,
)
@ -47,14 +45,20 @@ internal class WcSignTransactionContainerComponent(
@Composable
override fun Content(modifier: Modifier) {
val content by contentStack.subscribeAsState()
TangemModalBottomSheet<WcSignTransactionRoutes>(
TangemModalBottomSheet<WcTransactionRoutes>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = content.active.configuration,
),
containerColor = TangemTheme.colors.background.tertiary,
title = { config -> Title(route = config) },
title = { config ->
WcTransactionCommonBottomSheetTitle(
route = config,
onStartClick = ::toTransaction,
onEndClick = ::dismiss,
)
},
content = {
Box(
modifier = Modifier
@ -69,53 +73,30 @@ internal class WcSignTransactionContainerComponent(
)
}
@Composable
private fun Title(route: WcSignTransactionRoutes) {
TangemModalBottomSheetTitle(
title = resourceReference(R.string.wc_wallet_connect),
startIconRes = when (route) {
is WcSignTransactionRoutes.Transaction -> null
else -> R.drawable.ic_back_24
},
onStartClick = when (route) {
is WcSignTransactionRoutes.Transaction -> null
else -> ::toTransaction
},
endIconRes = when (route) {
is WcSignTransactionRoutes.Transaction -> R.drawable.ic_close_24
else -> null
},
onEndClick = when (route) {
is WcSignTransactionRoutes.Transaction -> ::dismiss
else -> null
},
)
}
private fun toTransactionRequestInfo() {
contentNavigation.navigate {
listOf(WcSignTransactionRoutes.TransactionRequestInfo)
listOf(WcTransactionRoutes.TransactionRequestInfo)
}
}
private fun toTransaction() {
contentNavigation.navigate {
listOf(WcSignTransactionRoutes.Transaction)
listOf(WcTransactionRoutes.Transaction)
}
}
private fun screenChild(
config: WcSignTransactionRoutes,
config: WcTransactionRoutes,
componentContext: ComponentContext,
): ComposableContentComponent = when (config) {
is WcSignTransactionRoutes.Transaction -> WcSignTransactionComponent(
is WcTransactionRoutes.Transaction -> WcSignTransactionComponent(
appComponentContext = childByContext(componentContext),
model = model,
transactionInfoOnClick = ::toTransactionRequestInfo,
)
is WcSignTransactionRoutes.TransactionRequestInfo -> WcSignTransactionRequestInfoComponent(
is WcTransactionRoutes.TransactionRequestInfo -> WcTransactionRequestInfoComponent(
appComponentContext = childByContext(componentContext),
model = model,
model = model.uiState.value?.transactionRequestInfo,
)
}

View file

@ -2,23 +2,20 @@ package com.tangem.features.walletconnect.transaction.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.walletconnect.transaction.model.WcSignTransactionModel
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionRequestInfoUM
import com.tangem.features.walletconnect.transaction.ui.common.TransactionRequestInfoContent
internal class WcSignTransactionRequestInfoComponent(
internal class WcTransactionRequestInfoComponent(
private val appComponentContext: AppComponentContext,
private val model: WcSignTransactionModel,
private val model: WcTransactionRequestInfoUM?,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
@Composable
override fun Content(modifier: Modifier) {
val content = model.uiState.collectAsStateWithLifecycle().value
if (content != null) {
TransactionRequestInfoContent(content.transactionRequestInfo)
if (model != null) {
TransactionRequestInfoContent(model)
}
}
}

View file

@ -0,0 +1,76 @@
package com.tangem.features.walletconnect.transaction.model
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.domain.walletconnect.WcRequestUseCaseFactory
import com.tangem.domain.walletconnect.usecase.method.WcAddNetworkUseCase
import com.tangem.domain.walletconnect.usecase.method.WcMethodUseCase
import com.tangem.features.walletconnect.transaction.components.WcSignTransactionContainerComponent
import com.tangem.features.walletconnect.transaction.entity.chain.WcAddEthereumChainUM
import com.tangem.features.walletconnect.transaction.entity.common.WcTransactionActionsUM
import com.tangem.features.walletconnect.transaction.utils.toUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class WcAddNetworkModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val clipboardManager: ClipboardManager,
private val useCaseFactory: WcRequestUseCaseFactory,
) : Model() {
private val _uiState = MutableStateFlow<WcAddEthereumChainUM?>(null)
val uiState: StateFlow<WcAddEthereumChainUM?> = _uiState
private val params = paramsContainer.require<WcSignTransactionContainerComponent.Params>()
init {
modelScope.launch {
val useCase: WcMethodUseCase = useCaseFactory.createUseCase(params.rawRequest)
val transactionUM = (useCase as? WcAddNetworkUseCase)?.toUM(
actions = WcTransactionActionsUM(
onDismiss = { cancel(useCase) },
onSign = { sign(useCase) },
onCopy = { copyData(useCase.rawSdkRequest.request.params) },
),
)
_uiState.emit(transactionUM)
}
}
fun dismiss() {
_uiState.value?.transaction?.onDismiss?.invoke() ?: router.pop()
}
private fun sign(useCase: WcAddNetworkUseCase) {
modelScope.launch {
_uiState.update { it?.copy(transaction = it.transaction.copy(isLoading = true)) }
useCase.approve().getOrNull()?.let {
router.pop()
} ?: run {
_uiState.update { it?.copy(transaction = it.transaction.copy(isLoading = false)) }
TODO("[REDACTED_JIRA]")
}
}
}
private fun cancel(useCase: WcAddNetworkUseCase) {
useCase.reject()
router.pop()
}
private fun copyData(text: String) {
clipboardManager.setText(text = text, isSensitive = true)
}
}

View file

@ -6,10 +6,10 @@ import kotlinx.serialization.Serializable
@Serializable
@Immutable
internal sealed class WcSignTransactionRoutes : TangemBottomSheetConfigContent {
internal sealed class WcTransactionRoutes : TangemBottomSheetConfigContent {
@Serializable
data object Transaction : WcSignTransactionRoutes()
data object Transaction : WcTransactionRoutes()
@Serializable
data object TransactionRequestInfo : WcSignTransactionRoutes()
data object TransactionRequestInfo : WcTransactionRoutes()
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.walletconnect.transaction.ui.common
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes
@Composable
internal fun WcTransactionCommonBottomSheetTitle(
route: WcTransactionRoutes,
onStartClick: () -> Unit,
onEndClick: () -> Unit,
) {
TangemModalBottomSheetTitle(
title = resourceReference(R.string.wc_wallet_connect),
startIconRes = when (route) {
is WcTransactionRoutes.Transaction -> null
else -> R.drawable.ic_back_24
},
onStartClick = when (route) {
is WcTransactionRoutes.Transaction -> null
else -> onStartClick
},
endIconRes = when (route) {
is WcTransactionRoutes.Transaction -> R.drawable.ic_close_24
else -> null
},
onEndClick = when (route) {
is WcTransactionRoutes.Transaction -> onEndClick
else -> null
},
)
}

View file

@ -1,22 +1,36 @@
package com.tangem.features.walletconnect.transaction.utils
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.walletconnect.model.WcEthMethod
import com.tangem.domain.walletconnect.model.WcSolanaMethod
import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest
import com.tangem.domain.walletconnect.usecase.method.WcMessageSignUseCase
import com.tangem.domain.walletconnect.usecase.method.WcSignState
import com.tangem.domain.walletconnect.usecase.method.WcSignStep
import com.tangem.domain.walletconnect.usecase.method.WcSignUseCase
import com.tangem.domain.walletconnect.usecase.method.*
import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.entity.chain.WcAddEthereumChainItemUM
import com.tangem.features.walletconnect.transaction.entity.chain.WcAddEthereumChainUM
import com.tangem.features.walletconnect.transaction.entity.common.*
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionItemUM
import com.tangem.features.walletconnect.transaction.entity.sign.WcSignTransactionUM
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
internal fun WcAddNetworkUseCase.toUM(actions: WcTransactionActionsUM): WcAddEthereumChainUM {
return WcAddEthereumChainUM(
transaction = WcAddEthereumChainItemUM(
onDismiss = actions.onDismiss,
onSign = actions.onSign,
appInfo = appInfo(),
walletName = session.wallet.name,
isLoading = false,
),
transactionRequestInfo = WcTransactionRequestInfoUM(
blocks = basicTransactionRequestInfoBlocks(),
onCopy = actions.onCopy,
),
)
}
internal fun WcSignUseCase<*>.toUM(signState: WcSignState<*>, actions: WcTransactionActionsUM): WcSignTransactionUM? {
return when (this) {
is WcMessageSignUseCase -> {
@ -46,17 +60,9 @@ private fun WcMessageSignUseCase.signTypedDataToUM(
transaction = WcSignTransactionItemUM(
onDismiss = actions.onDismiss,
onSign = actions.onSign,
appInfo = WcTransactionAppInfoContentUM(
appName = session.sdkModel.appMetaData.name,
appIcon = session.sdkModel.appMetaData.url,
isVerified = session.securityStatus == CheckDAppResult.SAFE,
appSubtitle = session.sdkModel.appMetaData.description,
),
appInfo = appInfo(),
walletName = session.wallet.name,
networkInfo = WcNetworkInfoUM(
name = network.name,
iconRes = getActiveIconRes(network.rawId),
),
networkInfo = networkInfo(),
addressText = walletAddress.toShortAddressText(),
isLoading = signState.domainStep == WcSignStep.Signing,
),
@ -95,17 +101,9 @@ private fun WcMessageSignUseCase.messageSignToUM(
transaction = WcSignTransactionItemUM(
onDismiss = actions.onDismiss,
onSign = actions.onSign,
appInfo = WcTransactionAppInfoContentUM(
appName = session.sdkModel.appMetaData.name,
appIcon = session.sdkModel.appMetaData.url,
isVerified = session.securityStatus == CheckDAppResult.SAFE,
appSubtitle = session.sdkModel.appMetaData.description,
),
appInfo = appInfo(),
walletName = session.wallet.name,
networkInfo = WcNetworkInfoUM(
name = network.name,
iconRes = getActiveIconRes(network.rawId),
),
networkInfo = networkInfo(),
isLoading = signState.domainStep == WcSignStep.Signing,
),
transactionRequestInfo = WcTransactionRequestInfoUM(
@ -130,4 +128,31 @@ private fun createInfoBlockUM(
),
),
)
}
}
private fun WcMethodContext.basicTransactionRequestInfoBlocks() = persistentListOf(
WcTransactionRequestBlockUM(
persistentListOf(
WcTransactionRequestInfoItemUM(
title = resourceReference(R.string.wc_signature_type),
description = rawSdkRequest.request.method,
),
WcTransactionRequestInfoItemUM(
title = resourceReference(R.string.wc_contents),
description = rawSdkRequest.request.params,
),
),
),
)
private fun WcMethodContext.appInfo() = WcTransactionAppInfoContentUM(
appName = session.sdkModel.appMetaData.name,
appIcon = session.sdkModel.appMetaData.url,
isVerified = session.securityStatus == com.domain.blockaid.models.dapp.CheckDAppResult.SAFE,
appSubtitle = session.sdkModel.appMetaData.description,
)
private fun WcMethodContext.networkInfo() = WcNetworkInfoUM(
name = network.name,
iconRes = getActiveIconRes(network.rawId),
)