From 312c446241770a22cc4a4abf93f69d5e15a754de Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Jul 2026 17:32:10 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../PortfolioSelectorComponent.kt | 21 +++- .../DefaultPortfolioSelectorController.kt | 28 ++--- .../PortfolioSelectorModel.kt | 103 +++++++++++++++--- .../entity/PortfolioSelectorUM.kt | 8 ++ .../ui/PortfolioSelectorContent.kt | 18 +++ 5 files changed, 145 insertions(+), 33 deletions(-) diff --git a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/portfolioselector/PortfolioSelectorComponent.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/portfolioselector/PortfolioSelectorComponent.kt index 411f9e94a8..20a6516c54 100644 --- a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/portfolioselector/PortfolioSelectorComponent.kt +++ b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/portfolioselector/PortfolioSelectorComponent.kt @@ -10,6 +10,7 @@ import com.tangem.domain.models.wallet.UserWallet import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map /** * How to use @@ -41,6 +42,7 @@ interface PortfolioSelectorComponent : ComposableBottomSheetComponent, Composabl */ data class Settings( val isWalletSelectionOnly: Boolean = false, + val isMultiChoice: Boolean = false, ) interface BottomSheetCallback { @@ -58,8 +60,12 @@ interface PortfolioSelectorComponent : ComposableBottomSheetComponent, Composabl */ interface PortfolioSelectorController { val isAccountMode: Flow - val selectedAccount: Flow - val selectedAccountSync: AccountId? + + val selectedAccounts: Flow> + val selectedAccount: Flow get() = selectedAccounts.map { it.firstOrNull() } + + val selectedAccountsSync: Set + val selectedAccountSync: AccountId? get() = selectedAccountsSync.firstOrNull() /** * for some Feature specific filtering @@ -68,8 +74,15 @@ interface PortfolioSelectorController { val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> suspend fun isAccountModeSync(): Boolean - fun selectAccount(accountId: AccountId?) + fun selectAccount(accountIds: Set) + fun selectAccount(accountId: AccountId?) = selectAccount(accountId?.let { setOf(it) }.orEmpty()) + + fun selectedAccountsWithData( + portfolioFetcher: PortfolioFetcher, + ): Flow>> + fun selectedAccountWithData( portfolioFetcher: PortfolioFetcher, - ): Flow?> + ): Flow?> = + selectedAccountsWithData(portfolioFetcher).map { it.firstOrNull() } } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/DefaultPortfolioSelectorController.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/DefaultPortfolioSelectorController.kt index 69065a9436..d10a3bf9d2 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/DefaultPortfolioSelectorController.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/DefaultPortfolioSelectorController.kt @@ -18,38 +18,38 @@ internal class DefaultPortfolioSelectorController @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, ) : PortfolioSelectorController { - private val _selectedAccount: MutableSharedFlow = MutableSharedFlow( + private val _selectedAccount: MutableSharedFlow> = MutableSharedFlow( replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, ) override val isAccountMode: Flow by lazy { isAccountsModeEnabledUseCase() } // without StateFlow and distinctUntilChanged to allow reselect and correct navigation - override val selectedAccount: Flow get() = _selectedAccount - override val selectedAccountSync: AccountId? get() = _selectedAccount.replayCache.firstOrNull() + override val selectedAccounts: Flow> get() = _selectedAccount + override val selectedAccountsSync: Set get() = _selectedAccount.replayCache.firstOrNull().orEmpty() override val isEnabled: MutableStateFlow<(UserWallet, AccountStatus) -> Boolean> = MutableStateFlow { _, _ -> true } override suspend fun isAccountModeSync(): Boolean = isAccountsModeEnabledUseCase.invokeSync() - override fun selectAccount(accountId: AccountId?) { - _selectedAccount.tryEmit(accountId) + override fun selectAccount(accountIds: Set) { + _selectedAccount.tryEmit(accountIds) } - override fun selectedAccountWithData( + override fun selectedAccountsWithData( portfolioFetcher: PortfolioFetcher, - ): Flow?> = combine( + ): Flow>> = combine( flow = _selectedAccount, flow2 = portfolioFetcher.data, - transform = { accountId, data -> - accountId ?: return@combine null - var result: Pair? = null + transform = { accountIds, data -> + val result = mutableSetOf>() - data.balances.forEach { wallet, balance -> - val accountStatuses = balance.accountsBalance.accountStatuses + data.balances.forEach { (_, balance) -> + val accounts = balance.accountsBalance.accountStatuses .filterCryptoPortfolio() - .find { accountId == it.account.accountId } - if (accountStatuses != null) result = balance.userWallet to accountStatuses + .filterTo(mutableSetOf()) { it.account.accountId in accountIds } + .map { balance.userWallet to it } + result.addAll(accounts) } return@combine result diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/PortfolioSelectorModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/PortfolioSelectorModel.kt index eaf50cc34e..b48d977b6f 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/PortfolioSelectorModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/PortfolioSelectorModel.kt @@ -21,6 +21,7 @@ import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent import com.tangem.features.commonfeatures.impl.R +import com.tangem.features.commonfeatures.impl.portfolioselector.entity.PortfolioSelectorButtonUM import com.tangem.features.commonfeatures.impl.portfolioselector.entity.PortfolioSelectorItemUM import com.tangem.features.commonfeatures.impl.portfolioselector.entity.PortfolioSelectorUM import com.tangem.features.wallet.utils.UserWalletImageFetcher @@ -48,16 +49,15 @@ internal class PortfolioSelectorModel @Inject constructor( internal val state: StateFlow field = MutableStateFlow(emptyState()) - init { - val selectedAccountState = selectorController.selectedAccount - .stateIn(modelScope, started = SharingStarted.Eagerly, initialValue = null) + private val innerSelectedAccounts = MutableStateFlow(selectorController.selectedAccountsSync) + init { combine( flow = isAccountsModeEnabledUseCase(), flow2 = balanceFetcher.data, flow3 = walletImageFetcher.allWallets(ArtworkSize.SMALL), flow4 = selectorController.isEnabled, - flow5 = selectedAccountState, + flow5 = innerSelectedAccounts, transform = { isAccountsMode, portfolioData, artworks, isEnabled, selectedAccount -> val isAccountsModeEffective = isAccountsMode && !params.settings.isWalletSelectionOnly val uiList = buildUiList( @@ -71,9 +71,14 @@ internal class PortfolioSelectorModel @Inject constructor( true -> resourceReference(R.string.common_choose_account) false -> resourceReference(R.string.common_choose_wallet) } + val button = PortfolioSelectorButtonUM( + text = resourceReference(R.string.common_apply), + onClick = { onApplyClick() }, + ) state.value = PortfolioSelectorUM( title = title, items = uiList.toImmutableList(), + button = button.takeIf { params.settings.isMultiChoice }, ) }, ) @@ -86,7 +91,7 @@ internal class PortfolioSelectorModel @Inject constructor( portfolioData: PortfolioFetcher.Data, artworks: Map, isEnabled: (UserWallet, AccountStatus) -> Boolean, - selectedAccount: AccountId?, + selectedAccount: Set, ): List = when (isAccountsMode) { true -> buildAccountsList( portfolioData = portfolioData, @@ -106,7 +111,7 @@ internal class PortfolioSelectorModel @Inject constructor( portfolioData: PortfolioFetcher.Data, artworks: Map, isEnabled: (UserWallet, AccountStatus) -> Boolean, - selectedAccount: AccountId?, + selectedAccount: Set, ): List = buildList { val appCurrency = portfolioData.appCurrency val isBalanceHidden = portfolioData.isBalanceHidden @@ -114,25 +119,29 @@ internal class PortfolioSelectorModel @Inject constructor( portfolioData.balances.forEach { (_, portfolio) -> val balance = portfolio.walletBalance val wallet = portfolio.userWallet + val mainAccount = portfolio.accountsBalance.mainAccount + val isSelected = mainAccount.accountId in selectedAccount + val endIcon = if (isSelected) { + UserWalletItemUM.EndIcon.Checkmark + } else { + UserWalletItemUM.EndIcon.None + } val walletItemUM = UserWalletItemUMConverter( - onClick = { - selectorController.selectAccount(portfolio.accountsBalance.mainAccount.account.accountId) - }, + onClick = { onWalletSelected(wallet.walletId, portfolioData, false) }, appCurrency = appCurrency, balance = balance, isBalanceHidden = isBalanceHidden, artwork = artworks[wallet.walletId], isAuthMode = false, + endIcon = endIcon, mode = UserWalletItemUMConverter.InfoField.Tokens( tokensCount = portfolio.accountsBalance.flattenCurrencies().size, ), ).convert(wallet) if (walletItemUM.isEnabled) { - val mainAccount = portfolio.accountsBalance.mainAccount val isEnabledByFeature = isEnabled(wallet, mainAccount) val finalWalletItemUM = if (isEnabledByFeature) walletItemUM else walletItemUM.copy(isEnabled = false) - val isSelected = mainAccount.isSelected(selectedAccount) add(PortfolioSelectorItemUM.Portfolio(finalWalletItemUM, isSelected)) } else { lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM, isSelected = false)) @@ -148,7 +157,7 @@ internal class PortfolioSelectorModel @Inject constructor( portfolioData: PortfolioFetcher.Data, artworks: Map, isEnabled: (UserWallet, AccountStatus) -> Boolean, - selectedAccount: AccountId?, + selectedAccount: Set, ): List = buildList { val appCurrency = portfolioData.appCurrency val isBalanceHidden = portfolioData.isBalanceHidden @@ -158,7 +167,7 @@ internal class PortfolioSelectorModel @Inject constructor( val wallet = portfolio.userWallet val walletItemUM = UserWalletItemUMConverter( onClick = { - selectorController.selectAccount(portfolio.accountsBalance.mainAccount.account.accountId) + onAccountSelected(portfolio.accountsBalance.mainAccount.account.accountId) }, appCurrency = appCurrency, balance = balance, @@ -176,9 +185,13 @@ internal class PortfolioSelectorModel @Inject constructor( is PortfolioFetcher.Mode.Wallet -> Unit is PortfolioFetcher.Mode.All -> { if (portfolioData.balances.size > 1) { + val isAllSelected = portfolio.accountsBalance.accountStatuses + .all { accountStatus -> accountStatus.account.accountId in selectedAccount } val walletTitle = PortfolioSelectorItemUM.GroupTitle( id = "GroupTitle ${wallet.walletId.stringValue}", name = stringReference(wallet.name), + isSelected = isAllSelected, + onClick = { onWalletSelected(wallet.walletId, portfolioData, true) }, deviceIcon = walletIconUMConverter.convert(getWalletIconUseCase(wallet)), ) @@ -191,14 +204,20 @@ internal class PortfolioSelectorModel @Inject constructor( val isEnabledByFeature = isEnabled(wallet, accountStatus) val account = accountStatus.account val accountBalance = accountStatus.tokenList.totalFiatBalance + val isSelected = accountStatus.accountId in selectedAccount + val endIcon = if (isSelected) { + UserWalletItemUM.EndIcon.Checkmark + } else { + UserWalletItemUM.EndIcon.None + } val accountItemUM = AccountPortfolioItemUMConverter( - onClick = { selectorController.selectAccount(account.accountId) }, + onClick = { onAccountSelected(account.accountId) }, appCurrency = appCurrency, accountBalance = accountBalance, isEnabled = isEnabledByFeature, + endIcon = endIcon, isBalanceHidden = isBalanceHidden, ).convert(account) - val isSelected = accountStatus.isSelected(selectedAccount) add(PortfolioSelectorItemUM.Portfolio(accountItemUM, isSelected)) } } @@ -207,11 +226,64 @@ internal class PortfolioSelectorModel @Inject constructor( } } + private fun onAccountSelected(accountId: AccountId) { + if (params.settings.isMultiChoice) { + innerSelectedAccounts.update { selected -> + if (accountId in selected) { + selected - accountId + } else { + selected + accountId + } + } + } else { + selectorController.selectAccount(accountId) + } + } + + // only isMultiChoice branch + private fun onWalletSelected( + walletId: UserWalletId, + portfolioData: PortfolioFetcher.Data, + isAccountMode: Boolean, + ) { + if (params.settings.isMultiChoice) { + val portfolio = portfolioData.balances[walletId] ?: return + innerSelectedAccounts.update { selected -> + val isAllSelected = if (isAccountMode) { + portfolio.accountsBalance.accountStatuses + .all { accountStatus -> accountStatus.account.accountId in selected } + } else { + portfolio.accountsBalance.mainAccount.account.accountId in selected + } + val walletAccounts = if (isAccountMode) { + portfolio.accountsBalance.accountStatuses + .mapTo(mutableSetOf()) { it.account.accountId } + } else { + mutableSetOf(portfolio.accountsBalance.mainAccount.account.accountId) + } + if (isAllSelected) { + selected - walletAccounts + } else { + selected + walletAccounts + } + } + } else { + portfolioData.balances[walletId]?.accountsBalance?.mainAccount?.account?.accountId + ?.let { accountId -> selectorController.selectAccount(accountId) } + } + } + + private fun onApplyClick() { + selectorController.selectAccount(innerSelectedAccounts.value) + } + private fun createLockedWallets(wallets: List): List { val lockedWalletsTitle = PortfolioSelectorItemUM.GroupTitle( id = "lockedWalletsTitleId", name = resourceReference(R.string.common_locked_wallets), deviceIcon = DeviceIconUM.Stub(cardsCount = 1), + isSelected = false, + onClick = {}, ) return listOf(lockedWalletsTitle) + wallets @@ -222,5 +294,6 @@ internal class PortfolioSelectorModel @Inject constructor( private fun emptyState() = PortfolioSelectorUM( items = persistentListOf(), title = TextReference.EMPTY, + button = null, ) } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/entity/PortfolioSelectorUM.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/entity/PortfolioSelectorUM.kt index 2690bb4bcf..69b9f614c8 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/entity/PortfolioSelectorUM.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/entity/PortfolioSelectorUM.kt @@ -9,6 +9,12 @@ import kotlinx.collections.immutable.ImmutableList data class PortfolioSelectorUM( val title: TextReference, val items: ImmutableList, + val button: PortfolioSelectorButtonUM?, +) + +data class PortfolioSelectorButtonUM( + val text: TextReference, + val onClick: () -> Unit, ) @Immutable @@ -19,6 +25,8 @@ sealed interface PortfolioSelectorItemUM { override val id: String, val name: TextReference, val deviceIcon: DeviceIconUM, + val isSelected: Boolean, + val onClick: () -> Unit, ) : PortfolioSelectorItemUM data class Portfolio( diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/ui/PortfolioSelectorContent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/ui/PortfolioSelectorContent.kt index 9752361e7e..377571ce23 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/ui/PortfolioSelectorContent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/portfolioselector/ui/PortfolioSelectorContent.kt @@ -37,6 +37,7 @@ import com.tangem.core.ui.res.LocalCanScrollBackward import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.commonfeatures.impl.R +import com.tangem.features.commonfeatures.impl.portfolioselector.entity.PortfolioSelectorButtonUM import com.tangem.features.commonfeatures.impl.portfolioselector.entity.PortfolioSelectorItemUM import com.tangem.features.commonfeatures.impl.portfolioselector.entity.PortfolioSelectorUM import kotlinx.collections.immutable.toImmutableList @@ -168,12 +169,19 @@ internal object PortfolioSelectorPreviewData { private val lockedWalletItem: UserWalletItemUM get() = walletItem.copy(isEnabled = false) + val button = PortfolioSelectorButtonUM( + text = resourceReference(R.string.common_apply), + onClick = { }, + ) + val firstList get() = listOf( PortfolioSelectorItemUM.GroupTitle( id = UUID.randomUUID().toString(), name = stringReference("Tangem 2.0"), deviceIcon = DeviceIconUM.Stub(cardsCount = 2), + isSelected = false, + onClick = {}, ), PortfolioSelectorItemUM.Portfolio(accountItem, false), PortfolioSelectorItemUM.Portfolio(lockedAccountItem, false), @@ -181,6 +189,8 @@ internal object PortfolioSelectorPreviewData { id = UUID.randomUUID().toString(), name = stringReference("Tangem White"), deviceIcon = DeviceIconUM.Stub(cardsCount = 1), + isSelected = false, + onClick = {}, ), PortfolioSelectorItemUM.Portfolio(accountItem, true), ) @@ -191,6 +201,8 @@ internal object PortfolioSelectorPreviewData { id = UUID.randomUUID().toString(), name = resourceReference(R.string.common_locked_wallets), deviceIcon = DeviceIconUM.Stub(cardsCount = 1), + isSelected = false, + onClick = {}, ), PortfolioSelectorItemUM.Portfolio(lockedWalletItem, false), PortfolioSelectorItemUM.Portfolio(lockedWalletItem, false), @@ -209,6 +221,8 @@ internal object PortfolioSelectorPreviewData { id = UUID.randomUUID().toString(), name = resourceReference(R.string.common_locked_wallets), deviceIcon = DeviceIconUM.Stub(cardsCount = 1), + isSelected = false, + onClick = {}, ), PortfolioSelectorItemUM.Portfolio(lockedWalletItem, false), PortfolioSelectorItemUM.Portfolio(lockedWalletItem, false), @@ -220,18 +234,22 @@ internal class PortfolioSelectorPreviewStateProvider : CollectionPreviewParamete PortfolioSelectorUM( title = resourceReference(R.string.common_choose_account), items = PortfolioSelectorPreviewData.firstList.toImmutableList(), + button = null, ), PortfolioSelectorUM( title = resourceReference(R.string.common_choose_account), items = PortfolioSelectorPreviewData.secondList.toImmutableList(), + button = PortfolioSelectorPreviewData.button, ), PortfolioSelectorUM( title = resourceReference(R.string.common_choose_wallet), items = PortfolioSelectorPreviewData.walletList.toImmutableList(), + button = null, ), PortfolioSelectorUM( title = resourceReference(R.string.common_choose_wallet), items = PortfolioSelectorPreviewData.lockedWalletList.toImmutableList(), + button = PortfolioSelectorPreviewData.button, ), ), ) \ No newline at end of file