Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-14 17:32:10 +04:00
parent 79fefb8300
commit 312c446241
5 changed files with 145 additions and 33 deletions

View file

@ -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<Boolean>
val selectedAccount: Flow<AccountId?>
val selectedAccountSync: AccountId?
val selectedAccounts: Flow<Set<AccountId>>
val selectedAccount: Flow<AccountId?> get() = selectedAccounts.map { it.firstOrNull() }
val selectedAccountsSync: Set<AccountId>
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<AccountId>)
fun selectAccount(accountId: AccountId?) = selectAccount(accountId?.let { setOf(it) }.orEmpty())
fun selectedAccountsWithData(
portfolioFetcher: PortfolioFetcher,
): Flow<Set<Pair<UserWallet, AccountStatus.CryptoPortfolio>>>
fun selectedAccountWithData(
portfolioFetcher: PortfolioFetcher,
): Flow<Pair<UserWallet, AccountStatus.CryptoPortfolio>?>
): Flow<Pair<UserWallet, AccountStatus.CryptoPortfolio>?> =
selectedAccountsWithData(portfolioFetcher).map { it.firstOrNull() }
}

View file

@ -18,38 +18,38 @@ internal class DefaultPortfolioSelectorController @Inject constructor(
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
) : PortfolioSelectorController {
private val _selectedAccount: MutableSharedFlow<AccountId?> = MutableSharedFlow(
private val _selectedAccount: MutableSharedFlow<Set<AccountId>> = MutableSharedFlow(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
override val isAccountMode: Flow<Boolean> by lazy { isAccountsModeEnabledUseCase() }
// without StateFlow and distinctUntilChanged to allow reselect and correct navigation
override val selectedAccount: Flow<AccountId?> get() = _selectedAccount
override val selectedAccountSync: AccountId? get() = _selectedAccount.replayCache.firstOrNull()
override val selectedAccounts: Flow<Set<AccountId>> get() = _selectedAccount
override val selectedAccountsSync: Set<AccountId> 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<AccountId>) {
_selectedAccount.tryEmit(accountIds)
}
override fun selectedAccountWithData(
override fun selectedAccountsWithData(
portfolioFetcher: PortfolioFetcher,
): Flow<Pair<UserWallet, AccountStatus.CryptoPortfolio>?> = combine(
): Flow<Set<Pair<UserWallet, AccountStatus.CryptoPortfolio>>> = combine(
flow = _selectedAccount,
flow2 = portfolioFetcher.data,
transform = { accountId, data ->
accountId ?: return@combine null
var result: Pair<UserWallet, AccountStatus.CryptoPortfolio>? = null
transform = { accountIds, data ->
val result = mutableSetOf<Pair<UserWallet, AccountStatus.CryptoPortfolio>>()
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

View file

@ -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<PortfolioSelectorUM>
field = MutableStateFlow<PortfolioSelectorUM>(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<UserWalletId, UserWalletItemUM.ImageState>,
isEnabled: (UserWallet, AccountStatus) -> Boolean,
selectedAccount: AccountId?,
selectedAccount: Set<AccountId>,
): List<PortfolioSelectorItemUM> = when (isAccountsMode) {
true -> buildAccountsList(
portfolioData = portfolioData,
@ -106,7 +111,7 @@ internal class PortfolioSelectorModel @Inject constructor(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
isEnabled: (UserWallet, AccountStatus) -> Boolean,
selectedAccount: AccountId?,
selectedAccount: Set<AccountId>,
): List<PortfolioSelectorItemUM> = 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<UserWalletId, UserWalletItemUM.ImageState>,
isEnabled: (UserWallet, AccountStatus) -> Boolean,
selectedAccount: AccountId?,
selectedAccount: Set<AccountId>,
): List<PortfolioSelectorItemUM> = 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<PortfolioSelectorItemUM>): List<PortfolioSelectorItemUM> {
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,
)
}

View file

@ -9,6 +9,12 @@ import kotlinx.collections.immutable.ImmutableList
data class PortfolioSelectorUM(
val title: TextReference,
val items: ImmutableList<PortfolioSelectorItemUM>,
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(

View file

@ -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,
),
),
)