From e7ebbb931e6a3dc8bf28f2e915526ed6600a2c65 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 31 Oct 2025 21:03:37 +0700 Subject: [PATCH] Updated on 2026-08-14 --- .../di/WalletConnectDataModule.kt | 3 + .../sessions/DefaultWcSessionsManager.kt | 24 +++ .../utils/WcNetworksConverter.kt | 5 +- .../entity/WcConnectedAppInfoUM.kt | 3 +- .../connections/entity/WcConnectionsState.kt | 36 ++++- .../model/WcConnectedAppInfoModel.kt | 5 +- .../connections/model/WcConnectionsModel.kt | 45 +++++- .../WcSessionsAccountModeTransformer.kt | 89 +++++++++++ .../transformers/WcSessionsTransformer.kt | 44 ++--- .../connections/ui/WcConnectedAppInfoBS.kt | 127 ++++++++++----- .../connections/ui/WcConnectionsContent.kt | 151 ++++++++++++++++-- .../ui/preview/WcConnectionsPreviewData.kt | 60 ++++++- 12 files changed, 516 insertions(+), 76 deletions(-) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 3a7c5c209d..47a63b25ac 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -24,6 +24,7 @@ import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.supplier.MultiAccountListSupplier +import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.WcRequestService @@ -185,11 +186,13 @@ internal object WalletConnectDataModule { walletManagersFacade: WalletManagersFacade, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + singleAccountSupplier: SingleAccountSupplier, ): WcNetworksConverter = WcNetworksConverter( namespaceConverters = namespaceConverters, walletManagersFacade = walletManagersFacade, singleAccountStatusListSupplier = singleAccountStatusListSupplier, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, + singleAccountSupplier = singleAccountSupplier, ) @Provides diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 659e65de2d..848442eff1 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -9,6 +9,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.utils.* import com.tangem.datasource.local.walletconnect.WalletConnectStore 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.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcSession @@ -37,12 +38,15 @@ internal class DefaultWcSessionsManager( ) : WcSessionsManager, WcSdkObserver { private val onSessionDelete = Channel(capacity = Channel.BUFFERED) + private val oneTimeMigration = MutableStateFlow(false) override val sessions: Flow>> get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore } .transform { pair -> val (wallets, inStore) = pair val inSdk: List = WalletKit.getListOfActiveSessions() + val someMigrate = migrateToAccountSession(inStore) + if (someMigrate) return@transform val associatedSessions: List = associate(inSdk, inStore, wallets) val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions) if (someRemove) return@transform // ignore emit, wait next one @@ -51,6 +55,26 @@ internal class DefaultWcSessionsManager( .distinctUntilChanged() .flowOn(dispatchers.io) + private suspend fun migrateToAccountSession(inStore: Set): 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() { listenOnSessionDelete() extendSessions() diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index ced2f6ecdb..880b142dbd 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -3,8 +3,10 @@ package com.tangem.data.walletconnect.utils import com.reown.walletkit.client.Wallet import com.tangem.data.common.currency.isCustomCoin 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.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountStatus @@ -24,6 +26,7 @@ internal class WcNetworksConverter @Inject constructor( private val namespaceConverters: Set, private val walletManagersFacade: WalletManagersFacade, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val singleAccountSupplier: SingleAccountSupplier, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, ) { @@ -101,7 +104,7 @@ internal class WcNetworksConverter @Inject constructor( } suspend fun getAccount(accountId: AccountId): Account? { - return getAccountStatus(accountId)?.account + return singleAccountSupplier.getSyncOrNull(SingleAccountProducer.Params(accountId)) } suspend fun convertNetworksForApprove(sessionForApprove: WcSessionApprove): List { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt index 02928cb95d..5fbe12d50d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt @@ -1,5 +1,6 @@ package com.tangem.features.walletconnect.connections.entity +import com.tangem.common.ui.account.AccountTitleUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import kotlinx.collections.immutable.ImmutableList @@ -10,7 +11,7 @@ data class WcConnectedAppInfoUM( val verifiedDAppState: VerifiedDAppState, val notification: WcAppInfoSecurityNotification? = null, val appSubtitle: String, - val walletName: String, + val portfolioName: AccountTitleUM?, val connectingTime: Long?, val networks: ImmutableList, val disconnectButtonConfig: WcPrimaryButtonConfig, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt index 1bdc7df0ae..e648886fc5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt @@ -1,12 +1,13 @@ 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.dropdownmenu.TangemDropdownMenuItem import kotlinx.collections.immutable.ImmutableList internal data class WcConnectionsState( val topAppBarConfig: WcConnectionsTopAppBarConfig, - val connections: ImmutableList, + val connections: WcConnections, val onNewConnectionClick: () -> Unit, ) @@ -15,6 +16,39 @@ internal data class WcConnectionsTopAppBarConfig( val disconnectAllItem: TangemDropdownMenuItem, ) +internal sealed interface WcConnections { + data object Loading : WcConnections + data class WalletMode( + val connections: ImmutableList, + ) : WcConnections + + data class AccountMode( + val items: ImmutableList, + ) : 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, + ) : WcConnectionsItem +} + internal data class WcConnectionsUM( val userWalletId: String, val walletName: String, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt index 4d36530516..0bb89d47c0 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt @@ -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.routes.ConnectedAppInfoRoutes import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.converter.WcPortfolioNameDelegate import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow @@ -39,10 +40,12 @@ internal class WcConnectedAppInfoModel @Inject constructor( private val wcSessionsUseCase: WcSessionsUseCase, private val messageSender: UiMessageSender, private val disconnectUseCase: WcDisconnectUseCase, + portfolioNameDelegateFactory: WcPortfolioNameDelegate.Factory, override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, ) : Model() { + private val portfolioNameDelegate = portfolioNameDelegateFactory.create(modelScope) private val params = paramsContainer.require() val uiState: StateFlow field = MutableStateFlow(null) @@ -68,7 +71,7 @@ internal class WcConnectedAppInfoModel @Inject constructor( isVerified = session.securityStatus == CheckDAppResult.SAFE, verifiedDAppState = extractVerifiedState(session), appSubtitle = WcAppSubtitleConverter.convert(session.sdkModel.appMetaData), - walletName = session.wallet.name, + portfolioName = portfolioNameDelegate.createAccountTitleUM(session), connectingTime = session.connectingTime, networks = session.networks .distinctBy { network -> network.rawId } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index 809f8192aa..fc0d38f971 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -18,6 +18,9 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage 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.SourceType 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.disconnect.WcDisconnectUseCase 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.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.routes.WcConnectionsBottomSheetConfig import com.tangem.features.walletconnect.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.transformer.Transformer import com.tangem.utils.transformer.update -import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -48,6 +53,9 @@ internal class WcConnectionsModel @Inject constructor( private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val wcSessionsUseCase: WcSessionsUseCase, private val wcDisconnectUseCase: WcDisconnectUseCase, + private val multiAccountListSupplier: MultiAccountListSupplier, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + accountsFeatureToggles: AccountsFeatureToggles, private val wcPairService: WcPairService, override val dispatchers: CoroutineDispatcherProvider, analytics: AnalyticsEventHandler, @@ -62,7 +70,11 @@ internal class WcConnectionsModel @Inject constructor( init { analytics.send(WcAnalyticEvents.ScreenOpened) listenQrUpdates() - listenWcSessions() + if (accountsFeatureToggles.isFeatureEnabled) { + listenWcSessions() + } else { + listenWcSessionsOld() + } } private fun listenQrUpdates() { @@ -86,7 +98,7 @@ internal class WcConnectionsModel @Inject constructor( .launchIn(modelScope) } - private fun listenWcSessions() { + private fun listenWcSessionsOld() { wcSessionsUseCase.invoke() .conflate() .distinctUntilChanged() @@ -101,6 +113,31 @@ internal class WcConnectionsModel @Inject constructor( .launchIn(modelScope) } + private fun listenWcSessions() { + combine( + flow = wcSessionsUseCase.invoke().distinctUntilChanged(), + flow2 = isAccountsModeEnabledUseCase(), + flow3 = multiAccountListSupplier(Unit), + transform = { sessionsMap, isAccountMode, accountList -> + val transformer: Transformer = + 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) { bottomSheetNavigation.activate(WcConnectionsBottomSheetConfig.ConnectedApp(session.sdkModel.topic)) } @@ -144,7 +181,7 @@ internal class WcConnectionsModel @Inject constructor( onClick = ::showDisconnectAllDialog, ), ), - connections = persistentListOf(), + connections = WcConnections.Loading, onNewConnectionClick = ::openQrScanScreen, ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt new file mode 100644 index 0000000000..a36142ec24 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt @@ -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>, + private val accountList: List, + private val openAppInfoModal: (WcSession) -> Unit, +) : Transformer { + + override fun transform(prevState: WcConnectionsState): WcConnectionsState { + val items = mutableListOf() + val lockedWallet = mutableListOf() + + 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, + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt index 97203a212d..5c39ea6c36 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt @@ -1,8 +1,9 @@ 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.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.WcConnectionsState import com.tangem.features.walletconnect.connections.entity.WcConnectionsUM import com.tangem.utils.transformer.Transformer @@ -14,25 +15,28 @@ internal class WcSessionsTransformer( ) : Transformer { override fun transform(prevState: WcConnectionsState): WcConnectionsState { - return prevState.copy( - connections = sessionsMap.map { (wallet, sessions) -> - WcConnectionsUM( - userWalletId = wallet.walletId.stringValue, - walletName = wallet.name, - connectedApps = sessions.map { session -> - with(session.sdkModel) { - WcConnectedAppInfo( - name = appMetaData.name, - iconUrl = appMetaData.icons.firstOrNull().orEmpty(), - subtitle = WcAppSubtitleConverter.convert(session.sdkModel.appMetaData), - verifiedState = WcDAppVerifiedStateConverter {} - .convert(session.securityStatus to appMetaData.name), - onClick = { openAppInfoModal(session) }, - ) - } - }.toPersistentList(), - ) - }.toPersistentList(), + val newConnections = sessionsMap.map { (wallet, sessions) -> + WcConnectionsUM( + userWalletId = wallet.walletId.stringValue, + walletName = wallet.name, + connectedApps = sessions.map { session -> + with(session.sdkModel) { + WcConnectedAppInfo( + name = appMetaData.name, + iconUrl = appMetaData.icons.firstOrNull().orEmpty(), + subtitle = WcAppSubtitleConverter.convert(session.sdkModel.appMetaData), + verifiedState = WcDAppVerifiedStateConverter {} + .convert(session.securityStatus to appMetaData.name), + onClick = { openAppInfoModal(session) }, + ) + } + }.toPersistentList(), + ) + }.toPersistentList() + return WcConnectionsState( + topAppBarConfig = prevState.topAppBarConfig, + connections = WcConnections.WalletMode(newConnections), + onNewConnectionClick = prevState.onNewConnectionClick, ) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt index 5fab3966ed..ae6bbb05ce 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt @@ -14,11 +14,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp 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.account.AccountIconSize 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 @@ -35,6 +37,7 @@ import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday import com.tangem.core.ui.utils.toTimeFormat import com.tangem.features.walletconnect.connections.entity.* 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.persistentListOf @@ -137,44 +140,54 @@ private fun AppInfoFirstBlock(state: WcConnectedAppInfoUM, modifier: Modifier = subtitle = state.appSubtitle, verifiedDAppState = state.verifiedDAppState, ) - HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - modifier = Modifier - .size(24.dp) - .testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_ICON), - painter = painterResource(R.drawable.ic_wallet_new_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - Text( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing4) - .weight(1f) - .testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_TITLE), - text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing16) - .weight(1f) - .testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_NAME), - text = state.walletName, - textAlign = TextAlign.End, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - ) + 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( + modifier = modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier + .size(24.dp) + .testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_ICON), + painter = painterResource(R.drawable.ic_wallet_new_24), + contentDescription = null, + 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( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing4) + .weight(1f) + .testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_TITLE), + text = leftText, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + AccountTitle( + modifier = Modifier + .padding(start = TangemTheme.dimens.spacing16) + .testTag(WalletConnectDetailsBottomSheetTestTags.WALLET_NAME), + accountTitleUM = portfolioName, + textStyle = TangemTheme.typography.body1, + textColor = TangemTheme.colors.text.tertiary, + iconSize = AccountIconSize.Small, + ) + } +} + @Composable private fun NetworksBlock(networks: ImmutableList, modifier: Modifier = Modifier) { Column(modifier = modifier) { @@ -194,7 +207,6 @@ private fun NetworksBlock(networks: ImmutableList, m } @Preview(showBackground = true) -@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun WcConnectedAppInfoBS_Preview() { TangemThemePreview { @@ -205,7 +217,48 @@ private fun WcConnectedAppInfoBS_Preview() { verifiedDAppState = VerifiedDAppState.Verified {}, isVerified = true, 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(), networks = persistentListOf( WcNetworkInfoItem.Required( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt index 8afd70342c..2fad45db0b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt @@ -8,17 +8,22 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn 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.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag 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.TextOverflow 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.util.fastForEach import coil.compose.AsyncImage +import com.tangem.common.ui.account.AccountTitle import com.tangem.core.ui.components.BottomFade import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.appbar.TopAppBarButton import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu 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.res.TangemTheme 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 kotlinx.collections.immutable.ImmutableList +private const val LOCKED_WALLET_ALPHA = 0.5f + @Composable internal fun WcConnectionsContent( state: WcConnectionsState, @@ -88,15 +97,21 @@ internal fun WcConnectionsContent( } @Composable -private fun Content( - connections: ImmutableList, - onNewConnectionClick: () -> Unit, - modifier: Modifier = Modifier, -) { - if (connections.isEmpty()) { - EmptyConnectionsBlock(modifier = modifier, onNewConnectionClick = onNewConnectionClick) - } else { - ConnectionsBlock(modifier = modifier, connections = connections) +private fun Content(connections: WcConnections, onNewConnectionClick: () -> Unit, modifier: Modifier = Modifier) { + when { + connections is WcConnections.Loading -> return + !connections.isNotEmpty() -> EmptyConnectionsBlock( + modifier = modifier, + onNewConnectionClick = onNewConnectionClick, + ) + connections is WcConnections.AccountMode -> AccountModeContent( + modifier = modifier, + items = connections.items, + ) + connections is WcConnections.WalletMode -> ConnectionsBlock( + modifier = modifier, + connections = connections.connections, + ) } } @@ -169,6 +184,59 @@ private fun ConnectionsBlock(connections: ImmutableList, modifi } } +@Composable +private fun AccountModeContent(items: ImmutableList, 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 private fun ConnectionItem(connection: WcConnectionsUM, modifier: 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 private fun AppInfoItem(appInfo: WcConnectedAppInfo, modifier: Modifier = Modifier) { Row( @@ -331,7 +462,9 @@ private fun WcConnectionsContentPreview( private class WcConnectionsStateProvider : CollectionPreviewParameterProvider( listOf( + WcConnectionsPreviewData.loadingState, WcConnectionsPreviewData.stateWithEmptyConnections, WcConnectionsPreviewData.fullState, + WcConnectionsPreviewData.accountState, ), ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt index 8a73a65d2f..56a76748db 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt @@ -7,8 +7,10 @@ import com.tangem.core.ui.extensions.themedColor import com.tangem.core.ui.res.TangemTheme import com.tangem.features.walletconnect.connections.entity.* import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.ui.sign.accountPortfolioName import kotlinx.collections.immutable.persistentListOf import timber.log.Timber +import java.util.UUID internal object WcConnectionsPreviewData { private const val BASE_URL = "https://raw.githubusercontent.com/TaranVH/LOGOS/refs/heads/master/Crypto" @@ -125,8 +127,62 @@ internal object WcConnectionsPreviewData { onClick = { }, ), ), - connections = persistentListOf(), + connections = WcConnections.WalletMode(persistentListOf()), 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) } \ No newline at end of file