Updated on 2026-08-14
This commit is contained in:
parent
79fefb8300
commit
312c446241
5 changed files with 145 additions and 33 deletions
|
|
@ -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() }
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue