Updated on 2026-08-14

This commit is contained in:
Tangem 2025-04-25 14:50:19 +05:00
parent f97a017f3f
commit 8cbaf872cf
13 changed files with 203 additions and 40 deletions

View file

@ -10,6 +10,7 @@ import com.reown.walletkit.client.WalletKit
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.pair.unsupportedDApps
import com.tangem.domain.walletconnect.model.legacy.Account
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper
@ -29,6 +30,7 @@ internal class DefaultLegacyWalletConnectRepositoryFacade constructor(
private val stub: LegacyWalletConnectRepositoryStub,
private val legacy: DefaultLegacyWalletConnectRepository,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val wcInitializeUseCase: WcInitializeUseCase,
) : LegacyWalletConnectRepository {
private val isNewWc by lazy {
walletConnectFeatureToggles.isRedesignedWalletConnectEnabled
@ -43,8 +45,13 @@ internal class DefaultLegacyWalletConnectRepositoryFacade constructor(
if (isNewWc) stub.currentSessions else legacy.currentSessions
}
override fun init(projectId: String) {
if (isNewWc) stub.init(projectId) else legacy.init(projectId)
override suspend fun init(projectId: String) {
if (isNewWc) {
stub.init(projectId)
wcInitializeUseCase.init() // TODO(wc) Nikolai: Delete after RoutingComponent finished.
} else {
legacy.init(projectId)
}
}
override fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>) {
@ -89,7 +96,7 @@ internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository
override val activeSessions: Flow<List<WalletConnectSession>> = emptyFlow()
override val currentSessions: List<WalletConnectSession> = listOf()
override fun init(projectId: String) = Unit
override suspend fun init(projectId: String) = Unit
override fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>) = Unit
@ -134,7 +141,7 @@ internal class DefaultLegacyWalletConnectRepository(
/**
* @param projectId Project ID at https://cloud.walletconnect.com/
*/
override fun init(projectId: String) {
override suspend fun init(projectId: String) {
val relayUrl = "relay.walletconnect.com"
val serverUrl = "wss://$relayUrl?projectId=$projectId"
val connectionType = ConnectionType.AUTOMATIC

View file

@ -8,6 +8,7 @@ import com.tangem.datasource.di.SdkMoshi
import com.tangem.datasource.files.FileReader
import com.tangem.domain.tokens.repository.CurrenciesRepository
import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository
import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles
@ -74,6 +75,7 @@ internal object WalletConnectModule {
wcRequestDeserializer: WcJrpcRequestsDeserializer,
analyticsHandler: AnalyticsEventHandler,
walletConnectFeatureToggles: WalletConnectFeatureToggles,
wcInitializeUseCase: WcInitializeUseCase,
): LegacyWalletConnectRepository {
val legacy = DefaultLegacyWalletConnectRepository(
application = application,
@ -85,6 +87,7 @@ internal object WalletConnectModule {
stub,
legacy,
walletConnectFeatureToggles,
wcInitializeUseCase,
)
}

View file

@ -13,7 +13,7 @@ interface LegacyWalletConnectRepository {
val currentSessions: List<WalletConnectSession>
fun init(projectId: String)
suspend fun init(projectId: String)
fun setUserNamespaces(userNamespaces: Map<NetworkNamespace, List<Account>>)

View file

@ -1307,6 +1307,8 @@
<string name="wc_new_connection">New connection</string>
<string name="wc_no_sessions_desc">Connect your wallet to a different dApps</string>
<string name="wc_no_sessions_title">No sessions</string>
<string name="wc_notification_security_risk_subtitle">This domain is flagged as unsafe by multiple security providers. Leave immediately to protect your assets</string>
<string name="wc_notification_security_risk_title">Known security risk</string>
<string name="wc_wallet_connect">Wallet connect</string>
<string name="welcome_interrupted_backup_alert_discard">Discard</string>
<string name="welcome_interrupted_backup_alert_message">You have an interrupted backup. Do you want to resume?</string>

View file

@ -52,6 +52,7 @@ import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsSta
fun Notification(
config: NotificationConfig,
modifier: Modifier = Modifier,
titleColor: Color = TangemTheme.colors.text.primary1,
subtitleColor: Color = TangemTheme.colors.text.tertiary,
containerColor: Color? = null,
iconTint: Color? = null,
@ -69,6 +70,7 @@ fun Notification(
iconResId = config.iconResId,
iconTint = iconTint,
title = config.title,
titleColor = titleColor,
subtitle = config.subtitle,
subtitleColor = subtitleColor,
showArrowIcon = isEnabled && config.showArrowIcon,
@ -129,6 +131,7 @@ private fun MainContent(
iconTint: Color?,
title: TextReference?,
subtitle: TextReference,
titleColor: Color,
subtitleColor: Color,
showArrowIcon: Boolean,
) {
@ -143,7 +146,7 @@ private fun MainContent(
SpacerW(width = TangemTheme.dimens.spacing10)
TextsBlock(title = title, subtitle = subtitle, subtitleColor = subtitleColor)
TextsBlock(title = title, titleColor = titleColor, subtitle = subtitle, subtitleColor = subtitleColor)
if (showArrowIcon) {
SpacerWMax()

View file

@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.luminance
import androidx.core.graphics.toColorInt
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
private const val LIGHT_LUMINANCE = 0.5f
private const val COLOR_HEX_START_INDEX = 2
@ -18,7 +19,15 @@ private const val COLOR_HEX_END_INDEX = 7
*/
@get:DrawableRes
val CryptoCurrency.networkIconResId: Int
get() = getActiveIconRes(network.id.value)
get() = network.iconResId
/**
* Retrieves the resource ID.
*
* @return Drawable resource ID for the network.
*/
val Network.iconResId: Int
get() = getActiveIconRes(id.value)
/**
* Tries to extract a background color from the contract address of a token.

View file

@ -10,7 +10,6 @@ import com.tangem.domain.walletconnect.model.WcPairError
import kotlinx.coroutines.async
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.suspendCancellableCoroutine
@ -24,7 +23,6 @@ internal class WcPairSdkDelegate : WcSdkObserver {
suspend fun pair(url: String): Either<WcPairError, Wallet.Model.SessionProposal> = coroutineScope {
suspend fun proposalCallback() = onSessionProposal
.receiveAsFlow()
.filter { proposal -> proposal.url == url }
.first()
val pairCall = async { sdkPair(url) }

View file

@ -24,6 +24,7 @@ dependencies {
implementation(projects.common.ui)
/** Domain models */
implementation(projects.domain.blockaid.models)
implementation(projects.domain.qrScanning.models)
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)

View file

@ -3,6 +3,9 @@ package com.tangem.features.walletconnect.connections.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.walletconnect.impl.R
import kotlinx.collections.immutable.ImmutableList
@Immutable
@ -21,7 +24,7 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent {
val appIcon: String,
val isVerified: Boolean,
val appSubtitle: String,
val notificationUM: NotificationUM?,
val notification: WcAppInfoSecurityNotification?,
val walletName: String,
val networksInfo: WcNetworksInfo,
override val connectButtonConfig: WcPrimaryButtonConfig,
@ -29,6 +32,21 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent {
) : WcAppInfoUM()
}
sealed class WcAppInfoSecurityNotification(
title: TextReference,
subtitle: TextReference,
) : NotificationUM.Info(title = title, subtitle = subtitle) {
data object UnknownDomain : WcAppInfoSecurityNotification(
title = resourceReference(R.string.wc_alert_audit_unknown_domain),
subtitle = resourceReference(R.string.wc_alert_domain_issues_description),
)
data object SecurityRisk : WcAppInfoSecurityNotification(
title = resourceReference(R.string.wc_notification_security_risk_title),
subtitle = resourceReference(R.string.wc_notification_security_risk_subtitle),
)
}
@Immutable
internal sealed class WcNetworksInfo {
data class MissingRequiredNetworkInfo(val networks: String) : WcNetworksInfo()

View file

@ -4,31 +4,85 @@ 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.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.walletconnect.model.WcSessionApprove
import com.tangem.domain.walletconnect.model.WcSessionProposal
import com.tangem.domain.walletconnect.usecase.pair.WcPairState
import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.walletconnect.connections.components.WcAppInfoContainerComponent
import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM
import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig
import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoTransformer
import com.tangem.features.walletconnect.connections.model.transformers.WcConnectButtonProgressTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.*
import javax.inject.Inject
import kotlin.properties.Delegates
import com.tangem.utils.transformer.update as transformerUpdate
@Stable
@ModelScoped
internal class WcAppInfoModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
wcPairUseCaseFactory: WcPairUseCase.Factory,
paramsContainer: ParamsContainer,
) : Model() {
private val params: WcAppInfoContainerComponent.Params = paramsContainer.require()
private val wcPairUseCase = wcPairUseCaseFactory.create(WcPairRequest(uri = params.wcUrl, source = params.source))
val appInfoUiState: StateFlow<WcAppInfoUM>
field = MutableStateFlow<WcAppInfoUM>(createLoadingState())
// TODO(wc) Doston: Temp solution, will be fixed in next PR`s
private var userWallet by Delegates.notNull<UserWallet>()
private var proposalNetwork by Delegates.notNull<WcSessionProposal.ProposalNetwork>()
init {
loadDAppInfo()
}
private fun loadDAppInfo() {
wcPairUseCase()
.onEach { state ->
when (state) {
is WcPairState.Approving.Loading -> appInfoUiState.transformerUpdate(
WcConnectButtonProgressTransformer(showProgress = true),
)
is WcPairState.Approving.Result -> {
appInfoUiState.transformerUpdate(
WcConnectButtonProgressTransformer(showProgress = false),
)
// TODO: wc dismiss and reload sessions
}
is WcPairState.Error -> {
// TODO: wc show toast/snackbar/alert?
}
is WcPairState.Loading -> appInfoUiState.update { createLoadingState() }
is WcPairState.Proposal -> {
userWallet = state.dAppSession.proposalNetwork.keys.first { !it.isLocked }
proposalNetwork = state.dAppSession.proposalNetwork.getValue(userWallet)
appInfoUiState.transformerUpdate(
WcAppInfoTransformer(
dAppSession = state.dAppSession,
onDismiss = ::dismiss,
onConnect = ::onConnect,
userWallet = userWallet,
proposalNetwork = proposalNetwork,
),
)
}
}
}
.launchIn(modelScope)
}
fun dismiss() {
params.onDismiss()
}
private fun onConnect() {
// TODO: [REDACTED_JIRA]
wcPairUseCase.approve(WcSessionApprove(wallet = userWallet, network = proposalNetwork.required.toList()))
}
private fun createLoadingState(): WcAppInfoUM.Loading {

View file

@ -0,0 +1,62 @@
package com.tangem.features.walletconnect.connections.model.transformers
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.tangem.core.ui.extensions.iconResId
import com.tangem.domain.walletconnect.model.WcSessionProposal
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification
import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM
import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo
import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toImmutableList
internal class WcAppInfoTransformer(
private val dAppSession: WcSessionProposal,
private val onDismiss: () -> Unit,
private val onConnect: () -> Unit,
private val userWallet: UserWallet,
private val proposalNetwork: WcSessionProposal.ProposalNetwork,
) : Transformer<WcAppInfoUM> {
override fun transform(prevState: WcAppInfoUM): WcAppInfoUM {
return WcAppInfoUM.Content(
appName = dAppSession.dAppMetaData.name,
appIcon = dAppSession.dAppMetaData.icons.firstOrNull().orEmpty(),
isVerified = dAppSession.securityStatus == CheckDAppResult.SAFE,
appSubtitle = dAppSession.dAppMetaData.description,
notification = createNotification(dAppSession.securityStatus),
walletName = userWallet.name,
networksInfo = convertNetworksInfo(proposalNetwork),
connectButtonConfig = WcPrimaryButtonConfig(
showProgress = false,
enabled = proposalNetwork.missingRequired.isEmpty(),
onClick = onConnect,
),
onDismiss = onDismiss,
)
}
private fun createNotification(securityStatus: CheckDAppResult): WcAppInfoSecurityNotification? {
return when (securityStatus) {
CheckDAppResult.SAFE -> null
CheckDAppResult.UNSAFE -> WcAppInfoSecurityNotification.SecurityRisk
CheckDAppResult.FAILED_TO_VERIFY -> WcAppInfoSecurityNotification.UnknownDomain
}
}
private fun convertNetworksInfo(proposalNetwork: WcSessionProposal.ProposalNetwork): WcNetworksInfo {
return if (proposalNetwork.missingRequired.isNotEmpty()) {
WcNetworksInfo.MissingRequiredNetworkInfo(
networks = proposalNetwork.missingRequired
.joinToString { it.name },
)
} else {
WcNetworksInfo.ContainsAllRequiredNetworks(
icons = (proposalNetwork.required + proposalNetwork.available)
.map { it.iconResId }
.toImmutableList(),
)
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.walletconnect.connections.model.transformers
import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM
import com.tangem.utils.transformer.Transformer
internal class WcConnectButtonProgressTransformer(private val showProgress: Boolean) : Transformer<WcAppInfoUM> {
override fun transform(prevState: WcAppInfoUM): WcAppInfoUM {
val contentState = prevState as? WcAppInfoUM.Content ?: return prevState
return contentState.copy(
connectButtonConfig = contentState.connectButtonConfig.copy(showProgress = showProgress),
)
}
}

View file

@ -28,15 +28,16 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification
import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM
import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo
import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig
@ -61,11 +62,23 @@ private fun WcAppInfoModalBottomSheetContent(state: WcAppInfoUM.Content, modifie
.background(color = TangemTheme.colors.background.action)
.fillMaxWidth()
WcAppInfoFirstBlock(modifier = blocksModifier, state = state)
state.notificationUM?.config?.let { config ->
state.notification?.let { notification ->
Notification(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing14),
config = config,
iconTint = TangemTheme.colors.icon.attention,
config = notification.config,
containerColor = when (notification) {
WcAppInfoSecurityNotification.SecurityRisk -> TangemColorPalette.Amaranth.copy(alpha = 0.1F)
WcAppInfoSecurityNotification.UnknownDomain -> null
},
titleColor = when (notification) {
WcAppInfoSecurityNotification.SecurityRisk -> TangemTheme.colors.text.warning
WcAppInfoSecurityNotification.UnknownDomain -> TangemTheme.colors.text.primary1
},
subtitleColor = TangemTheme.colors.text.primary1,
iconTint = when (notification) {
WcAppInfoSecurityNotification.SecurityRisk -> TangemTheme.colors.icon.warning
WcAppInfoSecurityNotification.UnknownDomain -> TangemTheme.colors.icon.attention
},
)
}
WcAppInfoSecondBlock(
@ -531,23 +544,6 @@ private fun WcLoadingAppInfoItem(modifier: Modifier = Modifier) {
}
// endregion
@Composable
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun WcAppInfoBottomSheetPreview() {
TangemThemePreview {
WcAppInfoModalBottomSheetLoading(
state = WcAppInfoUM.Loading(
onDismiss = {},
WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {}),
),
modifier = Modifier
.background(TangemTheme.colors.background.tertiary)
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}
@Composable
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO)
@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES)
@ -564,13 +560,13 @@ private fun WcAppInfoBottomSheetPreview(@PreviewParameter(WcAppInfoStateProvider
private class WcAppInfoStateProvider : CollectionPreviewParameterProvider<WcAppInfoUM>(
collection = listOf(
WcAppInfoUM.Loading(onDismiss = {}, WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {})),
// WcAppInfoUM.Loading(onDismiss = {}, WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {})),
WcAppInfoUM.Content(
appName = "React App",
appIcon = "",
isVerified = true,
appSubtitle = "react-app.walletconnect.com",
notificationUM = null,
notification = WcAppInfoSecurityNotification.SecurityRisk,
walletName = "Tangem 2.0",
networksInfo = WcNetworksInfo.ContainsAllRequiredNetworks(
icons = persistentListOf(
@ -589,10 +585,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider<WcAppI
appIcon = "",
isVerified = false,
appSubtitle = "react-app.walletconnect.com",
notificationUM = NotificationUM.Info(
title = resourceReference(R.string.wc_alert_audit_unknown_domain),
subtitle = resourceReference(R.string.wc_alert_domain_issues_description),
),
notification = WcAppInfoSecurityNotification.UnknownDomain,
walletName = "Tangem 2.0",
networksInfo = WcNetworksInfo.MissingRequiredNetworkInfo(networks = "Solana"),
onDismiss = {},