Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-29 15:58:48 +07:00
parent d92c319583
commit 71e8f40909
21 changed files with 734 additions and 422 deletions

View file

@ -21,4 +21,5 @@ dependencies {
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.account)
}

View file

@ -1,24 +0,0 @@
package com.tangem.features.account
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.models.account.Account
import kotlinx.coroutines.flow.StateFlow
interface AccountSelectorComponent : ComposableBottomSheetComponent {
data class Params(
val onDismiss: () -> Unit,
val accountsBalanceFetcher: AccountsBalanceFetcher,
val controller: AccountSelectorController,
)
interface Factory {
fun create(appComponentContext: AppComponentContext, params: Params): AccountSelectorComponent
}
}
interface AccountSelectorController {
val selectedAccount: StateFlow<Account?>
fun selectAccount(account: Account?)
}

View file

@ -1,9 +1,9 @@
package com.tangem.features.account
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tokens.error.TokenListError
@ -11,7 +11,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface AccountsBalanceFetcher {
interface PortfolioFetcher {
val data: Flow<Data>
@ -21,12 +21,15 @@ interface AccountsBalanceFetcher {
data class Data(
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val balances: Map<UserWallet, Map<Account, AccountBalance>>,
val balances: Map<UserWallet, PortfolioBalance>,
)
data class AccountBalance(
val balance: Lce<TokenListError, TotalFiatBalance>,
)
data class PortfolioBalance(
val walletBalance: Lce<TokenListError, TotalFiatBalance>,
val accountsBalance: AccountStatusList,
) {
val userWallet: UserWallet get() = accountsBalance.userWallet
}
sealed interface Mode {
data class All(val onlyMultiCurrency: Boolean) : Mode
@ -34,6 +37,6 @@ interface AccountsBalanceFetcher {
}
interface Factory {
fun create(mode: Mode, scope: CoroutineScope): AccountsBalanceFetcher
fun create(mode: Mode, scope: CoroutineScope): PortfolioFetcher
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.features.account
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface PortfolioSelectorComponent : ComposableBottomSheetComponent, ComposableContentComponent {
val title: StateFlow<TextReference>
data class Params(
val onDismiss: () -> Unit,
val portfolioFetcher: PortfolioFetcher,
val controller: PortfolioSelectorController,
)
interface Factory {
fun create(appComponentContext: AppComponentContext, params: Params): PortfolioSelectorComponent
}
}
/**
* if [isAccountMode] is false it's mean [selectedAccount] emit [AccountId] for Main account
*/
interface PortfolioSelectorController {
val isAccountMode: Flow<Boolean>
val selectedAccount: Flow<AccountId?>
fun selectAccount(accountId: AccountId?)
fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, Account>?>
}

View file

@ -14,6 +14,7 @@ android {
dependencies {
/** Api */
implementation(projects.features.account.api)
implementation(projects.features.wallet.api)
/** Core modules */
implementation(projects.core.analytics)
@ -29,6 +30,7 @@ dependencies {
/** Domain */
implementation(projects.domain.models)
implementation(projects.domain.account)
implementation(projects.domain.account.status)
implementation(projects.domain.core)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
@ -39,6 +41,9 @@ dependencies {
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
/** Tangem libraries */
implementation(tangemDeps.card.core)
/** Common */
implementation(projects.common.ui)
implementation(projects.common.routing)

View file

@ -1,17 +1,18 @@
package com.tangem.features.account.fetcher
import arrow.core.getOrElse
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isMultiCurrency
import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.account.AccountsBalanceFetcher
import com.tangem.features.account.AccountsBalanceFetcher.*
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioFetcher.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -20,14 +21,17 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
internal class DefaultAccountsBalanceFetcher @AssistedInject constructor(
@Suppress("LongParameterList")
internal class DefaultPortfolioFetcher @AssistedInject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase,
private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
private val getWallets: GetWalletsUseCase,
private val dispatchers: CoroutineDispatcherProvider,
dispatchers: CoroutineDispatcherProvider,
@Assisted mode: Mode,
@Assisted private val scope: CoroutineScope,
) : AccountsBalanceFetcher {
) : PortfolioFetcher {
private val _mode = MutableStateFlow(mode)
private val _data = MutableSharedFlow<Data>(
@ -73,33 +77,22 @@ internal class DefaultAccountsBalanceFetcher @AssistedInject constructor(
}
}
private fun balancesForWallets(wallets: List<UserWallet>): Flow<Map<UserWallet, Map<Account, AccountBalance>>> =
wallets.asFlow()
.map { walletAccountsBalancesFlow(it) }
.mapLatest { accountsBalances -> combine(accountsBalances) { pairs -> pairs.toMap() } }
.flattenConcat()
private fun walletAccountsBalancesFlow(wallet: UserWallet): Flow<Pair<UserWallet, Map<Account, AccountBalance>>> =
walletAccounts(wallet)
.distinctUntilChanged()
.map { list -> list.map(::accountBalanceFlow) }
.mapLatest { balanceFlows -> combine(balanceFlows) { balances -> balances.toMap() } }
.flattenConcat()
.map { accountBalance -> wallet to accountBalance }
private fun walletAccounts(wallet: UserWallet): Flow<List<Account>> = flow {
// todo account load accounts
val accounts: List<Account> = Account.CryptoPortfolio
.createMainAccount(wallet.walletId)
.let(::listOf)
emit(accounts)
private fun balancesForWallets(wallets: List<UserWallet>): Flow<Map<UserWallet, PortfolioBalance>> {
val balanceFlows = wallets.map { walletAccountsBalancesFlow(it) }
return combine(balanceFlows) { pairs -> pairs.toMap() }
}
private fun accountBalanceFlow(account: Account): Flow<Pair<Account, AccountBalance>> = flow {
// todo account load balance
val balance = AccountBalance(balance = Lce.Content(TotalFiatBalance.Loading))
emit(account to balance)
}
private fun walletAccountsBalancesFlow(wallet: UserWallet): Flow<Pair<UserWallet, PortfolioBalance>> = combine(
flow = accountStatusListFlow(wallet),
flow2 = getWalletTotalBalanceUseCase(wallet.walletId),
transform = { accountStatusList, walletBalance ->
val portfolioBalance = PortfolioBalance(walletBalance, accountStatusList)
wallet to portfolioBalance
},
)
private fun accountStatusListFlow(wallet: UserWallet): Flow<AccountStatusList> =
singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(wallet.walletId))
private fun appCurrencyFlow(): Flow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
@ -114,7 +107,7 @@ internal class DefaultAccountsBalanceFetcher @AssistedInject constructor(
}
@AssistedFactory
interface Factory : AccountsBalanceFetcher.Factory {
override fun create(mode: Mode, scope: CoroutineScope): DefaultAccountsBalanceFetcher
interface Factory : PortfolioFetcher.Factory {
override fun create(mode: Mode, scope: CoroutineScope): DefaultPortfolioFetcher
}
}

View file

@ -1,87 +0,0 @@
package com.tangem.features.account.selector
import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter
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.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.core.utils.getOrElse
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.features.account.AccountSelectorComponent
import com.tangem.features.account.AccountsBalanceFetcher
import com.tangem.features.account.selector.entity.AccountSelectorItemUM
import com.tangem.features.account.selector.entity.AccountSelectorUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import javax.inject.Inject
@ModelScoped
internal class AccountSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<AccountSelectorComponent.Params>()
private val balanceFetcher get() = params.accountsBalanceFetcher
private val selectorController get() = params.controller
internal val state: StateFlow<AccountSelectorUM>
field = MutableStateFlow<AccountSelectorUM>(emptyState())
init {
balanceFetcher.data
.map { data ->
AccountSelectorUM(
isSingleWallet = balanceFetcher.mode.value is AccountsBalanceFetcher.Mode.Wallet,
items = buildUiList(data).toImmutableList(),
)
}
.launchIn(modelScope)
}
private fun buildUiList(data: AccountsBalanceFetcher.Data) = buildList {
fun Account.accountItemState(balance: Lce<TokenListError, TotalFiatBalance>): TokenItemState {
val totalFiatBalance = balance.getOrElse(
ifError = { TotalFiatBalance.Failed },
ifLoading = { TotalFiatBalance.Loading },
)
return when (this) {
is Account.CryptoPortfolio -> AccountCryptoPortfolioItemStateConverter(
appCurrency = data.appCurrency,
account = this,
onItemClick = { selectorController.selectAccount(it) },
).convert(totalFiatBalance)
}
}
data.balances.forEach { wallet, accounts ->
if (wallet.isLocked) return@forEach
AccountSelectorItemUM.Wallet(
id = wallet.walletId.stringValue,
name = stringReference(wallet.name),
).let(::add)
accounts.forEach { account, balance ->
AccountSelectorItemUM.Account(
account = account.accountItemState(balance.balance),
isBalanceHidden = data.isBalanceHidden,
).let(::add)
}
}
}
private fun emptyState() = AccountSelectorUM(
items = persistentListOf(),
balanceFetcher.mode.value is AccountsBalanceFetcher.Mode.Wallet,
)
}

View file

@ -1,69 +0,0 @@
package com.tangem.features.account.selector
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.account.AccountSelectorComponent
import com.tangem.features.account.impl.R
import com.tangem.features.account.selector.ui.AccountSelectorContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultAccountSelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: AccountSelectorComponent.Params,
) : AppComponentContext by appComponentContext, AccountSelectorComponent {
private val model: AccountSelectorModel = getOrCreateModel(params)
override fun dismiss() {
params.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.state.collectAsStateWithLifecycle()
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = ::dismiss,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = ::dismiss,
containerColor = TangemTheme.colors.background.primary,
title = {
TangemModalBottomSheetTitle(
title = resourceReference(R.string.common_choose_wallet),
startIconRes = R.drawable.ic_back_24,
onStartClick = ::dismiss,
)
},
content = {
AccountSelectorContent(
state = state,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
)
},
)
}
@AssistedFactory
interface Factory : AccountSelectorComponent.Factory {
override fun create(
appComponentContext: AppComponentContext,
params: AccountSelectorComponent.Params,
): DefaultAccountSelectorComponent
}
}

View file

@ -1,18 +0,0 @@
package com.tangem.features.account.selector
import com.tangem.domain.models.account.Account
import com.tangem.features.account.AccountSelectorController
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
internal class DefaultAccountSelectorController @Inject constructor() : AccountSelectorController {
private val _selectedAccount: MutableStateFlow<Account?> = MutableStateFlow(null)
override val selectedAccount: StateFlow<Account?> get() = _selectedAccount
override fun selectAccount(account: Account?) {
_selectedAccount.update { account }
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.features.account.selector
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.account.selector.ui.PortfolioSelectorBS
import com.tangem.features.account.selector.ui.PortfolioSelectorContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
internal class DefaultPortfolioSelectorComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted private val params: PortfolioSelectorComponent.Params,
) : AppComponentContext by appComponentContext, PortfolioSelectorComponent {
private val model: PortfolioSelectorModel = getOrCreateModel(params)
override val title: StateFlow<TextReference>
get() = model.state
.map { it.title }
.stateIn(componentScope, SharingStarted.Lazily, model.state.value.title)
override fun dismiss() {
params.onDismiss()
}
@Composable
override fun BottomSheet() {
val state by model.state.collectAsStateWithLifecycle()
PortfolioSelectorBS(state, onDismiss = ::dismiss)
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
PortfolioSelectorContent(
state = state,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : PortfolioSelectorComponent.Factory {
override fun create(
appComponentContext: AppComponentContext,
params: PortfolioSelectorComponent.Params,
): DefaultPortfolioSelectorComponent
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.features.account.selector
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.account.PortfolioSelectorController
import com.tangem.features.account.PortfolioFetcher
import kotlinx.coroutines.flow.*
import javax.inject.Inject
internal class DefaultPortfolioSelectorController @Inject constructor(
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
) : PortfolioSelectorController {
private val _selectedAccount: MutableStateFlow<AccountId?> = MutableStateFlow(null)
override val isAccountMode: Flow<Boolean> by lazy { isAccountsModeEnabledUseCase() }
override val selectedAccount: StateFlow<AccountId?> get() = _selectedAccount
override fun selectAccount(accountId: AccountId?) {
_selectedAccount.update { accountId }
}
override fun selectedAccountWithData(portfolioFetcher: PortfolioFetcher): Flow<Pair<UserWallet, Account>?> =
combine(
flow = _selectedAccount,
flow2 = portfolioFetcher.data,
transform = { accountId, data ->
accountId ?: return@combine null
var result: Pair<UserWallet, Account>? = null
data.balances.forEach { wallet, balance ->
val accountStatuses = balance.accountsBalance.accountStatuses
.find { accountId == it.account.accountId }
if (accountStatuses != null) result = wallet to accountStatuses.account
}
return@combine result
},
)
}

View file

@ -0,0 +1,186 @@
package com.tangem.features.account.selector
import com.tangem.common.ui.account.AccountPortfolioItemUMConverter
import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
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.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.PortfolioFetcher
import com.tangem.features.account.PortfolioSelectorComponent
import com.tangem.features.account.impl.R
import com.tangem.features.account.selector.entity.PortfolioSelectorItemUM
import com.tangem.features.account.selector.entity.PortfolioSelectorUM
import com.tangem.features.wallet.utils.UserWalletImageFetcher
import com.tangem.operations.attestation.ArtworkSize
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ModelScoped
internal class PortfolioSelectorModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val walletImageFetcher: UserWalletImageFetcher,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
) : Model() {
private val params = paramsContainer.require<PortfolioSelectorComponent.Params>()
private val balanceFetcher get() = params.portfolioFetcher
private val selectorController get() = params.controller
internal val state: StateFlow<PortfolioSelectorUM>
field = MutableStateFlow<PortfolioSelectorUM>(emptyState())
init {
combine(
flow = isAccountsModeEnabledUseCase(),
flow2 = loadBalanceWithArtwork(),
transform = { isAccountsMode, (portfolioData, artworks) ->
val uiList = buildUiList(isAccountsMode, portfolioData, artworks)
val title = when (isAccountsMode) {
true -> resourceReference(R.string.common_choose_account)
false -> resourceReference(R.string.common_choose_wallet)
}
state.value = PortfolioSelectorUM(
title = title,
items = uiList.toImmutableList(),
)
},
)
.flowOn(dispatchers.default)
.launchIn(modelScope)
}
private fun buildUiList(
isAccountsMode: Boolean,
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): List<PortfolioSelectorItemUM> = when (isAccountsMode) {
true -> buildAccountsList(portfolioData, artworks)
false -> buildWalletList(portfolioData, artworks)
}
private fun buildWalletList(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): List<PortfolioSelectorItemUM> = buildList {
val appCurrency = portfolioData.appCurrency
val isBalanceHidden = portfolioData.isBalanceHidden
val lockedWallets = mutableListOf<PortfolioSelectorItemUM>()
portfolioData.balances.forEach { wallet, portfolio ->
val balance = portfolio.walletBalance.getOrNull()
val walletItemUM = UserWalletItemUMConverter(
onClick = {
// todo account
// selectorController.selectAccount(portfolio.accountsBalance.mainAccount)
},
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = isBalanceHidden,
artwork = artworks[wallet.walletId],
isAuthMode = false,
).convert(wallet)
if (walletItemUM.isEnabled) {
add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
} else {
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
}
}
if (lockedWallets.isNotEmpty()) {
val lockedWalletsTitle = PortfolioSelectorItemUM.GroupTitle(
id = "lockedWalletsTitleId",
name = resourceReference(R.string.common_locked_wallets),
)
add(lockedWalletsTitle)
addAll(lockedWallets)
}
}
private fun buildAccountsList(
portfolioData: PortfolioFetcher.Data,
artworks: Map<UserWalletId, UserWalletItemUM.ImageState>,
): List<PortfolioSelectorItemUM> = buildList {
val appCurrency = portfolioData.appCurrency
val isBalanceHidden = portfolioData.isBalanceHidden
val lockedWallets = mutableListOf<PortfolioSelectorItemUM>()
portfolioData.balances.forEach { wallet, portfolio ->
val balance = portfolio.walletBalance.getOrNull()
val walletItemUM = UserWalletItemUMConverter(
onClick = {
// todo account
// selectorController.selectAccount(portfolio.accountsBalance.mainAccount)
},
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = isBalanceHidden,
artwork = artworks[wallet.walletId],
isAuthMode = false,
).convert(wallet)
if (!walletItemUM.isEnabled) {
lockedWallets.add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
return@forEach
}
val walletTitle = PortfolioSelectorItemUM.GroupTitle(
id = "GroupTitle ${wallet.walletId.stringValue}",
name = stringReference(wallet.name),
)
add(walletTitle)
add(PortfolioSelectorItemUM.Portfolio(walletItemUM))
portfolio.accountsBalance.accountStatuses.forEach { accountStatus ->
val account = accountStatus.account
val accountBalance = when (accountStatus) {
is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance
}
val accountItemUM = AccountPortfolioItemUMConverter(
onClick = { selectorController.selectAccount(account.accountId) },
appCurrency = appCurrency,
accountBalance = accountBalance,
isBalanceHidden = isBalanceHidden,
).convert(account)
add(PortfolioSelectorItemUM.Portfolio(accountItemUM))
}
}
if (lockedWallets.isNotEmpty()) {
val lockedWalletsTitle = PortfolioSelectorItemUM.GroupTitle(
id = "lockedWalletsTitleId",
name = resourceReference(R.string.common_locked_wallets),
)
add(lockedWalletsTitle)
addAll(lockedWallets)
}
}
private fun loadBalanceWithArtwork():
Flow<Pair<PortfolioFetcher.Data, Map<UserWalletId, UserWalletItemUM.ImageState>>> {
val wallets = Channel<Set<UserWallet>>()
val portfolioFlow = balanceFetcher.data
.onEach { wallets.trySend(it.balances.keys) }
val artworksFlow = wallets.receiveAsFlow()
.distinctUntilChanged()
.flatMapLatest { walletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) }
return combine(
flow = portfolioFlow,
flow2 = artworksFlow,
) { portfolioData, artworks -> portfolioData to artworks }
}
private fun emptyState() = PortfolioSelectorUM(
items = persistentListOf(),
title = TextReference.EMPTY,
)
}

View file

@ -1,28 +0,0 @@
package com.tangem.features.account.selector.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
data class AccountSelectorUM(
val items: ImmutableList<AccountSelectorItemUM>,
val isSingleWallet: Boolean,
)
@Immutable
sealed interface AccountSelectorItemUM {
val id: String
data class Wallet(
override val id: String,
val name: TextReference,
) : AccountSelectorItemUM
data class Account(
val account: TokenItemState,
val isBalanceHidden: Boolean,
) : AccountSelectorItemUM {
override val id: String = account.id
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.account.selector.entity
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
data class PortfolioSelectorUM(
val title: TextReference,
val items: ImmutableList<PortfolioSelectorItemUM>,
)
@Immutable
sealed interface PortfolioSelectorItemUM {
val id: String
data class GroupTitle(
override val id: String,
val name: TextReference,
) : PortfolioSelectorItemUM
data class Portfolio(
val item: UserWalletItemUM,
) : PortfolioSelectorItemUM {
override val id: String = item.id.stringValue
}
}

View file

@ -1,146 +0,0 @@
package com.tangem.features.account.selector.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.token.AccountItemPreviewData
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.account.selector.entity.AccountSelectorItemUM
import com.tangem.features.account.selector.entity.AccountSelectorUM
import com.tangem.features.account.selector.ui.AccountSelectorPreviewData.firstList
import kotlinx.collections.immutable.toImmutableList
import java.util.UUID
@Composable
internal fun AccountSelectorContent(
state: AccountSelectorUM,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
) {
LazyColumn(
modifier = modifier,
contentPadding = contentPadding,
) {
val items = state.items
itemsIndexed(
items = items,
key = { _, item -> item.id },
) { index, item ->
val previewItem = items.getOrNull(index.dec())
val offsetModifier = when {
previewItem == null -> Modifier
state.isSingleWallet -> Modifier.padding(
top = TangemTheme.dimens.spacing16,
)
item is AccountSelectorItemUM.Wallet -> Modifier.padding(
top = TangemTheme.dimens.spacing16,
)
else -> Modifier.padding(
top = TangemTheme.dimens.spacing8,
)
}
when (item) {
is AccountSelectorItemUM.Account -> TokenItem(
state = item.account,
isBalanceHidden = item.isBalanceHidden,
modifier = offsetModifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.background(color = TangemTheme.colors.background.primary),
)
is AccountSelectorItemUM.Wallet -> WalletNameRow(
model = item,
modifier = offsetModifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}
}
}
@Composable
private fun WalletNameRow(model: AccountSelectorItemUM.Wallet, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = model.name.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun AccountSelectorContentPreview(
@PreviewParameter(AccountSelectorPreviewStateProvider::class) params: AccountSelectorUM,
) {
TangemThemePreview {
AccountSelectorContent(
state = params,
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
)
}
}
internal object AccountSelectorPreviewData {
val firstList
get() = buildList {
AccountSelectorItemUM.Wallet(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem 2.0"),
).let(::add)
AccountItemPreviewData.accountItem
.let { AccountSelectorItemUM.Account(it, false) }
.let(::add)
AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon)
.let { AccountSelectorItemUM.Account(it, false) }
.let(::add)
AccountSelectorItemUM.Wallet(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem White"),
).let(::add)
AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon)
.let { AccountSelectorItemUM.Account(it, false) }
.let(::add)
}
}
internal class AccountSelectorPreviewStateProvider : CollectionPreviewParameterProvider<AccountSelectorUM>(
buildList {
val secondList = listOf(
AccountItemPreviewData.accountItem,
AccountItemPreviewData.accountItem.copy(iconState = AccountItemPreviewData.accountLetterIcon),
).map { AccountSelectorItemUM.Account(it, false) }
val first = AccountSelectorUM(
items = firstList.toImmutableList(),
isSingleWallet = false,
)
val second = AccountSelectorUM(
items = secondList.toImmutableList(),
isSingleWallet = true,
)
add(first)
add(second)
},
)

View file

@ -13,14 +13,13 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.account.impl.R
import com.tangem.features.account.selector.entity.AccountSelectorUM
import com.tangem.features.account.selector.entity.PortfolioSelectorUM
@Composable
internal fun AccountSelectorBS(state: AccountSelectorUM, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
internal fun PortfolioSelectorBS(state: PortfolioSelectorUM, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
@ -32,13 +31,13 @@ internal fun AccountSelectorBS(state: AccountSelectorUM, onDismiss: () -> Unit,
containerColor = TangemTheme.colors.background.secondary,
title = {
TangemModalBottomSheetTitle(
title = resourceReference(R.string.common_choose_account),
title = state.title,
startIconRes = R.drawable.ic_back_24,
onStartClick = onDismiss,
)
},
content = {
AccountSelectorContent(
PortfolioSelectorContent(
state = state,
contentPadding = PaddingValues(bottom = 16.dp),
modifier = modifier.padding(horizontal = 16.dp),
@ -50,9 +49,9 @@ internal fun AccountSelectorBS(state: AccountSelectorUM, onDismiss: () -> Unit,
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview(@PreviewParameter(AccountSelectorPreviewStateProvider::class) params: AccountSelectorUM) {
private fun Preview(@PreviewParameter(PortfolioSelectorPreviewStateProvider::class) params: PortfolioSelectorUM) {
TangemThemePreview {
AccountSelectorBS(
PortfolioSelectorBS(
state = params,
onDismiss = {},
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),

View file

@ -0,0 +1,219 @@
package com.tangem.features.account.selector.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.common.ui.account.AccountIconPreviewData
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.common.ui.userwallet.state.UserWalletItemUM.ImageState
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.account.impl.R
import com.tangem.features.account.selector.entity.PortfolioSelectorItemUM
import com.tangem.features.account.selector.entity.PortfolioSelectorUM
import com.tangem.features.account.selector.ui.PortfolioSelectorPreviewData.firstList
import com.tangem.features.account.selector.ui.PortfolioSelectorPreviewData.lockedWalletList
import com.tangem.features.account.selector.ui.PortfolioSelectorPreviewData.secondList
import com.tangem.features.account.selector.ui.PortfolioSelectorPreviewData.walletList
import kotlinx.collections.immutable.toImmutableList
import java.util.UUID
private const val DISABLED_WALLET_ALPHA = 0.5f
@Composable
internal fun PortfolioSelectorContent(
state: PortfolioSelectorUM,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(),
) {
LazyColumn(
modifier = modifier,
contentPadding = contentPadding,
) {
val items = state.items
itemsIndexed(
items = items,
key = { _, item -> item.id },
) { index, item ->
val previewItem = items.getOrNull(index.dec())
val offsetModifier = when {
previewItem == null -> Modifier
item is PortfolioSelectorItemUM.GroupTitle -> Modifier.padding(
top = TangemTheme.dimens.spacing16,
)
else -> Modifier.padding(
top = TangemTheme.dimens.spacing8,
)
}
when (item) {
is PortfolioSelectorItemUM.Portfolio -> UserWalletItem(
state = item.item,
modifier = offsetModifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius14))
.background(color = TangemTheme.colors.background.primary)
.let { if (!item.item.isEnabled) it.alpha(DISABLED_WALLET_ALPHA) else it },
)
is PortfolioSelectorItemUM.GroupTitle -> WalletNameRow(
model = item,
modifier = offsetModifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
)
}
}
}
}
@Composable
private fun WalletNameRow(model: PortfolioSelectorItemUM.GroupTitle, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = model.name.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview(@PreviewParameter(PortfolioSelectorPreviewStateProvider::class) params: PortfolioSelectorUM) {
TangemThemePreview {
PortfolioSelectorContent(
state = params,
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
)
}
}
internal object PortfolioSelectorPreviewData {
val accountName get() = stringReference(value = "Portfolio")
val walletName get() = stringReference(value = "Tangem 2.0")
private val accountItem: UserWalletItemUM
get() = UserWalletItemUM(
id = UserWalletId(UUID.randomUUID().toString().encodeToByteArray()),
name = accountName,
information = UserWalletItemUM.Information.Loaded(stringReference("12 tokens")),
balance = UserWalletItemUM.Balance.Loaded("$726.04", false),
isEnabled = true,
onClick = { },
imageState = ImageState.Account(
name = accountName,
icon = AccountIconPreviewData.randomAccountIcon(),
),
label = null,
)
private val walletItem: UserWalletItemUM
get() = UserWalletItemUM(
id = UserWalletId(UUID.randomUUID().toString().encodeToByteArray()),
name = walletName,
information = UserWalletItemUM.Information.Loaded(stringReference("12 tokens")),
balance = UserWalletItemUM.Balance.Loaded("$726.04", false),
isEnabled = true,
onClick = { },
imageState = ImageState.MobileWallet,
label = null,
)
private val lockedWalletItem: UserWalletItemUM
get() = walletItem.copy(isEnabled = false)
val firstList
get() = buildList {
PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem 2.0"),
).let(::add)
accountItem
.let { PortfolioSelectorItemUM.Portfolio(it) }
.let(::add)
accountItem
.let { PortfolioSelectorItemUM.Portfolio(it) }
.let(::add)
PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = stringReference("Tangem White"),
).let(::add)
accountItem.let { PortfolioSelectorItemUM.Portfolio(it) }
.let(::add)
}
val secondList
get() = firstList + buildList {
PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = resourceReference(R.string.common_locked_wallets),
).let(::add)
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
}
val walletList
get() = buildList {
add(PortfolioSelectorItemUM.Portfolio(walletItem))
add(PortfolioSelectorItemUM.Portfolio(walletItem))
}
val lockedWalletList
get() = buildList {
add(PortfolioSelectorItemUM.Portfolio(walletItem))
val title = PortfolioSelectorItemUM.GroupTitle(
id = UUID.randomUUID().toString(),
name = resourceReference(R.string.common_locked_wallets),
)
add(title)
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
add(PortfolioSelectorItemUM.Portfolio(lockedWalletItem))
}
}
internal class PortfolioSelectorPreviewStateProvider : CollectionPreviewParameterProvider<PortfolioSelectorUM>(
buildList {
val first = PortfolioSelectorUM(
title = resourceReference(R.string.common_choose_account),
items = firstList.toImmutableList(),
)
val second = PortfolioSelectorUM(
title = resourceReference(R.string.common_choose_account),
items = secondList.toImmutableList(),
)
val walletListUM = PortfolioSelectorUM(
title = resourceReference(R.string.common_choose_wallet),
items = walletList.toImmutableList(),
)
val lockedWalletListUM =
PortfolioSelectorUM(
title = resourceReference(R.string.common_choose_wallet),
items = lockedWalletList.toImmutableList(),
)
add(first)
add(second)
add(walletListUM)
add(lockedWalletListUM)
},
)