Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-07 18:12:38 +05:00
parent 8eb575a1c2
commit 2c11ec0405
14 changed files with 406 additions and 253 deletions

View file

@ -157,7 +157,7 @@ fun TangemMessageBanner(
TangemMessageBanner( TangemMessageBanner(
modifier = modifier, modifier = modifier,
variant = state.variant, variant = state.variant,
showGlowRing = state.isShowGlowRing, showGlowRing = state.shouldShowGlowRing,
secondaryButton = state.secondaryButton, secondaryButton = state.secondaryButton,
primaryButton = state.primaryButton, primaryButton = state.primaryButton,
) { ) {
@ -341,7 +341,7 @@ object TangemMessageBanner {
* @param title Banner headline. * @param title Banner headline.
* @param variant Visual appearance background color + glow ring. * @param variant Visual appearance background color + glow ring.
* @param contentAlign Horizontal alignment of the text block. * @param contentAlign Horizontal alignment of the text block.
* @param isShowGlowRing Whether the glow ring is drawn around the banner. `false` shows only the * @param shouldShowGlowRing Whether the glow ring is drawn around the banner. `false` shows only the
* background. * background.
* @param description Secondary line under the [title]. `null` hides it. * @param description Secondary line under the [title]. `null` hides it.
* @param secondaryButton Start action. `null` hides it. * @param secondaryButton Start action. `null` hides it.
@ -353,7 +353,7 @@ object TangemMessageBanner {
val title: TextReference, val title: TextReference,
val variant: Variant = Variant.Default, val variant: Variant = Variant.Default,
val contentAlign: ContentAlign = ContentAlign.Start, val contentAlign: ContentAlign = ContentAlign.Start,
val isShowGlowRing: Boolean = true, val shouldShowGlowRing: Boolean = true,
val description: TextReference? = null, val description: TextReference? = null,
val secondaryButton: Button? = null, val secondaryButton: Button? = null,
val primaryButton: Button? = null, val primaryButton: Button? = null,

View file

@ -4,28 +4,29 @@ import androidx.compose.runtime.Immutable
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.ds.tabs.TangemSegmentUM import com.tangem.core.ui.ds.tabs.TangemSegmentUM
import com.tangem.core.ui.ds.tabs.TangemSegmentedPickerUM import com.tangem.core.ui.ds.tabs.TangemSegmentedPickerUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.features.foryou.impl.components.state.MarketChartUM
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletListUM import com.tangem.features.foryou.impl.model.ForYouNotification
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
internal data class ForYouUM( internal data class ForYouUM(
val walletListUM: WalletListUM,
val portfolioReviewUM: PortfolioReviewUM, val portfolioReviewUM: PortfolioReviewUM,
val notifications: ImmutableList<ForYouNotification>,
) )
@Immutable @Immutable
internal sealed interface PortfolioReviewUM { internal sealed interface PortfolioReviewUM {
val tokenList: ImmutableList<ForYouTokenListItemUM> val tokenList: ImmutableList<ForYouTokenListItemUM>
val marketChartUM: MarketChartUM
data class Loading( data class Loading(
override val tokenList: ImmutableList<ForYouTokenListItemUM>, override val tokenList: ImmutableList<ForYouTokenListItemUM>,
override val marketChartUM: MarketChartUM.NoData,
) : PortfolioReviewUM ) : PortfolioReviewUM
data class Content( data class Content(
override val tokenList: ImmutableList<ForYouTokenListItemUM>, override val tokenList: ImmutableList<ForYouTokenListItemUM>,
override val marketChartUM: MarketChartUM,
val periodPickerUM: TangemSegmentedPickerUM, val periodPickerUM: TangemSegmentedPickerUM,
val assetCount: TextReference,
val topHoldingPercent: TextReference,
val onPeriodClick: (TangemSegmentUM) -> Unit, val onPeriodClick: (TangemSegmentUM) -> Unit,
) : PortfolioReviewUM ) : PortfolioReviewUM
} }

View file

@ -2,27 +2,20 @@ package com.tangem.features.foryou.impl.model
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import arrow.core.getOrElse import arrow.core.getOrElse
import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter
import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.ds.tabs.TangemSegmentUM import com.tangem.core.ui.ds.tabs.TangemSegmentUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.UserWalletsListRepository
import com.tangem.domain.models.TotalFiatBalance import com.tangem.features.foryou.impl.components.state.MarketChartUM
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetWalletIconUseCase
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletListUM
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletTabUM
import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM
import com.tangem.features.foryou.impl.entity.ForYouUM import com.tangem.features.foryou.impl.entity.ForYouUM
import com.tangem.features.foryou.impl.entity.PortfolioReviewUM import com.tangem.features.foryou.impl.entity.PortfolioReviewUM
import com.tangem.features.foryou.impl.model.transformer.SetPortfolioReviewTransformer import com.tangem.features.foryou.impl.model.transformer.SetPortfolioReviewTransformer
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.update import com.tangem.utils.transformer.update
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
@ -36,19 +29,17 @@ internal class ForYouModel @Inject constructor(
multiAccountStatusListSupplier: MultiAccountStatusListSupplier, multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val walletIconUMConverter: WalletIconUMConverter,
private val getWalletIconUseCase: GetWalletIconUseCase,
) : Model() { ) : Model() {
private val locallySelectedWalletId = MutableStateFlow<UserWalletId?>(value = null)
private val expandedAssetIds = MutableStateFlow<Set<String>>(value = emptySet()) private val expandedAssetIds = MutableStateFlow<Set<String>>(value = emptySet())
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow() private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
val uiState: StateFlow<ForYouUM> val uiState: StateFlow<ForYouUM>
field = MutableStateFlow<ForYouUM>( field = MutableStateFlow<ForYouUM>(
ForYouUM( ForYouUM(
walletListUM = WalletListUM(persistentListOf()), notifications = persistentListOf(),
portfolioReviewUM = PortfolioReviewUM.Loading( portfolioReviewUM = PortfolioReviewUM.Loading(
marketChartUM = MarketChartUM.NoData,
tokenList = buildList<ForYouTokenListItemUM> { tokenList = buildList<ForYouTokenListItemUM> {
repeat(4) { index -> repeat(4) { index ->
add( add(
@ -69,35 +60,14 @@ internal class ForYouModel @Inject constructor(
init { init {
combine( combine(
flow = userWalletsListRepository.userWallets, flow = userWalletsListRepository.selectedUserWallet,
flow2 = userWalletsListRepository.selectedUserWallet, flow2 = multiAccountStatusListSupplier.invokeAsMap(),
flow3 = multiAccountStatusListSupplier.invokeAsMap(), flow3 = expandedAssetIds,
flow4 = locallySelectedWalletId, ) { globalSelectedWallet, accountStatusList, expanded ->
flow5 = expandedAssetIds, // TODO For You add choose portfolio flow
) { wallets, globalSelectedWallet, accountStatusList, locallySelected, expanded ->
val selectedId = locallySelected ?: globalSelectedWallet?.walletId
val tabs = wallets.orEmpty().map { wallet ->
WalletTabUM(
text = stringReference(wallet.name),
count = null,
isSelected = wallet.walletId == selectedId,
onClick = { onTabClick(wallet.walletId) },
deviceIcon = walletIconUMConverter.convert(getWalletIconUseCase(wallet)),
)
}
val selectedAccountStatusList = accountStatusList[selectedId]
val currencies = selectedAccountStatusList?.flattenCurrencies().orEmpty()
val loadedBalance = selectedAccountStatusList?.totalFiatBalance as? TotalFiatBalance.Loaded
val totalFiatBalance = loadedBalance?.amount.orZero()
uiState.update( uiState.update(
SetPortfolioReviewTransformer( SetPortfolioReviewTransformer(
walletListUM = WalletListUM( accountStatusList = accountStatusList[globalSelectedWallet?.walletId],
items = if (tabs.size != 1) tabs.toPersistentList() else persistentListOf(),
),
currencies = currencies,
totalFiatBalance = totalFiatBalance,
appCurrency = selectedAppCurrencyFlow.value, appCurrency = selectedAppCurrencyFlow.value,
expandedAssetIds = expanded, expandedAssetIds = expanded,
expandClick = ::onExpandClick, expandClick = ::onExpandClick,
@ -119,10 +89,6 @@ internal class ForYouModel @Inject constructor(
) )
} }
private fun onTabClick(walletId: UserWalletId) {
locallySelectedWalletId.value = walletId
}
private fun onExpandClick(assetId: String) { private fun onExpandClick(assetId: String) {
expandedAssetIds.update { ids -> expandedAssetIds.update { ids ->
if (assetId in ids) ids - assetId else ids + assetId if (assetId in ids) ids - assetId else ids + assetId

View file

@ -20,7 +20,7 @@ internal sealed class ForYouNotification(val state: TangemMessageBanner.State) {
tintReference = { TangemTheme.colors3.icon.primary }, tintReference = { TangemTheme.colors3.icon.primary },
), ),
variant = TangemMessageBanner.Variant.Warning, variant = TangemMessageBanner.Variant.Warning,
isShowGlowRing = false, shouldShowGlowRing = false,
), ),
) )
} }

View file

@ -0,0 +1,55 @@
package com.tangem.features.foryou.impl.model.converter
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.foryou.impl.components.state.*
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
internal class ForYouMarketChartConverter(
private val appCurrency: AppCurrency,
private val topAssets: List<Pair<List<CryptoCurrencyStatus>, BigDecimal>>,
) : Converter<TotalFiatBalance?, MarketChartUM> {
override fun convert(value: TotalFiatBalance?): MarketChartUM {
val topBalance = topAssets.sumOf { (_, assetBalance) -> assetBalance }
return when (value) {
is TotalFiatBalance.Loaded -> MarketChartUM.Loaded(
donutChart = DonutChartUM.Loaded(
totalAmount = value.amount.format {
fiat(
fiatCurrencySymbol = appCurrency.symbol,
fiatCurrencyCode = appCurrency.code,
)
},
donutSegmentList = topAssets.mapIndexed { index, (currencies, segmentBalance) ->
val segmentWeight = segmentBalance.toForYouPercent(value.amount).orZero()
DonutSegmentUM(
color = DonutSegmentColor.entries.getOrNull(index) ?: DonutSegmentColor.Brand,
weight = segmentWeight,
title = stringReference(currencies.firstOrNull()?.currency?.name.orEmpty()),
fiatValue = stringReference(segmentBalance.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}),
)
}.toPersistentList(),
),
aiInsight = AiInsightUM.Hide,
topHoldingPercent = stringReference(topBalance.toForYouPercent(value.amount).format { percent() }),
)
TotalFiatBalance.Loading,
TotalFiatBalance.Failed,
null,
-> MarketChartUM.NoData
}
}
}

View file

@ -4,12 +4,7 @@ import com.tangem.core.ui.ds.badge.TangemBadgeColor
import com.tangem.core.ui.ds.badge.TangemBadgeSize import com.tangem.core.ui.ds.badge.TangemBadgeSize
import com.tangem.core.ui.ds.badge.TangemBadgeType import com.tangem.core.ui.ds.badge.TangemBadgeType
import com.tangem.core.ui.ds.badge.TangemBadgeUM import com.tangem.core.ui.ds.badge.TangemBadgeUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.isZero
import java.math.BigDecimal import java.math.BigDecimal
@ -19,7 +14,7 @@ import java.math.RoundingMode
* Formatting helpers shared by the For You portfolio-review converters. * Formatting helpers shared by the For You portfolio-review converters.
* *
* Kept null-safe so that non-[CryptoCurrencyStatus.Loaded] states (which carry no fiat amount) degrade * Kept null-safe so that non-[CryptoCurrencyStatus.Loaded] states (which carry no fiat amount) degrade
* to a dash / empty instead of throwing. * to `null` instead of throwing.
*/ */
/** /**
@ -29,18 +24,13 @@ import java.math.RoundingMode
*/ */
internal fun CryptoCurrencyStatus.forYouGroupKey(): String = currency.id.rawCurrencyId?.value ?: currency.id.value internal fun CryptoCurrencyStatus.forYouGroupKey(): String = currency.id.rawCurrencyId?.value ?: currency.id.value
/** Formats a (possibly null) fiat amount in [appCurrency]; a `null` amount renders as a dash. */
internal fun BigDecimal?.toForYouFiatText(appCurrency: AppCurrency): TextReference = stringReference(
format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) },
)
/** /**
* Formats this fiat amount as a share of [totalFiatBalance]. Returns [TextReference.EMPTY] when the * Computes this fiat amount as a share of [totalFiatBalance]. Returns `null` when the share cannot be
* share cannot be computed (no amount, or a zero total / amount). * computed (no amount, or a zero total / amount).
*/ */
internal fun BigDecimal?.toForYouPercentText(totalFiatBalance: BigDecimal): TextReference { internal fun BigDecimal?.toForYouPercent(totalFiatBalance: BigDecimal): BigDecimal? {
if (this == null || totalFiatBalance.isZero() || isZero()) return TextReference.EMPTY if (this == null || totalFiatBalance.isZero() || isZero()) return null
return stringReference(divide(totalFiatBalance, RoundingMode.HALF_UP).format { percent() }) return divide(totalFiatBalance, RoundingMode.HALF_UP)
} }
// TODO For You: replace this placeholder with the real price-change badge once the design is wired. // TODO For You: replace this placeholder with the real price-change badge once the design is wired.

View file

@ -5,8 +5,12 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -29,14 +33,12 @@ import java.math.BigDecimal
* *
* Modelled on `TokenListStateConverter` (a list converter delegating to a per-item converter). * Modelled on `TokenListStateConverter` (a list converter delegating to a per-item converter).
*/ */
@Suppress("LongParameterList")
internal class ForYouTokenListConverter( internal class ForYouTokenListConverter(
private val appCurrency: AppCurrency, private val appCurrency: AppCurrency,
private val totalFiatBalance: BigDecimal, private val totalFiatBalance: BigDecimal,
private val expandedAssetIds: Set<String>, private val expandedAssetIds: Set<String>,
private val expandClick: (assetId: String) -> Unit, private val expandClick: (assetId: String) -> Unit,
private val otherAssetCount: Int, private val otherAssets: List<Pair<List<CryptoCurrencyStatus>, BigDecimal>>,
private val otherFiatBalance: BigDecimal,
) : Converter<List<CryptoCurrencyStatus>, ImmutableList<ForYouTokenListItemUM>> { ) : Converter<List<CryptoCurrencyStatus>, ImmutableList<ForYouTokenListItemUM>> {
private val iconConverter = CryptoCurrencyToIconStateConverter() private val iconConverter = CryptoCurrencyToIconStateConverter()
@ -48,7 +50,7 @@ internal class ForYouTokenListConverter(
.map { (assetId, currencies) -> createListItem(assetId, currencies) } .map { (assetId, currencies) -> createListItem(assetId, currencies) }
// Assets beyond the top ones are collapsed into a single non-expandable "Other" row at the bottom. // Assets beyond the top ones are collapsed into a single non-expandable "Other" row at the bottom.
return if (otherAssetCount > 0) { return if (otherAssets.count() > 0) {
assetItems + createOtherItem() assetItems + createOtherItem()
} else { } else {
assetItems assetItems
@ -87,16 +89,16 @@ internal class ForYouTokenListConverter(
val asset = currencies.first() val asset = currencies.first()
val assetFiatBalance = currencies.sumOf { it.value.fiatAmount.orZero() } val assetFiatBalance = currencies.sumOf { it.value.fiatAmount.orZero() }
val subtitle = if (networkCount > 1) { val endContent = rowConverter.toEndContent(statuses = currencies, fiatAmount = assetFiatBalance)
stringReference("$networkCount networks")
} else { val onlyCryptoCurrency = currencies.firstOrNull()?.currency
val onlyCryptoCurrency = currencies.firstOrNull()?.currency val isMain = onlyCryptoCurrency is CryptoCurrency.Coin
val isMain = onlyCryptoCurrency is CryptoCurrency.Coin
when { val subtitle = when {
isMain -> resourceReference(R.string.common_main_network) networkCount > 1 -> pluralReference(R.plurals.common_networks_count, networkCount)
onlyCryptoCurrency != null -> stringReference(onlyCryptoCurrency.network.standardType.name) isMain -> resourceReference(R.string.common_main_network)
else -> TextReference.EMPTY onlyCryptoCurrency != null -> stringReference(onlyCryptoCurrency.network.standardType.name)
} else -> TextReference.EMPTY
} }
return TangemTokenRowUM.Content( return TangemTokenRowUM.Content(
@ -109,38 +111,44 @@ internal class ForYouTokenListConverter(
subtitleUM = TangemTokenRowUM.SubtitleUM.Content( subtitleUM = TangemTokenRowUM.SubtitleUM.Content(
text = subtitle, text = subtitle,
), ),
topEndContentUM = TangemTokenRowUM.EndContentUM.Content( topEndContentUM = endContent.top,
text = assetFiatBalance.toForYouFiatText(appCurrency), bottomEndContentUM = endContent.bottom,
),
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = assetFiatBalance.toForYouPercentText(totalFiatBalance),
),
onItemClick = { expandClick(assetId) }, onItemClick = { expandClick(assetId) },
onItemLongClick = null, onItemLongClick = null,
) )
} }
private fun createOtherItem(): ForYouTokenListItemUM = ForYouTokenListItemUM( private fun createOtherItem(): ForYouTokenListItemUM {
tokenRowUM = TangemTokenRowUM.Content( val otherAssetsBalance = otherAssets.sumOf { (_, assetBalance) -> assetBalance }
id = OTHER_ROW_ID, return ForYouTokenListItemUM(
headIconUM = TangemIconUM.Currency(CurrencyIconState.Empty()), tokenRowUM = TangemTokenRowUM.Content(
titleUM = TangemTokenRowUM.TitleUM.Content(text = stringReference("Other")), id = OTHER_ROW_ID,
subtitleUM = TangemTokenRowUM.SubtitleUM.Content( headIconUM = TangemIconUM.Currency(CurrencyIconState.Empty()),
text = stringReference(if (otherAssetCount > 1) "$otherAssetCount assets" else "1 asset"), titleUM = TangemTokenRowUM.TitleUM.Content(text = resourceReference(R.string.common_other)),
subtitleUM = TangemTokenRowUM.SubtitleUM.Content(
text = pluralReference(R.plurals.common_assets, otherAssets.count()),
),
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference(
otherAssetsBalance.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
),
),
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference(otherAssetsBalance.toForYouPercent(totalFiatBalance).format { percent() }),
),
onItemClick = null,
onItemLongClick = null,
), ),
topEndContentUM = TangemTokenRowUM.EndContentUM.Content( tokenList = persistentListOf(),
text = otherFiatBalance.toForYouFiatText(appCurrency), isExpanded = false,
), isExpandable = false,
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content( )
text = otherFiatBalance.toForYouPercentText(totalFiatBalance), }
),
onItemClick = null,
onItemLongClick = null,
),
tokenList = persistentListOf(),
isExpanded = false,
isExpandable = false,
)
private companion object { private companion object {
const val OTHER_ROW_ID = "for_you_other_assets" const val OTHER_ROW_ID = "for_you_other_assets"

View file

@ -1,15 +1,25 @@
package com.tangem.features.foryou.impl.model.converter package com.tangem.features.foryou.impl.model.converter
import androidx.compose.ui.text.SpanStyle
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.styledResourceReference
import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.utils.StringsSigns import com.tangem.utils.StringsSigns
import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal import java.math.BigDecimal
/** /**
@ -18,6 +28,16 @@ import java.math.BigDecimal
* The input is all [CryptoCurrencyStatus]es of one asset on the *same* network (the asset may be held in * The input is all [CryptoCurrencyStatus]es of one asset on the *same* network (the asset may be held in
* several accounts on that network). They are aggregated into one row the crypto amount and fiat balance * several accounts on that network). They are aggregated into one row the crypto amount and fiat balance
* are the per-network totals so a network never appears twice within an asset's expanded breakdown. * are the per-network totals so a network never appears twice within an asset's expanded breakdown.
*
* 1. all [CryptoCurrencyStatus.Loading] a Loading row;
* 2. any [CryptoCurrencyStatus.MissedDerivation] "no address" treatment (missing address dominates
* the balance for that portion can't be trusted);
* 3. any [CryptoCurrencyStatus.Unreachable] / [CryptoCurrencyStatus.NoAmount] "unreachable" treatment;
* 4. otherwise a normal content row summing the loaded/custom/no-quote/no-account amounts.
*
* [CryptoCurrencyStatus.Loading] entries inside an otherwise-resolved group are ignored for
* classification (they contribute nothing yet). The cache/flicker indicators derive from the most
* conservative [CryptoCurrencyStatus.Sources.total] across the contributing statuses.
*/ */
internal class ForYouTokenRowConverter( internal class ForYouTokenRowConverter(
private val appCurrency: AppCurrency, private val appCurrency: AppCurrency,
@ -36,26 +56,170 @@ internal class ForYouTokenRowConverter(
val currency = representative.currency val currency = representative.currency
val cryptoAmount = statuses.sumOf { it.value.amount.orZero() } val cryptoAmount = statuses.sumOf { it.value.amount.orZero() }
val fiatAmount = statuses.sumOf { it.value.fiatAmount.orZero() } val fiatAmount = statuses.sumOf { it.value.fiatAmount.orZero() }
val state = statuses.classify()
return TangemTokenRowUM.Content( return TangemTokenRowUM.Content(
id = currency.id.value, id = currency.id.value,
headIconUM = TangemIconUM.Currency(iconConverter.convert(representative)), headIconUM = TangemIconUM.Currency(iconConverter.convert(representative)),
titleUM = TangemTokenRowUM.TitleUM.Content( titleUM = toRowTitle(currency),
text = stringReference(currency.symbol), subtitleUM = toRowSubtitle(state, currency, cryptoAmount),
badge = forYouPlaceholderBadge(), topEndContentUM = toRowTopEnd(state, fiatAmount),
), bottomEndContentUM = toRowBottomEnd(state, fiatAmount),
subtitleUM = TangemTokenRowUM.SubtitleUM.Content(
text = stringReference(
"${currency.network.name} ${StringsSigns.DOT} ${cryptoAmount.format { crypto(
cryptoCurrency = currency,
) }}",
),
),
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(text = fiatAmount.toForYouFiatText(appCurrency)),
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = fiatAmount.toForYouPercentText(totalFiatBalance),
),
onItemClick = null, onItemClick = null,
onItemLongClick = null, onItemLongClick = null,
) )
} }
/**
* Maps the aggregate status of [statuses] onto a row's top/bottom end content, rendering the given
* pre-summed [fiatAmount]. Reflects the same cache-flicker / could-not-refresh / no-address /
* unreachable treatment as [convertNetworkGroup], so the asset-level row surfaces the combined status
* of its holdings analogous to how `AccountCryptoPortfolioItemStateConverter` reflects a
* `TotalFiatBalance`'s status on the account row.
*
* Callers must handle the all-[CryptoCurrencyStatus.Loading] case (a Loading row) before calling this.
*/
fun toEndContent(statuses: List<CryptoCurrencyStatus>, fiatAmount: BigDecimal): EndContent {
val state = statuses.classify()
return EndContent(
top = toRowTopEnd(state, fiatAmount),
bottom = toRowBottomEnd(state, fiatAmount),
)
}
/** Title: For You always shows the asset symbol with the placeholder price-change badge. */
private fun toRowTitle(currency: CryptoCurrency): TangemTokenRowUM.TitleUM = TangemTokenRowUM.TitleUM.Content(
text = stringReference(currency.symbol),
badge = forYouPlaceholderBadge(),
)
/**
* Subtitle: `network amount` for resolved states, error messaging otherwise. Kept single-line to
* match For You's style (no separate price-change line as in the wallet).
*/
private fun toRowSubtitle(
state: RowState,
currency: CryptoCurrency,
cryptoAmount: BigDecimal,
): TangemTokenRowUM.SubtitleUM = when (state) {
is RowState.Normal -> TangemTokenRowUM.SubtitleUM.Content(
text = stringReference(
"${currency.network.name} ${StringsSigns.DOT} ${
cryptoAmount.format {
crypto(
cryptoCurrency = currency,
)
}
}",
),
isFlickering = state.isFlickering,
)
RowState.NoAddress -> TangemTokenRowUM.SubtitleUM.Content(
text = stringReference("${currency.network.name} ${StringsSigns.DOT} ${StringsSigns.DASH_SIGN}"),
)
RowState.Unreachable -> TangemTokenRowUM.SubtitleUM.Content(
text = stringReference(currency.network.name),
)
}
/** Top-end: fiat total for resolved states, dash / unreachable treatment otherwise. */
private fun toRowTopEnd(state: RowState, fiatAmount: BigDecimal): TangemTokenRowUM.EndContentUM = when (state) {
is RowState.Normal -> TangemTokenRowUM.EndContentUM.Content(
text = stringReference(
fiatAmount.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
),
isFlickering = state.isFlickering,
startIcons = buildList {
if (state.isOnlyCache) {
add(
TangemIconUM.Icon(
iconRes = R.drawable.ic_error_sync_default_24,
tintReference = { TangemTheme.colors3.icon.tertiary },
),
)
}
}.toImmutableList(),
)
RowState.NoAddress,
RowState.Unreachable,
-> TangemTokenRowUM.EndContentUM.Content(text = stringReference(StringsSigns.DASH_SIGN))
}
/** Bottom-end: percentage share for resolved states, no-address / unreachable treatment otherwise. */
private fun toRowBottomEnd(state: RowState, fiatAmount: BigDecimal): TangemTokenRowUM.EndContentUM = when (state) {
is RowState.Normal -> TangemTokenRowUM.EndContentUM.Content(
text = stringReference(fiatAmount.toForYouPercent(totalFiatBalance).format { percent() }),
isFlickering = state.isFlickering,
)
RowState.NoAddress -> attentionEndContent(R.string.common_no_address)
RowState.Unreachable -> attentionEndContent(R.string.common_unreachable)
}
private fun attentionEndContent(textRes: Int): TangemTokenRowUM.EndContentUM =
TangemTokenRowUM.EndContentUM.Content(
text = styledResourceReference(
id = textRes,
spanStyleReference = { SpanStyle(color = TangemTheme.colors3.text.status.warning) },
),
endIcons = persistentListOf(
TangemIconUM.Icon(
iconRes = R.drawable.ic_attention_default_24,
tintReference = { TangemTheme.colors3.icon.status.warning },
),
),
)
/**
* Collapses a mixed group into a single [RowState]. Loading-only groups are handled earlier, so a
* group reaching here has at least one non-loading status. See the class KDoc for the priority rule.
*/
private fun List<CryptoCurrencyStatus>.classify(): RowState {
val resolved = filterNot { it.value is CryptoCurrencyStatus.Loading }.map { it.value }
return when {
resolved.any { it is CryptoCurrencyStatus.MissedDerivation } -> RowState.NoAddress
resolved.any {
it is CryptoCurrencyStatus.Unreachable || it is CryptoCurrencyStatus.NoAmount
} -> RowState.Unreachable
else -> {
val worstSource = resolved.map { it.sources.total }.worst()
RowState.Normal(
isFlickering = worstSource == StatusSource.CACHE,
isOnlyCache = worstSource == StatusSource.ONLY_CACHE,
)
}
}
}
/**
* The most conservative status across the group: any [StatusSource.ONLY_CACHE] (could-not-refresh)
* dominates a [StatusSource.CACHE] (still refreshing), which in turn dominates [StatusSource.ACTUAL].
*/
private fun List<StatusSource>.worst(): StatusSource = when {
any { it == StatusSource.ONLY_CACHE } -> StatusSource.ONLY_CACHE
any { it == StatusSource.CACHE } -> StatusSource.CACHE
else -> StatusSource.ACTUAL
}
/** The top and bottom end content of a token row, produced together from one classified group. */
data class EndContent(
val top: TangemTokenRowUM.EndContentUM,
val bottom: TangemTokenRowUM.EndContentUM,
)
/** Rendering-relevant collapse of the group's per-currency-status states. */
private sealed interface RowState {
/** Loaded / Custom / NoQuote / NoAccount — normal amounts, with cache/flicker indicators. */
data class Normal(val isFlickering: Boolean, val isOnlyCache: Boolean) : RowState
/** At least one MissedDerivation — no blockchain address obtained. */
data object NoAddress : RowState
/** At least one Unreachable / NoAmount — network could not be reached. */
data object Unreachable : RowState
}
} }

View file

@ -3,38 +3,35 @@ package com.tangem.features.foryou.impl.model.transformer
import com.tangem.core.ui.ds.tabs.TangemSegmentUM import com.tangem.core.ui.ds.tabs.TangemSegmentUM
import com.tangem.core.ui.ds.tabs.TangemSegmentedPickerUM import com.tangem.core.ui.ds.tabs.TangemSegmentedPickerUM
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.account.models.AccountStatusList
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.StatusSource
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletListUM import com.tangem.domain.models.TotalFiatBalance
import com.tangem.features.foryou.impl.entity.ForYouUM import com.tangem.features.foryou.impl.entity.ForYouUM
import com.tangem.features.foryou.impl.entity.PortfolioReviewUM import com.tangem.features.foryou.impl.entity.PortfolioReviewUM
import com.tangem.features.foryou.impl.model.ForYouNotification
import com.tangem.features.foryou.impl.model.converter.ForYouMarketChartConverter
import com.tangem.features.foryou.impl.model.converter.ForYouTokenListConverter import com.tangem.features.foryou.impl.model.converter.ForYouTokenListConverter
import com.tangem.features.foryou.impl.model.converter.forYouGroupKey import com.tangem.features.foryou.impl.model.converter.forYouGroupKey
import com.tangem.utils.StringsSigns
import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.isZero
import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.Transformer import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import java.math.RoundingMode
/** /**
* Builds the [ForYouUM] state for the For You screen: the wallet tabs plus the portfolio * Builds the [ForYouUM] state for the For You screen: the outdated-data notifications plus the portfolio
* review (asset count, top-holding share, period picker and the grouped token list). * review (market chart, period picker and the grouped token list).
* *
* The token list is delegated to [ForYouTokenListConverter]; the period picker selection is carried * The token list is delegated to [ForYouTokenListConverter] and the market chart to
* over from the previous state so it is not reset on every balance refresh. * [ForYouMarketChartConverter]; the period picker selection is carried over from the previous state so
* it is not reset on every balance refresh.
* *
* Modelled on `SetTokenListTransformer` (a transformer that rebuilds the state while delegating the * Modelled on `SetTokenListTransformer` (a transformer that rebuilds the state while delegating the
* token-list construction to a dedicated converter). * token-list construction to a dedicated converter).
*/ */
@Suppress("LongParameterList") @Suppress("LongParameterList")
internal class SetPortfolioReviewTransformer( internal class SetPortfolioReviewTransformer(
private val walletListUM: WalletListUM, private val accountStatusList: AccountStatusList?,
private val currencies: List<CryptoCurrencyStatus>,
private val totalFiatBalance: BigDecimal,
private val appCurrency: AppCurrency, private val appCurrency: AppCurrency,
private val expandedAssetIds: Set<String>, private val expandedAssetIds: Set<String>,
private val expandClick: (assetId: String) -> Unit, private val expandClick: (assetId: String) -> Unit,
@ -42,10 +39,18 @@ internal class SetPortfolioReviewTransformer(
) : Transformer<ForYouUM> { ) : Transformer<ForYouUM> {
override fun transform(prevState: ForYouUM): ForYouUM { override fun transform(prevState: ForYouUM): ForYouUM {
// Drop empty networks, then aggregate the rest into assets (the same token across networks shares val currencies = accountStatusList?.flattenCurrencies().orEmpty()
// its forYouGroupKey) and rank assets by their *summed* fiat balance. val loadedBalance = accountStatusList?.totalFiatBalance as? TotalFiatBalance.Loaded
val totalFiatBalance = loadedBalance?.amount.orZero()
// Drop only assets we positively know are empty — a resolved, priced zero fiat balance. Currencies
// whose fiat we couldn't determine (unreachable / no-address / no-quote / still-loading — i.e. any
// non-content status, which all carry a null fiatAmount) are kept so the converter can still render
// them with the appropriate treatment instead of hiding a token the user actually holds.
// Then aggregate the rest into assets (the same token across networks shares its forYouGroupKey)
// and rank assets by their *summed* fiat balance.
val rankedAssets = currencies val rankedAssets = currencies
.filterNot { it.value.fiatAmount.orZero().isZero() } .filterNot { it.value.fiatAmount?.isZero() == true }
.groupBy { it.forYouGroupKey() } .groupBy { it.forYouGroupKey() }
.map { (_, networks) -> networks to networks.sumOf { it.value.fiatAmount.orZero() } } .map { (_, networks) -> networks to networks.sumOf { it.value.fiatAmount.orZero() } }
.sortedByDescending { (_, assetBalance) -> assetBalance } .sortedByDescending { (_, assetBalance) -> assetBalance }
@ -55,40 +60,38 @@ internal class SetPortfolioReviewTransformer(
val topAssets = rankedAssets.take(TOP_HOLDINGS_COUNT) val topAssets = rankedAssets.take(TOP_HOLDINGS_COUNT)
val otherAssets = rankedAssets.drop(TOP_HOLDINGS_COUNT) val otherAssets = rankedAssets.drop(TOP_HOLDINGS_COUNT)
val topCurrencies = topAssets.flatMap { (networks, _) -> networks } val topCurrencies = topAssets.flatMap { (networks, _) -> networks }
val topBalance = topAssets.sumOf { (_, assetBalance) -> assetBalance }
val tokenList = ForYouTokenListConverter( val tokenList = ForYouTokenListConverter(
appCurrency = appCurrency, appCurrency = appCurrency,
totalFiatBalance = totalFiatBalance, totalFiatBalance = totalFiatBalance,
expandedAssetIds = expandedAssetIds, expandedAssetIds = expandedAssetIds,
expandClick = expandClick, expandClick = expandClick,
otherAssetCount = otherAssets.size, otherAssets = otherAssets,
otherFiatBalance = otherAssets.sumOf { (_, assetBalance) -> assetBalance },
).convert(topCurrencies) ).convert(topCurrencies)
val marketChartUM = ForYouMarketChartConverter(
appCurrency = appCurrency,
topAssets = topAssets,
).convert(accountStatusList?.totalFiatBalance)
return prevState.copy( return prevState.copy(
walletListUM = walletListUM, notifications = if (loadedBalance?.source == StatusSource.ONLY_CACHE) {
persistentListOf(ForYouNotification.UsedOutdatedData)
} else {
persistentListOf()
},
portfolioReviewUM = PortfolioReviewUM.Content( portfolioReviewUM = PortfolioReviewUM.Content(
assetCount = stringReference("${rankedAssets.size} assets"), // TODO For You lokalize
topHoldingPercent = stringReference("Top holding ${topHoldingPercent(topBalance)}"),
periodPickerUM = when (prevState.portfolioReviewUM) { periodPickerUM = when (prevState.portfolioReviewUM) {
is PortfolioReviewUM.Content -> prevState.portfolioReviewUM.periodPickerUM is PortfolioReviewUM.Content -> prevState.portfolioReviewUM.periodPickerUM
is PortfolioReviewUM.Loading -> createPeriodPicker() is PortfolioReviewUM.Loading -> createPeriodPicker()
}, },
tokenList = tokenList, tokenList = tokenList,
marketChartUM = marketChartUM,
onPeriodClick = onPeriodClick, onPeriodClick = onPeriodClick,
), ),
) )
} }
private fun topHoldingPercent(topBalance: BigDecimal): String {
return if (!totalFiatBalance.isZero() && !topBalance.isZero()) {
topBalance.divide(totalFiatBalance, RoundingMode.HALF_UP).format { percent() }
} else {
StringsSigns.DASH_SIGN
}
}
private fun createPeriodPicker(): TangemSegmentedPickerUM { private fun createPeriodPicker(): TangemSegmentedPickerUM {
// TODO For you replace with data from backend // TODO For you replace with data from backend
val day = TangemSegmentUM(id = "0", title = stringReference("Day")) val day = TangemSegmentUM(id = "0", title = stringReference("Day"))

View file

@ -16,17 +16,16 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletListUM import com.tangem.features.foryou.impl.model.ForYouNotification
import com.tangem.features.commonfeatures.api.choosetoken.model.WalletTabUM
import com.tangem.features.foryou.impl.entity.ForYouUM import com.tangem.features.foryou.impl.entity.ForYouUM
import com.tangem.features.foryou.impl.ui.components.WalletTabsBlock
import com.tangem.features.foryou.impl.ui.preview.ForYouPortfolioReviewPreviewData import com.tangem.features.foryou.impl.ui.preview.ForYouPortfolioReviewPreviewData
import com.tangem.features.promobanners.api.PromoBannersBlockComponent import com.tangem.features.promobanners.api.PromoBannersBlockComponent
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -54,15 +53,27 @@ internal fun ForYouContent(
.padding(top = contentPadding.calculateTopPadding()) .padding(top = contentPadding.calculateTopPadding())
.drawBehind { drawRect(background.value) }, .drawBehind { drawRect(background.value) },
) { ) {
WalletTabsBlock(walletList = forYouUM.walletListUM)
SpacerH(12.dp)
promoBannersBlockComponent.ContentWithPadding( promoBannersBlockComponent.ContentWithPadding(
modifier = Modifier, modifier = Modifier.padding(top = 12.dp),
horizontalItemPadding = 16.dp, horizontalItemPadding = 16.dp,
) )
forYouUM.notifications.fastForEachIndexed { index, notification ->
key(notification.state) {
TangemMessageBanner(
state = notification.state,
modifier = Modifier
.padding(horizontal = 16.dp)
.conditional(index == 0) {
padding(top = 12.dp)
}
.conditional(index == forYouUM.notifications.lastIndex) {
padding(bottom = 48.dp)
},
)
}
}
ForYouPortfolioReview( ForYouPortfolioReview(
portfolioReviewUM = forYouUM.portfolioReviewUM, portfolioReviewUM = forYouUM.portfolioReviewUM,
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
@ -98,17 +109,7 @@ private class ForYouContentPreviewProvider : PreviewParameterProvider<ForYouUM>
override val values: Sequence<ForYouUM> override val values: Sequence<ForYouUM>
get() = sequenceOf( get() = sequenceOf(
ForYouUM( ForYouUM(
walletListUM = WalletListUM( notifications = persistentListOf(ForYouNotification.UsedOutdatedData),
items = persistentListOf(
WalletTabUM(
text = stringReference("Wallet 1"),
count = stringReference("1"),
isSelected = true,
onClick = {},
deviceIcon = DeviceIconUM.Mobile,
),
),
),
portfolioReviewUM = ForYouPortfolioReviewPreviewData.reviewContent, portfolioReviewUM = ForYouPortfolioReviewPreviewData.reviewContent,
), ),
) )

View file

@ -11,20 +11,23 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker
import com.tangem.core.ui.ds2.badge.TangemBadge import com.tangem.core.ui.ds2.badge.TangemBadge
import com.tangem.core.ui.ds2.shimmers.TangemShimmer
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_chevron_down_16 import com.tangem.core.ui.res.generated.icons.ic_chevron_down_16
import com.tangem.features.foryou.impl.R
import com.tangem.features.foryou.impl.components.MarketChart
import com.tangem.features.foryou.impl.components.state.MarketChartUM
import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM
import com.tangem.features.foryou.impl.entity.PortfolioReviewUM import com.tangem.features.foryou.impl.entity.PortfolioReviewUM
import com.tangem.features.foryou.impl.ui.components.ForYouMarketChartContent
import com.tangem.features.foryou.impl.ui.components.ForYouPortfolioTokenList import com.tangem.features.foryou.impl.ui.components.ForYouPortfolioTokenList
import com.tangem.features.foryou.impl.ui.preview.ForYouPortfolioReviewPreviewData import com.tangem.features.foryou.impl.ui.preview.ForYouPortfolioReviewPreviewData
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -39,7 +42,7 @@ internal fun ForYouPortfolioReview(portfolioReviewUM: PortfolioReviewUM, modifie
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Text(
text = "Portfolio Review", // TODO For You text = stringResourceSafe(R.string.for_you_portfolio_review_title),
style = TangemTheme.typography3.heading.small, style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary, color = TangemTheme.colors3.text.primary,
) )
@ -51,8 +54,13 @@ internal fun ForYouPortfolioReview(portfolioReviewUM: PortfolioReviewUM, modifie
) )
} }
SpacerH(16.dp) SpacerH(16.dp)
ForYouMarketChartContent(portfolioReviewUM)
MarketChart(
marketChart = portfolioReviewUM.marketChartUM,
modifier = Modifier.fillMaxWidth(),
)
SpacerH(8.dp) SpacerH(8.dp)
when (portfolioReviewUM) { when (portfolioReviewUM) {
is PortfolioReviewUM.Content -> { is PortfolioReviewUM.Content -> {
TangemSegmentedPicker( TangemSegmentedPicker(
@ -60,7 +68,7 @@ internal fun ForYouPortfolioReview(portfolioReviewUM: PortfolioReviewUM, modifie
onClick = portfolioReviewUM.onPeriodClick, onClick = portfolioReviewUM.onPeriodClick,
) )
} }
is PortfolioReviewUM.Loading -> RectangleShimmer( is PortfolioReviewUM.Loading -> TangemShimmer(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(40.dp), .height(40.dp),
@ -92,6 +100,7 @@ private class ForYouPortfolioReviewPreviewProvider : PreviewParameterProvider<Po
get() = sequenceOf( get() = sequenceOf(
ForYouPortfolioReviewPreviewData.reviewContent, ForYouPortfolioReviewPreviewData.reviewContent,
PortfolioReviewUM.Loading( PortfolioReviewUM.Loading(
marketChartUM = MarketChartUM.NoData,
tokenList = buildList { tokenList = buildList {
repeat(4) { index -> repeat(4) { index ->
add( add(

View file

@ -1,68 +0,0 @@
package com.tangem.features.foryou.impl.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color.Companion.Cyan
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.foryou.impl.entity.PortfolioReviewUM
@Composable
internal fun ForYouMarketChartContent(portfolioReviewUM: PortfolioReviewUM) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(24.dp))
.background(TangemTheme.colors3.bg.secondary)
.padding(16.dp),
) {
Box(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(
top = 16.dp,
end = 32.dp,
start = 32.dp,
bottom = 32.dp,
)
.background(Cyan, CircleShape)
.size(200.dp),
)
SpacerH(16.dp)
when (portfolioReviewUM) {
is PortfolioReviewUM.Content -> {
Text(
text = portfolioReviewUM.assetCount.resolveReference(),
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.secondary,
)
Text(
text = portfolioReviewUM.topHoldingPercent.resolveReference(),
style = TangemTheme.typography3.heading.small,
color = TangemTheme.colors3.text.primary,
)
}
is PortfolioReviewUM.Loading -> {
TextShimmer(
style = TangemTheme.typography3.heading.small,
modifier = Modifier.width(50.dp),
)
TextShimmer(
style = TangemTheme.typography3.heading.small,
modifier = Modifier.width(75.dp),
)
}
}
}
}

View file

@ -46,7 +46,7 @@ import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.Icons
import com.tangem.core.ui.res.generated.icons.ic_error_20 import com.tangem.core.ui.res.generated.icons.ic_chevron_collapse_20
import com.tangem.core.ui.utils.ProvideSharedTransitionScope import com.tangem.core.ui.utils.ProvideSharedTransitionScope
import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.core.ui.utils.lazyListItemPosition
import com.tangem.core.ui.utils.sharedBoundsSafely import com.tangem.core.ui.utils.sharedBoundsSafely
@ -129,7 +129,7 @@ private fun PortfolioAssetItem(listItem: ForYouTokenListItemUM, index: Int, oute
targetState = listItem.isExpanded, targetState = listItem.isExpanded,
transitionSpec = { portfolioAssetExpandFadeAnimation() }, transitionSpec = { portfolioAssetExpandFadeAnimation() },
) { isExpandedWrapped -> ) { isExpandedWrapped ->
val composables = remember { val composables = remember(isExpandedWrapped) {
SharedTokenRowComposables( SharedTokenRowComposables(
icon = { modifier -> icon = { modifier ->
PortfolioSharedAssetIcon( PortfolioSharedAssetIcon(
@ -307,7 +307,7 @@ private fun ForYouPortfolioListHeader(
} }
SpacerWMax() SpacerWMax()
Icon( Icon(
imageVector = Icons.ic_error_20, imageVector = Icons.ic_chevron_collapse_20,
tint = TangemTheme.colors3.icon.primary, tint = TangemTheme.colors3.icon.primary,
contentDescription = null, contentDescription = null,
) )

View file

@ -10,16 +10,19 @@ import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.ds.tabs.TangemSegmentUM import com.tangem.core.ui.ds.tabs.TangemSegmentUM
import com.tangem.core.ui.ds.tabs.TangemSegmentedPickerUM import com.tangem.core.ui.ds.tabs.TangemSegmentedPickerUM
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.foryou.impl.components.state.DonutChartUM
import com.tangem.features.foryou.impl.components.state.DonutSegmentColor
import com.tangem.features.foryou.impl.components.state.DonutSegmentUM
import com.tangem.features.foryou.impl.components.state.MarketChartUM
import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM
import com.tangem.features.foryou.impl.entity.PortfolioReviewUM import com.tangem.features.foryou.impl.entity.PortfolioReviewUM
import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.StringsSigns.DOT
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
internal object ForYouPortfolioReviewPreviewData { internal object ForYouPortfolioReviewPreviewData {
val reviewContent = PortfolioReviewUM.Content( val reviewContent = PortfolioReviewUM.Content(
assetCount = stringReference("5 assets"),
topHoldingPercent = stringReference("Top holding 42%"),
periodPickerUM = TangemSegmentedPickerUM( periodPickerUM = TangemSegmentedPickerUM(
items = persistentListOf( items = persistentListOf(
TangemSegmentUM(id = "0", title = stringReference("Day")), TangemSegmentUM(id = "0", title = stringReference("Day")),
@ -31,6 +34,27 @@ internal object ForYouPortfolioReviewPreviewData {
isAltSurface = true, isAltSurface = true,
), ),
onPeriodClick = {}, onPeriodClick = {},
marketChartUM = MarketChartUM.Loaded(
donutChart = DonutChartUM.Loaded(
totalAmount = "10000$",
// Colours are assigned in segment order (rank), matching the transformer's palette-by-index.
donutSegmentList = persistentListOf(
DonutSegmentUM(
color = DonutSegmentColor.Brand,
weight = BigDecimal("0.55"),
title = stringReference("Ethereum"),
fiatValue = stringReference("\$5,720.22"),
),
DonutSegmentUM(
color = DonutSegmentColor.Green,
weight = BigDecimal("0.45"),
title = stringReference("Solana"),
fiatValue = stringReference("\$728.30"),
),
),
),
topHoldingPercent = stringReference("Top holding 42%"),
),
tokenList = persistentListOf( tokenList = persistentListOf(
ForYouTokenListItemUM( ForYouTokenListItemUM(
tokenRowUM = TangemTokenRowUM.Content( tokenRowUM = TangemTokenRowUM.Content(