Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-31 21:03:37 +07:00
parent add9d552b4
commit e7ebbb931e
12 changed files with 516 additions and 76 deletions

View file

@ -24,6 +24,7 @@ import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier
import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.WcPairService
import com.tangem.domain.walletconnect.WcRequestService import com.tangem.domain.walletconnect.WcRequestService
@ -185,11 +186,13 @@ internal object WalletConnectDataModule {
walletManagersFacade: WalletManagersFacade, walletManagersFacade: WalletManagersFacade,
multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
singleAccountStatusListSupplier: SingleAccountStatusListSupplier, singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
singleAccountSupplier: SingleAccountSupplier,
): WcNetworksConverter = WcNetworksConverter( ): WcNetworksConverter = WcNetworksConverter(
namespaceConverters = namespaceConverters, namespaceConverters = namespaceConverters,
walletManagersFacade = walletManagersFacade, walletManagersFacade = walletManagersFacade,
singleAccountStatusListSupplier = singleAccountStatusListSupplier, singleAccountStatusListSupplier = singleAccountStatusListSupplier,
multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier,
singleAccountSupplier = singleAccountSupplier,
) )
@Provides @Provides

View file

@ -9,6 +9,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.walletconnect.utils.* import com.tangem.data.walletconnect.utils.*
import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.datasource.local.walletconnect.WalletConnectStore
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.WcAnalyticEvents
import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSession
@ -37,12 +38,15 @@ internal class DefaultWcSessionsManager(
) : WcSessionsManager, WcSdkObserver { ) : WcSessionsManager, WcSdkObserver {
private val onSessionDelete = Channel<Wallet.Model.SessionDelete>(capacity = Channel.BUFFERED) private val onSessionDelete = Channel<Wallet.Model.SessionDelete>(capacity = Channel.BUFFERED)
private val oneTimeMigration = MutableStateFlow(false)
override val sessions: Flow<Map<UserWallet, List<WcSession>>> override val sessions: Flow<Map<UserWallet, List<WcSession>>>
get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore } get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore }
.transform { pair -> .transform { pair ->
val (wallets, inStore) = pair val (wallets, inStore) = pair
val inSdk: List<Wallet.Model.Session> = WalletKit.getListOfActiveSessions() val inSdk: List<Wallet.Model.Session> = WalletKit.getListOfActiveSessions()
val someMigrate = migrateToAccountSession(inStore)
if (someMigrate) return@transform
val associatedSessions: List<WcSession> = associate(inSdk, inStore, wallets) val associatedSessions: List<WcSession> = associate(inSdk, inStore, wallets)
val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions) val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions)
if (someRemove) return@transform // ignore emit, wait next one if (someRemove) return@transform // ignore emit, wait next one
@ -51,6 +55,26 @@ internal class DefaultWcSessionsManager(
.distinctUntilChanged() .distinctUntilChanged()
.flowOn(dispatchers.io) .flowOn(dispatchers.io)
private suspend fun migrateToAccountSession(inStore: Set<WcSessionDTO>): Boolean {
if (!accountsFeatureToggles.isFeatureEnabled) return false
if (oneTimeMigration.value) return false
var someMigrated = false
val updatedSessions = inStore.mapTo(mutableSetOf()) { sessionDTO ->
if (sessionDTO.accountId == null) {
someMigrated = true
val mainAccountId = AccountId.forMainCryptoPortfolio(sessionDTO.walletId)
sessionDTO.copy(accountId = mainAccountId)
} else {
sessionDTO
}
}
if (someMigrated) store.saveSessions(updatedSessions)
oneTimeMigration.value = true
return someMigrated
}
override fun onWcSdkInit() { override fun onWcSdkInit() {
listenOnSessionDelete() listenOnSessionDelete()
extendSessions() extendSessions()

View file

@ -3,8 +3,10 @@ package com.tangem.data.walletconnect.utils
import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.Wallet
import com.tangem.data.common.currency.isCustomCoin import com.tangem.data.common.currency.isCustomCoin
import com.tangem.data.walletconnect.model.CAIP10 import com.tangem.data.walletconnect.model.CAIP10
import com.tangem.domain.account.producer.SingleAccountProducer
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.supplier.SingleAccountSupplier
import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.AccountStatus
@ -24,6 +26,7 @@ internal class WcNetworksConverter @Inject constructor(
private val namespaceConverters: Set<WcNamespaceConverter>, private val namespaceConverters: Set<WcNamespaceConverter>,
private val walletManagersFacade: WalletManagersFacade, private val walletManagersFacade: WalletManagersFacade,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val singleAccountSupplier: SingleAccountSupplier,
private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier,
) { ) {
@ -101,7 +104,7 @@ internal class WcNetworksConverter @Inject constructor(
} }
suspend fun getAccount(accountId: AccountId): Account? { suspend fun getAccount(accountId: AccountId): Account? {
return getAccountStatus(accountId)?.account return singleAccountSupplier.getSyncOrNull(SingleAccountProducer.Params(accountId))
} }
suspend fun convertNetworksForApprove(sessionForApprove: WcSessionApprove): List<Network> { suspend fun convertNetworksForApprove(sessionForApprove: WcSessionApprove): List<Network> {

View file

@ -1,5 +1,6 @@
package com.tangem.features.walletconnect.connections.entity package com.tangem.features.walletconnect.connections.entity
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
@ -10,7 +11,7 @@ data class WcConnectedAppInfoUM(
val verifiedDAppState: VerifiedDAppState, val verifiedDAppState: VerifiedDAppState,
val notification: WcAppInfoSecurityNotification? = null, val notification: WcAppInfoSecurityNotification? = null,
val appSubtitle: String, val appSubtitle: String,
val walletName: String, val portfolioName: AccountTitleUM?,
val connectingTime: Long?, val connectingTime: Long?,
val networks: ImmutableList<WcNetworkInfoItem.Required>, val networks: ImmutableList<WcNetworkInfoItem.Required>,
val disconnectButtonConfig: WcPrimaryButtonConfig, val disconnectButtonConfig: WcPrimaryButtonConfig,

View file

@ -1,12 +1,13 @@
package com.tangem.features.walletconnect.connections.entity package com.tangem.features.walletconnect.connections.entity
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
internal data class WcConnectionsState( internal data class WcConnectionsState(
val topAppBarConfig: WcConnectionsTopAppBarConfig, val topAppBarConfig: WcConnectionsTopAppBarConfig,
val connections: ImmutableList<WcConnectionsUM>, val connections: WcConnections,
val onNewConnectionClick: () -> Unit, val onNewConnectionClick: () -> Unit,
) )
@ -15,6 +16,39 @@ internal data class WcConnectionsTopAppBarConfig(
val disconnectAllItem: TangemDropdownMenuItem, val disconnectAllItem: TangemDropdownMenuItem,
) )
internal sealed interface WcConnections {
data object Loading : WcConnections
data class WalletMode(
val connections: ImmutableList<WcConnectionsUM>,
) : WcConnections
data class AccountMode(
val items: ImmutableList<WcConnectionsItem>,
) : WcConnections
fun isNotEmpty(): Boolean = when (this) {
is AccountMode -> items.isNotEmpty()
is WalletMode -> connections.isNotEmpty()
Loading -> false
}
}
internal sealed interface WcConnectionsItem {
val id: String
data class WalletHeader(
override val id: String,
val walletName: String,
val isLocked: Boolean,
) : WcConnectionsItem
data class PortfolioConnections(
override val id: String,
val portfolioTitle: AccountTitleUM,
val connectedApps: ImmutableList<WcConnectedAppInfo>,
) : WcConnectionsItem
}
internal data class WcConnectionsUM( internal data class WcConnectionsUM(
val userWalletId: String, val userWalletId: String,
val walletName: String, val walletName: String,

View file

@ -24,6 +24,7 @@ import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNot
import com.tangem.features.walletconnect.connections.model.transformers.WcAppSubtitleConverter import com.tangem.features.walletconnect.connections.model.transformers.WcAppSubtitleConverter
import com.tangem.features.walletconnect.connections.routes.ConnectedAppInfoRoutes import com.tangem.features.walletconnect.connections.routes.ConnectedAppInfoRoutes
import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.converter.WcPortfolioNameDelegate
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -39,10 +40,12 @@ internal class WcConnectedAppInfoModel @Inject constructor(
private val wcSessionsUseCase: WcSessionsUseCase, private val wcSessionsUseCase: WcSessionsUseCase,
private val messageSender: UiMessageSender, private val messageSender: UiMessageSender,
private val disconnectUseCase: WcDisconnectUseCase, private val disconnectUseCase: WcDisconnectUseCase,
portfolioNameDelegateFactory: WcPortfolioNameDelegate.Factory,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
paramsContainer: ParamsContainer, paramsContainer: ParamsContainer,
) : Model() { ) : Model() {
private val portfolioNameDelegate = portfolioNameDelegateFactory.create(modelScope)
private val params = paramsContainer.require<WcConnectedAppInfoContainerComponent.Params>() private val params = paramsContainer.require<WcConnectedAppInfoContainerComponent.Params>()
val uiState: StateFlow<WcConnectedAppInfoUM?> val uiState: StateFlow<WcConnectedAppInfoUM?>
field = MutableStateFlow<WcConnectedAppInfoUM?>(null) field = MutableStateFlow<WcConnectedAppInfoUM?>(null)
@ -68,7 +71,7 @@ internal class WcConnectedAppInfoModel @Inject constructor(
isVerified = session.securityStatus == CheckDAppResult.SAFE, isVerified = session.securityStatus == CheckDAppResult.SAFE,
verifiedDAppState = extractVerifiedState(session), verifiedDAppState = extractVerifiedState(session),
appSubtitle = WcAppSubtitleConverter.convert(session.sdkModel.appMetaData), appSubtitle = WcAppSubtitleConverter.convert(session.sdkModel.appMetaData),
walletName = session.wallet.name, portfolioName = portfolioNameDelegate.createAccountTitleUM(session),
connectingTime = session.connectingTime, connectingTime = session.connectingTime,
networks = session.networks networks = session.networks
.distinctBy { network -> network.rawId } .distinctBy { network -> network.rawId }

View file

@ -18,6 +18,9 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.supplier.MultiAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.qrscanning.models.QrResultSource import com.tangem.domain.qrscanning.models.QrResultSource
import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase
@ -28,14 +31,16 @@ import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase
import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase
import com.tangem.features.walletconnect.connections.components.WcConnectionsComponent import com.tangem.features.walletconnect.connections.components.WcConnectionsComponent
import com.tangem.features.walletconnect.connections.entity.WcConnections
import com.tangem.features.walletconnect.connections.entity.WcConnectionsState import com.tangem.features.walletconnect.connections.entity.WcConnectionsState
import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig
import com.tangem.features.walletconnect.connections.model.transformers.WcSessionsAccountModeTransformer
import com.tangem.features.walletconnect.connections.model.transformers.WcSessionsTransformer import com.tangem.features.walletconnect.connections.model.transformers.WcSessionsTransformer
import com.tangem.features.walletconnect.connections.routes.WcConnectionsBottomSheetConfig import com.tangem.features.walletconnect.connections.routes.WcConnectionsBottomSheetConfig
import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.transformer.Transformer
import com.tangem.utils.transformer.update import com.tangem.utils.transformer.update
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -48,6 +53,9 @@ internal class WcConnectionsModel @Inject constructor(
private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase,
private val wcSessionsUseCase: WcSessionsUseCase, private val wcSessionsUseCase: WcSessionsUseCase,
private val wcDisconnectUseCase: WcDisconnectUseCase, private val wcDisconnectUseCase: WcDisconnectUseCase,
private val multiAccountListSupplier: MultiAccountListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
accountsFeatureToggles: AccountsFeatureToggles,
private val wcPairService: WcPairService, private val wcPairService: WcPairService,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
analytics: AnalyticsEventHandler, analytics: AnalyticsEventHandler,
@ -62,7 +70,11 @@ internal class WcConnectionsModel @Inject constructor(
init { init {
analytics.send(WcAnalyticEvents.ScreenOpened) analytics.send(WcAnalyticEvents.ScreenOpened)
listenQrUpdates() listenQrUpdates()
if (accountsFeatureToggles.isFeatureEnabled) {
listenWcSessions() listenWcSessions()
} else {
listenWcSessionsOld()
}
} }
private fun listenQrUpdates() { private fun listenQrUpdates() {
@ -86,7 +98,7 @@ internal class WcConnectionsModel @Inject constructor(
.launchIn(modelScope) .launchIn(modelScope)
} }
private fun listenWcSessions() { private fun listenWcSessionsOld() {
wcSessionsUseCase.invoke() wcSessionsUseCase.invoke()
.conflate() .conflate()
.distinctUntilChanged() .distinctUntilChanged()
@ -101,6 +113,31 @@ internal class WcConnectionsModel @Inject constructor(
.launchIn(modelScope) .launchIn(modelScope)
} }
private fun listenWcSessions() {
combine(
flow = wcSessionsUseCase.invoke().distinctUntilChanged(),
flow2 = isAccountsModeEnabledUseCase(),
flow3 = multiAccountListSupplier(Unit),
transform = { sessionsMap, isAccountMode, accountList ->
val transformer: Transformer<WcConnectionsState> =
if (isAccountMode) {
WcSessionsAccountModeTransformer(
sessionsMap = sessionsMap,
accountList = accountList,
openAppInfoModal = ::openAppInfoModal,
)
} else {
WcSessionsTransformer(
sessionsMap = sessionsMap,
openAppInfoModal = ::openAppInfoModal,
)
}
uiState.update(transformer)
},
)
.launchIn(modelScope)
}
private fun openAppInfoModal(session: WcSession) { private fun openAppInfoModal(session: WcSession) {
bottomSheetNavigation.activate(WcConnectionsBottomSheetConfig.ConnectedApp(session.sdkModel.topic)) bottomSheetNavigation.activate(WcConnectionsBottomSheetConfig.ConnectedApp(session.sdkModel.topic))
} }
@ -144,7 +181,7 @@ internal class WcConnectionsModel @Inject constructor(
onClick = ::showDisconnectAllDialog, onClick = ::showDisconnectAllDialog,
), ),
), ),
connections = persistentListOf(), connections = WcConnections.Loading,
onNewConnectionClick = ::openQrScanScreen, onNewConnectionClick = ::openQrScanScreen,
) )
} }

View file

@ -0,0 +1,89 @@
package com.tangem.features.walletconnect.connections.model.transformers
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.account.models.AccountList
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfo
import com.tangem.features.walletconnect.connections.entity.WcConnections
import com.tangem.features.walletconnect.connections.entity.WcConnectionsItem
import com.tangem.features.walletconnect.connections.entity.WcConnectionsState
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal class WcSessionsAccountModeTransformer(
private val sessionsMap: Map<UserWallet, List<WcSession>>,
private val accountList: List<AccountList>,
private val openAppInfoModal: (WcSession) -> Unit,
) : Transformer<WcConnectionsState> {
override fun transform(prevState: WcConnectionsState): WcConnectionsState {
val items = mutableListOf<WcConnectionsItem>()
val lockedWallet = mutableListOf<WcConnectionsItem.WalletHeader>()
accountList.forEach { accountList ->
val userWallet = sessionsMap.keys.find { it.walletId == accountList.userWalletId }
?: return@forEach
val sessions = sessionsMap[userWallet] ?: return@forEach
if (userWallet.isLocked) {
val lockedItem = WcConnectionsItem.WalletHeader(
id = userWallet.walletId.stringValue,
walletName = userWallet.name,
isLocked = true,
)
lockedWallet.add(lockedItem)
return@forEach
}
val walletHeader = WcConnectionsItem.WalletHeader(
id = userWallet.walletId.stringValue,
walletName = userWallet.name,
isLocked = false,
)
items.add(walletHeader)
accountList.accounts.forEach accountsForEach@{ account ->
val accountSessions = sessions.filter { it.account?.accountId == account.accountId }
if (accountSessions.isEmpty()) return@accountsForEach
val connectedApps = accountSessions.map { dappSession ->
with(dappSession.sdkModel) {
WcConnectedAppInfo(
name = appMetaData.name,
iconUrl = appMetaData.icons.firstOrNull().orEmpty(),
subtitle = WcAppSubtitleConverter.convert(appMetaData),
verifiedState = WcDAppVerifiedStateConverter {}
.convert(dappSession.securityStatus to appMetaData.name),
onClick = { openAppInfoModal(dappSession) },
)
}
}
val accountIcon = when (account) {
is Account.CryptoPortfolio -> account.icon
}
val accountTitle = AccountTitleUM.Account(
prefixText = TextReference.EMPTY,
name = account.accountName.toUM().value,
icon = accountIcon.toUM(),
)
val accountConnections = WcConnectionsItem.PortfolioConnections(
id = userWallet.walletId.stringValue + account.accountId.value,
portfolioTitle = accountTitle,
connectedApps = connectedApps.toPersistentList(),
)
items.add(accountConnections)
}
}
return WcConnectionsState(
topAppBarConfig = prevState.topAppBarConfig,
connections = WcConnections.AccountMode(items.plus(lockedWallet).toPersistentList()),
onNewConnectionClick = prevState.onNewConnectionClick,
)
}
}

View file

@ -1,8 +1,9 @@
package com.tangem.features.walletconnect.connections.model.transformers package com.tangem.features.walletconnect.connections.model.transformers
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.walletconnect.model.WcSession
import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfo import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfo
import com.tangem.features.walletconnect.connections.entity.WcConnections
import com.tangem.features.walletconnect.connections.entity.WcConnectionsState import com.tangem.features.walletconnect.connections.entity.WcConnectionsState
import com.tangem.features.walletconnect.connections.entity.WcConnectionsUM import com.tangem.features.walletconnect.connections.entity.WcConnectionsUM
import com.tangem.utils.transformer.Transformer import com.tangem.utils.transformer.Transformer
@ -14,8 +15,7 @@ internal class WcSessionsTransformer(
) : Transformer<WcConnectionsState> { ) : Transformer<WcConnectionsState> {
override fun transform(prevState: WcConnectionsState): WcConnectionsState { override fun transform(prevState: WcConnectionsState): WcConnectionsState {
return prevState.copy( val newConnections = sessionsMap.map { (wallet, sessions) ->
connections = sessionsMap.map { (wallet, sessions) ->
WcConnectionsUM( WcConnectionsUM(
userWalletId = wallet.walletId.stringValue, userWalletId = wallet.walletId.stringValue,
walletName = wallet.name, walletName = wallet.name,
@ -32,7 +32,11 @@ internal class WcSessionsTransformer(
} }
}.toPersistentList(), }.toPersistentList(),
) )
}.toPersistentList(), }.toPersistentList()
return WcConnectionsState(
topAppBarConfig = prevState.topAppBarConfig,
connections = WcConnections.WalletMode(newConnections),
onNewConnectionClick = prevState.onNewConnectionClick,
) )
} }
} }

View file

@ -14,11 +14,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import com.tangem.common.ui.account.AccountTitle
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig 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.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
@ -35,6 +37,7 @@ import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.core.ui.utils.toTimeFormat import com.tangem.core.ui.utils.toTimeFormat
import com.tangem.features.walletconnect.connections.entity.* import com.tangem.features.walletconnect.connections.entity.*
import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.ui.sign.accountPortfolioName
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -137,9 +140,17 @@ private fun AppInfoFirstBlock(state: WcConnectedAppInfoUM, modifier: Modifier =
subtitle = state.appSubtitle, subtitle = state.appSubtitle,
verifiedDAppState = state.verifiedDAppState, verifiedDAppState = state.verifiedDAppState,
) )
HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) if (state.portfolioName != null) {
HorizontalDivider(thickness = 0.5.dp, color = TangemTheme.colors.stroke.primary)
PortfolioName(state.portfolioName)
}
}
}
@Composable
private fun PortfolioName(portfolioName: AccountTitleUM, modifier: Modifier = Modifier) {
Row( Row(
modifier = Modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(TangemTheme.dimens.spacing12), .padding(TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -152,27 +163,29 @@ private fun AppInfoFirstBlock(state: WcConnectedAppInfoUM, modifier: Modifier =
contentDescription = null, contentDescription = null,
tint = TangemTheme.colors.icon.accent, tint = TangemTheme.colors.icon.accent,
) )
val leftText: String = when (portfolioName) {
is AccountTitleUM.Account -> stringResourceSafe(R.string.common_account)
is AccountTitleUM.Text -> stringResourceSafe(R.string.wc_common_wallet)
}
Text( Text(
modifier = Modifier modifier = Modifier
.padding(start = TangemTheme.dimens.spacing4) .padding(start = TangemTheme.dimens.spacing4)
.weight(1f) .weight(1f)
.testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_TITLE), .testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_TITLE),
text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet), text = leftText,
style = TangemTheme.typography.body1, style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1, color = TangemTheme.colors.text.primary1,
) )
Text( AccountTitle(
modifier = Modifier modifier = Modifier
.padding(start = TangemTheme.dimens.spacing16) .padding(start = TangemTheme.dimens.spacing16)
.weight(1f)
.testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_NAME), .testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_NAME),
text = state.walletName, accountTitleUM = portfolioName,
textAlign = TextAlign.End, textStyle = TangemTheme.typography.body1,
style = TangemTheme.typography.body1, textColor = TangemTheme.colors.text.tertiary,
color = TangemTheme.colors.text.tertiary, iconSize = AccountIconSize.Small,
) )
} }
}
} }
@Composable @Composable
@ -194,7 +207,6 @@ private fun NetworksBlock(networks: ImmutableList<WcNetworkInfoItem.Required>, m
} }
@Preview(showBackground = true) @Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable @Composable
private fun WcConnectedAppInfoBS_Preview() { private fun WcConnectedAppInfoBS_Preview() {
TangemThemePreview { TangemThemePreview {
@ -205,7 +217,48 @@ private fun WcConnectedAppInfoBS_Preview() {
verifiedDAppState = VerifiedDAppState.Verified {}, verifiedDAppState = VerifiedDAppState.Verified {},
isVerified = true, isVerified = true,
appSubtitle = "react-app.walletconnect.com", appSubtitle = "react-app.walletconnect.com",
walletName = "Tangem 2.0", portfolioName = accountPortfolioName,
connectingTime = System.currentTimeMillis(),
networks = persistentListOf(
WcNetworkInfoItem.Required(
id = "1",
icon = R.drawable.img_optimism_22,
name = "img_optimism_22img_optimism_22",
symbol = "optimism",
),
WcNetworkInfoItem.Required(
id = "2",
icon = R.drawable.img_bsc_22,
name = "img_bsc_22",
symbol = "bsc",
),
WcNetworkInfoItem.Required(
id = "3",
icon = R.drawable.img_solana_22,
name = "img_solana_22",
symbol = "solana",
),
),
disconnectButtonConfig = WcPrimaryButtonConfig(showProgress = false, enabled = true, onClick = {}),
onDismiss = {},
),
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun WcConnectedAppInfoBS_OneWallet_Preview() {
TangemThemePreview {
WcConnectedAppInfoBS(
state = WcConnectedAppInfoUM(
appName = "React App",
appIcon = "",
verifiedDAppState = VerifiedDAppState.Verified {},
isVerified = true,
appSubtitle = "react-app.walletconnect.com",
portfolioName = null,
connectingTime = System.currentTimeMillis(), connectingTime = System.currentTimeMillis(),
networks = persistentListOf( networks = persistentListOf(
WcNetworkInfoItem.Required( WcNetworkInfoItem.Required(

View file

@ -8,17 +8,22 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -27,12 +32,14 @@ import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameter
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEach
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.tangem.common.ui.account.AccountTitle
import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.TopAppBarButton import com.tangem.core.ui.components.appbar.TopAppBarButton
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu
import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.components.snackbar.TangemSnackbarHost
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreview
@ -42,6 +49,8 @@ import com.tangem.features.walletconnect.connections.ui.preview.WcConnectionsPre
import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.impl.R
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
private const val LOCKED_WALLET_ALPHA = 0.5f
@Composable @Composable
internal fun WcConnectionsContent( internal fun WcConnectionsContent(
state: WcConnectionsState, state: WcConnectionsState,
@ -88,15 +97,21 @@ internal fun WcConnectionsContent(
} }
@Composable @Composable
private fun Content( private fun Content(connections: WcConnections, onNewConnectionClick: () -> Unit, modifier: Modifier = Modifier) {
connections: ImmutableList<WcConnectionsUM>, when {
onNewConnectionClick: () -> Unit, connections is WcConnections.Loading -> return
modifier: Modifier = Modifier, !connections.isNotEmpty() -> EmptyConnectionsBlock(
) { modifier = modifier,
if (connections.isEmpty()) { onNewConnectionClick = onNewConnectionClick,
EmptyConnectionsBlock(modifier = modifier, onNewConnectionClick = onNewConnectionClick) )
} else { connections is WcConnections.AccountMode -> AccountModeContent(
ConnectionsBlock(modifier = modifier, connections = connections) modifier = modifier,
items = connections.items,
)
connections is WcConnections.WalletMode -> ConnectionsBlock(
modifier = modifier,
connections = connections.connections,
)
} }
} }
@ -169,6 +184,59 @@ private fun ConnectionsBlock(connections: ImmutableList<WcConnectionsUM>, modifi
} }
} }
@Composable
private fun AccountModeContent(items: ImmutableList<WcConnectionsItem>, modifier: Modifier = Modifier) {
Box(modifier = modifier) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing76 + bottomBarHeight,
),
) {
itemsIndexed(
items = items,
key = { _, item -> item.id },
itemContent = { index, item ->
val previousItem = items.getOrNull(index.dec())
val itemModifier = Modifier
.fillMaxWidth()
.getOffsetModifier(item, previousItem)
when (item) {
is WcConnectionsItem.WalletHeader -> WalletHeader(item, itemModifier)
is WcConnectionsItem.PortfolioConnections -> PortfolioItem(
modifier = itemModifier
.background(
color = TangemTheme.colors.background.primary,
shape = RoundedCornerShape(TangemTheme.dimens.radius14),
)
.clip(RoundedCornerShape(TangemTheme.dimens.radius14)),
connection = item,
)
}
},
)
}
BottomFade(modifier = Modifier.align(Alignment.BottomCenter))
}
}
private fun Modifier.getOffsetModifier(item: WcConnectionsItem, previousItem: WcConnectionsItem?): Modifier = when {
item is WcConnectionsItem.WalletHeader && previousItem == null -> padding(top = 8.dp)
item is WcConnectionsItem.WalletHeader && previousItem !is WcConnectionsItem.WalletHeader ->
padding(top = 16.dp)
item is WcConnectionsItem.WalletHeader && previousItem is WcConnectionsItem.WalletHeader ->
padding(top = 8.dp)
item is WcConnectionsItem.PortfolioConnections && previousItem is WcConnectionsItem.WalletHeader ->
padding(top = 8.dp)
item is WcConnectionsItem.PortfolioConnections && previousItem is WcConnectionsItem.PortfolioConnections ->
padding(top = 8.dp)
else -> this
}
@Composable @Composable
private fun ConnectionItem(connection: WcConnectionsUM, modifier: Modifier = Modifier) { private fun ConnectionItem(connection: WcConnectionsUM, modifier: Modifier = Modifier) {
Column(modifier = modifier) { Column(modifier = modifier) {
@ -195,6 +263,69 @@ private fun ConnectionItem(connection: WcConnectionsUM, modifier: Modifier = Mod
} }
} }
@Composable
private fun WalletHeader(connection: WcConnectionsItem.WalletHeader, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.padding(horizontal = 8.dp, vertical = 2.dp)
.conditional(connection.isLocked) { alpha(LOCKED_WALLET_ALPHA) },
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
Text(
modifier = Modifier.weight(1f),
text = connection.walletName,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
if (connection.isLocked) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.background(
color = TangemTheme.colors.button.secondary,
shape = CircleShape,
)
.padding(4.dp),
) {
Icon(
modifier = Modifier.size(16.dp),
tint = TangemTheme.colors.icon.informative,
imageVector = ImageVector.vectorResource(id = R.drawable.ic_locked_24),
contentDescription = null,
)
}
}
}
}
@Composable
private fun PortfolioItem(connection: WcConnectionsItem.PortfolioConnections, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
key(connection.id) {
AccountTitle(
textStyle = TangemTheme.typography.caption1,
textColor = TangemTheme.colors.text.primary1,
accountTitleUM = connection.portfolioTitle,
modifier = Modifier.padding(top = 12.dp, bottom = 4.dp, start = 12.dp, end = 12.dp),
)
}
connection.connectedApps.fastForEach { appInfo ->
key("${connection.id}_${appInfo.name}_${appInfo.iconUrl}") {
AppInfoItem(
appInfo = appInfo,
modifier = Modifier
.clickable(onClick = appInfo.onClick)
.padding(all = 12.dp),
)
}
}
}
}
@Composable @Composable
private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifier) { private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifier) {
Row( Row(
@ -331,7 +462,9 @@ private fun WcConnectionsContentPreview(
private class WcConnectionsStateProvider : CollectionPreviewParameterProvider<WcConnectionsState>( private class WcConnectionsStateProvider : CollectionPreviewParameterProvider<WcConnectionsState>(
listOf( listOf(
WcConnectionsPreviewData.loadingState,
WcConnectionsPreviewData.stateWithEmptyConnections, WcConnectionsPreviewData.stateWithEmptyConnections,
WcConnectionsPreviewData.fullState, WcConnectionsPreviewData.fullState,
WcConnectionsPreviewData.accountState,
), ),
) )

View file

@ -7,8 +7,10 @@ import com.tangem.core.ui.extensions.themedColor
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.walletconnect.connections.entity.* import com.tangem.features.walletconnect.connections.entity.*
import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.impl.R
import com.tangem.features.walletconnect.transaction.ui.sign.accountPortfolioName
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import timber.log.Timber import timber.log.Timber
import java.util.UUID
internal object WcConnectionsPreviewData { internal object WcConnectionsPreviewData {
private const val BASE_URL = "https://raw.githubusercontent.com/TaranVH/LOGOS/refs/heads/master/Crypto" private const val BASE_URL = "https://raw.githubusercontent.com/TaranVH/LOGOS/refs/heads/master/Crypto"
@ -125,8 +127,62 @@ internal object WcConnectionsPreviewData {
onClick = { }, onClick = { },
), ),
), ),
connections = persistentListOf(), connections = WcConnections.WalletMode(persistentListOf()),
onNewConnectionClick = {}, onNewConnectionClick = {},
) )
val fullState = stateWithEmptyConnections.copy(connections = connections)
val connectedDapp
get() = WcConnectedAppInfo(
name = "React app",
iconUrl = "$BASE_URL/LiteCoin.png",
subtitle = "https://react-app.walletconnect.com/",
verifiedState = VerifiedDAppState.Verified {},
onClick = { Timber.d("React app clicked") },
)
val walletHeader
get() = WcConnectionsItem.WalletHeader(
id = UUID.randomUUID().toString(),
walletName = "User Wallet",
isLocked = false,
)
val lockedWallet
get() = WcConnectionsItem.WalletHeader(
id = UUID.randomUUID().toString(),
walletName = "Locked Wallet",
isLocked = true,
)
val accountStateItems
get() = persistentListOf(
walletHeader,
WcConnectionsItem.PortfolioConnections(
id = UUID.randomUUID().toString(),
portfolioTitle = accountPortfolioName,
connectedApps = persistentListOf(connectedDapp),
),
walletHeader,
WcConnectionsItem.PortfolioConnections(
id = UUID.randomUUID().toString(),
portfolioTitle = accountPortfolioName,
connectedApps = persistentListOf(connectedDapp, connectedDapp),
),
WcConnectionsItem.PortfolioConnections(
id = UUID.randomUUID().toString(),
portfolioTitle = accountPortfolioName,
connectedApps = persistentListOf(connectedDapp),
),
WcConnectionsItem.PortfolioConnections(
id = UUID.randomUUID().toString(),
portfolioTitle = accountPortfolioName,
connectedApps = persistentListOf(connectedDapp),
),
lockedWallet,
lockedWallet,
)
val fullState = stateWithEmptyConnections.copy(connections = WcConnections.WalletMode(connections))
val accountState = stateWithEmptyConnections.copy(connections = WcConnections.AccountMode(accountStateItems))
val loadingState = stateWithEmptyConnections.copy(connections = WcConnections.Loading)
} }