Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-11 13:55:41 +07:00
parent 18474637b9
commit a252e33515
17 changed files with 796 additions and 37 deletions

View file

@ -0,0 +1,100 @@
package com.tangem.common.ui.account
import com.tangem.common.ui.R
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.Account
import com.tangem.utils.converter.Converter
class AccountCryptoPortfolioItemStateConverter(
private val appCurrency: AppCurrency,
private val account: Account.CryptoPortfolio,
private val onItemClick: ((Account.CryptoPortfolio) -> Unit)? = null,
private val onItemLongClick: ((Account.CryptoPortfolio) -> Unit)? = null,
) : Converter<TotalFiatBalance, TokenItemState> {
override fun convert(value: TotalFiatBalance): TokenItemState {
return when (value) {
is TotalFiatBalance.Loaded -> account.mapToContentState(value)
TotalFiatBalance.Failed -> account.mapToUnreachableState()
TotalFiatBalance.Loading -> account.mapToLoadingState()
}
}
private fun Account.CryptoPortfolio.mapToContentState(
fiatBalance: TotalFiatBalance.Loaded,
): TokenItemState.Content {
return TokenItemState.Content(
id = account.accountId.value,
iconState = AccountIconItemStateConverter.convert(this),
titleState = TokenItemState.TitleState.Content(
text = accountName.toUM().value,
),
subtitleState = TokenItemState.SubtitleState.TextContent(
value = pluralReference(
R.plurals.common_tokens_count,
count = tokensCount,
formatArgs = wrappedList(tokensCount),
),
isAvailable = false,
),
fiatAmountState = FiatAmountState.Content(
text = fiatBalance.amount
.format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) },
isFlickering = fiatBalance.source == StatusSource.CACHE,
),
subtitle2State = null,
onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } },
onItemLongClick = onItemLongClick?.let { onItemLongClick -> { onItemLongClick(account) } },
)
}
private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Loading {
return TokenItemState.Loading(
id = account.accountId.value,
iconState = AccountIconItemStateConverter.convert(account),
titleState = TokenItemState.TitleState.Content(
text = accountName.toUM().value,
),
subtitleState = TokenItemState.SubtitleState.TextContent(
value = pluralReference(
R.plurals.common_tokens_count,
count = tokensCount,
formatArgs = wrappedList(tokensCount),
),
isAvailable = false,
),
)
}
private fun Account.CryptoPortfolio.mapToUnreachableState(): TokenItemState.Unreachable {
return TokenItemState.Unreachable(
id = account.accountId.value,
iconState = AccountIconItemStateConverter.convert(account),
titleState = TokenItemState.TitleState.Content(
text = accountName.toUM().value,
),
subtitleState = TokenItemState.SubtitleState.TextContent(
value = pluralReference(
R.plurals.common_tokens_count,
count = tokensCount,
formatArgs = wrappedList(tokensCount),
),
isAvailable = false,
),
onItemClick = onItemClick?.let { onItemClick ->
{ onItemClick(account) }
},
onItemLongClick = onItemLongClick?.let { onItemLongClick ->
{ onItemLongClick(account) }
},
)
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.common.ui.account
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.CryptoPortfolioIcon
import com.tangem.utils.converter.Converter
object AccountIconItemStateConverter : Converter<Account, CurrencyIconState.CryptoPortfolio> {
override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) {
is Account.CryptoPortfolio -> when {
value.icon.value == CryptoPortfolioIcon.Icon.Letter -> CurrencyIconState.CryptoPortfolio.Letter(
char = value.accountName.toUM().value,
color = value.icon.color.getUiColor(),
isGrayscale = false,
)
else -> CurrencyIconState.CryptoPortfolio.Icon(
resId = value.icon.value.getResId(),
color = value.icon.color.getUiColor(),
isGrayscale = false,
)
}
}
}

View file

@ -50,6 +50,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheet(
containerColor: Color = TangemTheme.colors.background.primary,
skipPartiallyExpanded: Boolean = true,
dismissOnClickOutside: Boolean = true,
scrollableContent: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable BoxScope.(T) -> Unit = {},
crossinline content: @Composable ColumnScope.(T) -> Unit,
@ -62,6 +63,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheet(
containerColor = containerColor,
title = title,
content = content,
scrollableContent = scrollableContent,
skipPartiallyExpanded = skipPartiallyExpanded,
)
} else {
@ -71,6 +73,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemModalBottomSheet(
title = title,
content = content,
dismissOnClickOutside = dismissOnClickOutside,
scrollableContent = scrollableContent,
onBack = onBack,
skipPartiallyExpanded = skipPartiallyExpanded,
)
@ -84,6 +87,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheet(
containerColor: Color,
skipPartiallyExpanded: Boolean = true,
dismissOnClickOutside: Boolean = true,
scrollableContent: Boolean = true,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
@ -104,10 +108,16 @@ inline fun <reified T : TangemBottomSheetConfigContent> DefaultModalBottomSheet(
BasicModalBottomSheet<T>(
config = config,
sheetState = sheetState,
containerColor = containerColor,
title = title,
onBack = onBack,
content = content,
bsContent = {
BsContent(
config = config,
containerColor = containerColor,
scrollableContent = scrollableContent,
title = title,
content = content,
)
},
)
}
@ -126,6 +136,7 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheet(
config: TangemBottomSheetConfig,
containerColor: Color,
skipPartiallyExpanded: Boolean = true,
scrollableContent: Boolean = true,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
@ -137,49 +148,64 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheet(
density = LocalDensity.current,
),
onBack = null,
containerColor = containerColor,
title = title,
content = content,
bsContent = {
BsContent(
config = config,
containerColor = containerColor,
scrollableContent = scrollableContent,
title = title,
content = content,
)
},
)
}
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> BsContent(
config: TangemBottomSheetConfig,
containerColor: Color,
scrollableContent: Boolean = true,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
) {
val model = config.content as? T ?: return
val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT
Column(
modifier = Modifier
.systemBarsPadding()
.padding(horizontal = 8.dp, vertical = 8.dp)
.clip(TangemTheme.shapes.roundedCornersLarge)
.background(containerColor)
.heightIn(max = maxHeight.dp)
.fillMaxWidth(),
) {
Box(modifier = Modifier.fillMaxWidth()) {
title(model)
}
if (scrollableContent) {
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
content(model)
}
} else {
content(model)
}
}
}
@Suppress("LongParameterList")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
config: TangemBottomSheetConfig,
sheetState: SheetState,
containerColor: Color,
noinline onBack: (() -> Unit)? = null,
crossinline title: @Composable (BoxScope.(T) -> Unit),
crossinline content: @Composable (ColumnScope.(T) -> Unit),
noinline bsContent: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
) {
val model = config.content as? T ?: return
val bsContent: @Composable ColumnScope.() -> Unit = {
val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT
Column(
modifier = Modifier
.systemBarsPadding()
.padding(horizontal = 8.dp, vertical = 8.dp)
.clip(TangemTheme.shapes.roundedCornersLarge)
.background(containerColor)
.heightIn(max = maxHeight.dp)
.fillMaxWidth(),
) {
Box(modifier = Modifier.fillMaxWidth()) {
title(model)
}
Column(
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
content(model)
}
}
}
if (onBack != null) {
ModalBottomSheetWithBackHandling(
modifier = modifier,

View file

@ -17,6 +17,7 @@ import com.tangem.core.ui.components.account.AccountCharIcon
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.account.AccountResIcon
import com.tangem.core.ui.components.currency.DefaultCurrencyIcon
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
@ -69,7 +70,7 @@ internal fun ContentIcon(
)
is CurrencyIconState.CryptoPortfolio.Letter -> AccountCharIcon(
modifier = modifier,
char = icon.char,
char = icon.char.resolveReference().first(),
color = icon.color,
size = AccountIconSize.Default,
)

View file

@ -4,6 +4,7 @@ import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
/**
* Represents the various states an icon can be in.
@ -86,6 +87,7 @@ sealed class CurrencyIconState {
override val topBadgeIconResId: Int? = null
}
@Immutable
sealed class CryptoPortfolio : CurrencyIconState() {
override val showCustomBadge: Boolean = false
override val topBadgeIconResId: Int? = null
@ -98,7 +100,7 @@ sealed class CurrencyIconState {
) : CryptoPortfolio()
data class Letter(
val char: Char,
val char: TextReference,
override val color: Color,
override val isGrayscale: Boolean,
) : CryptoPortfolio()

View file

@ -661,7 +661,7 @@ object AccountItemPreviewData {
)
val accountLetterIcon: CurrencyIconState.CryptoPortfolio.Letter
get() = CurrencyIconState.CryptoPortfolio.Letter(
char = 'A',
char = stringReference("A"),
color = Color.Blue,
isGrayscale = false,
)

View file

@ -13,7 +13,12 @@ dependencies {
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.common.ui)
/* Project - Domain */
implementation(projects.domain.models)
implementation(projects.domain.core)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
}

View file

@ -0,0 +1,24 @@
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

@ -0,0 +1,39 @@
package com.tangem.features.account
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
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
interface AccountsBalanceFetcher {
val data: Flow<Data>
val mode: StateFlow<Mode>
fun updateMode(mode: Mode)
data class Data(
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val balances: Map<UserWallet, Map<Account, AccountBalance>>,
)
data class AccountBalance(
val balance: Lce<TokenListError, TotalFiatBalance>,
)
sealed interface Mode {
data class All(val onlyMultiCurrency: Boolean) : Mode
data class Wallet(val walletId: UserWalletId) : Mode
}
interface Factory {
fun create(mode: Mode, scope: CoroutineScope): AccountsBalanceFetcher
}
}

View file

@ -29,6 +29,15 @@ dependencies {
/** Domain */
implementation(projects.domain.models)
implementation(projects.domain.account)
implementation(projects.domain.core)
implementation(projects.domain.appCurrency)
implementation(projects.domain.appCurrency.models)
implementation(projects.domain.tokens)
implementation(projects.domain.tokens.models)
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
/** Common */
implementation(projects.common.ui)

View file

@ -0,0 +1,120 @@
package com.tangem.features.account.fetcher
import arrow.core.getOrElse
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.wallets.usecase.GetWalletsUseCase
import com.tangem.features.account.AccountsBalanceFetcher
import com.tangem.features.account.AccountsBalanceFetcher.*
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.*
internal class DefaultAccountsBalanceFetcher @AssistedInject constructor(
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getWallets: GetWalletsUseCase,
private val dispatchers: CoroutineDispatcherProvider,
@Assisted mode: Mode,
@Assisted private val scope: CoroutineScope,
) : AccountsBalanceFetcher {
private val _mode = MutableStateFlow(mode)
private val _data = MutableSharedFlow<Data>(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
override val data: Flow<Data>
get() = _data.distinctUntilChanged()
override val mode: StateFlow<Mode>
get() = _mode
init {
_mode.flatMapLatest(::combineUseCases)
.flowOn(dispatchers.default)
.onEach { _data.emit(it) }
.launchIn(scope)
}
override fun updateMode(mode: Mode) {
_mode.value = mode
}
private fun combineUseCases(mode: Mode) = combine(
flow = getWallets()
.map { it.filterWallets(mode) }
.distinctUntilChanged()
.flatMapLatest { wallets -> balancesForWallets(wallets) },
flow2 = appCurrencyFlow(),
flow3 = balanceHidingFlow(),
) { balances, appCurrency, isBalanceHiding ->
Data(
appCurrency = appCurrency,
isBalanceHidden = isBalanceHiding,
balances = balances,
)
}
private fun List<UserWallet>.filterWallets(mode: Mode): List<UserWallet> = this.filter { wallet ->
when (mode) {
is Mode.All -> if (mode.onlyMultiCurrency) wallet.isMultiCurrency else true
is Mode.Wallet -> wallet.walletId == mode.walletId
}
}
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 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 appCurrencyFlow(): Flow<AppCurrency> {
return getSelectedAppCurrencyUseCase()
.map { it.getOrElse { AppCurrency.Default } }
.distinctUntilChanged()
}
private fun balanceHidingFlow(): Flow<Boolean> {
return getBalanceHidingSettingsUseCase()
.map { it.isBalanceHidden }
.distinctUntilChanged()
}
@AssistedFactory
interface Factory : AccountsBalanceFetcher.Factory {
override fun create(mode: Mode, scope: CoroutineScope): DefaultAccountsBalanceFetcher
}
}

View file

@ -0,0 +1,87 @@
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

@ -0,0 +1,69 @@
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

@ -0,0 +1,18 @@
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,28 @@
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,61 @@
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.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
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
@Composable
internal fun AccountSelectorBS(state: AccountSelectorUM, onDismiss: () -> Unit, modifier: Modifier = Modifier) {
TangemModalBottomSheet<TangemBottomSheetConfigContent.Empty>(
config = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
onBack = onDismiss,
scrollableContent = false,
containerColor = TangemTheme.colors.background.secondary,
title = {
TangemModalBottomSheetTitle(
title = resourceReference(R.string.common_choose_account),
startIconRes = R.drawable.ic_back_24,
onStartClick = onDismiss,
)
},
content = {
AccountSelectorContent(
state = state,
contentPadding = PaddingValues(bottom = 16.dp),
modifier = modifier.padding(horizontal = 16.dp),
)
},
)
}
@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) {
TangemThemePreview {
AccountSelectorBS(
state = params,
onDismiss = {},
modifier = Modifier.background(color = TangemTheme.colors.background.secondary),
)
}
}

View file

@ -0,0 +1,146 @@
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)
},
)