Updated on 2026-08-14
This commit is contained in:
parent
18474637b9
commit
a252e33515
17 changed files with 796 additions and 37 deletions
|
|
@ -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) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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?)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue