diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt index 91b1895006..caa4ec4ce0 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/PortfolioSelectRow.kt @@ -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 { + override val values: Sequence + get() = sequenceOf(PortfolioSelectRowPreviewData.account, PortfolioSelectRowPreviewData.wallet) +} + +object PortfolioSelectRowPreviewData { val account get() = PortfolioSelectUM( icon = AccountIconPreviewData.randomAccountIcon(), @@ -113,7 +124,4 @@ private class PreviewProvider : PreviewParameterProvider { isAccountMode = false, onClick = {}, ) - - override val values: Sequence - get() = sequenceOf(account, wallet) } \ No newline at end of file diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketCryptoCurrency.kt b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketCryptoCurrency.kt index 224de4e906..4cdd14d937 100644 --- a/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketCryptoCurrency.kt +++ b/domain/markets/src/main/java/com/tangem/domain/markets/GetTokenMarketCryptoCurrency.kt @@ -12,7 +12,7 @@ class GetTokenMarketCryptoCurrency( userWalletId: UserWalletId, tokenMarketParams: TokenMarketParams, network: TokenMarketInfo.Network, - accountIndex: DerivationIndex? = null, + accountIndex: DerivationIndex, ): CryptoCurrency? { return marketsTokenRepository.createCryptoCurrency( userWalletId = userWalletId, diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt index 132a4b8ba2..904a43744f 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt @@ -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, + ) + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt index a1b8a6c745..fbff4d7a53 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt @@ -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) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt index 6f43d5f90e..bc1e3ae6d6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt @@ -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)) + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt index 20dd17c10c..faed05e1b5 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt @@ -87,19 +87,26 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor( private fun onAvailableNetworksFlow(): Flow = 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 = 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 { fun Portfolio.actionsFoAccountCurrencies(): List>> = accountsWithAdded.map { account -> diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index 1d3a97b3a6..fd948afd0b 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -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) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt index 80430447de..39193ee405 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt @@ -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, + ), + ) } } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt index a9cd0e17c8..cf9d41811a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt @@ -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, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index c0ac9da317..aaf93e1932 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -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() + val portfolioFetcher: PortfolioFetcher? + val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback { + override val onDismiss: () -> Unit = { stackNavigation.pop() } + override val onBack: () -> Unit = { stackNavigation.pop() } + } private val selectedUserWalletFlow: MutableStateFlow by lazy { MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }) } + private val selectedPortfolio = MutableSharedFlow>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) private var proposalNetwork by Delegates.notNull() private var sessionProposal by Delegates.notNull() private var additionallyEnabledNetworks = setOf() @@ -87,75 +116,180 @@ internal class WcPairModel @Inject constructor( field = MutableStateFlow(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? = 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? = 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?, + 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) + }, ) } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt index 41dd25542e..d6099eaaa2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -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, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt index c83d3cd1a1..e2a866155b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt @@ -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() diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt index 30c6d10c14..44524c20d9 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routing/DefaultWcRoutingComponent.kt @@ -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, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt index 0f5487ae09..9bfbb0ec01 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt @@ -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( 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