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.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

View file

@ -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<Wallet.Model.SessionDelete>(capacity = Channel.BUFFERED)
private val oneTimeMigration = MutableStateFlow(false)
override val sessions: Flow<Map<UserWallet, List<WcSession>>>
get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore }
.transform { pair ->
val (wallets, inStore) = pair
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 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<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() {
listenOnSessionDelete()
extendSessions()

View file

@ -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<WcNamespaceConverter>,
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<Network> {

View file

@ -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<WcNetworkInfoItem.Required>,
val disconnectButtonConfig: WcPrimaryButtonConfig,

View file

@ -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<WcConnectionsUM>,
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<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(
val userWalletId: 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.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<WcConnectedAppInfoContainerComponent.Params>()
val uiState: StateFlow<WcConnectedAppInfoUM?>
field = MutableStateFlow<WcConnectedAppInfoUM?>(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 }

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.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()
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<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) {
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,
)
}

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
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,8 +15,7 @@ internal class WcSessionsTransformer(
) : Transformer<WcConnectionsState> {
override fun transform(prevState: WcConnectionsState): WcConnectionsState {
return prevState.copy(
connections = sessionsMap.map { (wallet, sessions) ->
val newConnections = sessionsMap.map { (wallet, sessions) ->
WcConnectionsUM(
userWalletId = wallet.walletId.stringValue,
walletName = wallet.name,
@ -32,7 +32,11 @@ internal class WcSessionsTransformer(
}
}.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.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,9 +140,17 @@ private fun AppInfoFirstBlock(state: WcConnectedAppInfoUM, modifier: Modifier =
subtitle = state.appSubtitle,
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(
modifier = Modifier
modifier = modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
verticalAlignment = Alignment.CenterVertically,
@ -152,27 +163,29 @@ private fun AppInfoFirstBlock(state: WcConnectedAppInfoUM, modifier: Modifier =
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 = stringResourceSafe(R.string.manage_tokens_network_selector_wallet),
text = leftText,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
Text(
AccountTitle(
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,
accountTitleUM = portfolioName,
textStyle = TangemTheme.typography.body1,
textColor = TangemTheme.colors.text.tertiary,
iconSize = AccountIconSize.Small,
)
}
}
}
@Composable
@ -194,7 +207,6 @@ private fun NetworksBlock(networks: ImmutableList<WcNetworkInfoItem.Required>, 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(

View file

@ -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<WcConnectionsUM>,
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<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
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<WcConnectionsState>(
listOf(
WcConnectionsPreviewData.loadingState,
WcConnectionsPreviewData.stateWithEmptyConnections,
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.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)
}