Updated on 2026-08-14

This commit is contained in:
Tangem 2025-10-27 18:02:51 +07:00
parent a04992591a
commit d3320b9e8b
14 changed files with 370 additions and 111 deletions

View file

@ -4,6 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
@ -30,13 +31,18 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.account.AccountName
@Composable
fun PortfolioSelectRow(state: PortfolioSelectUM, modifier: Modifier = Modifier) {
fun PortfolioSelectRow(
state: PortfolioSelectUM,
modifier: Modifier = Modifier,
leftContent: @Composable RowScope.() -> Unit = {},
) {
Row(
modifier = modifier
.clickable(enabled = state.isMultiChoice, onClick = state.onClick)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
leftContent()
val leftText = if (state.isAccountMode) R.string.account_details_title else R.string.wc_common_wallet
Text(
modifier = Modifier.weight(1f),
@ -97,6 +103,11 @@ private fun PortfolioSelectRowPreview(@PreviewParameter(PreviewProvider::class)
private class PreviewProvider : PreviewParameterProvider<PortfolioSelectUM> {
override val values: Sequence<PortfolioSelectUM>
get() = sequenceOf(PortfolioSelectRowPreviewData.account, PortfolioSelectRowPreviewData.wallet)
}
object PortfolioSelectRowPreviewData {
val account
get() = PortfolioSelectUM(
icon = AccountIconPreviewData.randomAccountIcon(),
@ -113,7 +124,4 @@ private class PreviewProvider : PreviewParameterProvider<PortfolioSelectUM> {
isAccountMode = false,
onClick = {},
)
override val values: Sequence<PortfolioSelectUM>
get() = sequenceOf(account, wallet)
}

View file

@ -12,7 +12,7 @@ class GetTokenMarketCryptoCurrency(
userWalletId: UserWalletId,
tokenMarketParams: TokenMarketParams,
network: TokenMarketInfo.Network,
accountIndex: DerivationIndex? = null,
accountIndex: DerivationIndex,
): CryptoCurrency? {
return marketsTokenRepository.createCryptoCurrency(
userWalletId = userWalletId,

View file

@ -5,6 +5,7 @@ import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase
import com.tangem.domain.markets.GetTokenMarketCryptoCurrency
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
@ -29,7 +30,8 @@ internal class AvailableToAddDataConverter @Inject constructor(
): AvailableToAddData {
suspend fun AccountStatus.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount {
val addedNetworks = availableNetworks
.mapNotNull { createCryptoCurrency(wallet, it, marketParams) }
.mapNotNull { createCryptoCurrency(wallet, it, marketParams, this.account) }
// todo account poor performance, investigate, e.g 20 accounts and USDC token for adding
.mapNotNull { getAccountCurrencyStatusUseCase.invokeSync(wallet.walletId, it) }
.mapNotNull { it.getOrNull()?.status?.currency?.network }
.toSet()
@ -82,9 +84,16 @@ internal class AvailableToAddDataConverter @Inject constructor(
userWallet: UserWallet,
network: TokenMarketInfo.Network,
marketParams: TokenMarketParams,
): CryptoCurrency? = getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = marketParams,
network = network,
)
account: Account,
): CryptoCurrency? {
val derivationIndex = when (account) {
is Account.CryptoPortfolio -> account.derivationIndex
}
return getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = marketParams,
network = network,
accountIndex = derivationIndex,
)
}
}

View file

@ -257,12 +257,17 @@ internal class AddToPortfolioModel @Inject constructor(
userWallet: UserWallet,
network: TokenMarketInfo.Network,
account: AvailableToAddAccount,
): CryptoCurrency? = getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = addToPortfolioManager.token,
network = network,
accountIndex = (account.account as? AccountStatus.CryptoPortfolio)?.account?.derivationIndex,
)
): CryptoCurrency? {
val accountIndex = when (account.account) {
is AccountStatus.CryptoPortfolio -> account.account.account.derivationIndex
}
return getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = addToPortfolioManager.token,
network = network,
accountIndex = accountIndex,
)
}
private fun routeToNetworkSelector(portfolio: SelectedPortfolio): AddToPortfolioRoutes.NetworkSelector {
return AddToPortfolioRoutes.NetworkSelector(selectedPortfolio = portfolio)

View file

@ -4,9 +4,14 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.add.api.SelectedNetwork
import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio
import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent
@ -25,6 +30,7 @@ import javax.inject.Inject
internal class AddTokenModel @Inject constructor(
paramsContainer: ParamsContainer,
private val uiBuilder: AddTokenUiBuilder,
private val messageSender: UiMessageSender,
private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase,
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
@ -70,13 +76,22 @@ internal class AddTokenModel @Inject constructor(
val cryptoCurrency = selectedNetwork.cryptoCurrency
val accountId = selectedPortfolio.account.account.account.accountId
manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency)
.onLeft {
processError(error = it)
uiState.value = um.toggleProgress(false)
return@launch
}
val status = getAccountCurrencyStatusUseCase.invokeSync(
userWalletId = accountId.userWalletId,
currencyId = cryptoCurrency.id,
network = cryptoCurrency.network,
).getOrNull() ?: return@launch
params.callbacks.onTokenAdded(status.status)
).getOrNull()
if (status == null) {
processError(error = null)
} else {
params.callbacks.onTokenAdded(status.status)
}
uiState.value = um.toggleProgress(false)
}
@ -87,4 +102,10 @@ internal class AddTokenModel @Inject constructor(
userWalletId = selectedPortfolio.userWallet.walletId,
networksWithDerivationPath = mapOf(selectedNetwork.selectedNetwork.networkId to null),
)
private fun processError(error: Throwable?) {
val message = error?.message?.let { stringReference(it) }
?: resourceReference(R.string.common_something_went_wrong)
messageSender.send(ToastMessage(message = message))
}
}

View file

@ -87,19 +87,26 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor(
private fun onAvailableNetworksFlow(): Flow<MyPortfolioUM> =
portfolioWithThisCurrencyFLow().transformLatest { portfolioWithCurrency ->
fun addFirstTokenUM() = MyPortfolioUM.AddFirstToken(
onAddClick = onAddClick,
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
)
when (portfolioWithCurrency.flattenAddedCurrency.isEmpty()) {
false -> emitAll(contentFlow(portfolioWithCurrency).distinctUntilChanged())
true -> when (portfolioWithCurrency.hasMultiWallets) {
true -> emit(addFirstTokenUM())
true -> emitAll(addFirstTokenFlow())
false -> emit(MyPortfolioUM.UnavailableForWallet)
}
}
}
private fun addFirstTokenFlow(): Flow<MyPortfolioUM> = buttonState.map {
when (it) {
AddButtonState.Loading -> MyPortfolioUM.Loading
AddButtonState.Available -> MyPortfolioUM.AddFirstToken(
onAddClick = onAddClick,
addToPortfolioBSConfig = TangemBottomSheetConfig.Empty,
)
AddButtonState.Unavailable -> MyPortfolioUM.Unavailable
}
}
private fun contentFlow(portfolio: PortfoliosWithThisCurrency): Flow<MyPortfolioUM.Content> {
fun Portfolio.actionsFoAccountCurrencies(): List<Flow<Pair<CryptoCurrency, TokenActionsState>>> =
accountsWithAdded.map { account ->

View file

@ -12,6 +12,7 @@ android {
}
dependencies {
implementation(projects.features.account.api)
implementation(projects.features.wallet.api)
implementation(projects.features.walletconnect.api)
implementation(projects.features.sendV2.api)
@ -28,6 +29,7 @@ dependencies {
implementation(projects.core.ui)
/** Domain models */
implementation(projects.domain.account)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.blockaid.models)

View file

@ -17,6 +17,9 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.walletconnect.model.WcPairRequest
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.walletconnect.connections.components.WcSelectNetworksComponent.*
import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent.*
import com.tangem.features.walletconnect.connections.model.WcPairModel
import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes
import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes.Alert
@ -24,6 +27,7 @@ import com.tangem.features.walletconnect.connections.utils.WcAlertsFactory
internal class WcPairComponent(
private val appComponentContext: AppComponentContext,
private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
params: Params,
) : AppComponentContext by appComponentContext, ComposableContentComponent {
@ -65,6 +69,7 @@ internal class WcPairComponent(
}
is WcAppInfoRoutes.SelectNetworks,
is WcAppInfoRoutes.SelectWallet,
is WcAppInfoRoutes.PortfolioSelector,
-> model.stackNavigation.pop()
}
}
@ -107,6 +112,14 @@ internal class WcPairComponent(
callback = model,
),
)
WcAppInfoRoutes.PortfolioSelector -> portfolioSelectorComponentFactory.create(
context = appComponentContext,
params = PortfolioSelectorComponent.Params(
portfolioFetcher = model.portfolioFetcher!!,
bsCallback = model.portfolioSelectorCallback,
controller = model.selectorController,
),
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.walletconnect.connections.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.TextReference
@ -17,6 +18,7 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent {
data class Loading(
override val onDismiss: () -> Unit,
override val connectButtonConfig: WcPrimaryButtonConfig,
val portfolioName: TextReference,
) : WcAppInfoUM()
data class Content(
@ -25,6 +27,7 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent {
val verifiedDAppState: VerifiedDAppState,
val appSubtitle: String,
val notification: WcAppInfoSecurityNotification?,
val portfolioSelectRow: PortfolioSelectUM?,
val walletName: String,
val onWalletClick: (() -> Unit)?,
val networksInfo: WcNetworksInfo,

View file

@ -5,17 +5,27 @@ import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.pushNew
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.tangem.common.ui.account.CryptoPortfolioIconUM
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.common.ui.account.toUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.core.ui.message.ToastMessage
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
import com.tangem.domain.account.producer.SingleAccountListProducer
import com.tangem.domain.account.supplier.SingleAccountListSupplier
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
@ -31,6 +41,9 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData
import com.tangem.domain.walletconnect.usecase.pair.WcPairState
import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.features.walletconnect.connections.components.WcPairComponent
import com.tangem.features.walletconnect.connections.components.WcSelectNetworksComponent
import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent
@ -40,7 +53,9 @@ import com.tangem.features.walletconnect.connections.model.transformers.*
import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes
import com.tangem.features.walletconnect.impl.R
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
import kotlin.properties.Delegates
import com.tangem.utils.transformer.update as transformerUpdate
@ -53,12 +68,17 @@ private const val WC_WALLETS_SELECTOR_MIN_COUNT = 2
@Stable
@ModelScoped
@Suppress("LongParameterList")
@Suppress("LongParameterList", "LargeClass")
internal class WcPairModel @Inject constructor(
private val router: Router,
private val messageSender: UiMessageSender,
override val dispatchers: CoroutineDispatcherProvider,
private val analytics: AnalyticsEventHandler,
private val accountsFeatureToggles: AccountsFeatureToggles,
val selectorController: PortfolioSelectorController,
private val singleAccountListSupplier: SingleAccountListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
portfolioFetcherFactory: PortfolioFetcher.Factory,
wcPairUseCaseFactory: WcPairUseCase.Factory,
getWalletsUseCase: GetWalletsUseCase,
paramsContainer: ParamsContainer,
@ -74,10 +94,19 @@ internal class WcPairModel @Inject constructor(
)
val stackNavigation = StackNavigation<WcAppInfoRoutes>()
val portfolioFetcher: PortfolioFetcher?
val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback {
override val onDismiss: () -> Unit = { stackNavigation.pop() }
override val onBack: () -> Unit = { stackNavigation.pop() }
}
private val selectedUserWalletFlow: MutableStateFlow<UserWallet> by lazy {
MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId })
}
private val selectedPortfolio = MutableSharedFlow<Pair<UserWallet, AccountStatus>>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
private var proposalNetwork by Delegates.notNull<WcSessionProposal.ProposalNetwork>()
private var sessionProposal by Delegates.notNull<WcSessionProposal>()
private var additionallyEnabledNetworks = setOf<Network>()
@ -87,75 +116,180 @@ internal class WcPairModel @Inject constructor(
field = MutableStateFlow<WcAppInfoUM>(createLoadingState())
init {
loadDAppInfo()
if (accountsFeatureToggles.isFeatureEnabled) {
portfolioFetcher = portfolioFetcherFactory.create(
mode = PortfolioFetcher.Mode.All(onlyMultiCurrency = true),
scope = modelScope,
)
modelScope.launch {
val params = SingleAccountListProducer.Params(params.userWalletId)
val accountList = singleAccountListSupplier.getSyncOrNull(params)
if (accountList == null) {
router.pop()
return@launch
}
val firstAccount = accountList.accounts.first()
selectorController.selectAccount(firstAccount.accountId)
combineFlows(portfolioFetcher)
}
} else {
portfolioFetcher = null
loadDAppInfo()
}
}
private fun combineFlows(portfolioFetcher: PortfolioFetcher) {
combine(
flow = portfolioFetcher.data,
flow2 = selectorController.selectedAccountWithData(portfolioFetcher)
.distinctUntilChanged()
.filterNotNull()
.onEach { selectedPortfolio.tryEmit(it) }
.onEach { stackNavigation.pop() },
flow3 = wcPairUseCase(),
flow4 = isAccountsModeEnabledUseCase(),
transform = { portfolios, selected, pairState, isAccountMode ->
handlePairState(
pairState,
portfolios,
selected,
isAccountMode,
)
},
)
.launchIn(modelScope)
}
private fun loadDAppInfo() {
wcPairUseCase()
.onEach { pairState ->
when (pairState) {
is WcPairState.Approving.Loading -> appInfoUiState.transformerUpdate(
WcConnectButtonProgressTransformer(showProgress = true),
)
is WcPairState.Approving.Result -> {
appInfoUiState.transformerUpdate(
WcConnectButtonProgressTransformer(showProgress = false),
)
pairState
.result
.onLeft(::processError)
.onRight(::processSuccessfullyConnected)
router.pop()
}
is WcPairState.Error -> {
appInfoUiState.transformerUpdate(
WcConnectButtonProgressTransformer(showProgress = false),
)
processError(pairState.error)
}
is WcPairState.Loading -> appInfoUiState.update { createLoadingState() }
is WcPairState.Proposal -> {
val availableWallets = pairState.dAppSession.proposalNetwork.keys
.filter { !it.isLocked && it.isMultiCurrency }
sessionProposal = pairState.dAppSession
val foundNetwork = sessionProposal.proposalNetwork[selectedUserWalletFlow.value]
if (foundNetwork == null) {
processError(Unknown("Selected wallet not found"))
} else {
proposalNetwork = foundNetwork
additionallyEnabledNetworks = proposalNetwork.available
appInfoUiState.transformerUpdate(
WcAppInfoTransformer(
dAppSession = sessionProposal,
dAppVerifiedStateConverter = dAppVerifiedStateConverter,
onDismiss = ::rejectPairing,
onConnect = ::onConnect,
onWalletClick = {
stackNavigation.pushNew(
WcAppInfoRoutes.SelectWallet(selectedUserWalletFlow.value.walletId),
)
}.takeIf { availableWallets.size >= WC_WALLETS_SELECTOR_MIN_COUNT },
onNetworksClick = {
stackNavigation.pushNew(
WcAppInfoRoutes.SelectNetworks(
missingRequiredNetworks = proposalNetwork.missingRequired,
requiredNetworks = proposalNetwork.required,
availableNetworks = proposalNetwork.available,
enabledAvailableNetworks = additionallyEnabledNetworks,
notAddedNetworks = proposalNetwork.notAdded,
),
)
},
userWallet = selectedUserWalletFlow.value,
proposalNetwork = proposalNetwork,
additionallyEnabledNetworks = additionallyEnabledNetworks,
),
)
}
}
.onEach { pairState -> handlePairState(pairState) }
.launchIn(modelScope)
}
private suspend fun handlePairState(
pairState: WcPairState,
portfolios: PortfolioFetcher.Data? = null,
selected: Pair<UserWallet, AccountStatus>? = null,
isAccountMode: Boolean? = null,
) {
when (pairState) {
is WcPairState.Approving.Loading -> appInfoUiState.transformerUpdate(
WcConnectButtonProgressTransformer(showProgress = true),
)
is WcPairState.Approving.Result -> {
appInfoUiState.transformerUpdate(
WcConnectButtonProgressTransformer(showProgress = false),
)
pairState
.result
.onLeft(::processError)
.onRight(::processSuccessfullyConnected)
router.pop()
}
is WcPairState.Error -> {
appInfoUiState.transformerUpdate(
WcConnectButtonProgressTransformer(showProgress = false),
)
processError(pairState.error)
}
is WcPairState.Loading -> appInfoUiState.update { createLoadingState(isAccountMode ?: false) }
is WcPairState.Proposal -> handleProposalState(
pairState = pairState,
portfolios = portfolios,
selected = selected,
)
}
}
private suspend fun handleProposalState(
pairState: WcPairState.Proposal,
portfolios: PortfolioFetcher.Data? = null,
selected: Pair<UserWallet, AccountStatus>? = null,
) {
val availableWallets = pairState.dAppSession.proposalNetwork.keys
.filter { !it.isLocked && it.isMultiCurrency }
sessionProposal = pairState.dAppSession
val selectedUserWalletFlow = this.selectedUserWalletFlow
val portfolioWallet = selected?.first
val portfolioAccount = selected?.second
val portfolioAccountId = portfolioAccount?.account?.accountId
val proposalAccountNetwork = sessionProposal.proposalAccountNetwork
val foundNetwork = if (portfolioAccountId != null) {
requireNotNull(proposalAccountNetwork)[portfolioAccountId]
} else {
sessionProposal.proposalNetwork[selectedUserWalletFlow.value]
}
if (foundNetwork == null) {
processError(Unknown("Selected wallet not found"))
} else {
val portfolioSelectRow = tryToCreatePortfolioSelectRow(selected, portfolios)
if (proposalAccountNetwork != null) {
selectorController.isEnabled.value = { wallet, account ->
proposalAccountNetwork.contains(account.account.accountId)
}
}
.launchIn(modelScope)
proposalNetwork = foundNetwork
additionallyEnabledNetworks = proposalNetwork.available
appInfoUiState.transformerUpdate(
WcAppInfoTransformer(
dAppSession = sessionProposal,
dAppVerifiedStateConverter = dAppVerifiedStateConverter,
onDismiss = ::rejectPairing,
onConnect = ::onConnect,
portfolioSelectRow = portfolioSelectRow,
onWalletClick = {
stackNavigation.pushNew(
WcAppInfoRoutes.SelectWallet(selectedUserWalletFlow.value.walletId),
)
}.takeIf {
portfolioSelectRow == null && availableWallets.size >= WC_WALLETS_SELECTOR_MIN_COUNT
},
onNetworksClick = {
stackNavigation.pushNew(
WcAppInfoRoutes.SelectNetworks(
missingRequiredNetworks = proposalNetwork.missingRequired,
requiredNetworks = proposalNetwork.required,
availableNetworks = proposalNetwork.available,
enabledAvailableNetworks = additionallyEnabledNetworks,
notAddedNetworks = proposalNetwork.notAdded,
),
)
},
userWallet = portfolioWallet ?: selectedUserWalletFlow.value,
proposalNetwork = proposalNetwork,
additionallyEnabledNetworks = additionallyEnabledNetworks,
),
)
}
}
private suspend fun tryToCreatePortfolioSelectRow(
selectedPortfolio: Pair<UserWallet, AccountStatus>?,
portfolios: PortfolioFetcher.Data?,
): PortfolioSelectUM? {
selectedPortfolio ?: return null
portfolios ?: return null
val (wallet, portfolioAccount) = selectedPortfolio
val account = when (val account = portfolioAccount.account) {
is Account.CryptoPortfolio -> account
}
val isAccountMode = selectorController.isAccountMode.first()
val icon: CryptoPortfolioIconUM?
val name: TextReference
if (isAccountMode) {
icon = account.icon.toUM()
name = account.accountName.toUM().value
} else {
icon = null
name = stringReference(wallet.name)
}
return PortfolioSelectUM(
icon = icon,
name = name,
isAccountMode = selectorController.isAccountMode.first(),
isMultiChoice = !portfolios.isSingleChoice,
onClick = { stackNavigation.pushNew(WcAppInfoRoutes.PortfolioSelector) },
)
}
fun rejectPairing() {
@ -182,11 +316,14 @@ internal class WcPairModel @Inject constructor(
private fun connect() {
val enabledAvailableNetworks =
proposalNetwork.available.filter { network -> network in additionallyEnabledNetworks }
val selectedPortfolio = selectedPortfolio.replayCache.firstOrNull()
val wallet = selectedPortfolio?.first ?: selectedUserWalletFlow.value
val account = selectedPortfolio?.second?.account
wcPairUseCase.approve(
WcSessionApprove(
wallet = selectedUserWalletFlow.value,
wallet = wallet,
network = enabledAvailableNetworks + proposalNetwork.required,
account = null, // todo account
account = account,
),
)
}
@ -269,10 +406,15 @@ internal class WcPairModel @Inject constructor(
)
}
private fun createLoadingState(): WcAppInfoUM.Loading {
private fun createLoadingState(isAccountMode: Boolean = false): WcAppInfoUM.Loading {
return WcAppInfoUM.Loading(
onDismiss = ::rejectPairing,
connectButtonConfig = WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {}),
portfolioName = if (isAccountMode) {
resourceReference(R.string.account_details_title)
} else {
resourceReference(R.string.wc_common_wallet)
},
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.walletconnect.connections.model.transformers
import com.domain.blockaid.models.dapp.CheckDAppResult
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.walletconnect.model.WcSessionProposal
@ -15,6 +16,7 @@ internal class WcAppInfoTransformer(
private val dAppVerifiedStateConverter: WcDAppVerifiedStateConverter,
private val onDismiss: () -> Unit,
private val onConnect: (securityStatus: CheckDAppResult) -> Unit,
private val portfolioSelectRow: PortfolioSelectUM?,
private val onWalletClick: (() -> Unit)?,
private val onNetworksClick: () -> Unit,
private val userWallet: UserWallet,
@ -33,6 +35,7 @@ internal class WcAppInfoTransformer(
notification = createNotification(dAppSession.securityStatus),
walletName = userWallet.name,
onWalletClick = onWalletClick,
portfolioSelectRow = portfolioSelectRow,
networksInfo = WcNetworksInfoConverter.convert(
value = WcNetworksInfoConverter.Input(
missingNetworks = proposalNetwork.missingRequired,

View file

@ -12,6 +12,9 @@ internal sealed class WcAppInfoRoutes : Route {
@Serializable
data object AppInfo : WcAppInfoRoutes()
@Serializable
data object PortfolioSelector : WcAppInfoRoutes()
@Serializable
data class SelectWallet(val selectedWalletId: UserWalletId) : WcAppInfoRoutes()

View file

@ -14,6 +14,7 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
import com.tangem.features.send.v2.api.FeeSelectorComponent
import com.tangem.features.walletconnect.components.WcRoutingComponent
@ -34,6 +35,7 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor(
@Assisted private val appComponentContext: AppComponentContext,
@Assisted params: Unit,
private val feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory,
private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory,
private val feeSelectorComponentFactory: FeeSelectorComponent.Factory,
) : AppComponentContext by appComponentContext, WcRoutingComponent {
@ -85,6 +87,7 @@ internal class DefaultWcRoutingComponent @AssistedInject constructor(
)
is WcInnerRoute.Pair -> WcPairComponent(
appComponentContext = childContext,
portfolioSelectorComponentFactory = portfolioSelectorComponentFactory,
params = WcPairComponent.Params(
userWalletId = config.request.userWalletId,
wcUrl = config.request.uri,

View file

@ -29,6 +29,9 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.account.PortfolioSelectRow
import com.tangem.common.ui.account.PortfolioSelectRowPreviewData
import com.tangem.common.ui.account.PortfolioSelectUM
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
@ -36,10 +39,7 @@ import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTi
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetWithFooter
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -87,7 +87,7 @@ internal fun WcAppInfoModalBottomSheet(state: WcAppInfoUM, onBack: () -> Unit, o
private fun WcAppInfoModalBottomSheet(state: WcAppInfoUM, modifier: Modifier = Modifier) {
when (state) {
is WcAppInfoUM.Content -> WcAppInfoModalBottomSheetContent(state, modifier)
is WcAppInfoUM.Loading -> WcAppInfoModalBottomSheetLoading(modifier)
is WcAppInfoUM.Loading -> WcAppInfoModalBottomSheetLoading(state, modifier)
}
}
@ -273,15 +273,19 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier
val itemsModifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12)
WalletRowItem(
modifier = if (state.onWalletClick != null) {
Modifier.clickableSingle(onClick = state.onWalletClick)
} else {
Modifier
}.then(itemsModifier),
walletName = state.walletName,
showEndIcon = state.onWalletClick != null,
)
if (state.portfolioSelectRow != null) {
PortfolioRowItem(portfolioSelectRow = state.portfolioSelectRow)
} else {
WalletRowItem(
modifier = if (state.onWalletClick != null) {
Modifier.clickableSingle(onClick = state.onWalletClick)
} else {
Modifier
}.then(itemsModifier),
walletName = state.walletName,
showEndIcon = state.onWalletClick != null,
)
}
HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary)
SelectNetworksBlock(
modifier = Modifier
@ -322,6 +326,21 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier
}
}
@Composable
private fun PortfolioRowItem(portfolioSelectRow: PortfolioSelectUM, modifier: Modifier = Modifier) {
PortfolioSelectRow(modifier = modifier, state = portfolioSelectRow) {
Icon(
modifier = Modifier
.size(24.dp)
.testTag(WalletConnectBottomSheetTestTags.WALLET_ICON),
painter = painterResource(R.drawable.ic_wallet_new_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
)
SpacerW4()
}
}
@Composable
private fun WalletRowItem(walletName: String, showEndIcon: Boolean, modifier: Modifier = Modifier) {
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
@ -494,7 +513,7 @@ private fun WcAppInfoButtons(
// region Loading state
@Composable
private fun WcAppInfoModalBottomSheetLoading(modifier: Modifier = Modifier) {
private fun WcAppInfoModalBottomSheetLoading(state: WcAppInfoUM.Loading, modifier: Modifier = Modifier) {
Column(modifier = modifier) {
val blocksModifier = Modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
@ -502,6 +521,7 @@ private fun WcAppInfoModalBottomSheetLoading(modifier: Modifier = Modifier) {
.fillMaxWidth()
WcAppInfoFirstLoadingBlock(blocksModifier)
WcAppInfoSecondLoadingBlock(
portfolioName = state.portfolioName,
modifier = Modifier
.padding(top = 14.dp)
.then(blocksModifier),
@ -535,7 +555,7 @@ private fun WcAppInfoFirstLoadingBlock(modifier: Modifier = Modifier) {
}
@Composable
private fun WcAppInfoSecondLoadingBlock(modifier: Modifier = Modifier) {
private fun WcAppInfoSecondLoadingBlock(portfolioName: TextReference, modifier: Modifier = Modifier) {
Column(modifier) {
Row(modifier = Modifier.padding(all = 12.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(
@ -546,9 +566,9 @@ private fun WcAppInfoSecondLoadingBlock(modifier: Modifier = Modifier) {
)
Text(
modifier = Modifier
.padding(start = 8.dp)
.padding(start = 4.dp)
.weight(1f),
text = stringResourceSafe(R.string.wc_common_wallet),
text = portfolioName.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.primary1,
)
@ -564,7 +584,7 @@ private fun WcAppInfoSecondLoadingBlock(modifier: Modifier = Modifier) {
)
Text(
modifier = Modifier
.padding(start = 8.dp)
.padding(start = 4.dp)
.weight(1f),
text = stringResourceSafe(R.string.wc_common_networks),
style = TangemTheme.typography.body1,
@ -642,7 +662,11 @@ private fun WcAppInfoBottomSheetPreview(@PreviewParameter(WcAppInfoStateProvider
private class WcAppInfoStateProvider : CollectionPreviewParameterProvider<WcAppInfoUM>(
collection = listOf(
WcAppInfoUM.Loading(onDismiss = {}, WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {})),
WcAppInfoUM.Loading(
onDismiss = {},
portfolioName = stringReference("Wallet"),
connectButtonConfig = WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {}),
),
WcAppInfoUM.Content(
appName = "React App",
appIcon = "",
@ -651,6 +675,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider<WcAppI
notification = WcAppInfoSecurityNotification.SecurityRisk,
walletName = "Tangem 2.0 Tangem 2.0 Tangem 2.0 Tangem 2.0",
onWalletClick = null,
portfolioSelectRow = null,
networksInfo = WcNetworksInfo.ContainsAllRequiredNetworks(
items = persistentListOf(
WcNetworkInfoItem.Required(
@ -695,6 +720,21 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider<WcAppI
verifiedDAppState = VerifiedDAppState.Unknown,
appSubtitle = "react-app.walletconnect.com",
notification = WcAppInfoSecurityNotification.UnknownDomain,
portfolioSelectRow = null,
walletName = "Tangem 2.0 Tangem 2.0 Tangem 2.0 Tangem 2.0",
onWalletClick = {},
networksInfo = WcNetworksInfo.MissingRequiredNetworkInfo(networks = "Solana"),
onNetworksClick = {},
onDismiss = {},
connectButtonConfig = WcPrimaryButtonConfig(showProgress = false, enabled = true, onClick = {}),
),
WcAppInfoUM.Content(
appName = "React App",
appIcon = "",
verifiedDAppState = VerifiedDAppState.Unknown,
appSubtitle = "react-app.walletconnect.com",
notification = WcAppInfoSecurityNotification.UnknownDomain,
portfolioSelectRow = PortfolioSelectRowPreviewData.account,
walletName = "Tangem 2.0 Tangem 2.0 Tangem 2.0 Tangem 2.0",
onWalletClick = {},
networksInfo = WcNetworksInfo.MissingRequiredNetworkInfo(networks = "Solana"),