Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-15 21:59:31 +05:00
parent d894698a9b
commit c744af9a6a
17 changed files with 402 additions and 109 deletions

View file

@ -908,9 +908,11 @@
<item quantity="one">%d asset</item> <item quantity="one">%d asset</item>
<item quantity="other">%d assets</item> <item quantity="other">%d assets</item>
</plurals> </plurals>
<string name="market_chart_bubble_no_amount">No amount on tokens</string>
<string name="market_chart_bubble_no_data">No data</string> <string name="market_chart_bubble_no_data">No data</string>
<string name="market_chart_bubble_total_value">Total value</string> <string name="market_chart_bubble_total_value">Total value</string>
<string name="market_chart_can_not_load_data">Cant load data</string> <string name="market_chart_can_not_load_data">Cant load data</string>
<string name="market_chart_no_amount">You dont have any tokens with amount</string>
<string name="market_chart_top_holding">Top holding %s</string> <string name="market_chart_top_holding">Top holding %s</string>
<string name="markets_about_coin_header">About coin</string> <string name="markets_about_coin_header">About coin</string>
<string name="markets_add_to_my_portfolio_description">To buy, exchange, or receive this asset, add it to your portfolio</string> <string name="markets_add_to_my_portfolio_description">To buy, exchange, or receive this asset, add it to your portfolio</string>

View file

@ -69,7 +69,14 @@ internal class DefaultForYouComponent @AssistedInject constructor(
childFactory = { config, componentContext -> childFactory = { config, componentContext ->
when (config) { when (config) {
ForYouBottomSheetConfig.AddToPortfolio -> portfolioSelectorChild(componentContext) ForYouBottomSheetConfig.AddToPortfolio -> portfolioSelectorChild(componentContext)
is ForYouBottomSheetConfig.ManageFunds -> manageFundsChild(config, componentContext) is ForYouBottomSheetConfig.ManageFunds -> manageFundsChild(
componentContext = componentContext,
launchMode = ManageFundsComponent.LaunchMode.FilteredByRawId(config.rawCurrencyId),
)
is ForYouBottomSheetConfig.AddFunds -> manageFundsChild(
componentContext = componentContext,
launchMode = ManageFundsComponent.LaunchMode.ChooseToken(config.userWalletId),
)
} }
}, },
) )
@ -129,12 +136,12 @@ internal class DefaultForYouComponent @AssistedInject constructor(
) )
private fun manageFundsChild( private fun manageFundsChild(
config: ForYouBottomSheetConfig.ManageFunds, launchMode: ManageFundsComponent.LaunchMode,
componentContext: ComponentContext, componentContext: ComponentContext,
): ComposableBottomSheetComponent = manageFundsComponentFactory.create( ): ComposableBottomSheetComponent = manageFundsComponentFactory.create(
context = childByContext(componentContext), context = childByContext(componentContext),
params = ManageFundsComponent.Params( params = ManageFundsComponent.Params(
launchMode = ManageFundsComponent.LaunchMode.FilteredByRawId(config.rawCurrencyId), launchMode = launchMode,
onDismiss = { model.bottomSheetNavigation.dismiss() }, onDismiss = { model.bottomSheetNavigation.dismiss() },
), ),
) )

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview 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
@ -51,13 +52,16 @@ internal fun MarketChart(marketChart: MarketChartUM, modifier: Modifier = Modifi
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
DonutChartBlock(marketChart.donutChart, cardBoundsInWindow) DonutChartBlock(marketChart.donutChart, cardBoundsInWindow)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
if (marketChart is MarketChartUM.Loaded) { when (marketChart) {
TopHoldingBlock( is MarketChartUM.Loaded -> {
assetCount = marketChart.assetCount, TopHoldingBlock(
topHoldingPercent = marketChart.topHoldingPercent, assetCount = marketChart.assetCount,
) topHoldingPercent = marketChart.topHoldingPercent,
} else { )
CantLoadDataBlock() }
is MarketChartUM.NoData -> {
CantLoadDataBlock(text = marketChart.title)
}
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@ -113,36 +117,40 @@ private fun ColumnScope.DonutChartBlock(donutChartUM: DonutChartUM, cardBoundsIn
}, },
segments = segments, segments = segments,
) { ) {
if (donutChartUM is DonutChartUM.Loaded) { when (donutChartUM) {
Text( is DonutChartUM.Loaded -> {
text = donutChartUM.totalAmount, Text(
color = TangemTheme.colors3.text.primary, text = donutChartUM.totalAmount,
style = TangemTheme.typography3.body.medium, color = TangemTheme.colors3.text.primary,
maxLines = 1, style = TangemTheme.typography3.body.medium,
autoSize = TextAutoSize.StepBased( maxLines = 1,
minFontSize = 8.sp, autoSize = TextAutoSize.StepBased(
maxFontSize = TangemTheme.typography3.body.medium.fontSize, minFontSize = 8.sp,
), maxFontSize = TangemTheme.typography3.body.medium.fontSize,
) ),
Text( )
text = stringResourceSafe(R.string.market_chart_bubble_total_value), Text(
color = TangemTheme.colors3.text.secondary, text = stringResourceSafe(R.string.market_chart_bubble_total_value),
style = TangemTheme.typography3.caption.medium, color = TangemTheme.colors3.text.secondary,
maxLines = 1, style = TangemTheme.typography3.caption.medium,
autoSize = TextAutoSize.StepBased( maxLines = 1,
maxFontSize = TangemTheme.typography3.caption.medium.fontSize, autoSize = TextAutoSize.StepBased(
), maxFontSize = TangemTheme.typography3.caption.medium.fontSize,
) ),
} else { )
Text( }
text = stringResourceSafe(R.string.market_chart_bubble_no_data), is DonutChartUM.NoData -> {
color = TangemTheme.colors3.text.secondary, Text(
style = TangemTheme.typography3.body.medium, text = donutChartUM.title.resolveReference(),
maxLines = 1, color = TangemTheme.colors3.text.secondary,
autoSize = TextAutoSize.StepBased( style = TangemTheme.typography3.body.medium,
maxFontSize = TangemTheme.typography3.caption.medium.fontSize, maxLines = 2,
), textAlign = TextAlign.Center,
) autoSize = TextAutoSize.StepBased(
maxFontSize = TangemTheme.typography3.caption.medium.fontSize,
),
)
}
} }
} }
@ -233,10 +241,10 @@ private fun ColumnScope.TopHoldingBlock(assetCount: Int, topHoldingPercent: Text
} }
@Composable @Composable
private fun ColumnScope.CantLoadDataBlock() { private fun ColumnScope.CantLoadDataBlock(text: TextReference) {
Text( Text(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
text = stringResourceSafe(R.string.market_chart_can_not_load_data), text = text.resolveReference(),
color = TangemTheme.colors3.text.secondary, color = TangemTheme.colors3.text.secondary,
style = TangemTheme.typography3.heading.small, style = TangemTheme.typography3.heading.small,
) )
@ -306,7 +314,10 @@ private fun previewMarketChartState(scenario: MarketChartPreviewScenario): Marke
), ),
), ),
) )
MarketChartPreviewScenario.NO_DATA -> MarketChartUM.NoData MarketChartPreviewScenario.NO_DATA -> MarketChartUM.NoData(
title = resourceReference(R.string.market_chart_can_not_load_data),
donutText = resourceReference(R.string.market_chart_bubble_no_data),
)
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")

View file

@ -21,8 +21,11 @@ internal sealed class MarketChartUM(
val assetCount: Int = donutChart.donutSegmentList.size val assetCount: Int = donutChart.donutSegmentList.size
} }
data object NoData : MarketChartUM( data class NoData(
donutChart = DonutChartUM.NoData, val title: TextReference,
private val donutText: TextReference,
) : MarketChartUM(
donutChart = DonutChartUM.NoData(title = donutText),
aiInsight = AiInsightUM.Hide, aiInsight = AiInsightUM.Hide,
) )
} }
@ -36,7 +39,9 @@ internal sealed class DonutChartUM(
override val donutSegmentList: ImmutableList<DonutSegmentUM>, override val donutSegmentList: ImmutableList<DonutSegmentUM>,
) : DonutChartUM(donutSegmentList = donutSegmentList) ) : DonutChartUM(donutSegmentList = donutSegmentList)
data object NoData : DonutChartUM(donutSegmentList = persistentListOf()) data class NoData(
val title: TextReference,
) : DonutChartUM(donutSegmentList = persistentListOf())
} }
@Immutable @Immutable

View file

@ -1,6 +1,7 @@
package com.tangem.features.foryou.impl.entity package com.tangem.features.foryou.impl.entity
import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWalletId
internal sealed interface ForYouBottomSheetConfig { internal sealed interface ForYouBottomSheetConfig {
@ -9,4 +10,8 @@ internal sealed interface ForYouBottomSheetConfig {
data class ManageFunds( data class ManageFunds(
val rawCurrencyId: CryptoCurrency.RawID, val rawCurrencyId: CryptoCurrency.RawID,
) : ForYouBottomSheetConfig ) : ForYouBottomSheetConfig
data class AddFunds(
val userWalletId: UserWalletId,
) : ForYouBottomSheetConfig
} }

View file

@ -31,6 +31,7 @@ internal sealed interface PortfolioReviewUM {
data class Content( data class Content(
override val tokenList: ImmutableList<ForYouTokenListItemUM>, override val tokenList: ImmutableList<ForYouTokenListItemUM>,
override val marketChartUM: MarketChartUM, override val marketChartUM: MarketChartUM,
val onAddFundsClick: (() -> Unit)?,
) : PortfolioReviewUM ) : PortfolioReviewUM
} }

View file

@ -16,6 +16,7 @@ import com.tangem.core.decompose.model.ParamsContainer
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.resourceReference
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.account.status.usecase.IsAccountsModeEnabledUseCase
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -32,6 +33,7 @@ import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager
import com.tangem.features.foryou.ForYouComponent import com.tangem.features.foryou.ForYouComponent
import com.tangem.features.foryou.impl.R
import com.tangem.features.foryou.impl.components.state.MarketChartUM import com.tangem.features.foryou.impl.components.state.MarketChartUM
import com.tangem.features.foryou.impl.entity.* import com.tangem.features.foryou.impl.entity.*
import com.tangem.features.foryou.impl.model.converter.TOP_EARN_TOKENS_BATCH_SIZE import com.tangem.features.foryou.impl.model.converter.TOP_EARN_TOKENS_BATCH_SIZE
@ -106,7 +108,10 @@ internal class ForYouModel @Inject constructor(
), ),
onPeriodClick = ::onPeriodClick, onPeriodClick = ::onPeriodClick,
portfolioReviewUM = PortfolioReviewUM.Loading( portfolioReviewUM = PortfolioReviewUM.Loading(
marketChartUM = MarketChartUM.NoData, marketChartUM = MarketChartUM.NoData(
title = resourceReference(R.string.market_chart_can_not_load_data),
donutText = resourceReference(R.string.market_chart_bubble_no_data),
),
tokenList = buildList<ForYouTokenListItemUM> { tokenList = buildList<ForYouTokenListItemUM> {
repeat(4) { index -> repeat(4) { index ->
add( add(
@ -154,6 +159,7 @@ internal class ForYouModel @Inject constructor(
expandedAssetIds = expandedPortfolioReview, expandedAssetIds = expandedPortfolioReview,
expandClick = ::onExpandPortfolioReviewClick, expandClick = ::onExpandPortfolioReviewClick,
onTokenClick = ::onPortfolioReviewTokenClick, onTokenClick = ::onPortfolioReviewTokenClick,
onAddFundsClick = ::onAddFundsClick,
).convert(accountStatusList) ).convert(accountStatusList)
val earnOpportunitiesUM = ForYouEarnOpportunitiesConverter( val earnOpportunitiesUM = ForYouEarnOpportunitiesConverter(
@ -363,4 +369,8 @@ internal class ForYouModel @Inject constructor(
}, },
) )
} }
private fun onAddFundsClick(userWalletId: UserWalletId) {
bottomSheetNavigation.activate(ForYouBottomSheetConfig.AddFunds(userWalletId))
}
} }

View file

@ -15,8 +15,10 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.foryou.impl.R import com.tangem.features.foryou.impl.R
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.model.converter.FOR_YOU_TOP_EARN_TOKENS_COUNT
import com.tangem.features.foryou.impl.model.converter.forYouGroupKey import com.tangem.features.foryou.impl.model.converter.forYouGroupKey
import com.tangem.features.foryou.impl.model.converter.forYouPlaceholderBadge import com.tangem.features.foryou.impl.model.converter.forYouPlaceholderBadge
import com.tangem.features.foryou.impl.model.converter.toForYouPercent import com.tangem.features.foryou.impl.model.converter.toForYouPercent
@ -42,6 +44,7 @@ internal class ForYouPortfolioReviewConverter(
private val expandedAssetIds: Set<String>, private val expandedAssetIds: Set<String>,
private val expandClick: (assetId: String) -> Unit, private val expandClick: (assetId: String) -> Unit,
private val onTokenClick: (UserWalletId, CryptoCurrency) -> Unit, private val onTokenClick: (UserWalletId, CryptoCurrency) -> Unit,
private val onAddFundsClick: (UserWalletId) -> Unit,
) : Converter<AccountStatusList?, PortfolioReviewUM> { ) : Converter<AccountStatusList?, PortfolioReviewUM> {
private val iconConverter = CryptoCurrencyToIconStateConverter() private val iconConverter = CryptoCurrencyToIconStateConverter()
@ -51,6 +54,26 @@ internal class ForYouPortfolioReviewConverter(
val loadedBalance = value?.totalFiatBalance as? TotalFiatBalance.Loaded val loadedBalance = value?.totalFiatBalance as? TotalFiatBalance.Loaded
val totalFiatBalance = loadedBalance?.amount.orZero() val totalFiatBalance = loadedBalance?.amount.orZero()
if (currencies.all { it.value.fiatAmount?.isZero() == true }) {
return PortfolioReviewUM.Content(
tokenList = currencies.take(FOR_YOU_TOP_EARN_TOKENS_COUNT)
.groupBy { it.forYouGroupKey() }
.map { (assetId, currencies) ->
createListItem(
userWalletId = value?.userWalletId,
assetId = assetId,
currencies = currencies,
totalFiatBalance = totalFiatBalance,
)
}.toPersistentList(),
marketChartUM = MarketChartUM.NoData(
title = resourceReference(R.string.market_chart_no_amount),
donutText = resourceReference(R.string.market_chart_bubble_no_amount),
),
onAddFundsClick = { value?.userWalletId?.let(onAddFundsClick) },
)
}
// Drop only assets we positively know are empty — a resolved, priced zero fiat balance. Currencies // 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 // 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 // non-content status, which all carry a null fiatAmount) are kept so the converter can still render
@ -95,6 +118,7 @@ internal class ForYouPortfolioReviewConverter(
return PortfolioReviewUM.Content( return PortfolioReviewUM.Content(
tokenList = tokenList, tokenList = tokenList,
marketChartUM = marketChartUM, marketChartUM = marketChartUM,
onAddFundsClick = null,
) )
} }
@ -127,7 +151,7 @@ internal class ForYouPortfolioReviewConverter(
networkCount = networkGroups.size, networkCount = networkGroups.size,
totalFiatBalance = totalFiatBalance, totalFiatBalance = totalFiatBalance,
), ),
tokenList = networkGroups.map(rowConverter::convertNetworkGroup).toPersistentList(), tokenList = networkGroups.map(rowConverter::convert).toPersistentList(),
isExpanded = assetId in expandedAssetIds, isExpanded = assetId in expandedAssetIds,
isExpandable = true, isExpandable = true,
) )

View file

@ -1,5 +1,6 @@
package com.tangem.features.foryou.impl.model.converter.portfolioReview package com.tangem.features.foryou.impl.model.converter.portfolioReview
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.fiat
import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.format
@ -7,6 +8,7 @@ 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.TotalFiatBalance import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.features.foryou.impl.R
import com.tangem.features.foryou.impl.components.state.AiInsightUM import com.tangem.features.foryou.impl.components.state.AiInsightUM
import com.tangem.features.foryou.impl.components.state.DonutChartUM 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.DonutSegmentColor
@ -54,7 +56,10 @@ internal class ForYouPortfolioReviewMarketChartConverter(
TotalFiatBalance.Loading, TotalFiatBalance.Loading,
TotalFiatBalance.Failed, TotalFiatBalance.Failed,
null, null,
-> MarketChartUM.NoData -> MarketChartUM.NoData(
title = resourceReference(R.string.market_chart_can_not_load_data),
donutText = resourceReference(R.string.market_chart_bubble_no_data),
)
} }
} }
} }

View file

@ -20,6 +20,7 @@ import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.foryou.impl.model.converter.forYouPlaceholderBadge import com.tangem.features.foryou.impl.model.converter.forYouPlaceholderBadge
import com.tangem.features.foryou.impl.model.converter.toForYouPercent import com.tangem.features.foryou.impl.model.converter.toForYouPercent
import com.tangem.utils.StringsSigns import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -47,21 +48,20 @@ internal class ForYouPortfolioReviewTokenRowConverter(
private val userWalletId: UserWalletId?, private val userWalletId: UserWalletId?,
private val totalFiatBalance: BigDecimal, private val totalFiatBalance: BigDecimal,
private val onTokenClick: (UserWalletId, CryptoCurrency) -> Unit, private val onTokenClick: (UserWalletId, CryptoCurrency) -> Unit,
) { ) : Converter<List<CryptoCurrencyStatus>, TangemTokenRowUM> {
private val iconConverter = CryptoCurrencyToIconStateConverter() private val iconConverter = CryptoCurrencyToIconStateConverter()
/** Builds one row for all [statuses] of a single asset on the same network. */ override fun convert(value: List<CryptoCurrencyStatus>): TangemTokenRowUM {
fun convertNetworkGroup(statuses: List<CryptoCurrencyStatus>): TangemTokenRowUM { val representative = value.first()
val representative = statuses.first() if (value.all { it.value is CryptoCurrencyStatus.Loading }) {
if (statuses.all { it.value is CryptoCurrencyStatus.Loading }) {
return TangemTokenRowUM.Loading(id = representative.currency.id.value) return TangemTokenRowUM.Loading(id = representative.currency.id.value)
} }
val currency = representative.currency val currency = representative.currency
val cryptoAmount = statuses.sumOf { it.value.amount.orZero() } val cryptoAmount = value.sumOf { it.value.amount.orZero() }
val fiatAmount = statuses.sumOf { it.value.fiatAmount.orZero() } val fiatAmount = value.sumOf { it.value.fiatAmount.orZero() }
val state = statuses.classify() val state = value.classify()
return TangemTokenRowUM.Content( return TangemTokenRowUM.Content(
id = currency.id.value, id = currency.id.value,
@ -78,7 +78,7 @@ internal class ForYouPortfolioReviewTokenRowConverter(
/** /**
* Maps the aggregate status of [statuses] onto a row's top/bottom end content, rendering the given * 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 / * 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 * unreachable treatment as [convert], so the asset-level row surfaces the combined status
* of its holdings analogous to how `AccountCryptoPortfolioItemStateConverter` reflects a * of its holdings analogous to how `AccountCryptoPortfolioItemStateConverter` reflects a
* `TotalFiatBalance`'s status on the account row. * `TotalFiatBalance`'s status on the account row.
* *
@ -158,7 +158,7 @@ internal class ForYouPortfolioReviewTokenRowConverter(
/** Bottom-end: percentage share for resolved states, no-address / unreachable treatment otherwise. */ /** Bottom-end: percentage share for resolved states, no-address / unreachable treatment otherwise. */
private fun toRowBottomEnd(state: RowState, fiatAmount: BigDecimal): TangemTokenRowUM.EndContentUM = when (state) { private fun toRowBottomEnd(state: RowState, fiatAmount: BigDecimal): TangemTokenRowUM.EndContentUM = when (state) {
is RowState.Normal -> TangemTokenRowUM.EndContentUM.Content( is RowState.Normal -> TangemTokenRowUM.EndContentUM.Content(
text = stringReference(fiatAmount.toForYouPercent(totalFiatBalance).format { percent() }), text = stringReference(fiatAmount.toForYouPercent(totalFiatBalance).orZero().format { percent() }),
isFlickering = state.isFlickering, isFlickering = state.isFlickering,
) )
RowState.NoAddress -> attentionEndContent(R.string.common_no_address) RowState.NoAddress -> attentionEndContent(R.string.common_no_address)

View file

@ -58,7 +58,11 @@ internal fun ForYouContent(
.drawBehind { drawRect(background.value) }, .drawBehind { drawRect(background.value) },
) { ) {
promoBannersBlockComponent.ContentWithPadding( promoBannersBlockComponent.ContentWithPadding(
modifier = Modifier.padding(top = 12.dp), modifier = Modifier
.padding(top = 12.dp)
.conditional(forYouUM.notifications.isEmpty()) {
padding(bottom = 48.dp)
},
walletId = null, walletId = null,
horizontalItemPadding = 16.dp, horizontalItemPadding = 16.dp,
) )

View file

@ -13,12 +13,13 @@ 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.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.tabs.TangemSegmentUM import com.tangem.core.ui.ds.tabs.TangemSegmentUM
import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker
import com.tangem.core.ui.ds.tabs.TangemSegmentedPickerUM import com.tangem.core.ui.ds.tabs.TangemSegmentedPickerUM
import com.tangem.core.ui.ds2.badge.TangemBadge import com.tangem.core.ui.ds2.badge.TangemBadge
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.shimmers.TangemShimmer import com.tangem.core.ui.ds2.shimmers.TangemShimmer
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.extensions.stringResourceSafe import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemTheme
@ -27,13 +28,10 @@ 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.R
import com.tangem.features.foryou.impl.components.MarketChart 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.PortfolioReviewUM import com.tangem.features.foryou.impl.entity.PortfolioReviewUM
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
import kotlinx.collections.immutable.toPersistentList
@Composable @Composable
internal fun ForYouPortfolioReview( internal fun ForYouPortfolioReview(
@ -84,6 +82,18 @@ internal fun ForYouPortfolioReview(
} }
ForYouPortfolioTokenList(tokenList = portfolioReviewUM.tokenList) ForYouPortfolioTokenList(tokenList = portfolioReviewUM.tokenList)
if (portfolioReviewUM is PortfolioReviewUM.Content && portfolioReviewUM.onAddFundsClick != null) {
TangemButton(
text = resourceReference(R.string.common_add_funds),
onClick = portfolioReviewUM.onAddFundsClick,
variant = TangemButton.Variant.Secondary,
size = TangemButton.Size.X9,
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
)
}
} }
} }
@ -117,23 +127,8 @@ private class ForYouPortfolioReviewPreviewProvider : PreviewParameterProvider<Po
override val values: Sequence<PortfolioReviewUM> override val values: Sequence<PortfolioReviewUM>
get() = sequenceOf( get() = sequenceOf(
ForYouPortfolioReviewPreviewData.reviewContent, ForYouPortfolioReviewPreviewData.reviewContent,
PortfolioReviewUM.Loading( ForYouPortfolioReviewPreviewData.loadingState,
marketChartUM = MarketChartUM.NoData, ForYouPortfolioReviewPreviewData.zeroPortfolioState,
tokenList = buildList {
repeat(4) { index ->
add(
ForYouTokenListItemUM(
tokenRowUM = TangemTokenRowUM.Loading(
id = index.toString(),
),
tokenList = persistentListOf(),
isExpanded = false,
isExpandable = false,
),
)
}
}.toPersistentList(),
),
) )
} }
// endregion // endregion

View file

@ -7,7 +7,9 @@ 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.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.resourceReference
import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.foryou.impl.R
import com.tangem.features.foryou.impl.components.state.DonutChartUM 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.DonutSegmentColor
import com.tangem.features.foryou.impl.components.state.DonutSegmentUM import com.tangem.features.foryou.impl.components.state.DonutSegmentUM
@ -16,6 +18,7 @@ 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 kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal import java.math.BigDecimal
internal object ForYouPortfolioReviewPreviewData { internal object ForYouPortfolioReviewPreviewData {
@ -151,5 +154,70 @@ internal object ForYouPortfolioReviewPreviewData {
isExpandable = false, isExpandable = false,
), ),
), ),
onAddFundsClick = null,
)
val loadingState = PortfolioReviewUM.Loading(
marketChartUM = MarketChartUM.NoData(
title = resourceReference(R.string.market_chart_can_not_load_data),
donutText = resourceReference(R.string.market_chart_bubble_no_data),
),
tokenList = buildList {
repeat(5) { index ->
add(
ForYouTokenListItemUM(
tokenRowUM = TangemTokenRowUM.Loading(
id = index.toString(),
),
tokenList = persistentListOf(),
isExpanded = false,
isExpandable = false,
),
)
}
}.toPersistentList(),
)
val zeroPortfolioState = PortfolioReviewUM.Content(
marketChartUM = MarketChartUM.NoData(
title = stringReference("You dont have any tokens with amount"),
donutText = stringReference("No amount on tokens"),
),
tokenList = buildList {
repeat(5) { index ->
add(
ForYouTokenListItemUM(
tokenRowUM = TangemTokenRowUM.Content(
id = "token_$index",
headIconUM = TangemIconUM.Currency(CurrencyIconState.Loading),
titleUM = TangemTokenRowUM.TitleUM.Content(
text = stringReference("Token $index"),
badge = TangemBadgeUM(
text = stringReference("Positive"),
size = TangemBadgeSize.X4,
type = TangemBadgeType.Tinted,
color = TangemBadgeColor.Green,
),
),
subtitleUM = TangemTokenRowUM.SubtitleUM.Content(
text = stringReference("Some network"),
),
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("\$0"),
),
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("0.00%"),
),
onItemClick = {},
onItemLongClick = { _, _ -> },
),
tokenList = persistentListOf(),
isExpanded = false,
isExpandable = false,
),
)
}
}.toPersistentList(),
onAddFundsClick = { },
) )
} }

View file

@ -108,7 +108,7 @@ internal class ForYouModelTest {
val loading = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Loading val loading = model.uiState.value.portfolioReviewUM as PortfolioReviewUM.Loading
assertThat(loading.tokenList).hasSize(4) assertThat(loading.tokenList).hasSize(4)
assertThat(loading.tokenList.all { it.tokenRowUM is TangemTokenRowUM.Loading }).isTrue() assertThat(loading.tokenList.all { it.tokenRowUM is TangemTokenRowUM.Loading }).isTrue()
assertThat(loading.marketChartUM).isEqualTo(MarketChartUM.NoData) assertThat(loading.marketChartUM).isInstanceOf(MarketChartUM.NoData::class.java)
} }
@Test @Test

View file

@ -341,7 +341,7 @@ internal class ForYouPortfolioReviewConverterTest {
val result = createConverter().convert(statusList) as PortfolioReviewUM.Content val result = createConverter().convert(statusList) as PortfolioReviewUM.Content
// Assert // Assert
assertThat(result.marketChartUM).isEqualTo(MarketChartUM.NoData) assertThat(result.marketChartUM).isInstanceOf(MarketChartUM.NoData::class.java)
} }
@Test @Test
@ -350,7 +350,155 @@ internal class ForYouPortfolioReviewConverterTest {
val result = createConverter().convert(null) as PortfolioReviewUM.Content val result = createConverter().convert(null) as PortfolioReviewUM.Content
// Assert // Assert
assertThat(result.marketChartUM).isEqualTo(MarketChartUM.NoData) assertThat(result.marketChartUM).isInstanceOf(MarketChartUM.NoData::class.java)
}
}
@Nested
inner class ZeroBalancePortfolio {
@Test
fun `GIVEN all currencies have zero fiat WHEN convert THEN market chart is the no-amount NoData`() {
// Arrange
val statuses = listOf(
createStatus(
createCoin(rawCurrencyId = "btc", symbol = "BTC", networkId = "bitcoin"),
loadedValue(BigDecimal.ZERO, BigDecimal.ZERO),
),
createStatus(
createCoin(rawCurrencyId = "eth", symbol = "ETH", networkId = "ethereum"),
loadedValue(BigDecimal.ZERO, BigDecimal.ZERO),
),
)
// Act
val result = convert(statuses, totalFiatBalance = BigDecimal.ZERO)
// Assert — the zero-balance treatment, not the generic can-not-load-data chart
assertThat(result.marketChartUM).isEqualTo(
MarketChartUM.NoData(
title = resourceReference(R.string.market_chart_no_amount),
donutText = resourceReference(R.string.market_chart_bubble_no_amount),
),
)
}
@Test
fun `GIVEN all currencies have zero fiat WHEN add funds clicked THEN callback receives the wallet id`() {
// Arrange
val statuses = listOf(
createStatus(
createCoin(rawCurrencyId = "btc", symbol = "BTC", networkId = "bitcoin"),
loadedValue(BigDecimal.ZERO, BigDecimal.ZERO),
),
)
var addFundsWalletId: UserWalletId? = null
val converter = createConverter(onAddFundsClick = { addFundsWalletId = it })
// Act
val result = converter.convert(
accountStatusList(statuses, BigDecimal.ZERO),
) as PortfolioReviewUM.Content
result.onAddFundsClick?.invoke()
// Assert
assertThat(result.onAddFundsClick).isNotNull()
assertThat(addFundsWalletId).isEqualTo(UserWalletId("01"))
}
@Test
fun `GIVEN a non-zero balance WHEN convert THEN add funds action is absent`() {
// Arrange
val statuses = listOf(
createStatus(
createCoin(rawCurrencyId = "btc", symbol = "BTC", networkId = "bitcoin"),
loadedValue(BigDecimal.ONE, BigDecimal("100")),
),
)
// Act
val result = convert(statuses, totalFiatBalance = BigDecimal("100"))
// Assert
assertThat(result.onAddFundsClick).isNull()
}
@Test
fun `GIVEN null account status list WHEN add funds clicked THEN callback is not invoked`() {
// Arrange — without a wallet there is nowhere to add funds, so the click must be a no-op
var clicked = false
val converter = createConverter(onAddFundsClick = { clicked = true })
// Act
val result = converter.convert(null) as PortfolioReviewUM.Content
result.onAddFundsClick?.invoke()
// Assert
assertThat(clicked).isFalse()
}
@Test
fun `GIVEN more than five zero-fiat currencies WHEN convert THEN list is capped with no Other row`() {
// Arrange — 7 distinct zero-balance assets; the zero-balance branch shows the first 5
// as-is instead of ranking and collapsing the excess into an "Other" row
val statuses = (1..7).map { index ->
createStatus(
createCoin(rawCurrencyId = "asset-$index", symbol = "A$index", networkId = "net-$index"),
loadedValue(BigDecimal.ZERO, BigDecimal.ZERO),
)
}
// Act
val result = convert(statuses, totalFiatBalance = BigDecimal.ZERO)
// Assert
assertThat(result.tokenList.map { it.tokenRowUM.id })
.containsExactly("asset-1", "asset-2", "asset-3", "asset-4", "asset-5")
.inOrder()
}
@Test
fun `GIVEN zero-fiat asset on several networks WHEN convert THEN grouped into one expandable item`() {
// Arrange — the same asset (shared rawCurrencyId) with zero balances on two networks
val onEth = createToken(rawCurrencyId = "usdc", symbol = "USDC", networkId = "ethereum")
val onSol = createToken(rawCurrencyId = "usdc", symbol = "USDC", networkId = "solana")
val statuses = listOf(
createStatus(onEth, loadedValue(BigDecimal.ZERO, BigDecimal.ZERO)),
createStatus(onSol, loadedValue(BigDecimal.ZERO, BigDecimal.ZERO)),
)
// Act
val result = convert(statuses, totalFiatBalance = BigDecimal.ZERO)
// Assert
val item = result.tokenList.single()
assertThat(item.tokenRowUM.id).isEqualTo("usdc")
assertThat(item.tokenList).hasSize(2)
assertThat(item.isExpandable).isTrue()
}
@Test
fun `GIVEN zero and null fiat currencies mixed WHEN convert THEN zero-balance treatment is not applied`() {
// Arrange — an unreachable holding has an *unknown* balance, not a resolved zero, so the
// portfolio must not collapse into the add-funds empty state
val statuses = listOf(
createStatus(
createCoin(rawCurrencyId = "eth", symbol = "ETH", networkId = "ethereum"),
loadedValue(BigDecimal.ZERO, BigDecimal.ZERO),
),
createStatus(
createCoin(rawCurrencyId = "btc", symbol = "BTC", networkId = "bitcoin"),
unreachableValue(),
),
)
// Act
val result = convert(statuses, totalFiatBalance = BigDecimal.ZERO)
// Assert — falls through to the ranked branch: no add-funds action, the resolved zero is
// dropped, the unknown-balance holding stays visible
assertThat(result.onAddFundsClick).isNull()
assertThat(result.tokenList.map { it.tokenRowUM.id }).containsExactly("btc")
} }
} }
@ -364,11 +512,13 @@ internal class ForYouPortfolioReviewConverterTest {
expandedAssetIds: Set<String> = emptySet(), expandedAssetIds: Set<String> = emptySet(),
expandClick: (String) -> Unit = {}, expandClick: (String) -> Unit = {},
onTokenClick: (UserWalletId, CryptoCurrency) -> Unit = { _, _ -> }, onTokenClick: (UserWalletId, CryptoCurrency) -> Unit = { _, _ -> },
onAddFundsClick: (UserWalletId) -> Unit = {},
): ForYouPortfolioReviewConverter = ForYouPortfolioReviewConverter( ): ForYouPortfolioReviewConverter = ForYouPortfolioReviewConverter(
appCurrency = appCurrency, appCurrency = appCurrency,
expandedAssetIds = expandedAssetIds, expandedAssetIds = expandedAssetIds,
expandClick = expandClick, expandClick = expandClick,
onTokenClick = onTokenClick, onTokenClick = onTokenClick,
onAddFundsClick = onAddFundsClick,
) )
private fun accountStatusList( private fun accountStatusList(

View file

@ -25,24 +25,24 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
private val appCurrency: AppCurrency = AppCurrency.Default private val appCurrency: AppCurrency = AppCurrency.Default
@Nested @Nested
inner class ConvertNetworkGroup { inner class Convert {
@Test @Test
fun `GIVEN all statuses Loading WHEN convertNetworkGroup THEN row is Loading with representative id`() { fun `GIVEN all statuses Loading WHEN convert THEN row is Loading with representative id`() {
// Arrange // Arrange
val currency = createCurrency(id = "coin-eth", symbol = "ETH") val currency = createCurrency(id = "coin-eth", symbol = "ETH")
val statuses = listOf(createStatus(currency, CryptoCurrencyStatus.Loading)) val statuses = listOf(createStatus(currency, CryptoCurrencyStatus.Loading))
val converter = createConverter(totalFiatBalance = BigDecimal("1000")) val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
// Act // Act
val result = converter.convertNetworkGroup(statuses) val result = converter.convert(statuses)
// Assert // Assert
assertThat(result).isEqualTo(TangemTokenRowUM.Loading(id = "coin-eth")) assertThat(result).isEqualTo(TangemTokenRowUM.Loading(id = "coin-eth"))
} }
@Test @Test
fun `GIVEN single loaded status WHEN convertNetworkGroup THEN row is Content with its amounts`() { fun `GIVEN single loaded status WHEN convert THEN row is Content with its amounts`() {
// Arrange // Arrange
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum") val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
val statuses = listOf( val statuses = listOf(
@ -51,7 +51,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
val converter = createConverter(totalFiatBalance = BigDecimal("1000")) val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
// Act // Act
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content val result = converter.convert(statuses) as TangemTokenRowUM.Content
// Assert // Assert
assertThat(result.id).isEqualTo("coin-eth") assertThat(result.id).isEqualTo("coin-eth")
@ -62,7 +62,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
} }
@Test @Test
fun `GIVEN several statuses of the same asset on one network WHEN convertNetworkGroup THEN amounts are summed`() { fun `GIVEN several statuses of the same asset on one network WHEN convert THEN amounts are summed`() {
// Arrange — same asset held in two accounts on the same network aggregates into one row // Arrange — same asset held in two accounts on the same network aggregates into one row
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum") val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
val statuses = listOf( val statuses = listOf(
@ -72,7 +72,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
val converter = createConverter(totalFiatBalance = BigDecimal("1000")) val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
// Act // Act
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content val result = converter.convert(statuses) as TangemTokenRowUM.Content
// Assert // Assert
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content
@ -80,7 +80,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
} }
@Test @Test
fun `GIVEN mixed Loading and Loaded statuses WHEN convertNetworkGroup THEN row is Content`() { fun `GIVEN mixed Loading and Loaded statuses WHEN convert THEN row is Content`() {
// Arrange — not *all* statuses are Loading, so it should not collapse to a Loading row // Arrange — not *all* statuses are Loading, so it should not collapse to a Loading row
val currency = createCurrency(id = "coin-eth", symbol = "ETH") val currency = createCurrency(id = "coin-eth", symbol = "ETH")
val statuses = listOf( val statuses = listOf(
@ -90,14 +90,14 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
val converter = createConverter(totalFiatBalance = BigDecimal("1000")) val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
// Act // Act
val result = converter.convertNetworkGroup(statuses) val result = converter.convert(statuses)
// Assert // Assert
assertThat(result).isInstanceOf(TangemTokenRowUM.Content::class.java) assertThat(result).isInstanceOf(TangemTokenRowUM.Content::class.java)
} }
@Test @Test
fun `GIVEN loaded status from cache WHEN convertNetworkGroup THEN content flickers`() { fun `GIVEN loaded status from cache WHEN convert THEN content flickers`() {
// Arrange // Arrange
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum") val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
val statuses = listOf( val statuses = listOf(
@ -109,7 +109,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
val converter = createConverter(totalFiatBalance = BigDecimal("1000")) val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
// Act // Act
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content val result = converter.convert(statuses) as TangemTokenRowUM.Content
// Assert // Assert
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content
@ -120,7 +120,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
} }
@Test @Test
fun `GIVEN loaded status only-cache WHEN convertNetworkGroup THEN error-sync start icon shown`() { fun `GIVEN loaded status only-cache WHEN convert THEN error-sync start icon shown`() {
// Arrange // Arrange
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum") val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
val statuses = listOf( val statuses = listOf(
@ -136,7 +136,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
val converter = createConverter(totalFiatBalance = BigDecimal("1000")) val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
// Act // Act
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content val result = converter.convert(statuses) as TangemTokenRowUM.Content
// Assert // Assert
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content
@ -145,14 +145,14 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
} }
@Test @Test
fun `GIVEN missed derivation status WHEN convertNetworkGroup THEN no-address treatment`() { fun `GIVEN missed derivation status WHEN convert THEN no-address treatment`() {
// Arrange // Arrange
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum") val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
val statuses = listOf(createStatus(currency, missedDerivationValue())) val statuses = listOf(createStatus(currency, missedDerivationValue()))
val converter = createConverter(totalFiatBalance = BigDecimal("1000")) val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
// Act // Act
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content val result = converter.convert(statuses) as TangemTokenRowUM.Content
// Assert — top-end is a dash, bottom-end carries the attention "no address" icon // Assert — top-end is a dash, bottom-end carries the attention "no address" icon
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content
@ -162,14 +162,14 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
} }
@Test @Test
fun `GIVEN unreachable status WHEN convertNetworkGroup THEN dash on top and attention icon on bottom`() { fun `GIVEN unreachable status WHEN convert THEN dash on top and attention icon on bottom`() {
// Arrange // Arrange
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum") val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
val statuses = listOf(createStatus(currency, unreachableValue())) val statuses = listOf(createStatus(currency, unreachableValue()))
val converter = createConverter(totalFiatBalance = BigDecimal("1000")) val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
// Act // Act
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content val result = converter.convert(statuses) as TangemTokenRowUM.Content
// Assert — top-end is a bare dash, the attention "unreachable" icon lives on the bottom end // Assert — top-end is a bare dash, the attention "unreachable" icon lives on the bottom end
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content
@ -179,7 +179,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
} }
@Test @Test
fun `GIVEN mixed Loaded and Unreachable WHEN convertNetworkGroup THEN collapses to unreachable`() { fun `GIVEN mixed Loaded and Unreachable WHEN convert THEN collapses to unreachable`() {
// Arrange — one account resolved, another unreachable: the row must surface the error state // Arrange — one account resolved, another unreachable: the row must surface the error state
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum") val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
val statuses = listOf( val statuses = listOf(
@ -189,7 +189,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
val converter = createConverter(totalFiatBalance = BigDecimal("1000")) val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
// Act // Act
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content val result = converter.convert(statuses) as TangemTokenRowUM.Content
// Assert — the unreachable treatment (attention icon on the bottom end) wins over the loaded amount // Assert — the unreachable treatment (attention icon on the bottom end) wins over the loaded amount
val bottomEnd = result.bottomEndContentUM as TangemTokenRowUM.EndContentUM.Content val bottomEnd = result.bottomEndContentUM as TangemTokenRowUM.EndContentUM.Content
@ -212,7 +212,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
) )
// Act // Act
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content val result = converter.convert(statuses) as TangemTokenRowUM.Content
result.onItemClick?.invoke() result.onItemClick?.invoke()
// Assert // Assert
@ -234,7 +234,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
) )
// Act // Act
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content val result = converter.convert(statuses) as TangemTokenRowUM.Content
result.onItemClick?.invoke() result.onItemClick?.invoke()
// Assert // Assert
@ -242,7 +242,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
} }
@Test @Test
fun `GIVEN mixed MissedDerivation and Unreachable WHEN convertNetworkGroup THEN missed-derivation wins`() { fun `GIVEN mixed MissedDerivation and Unreachable WHEN convert THEN missed-derivation wins`() {
// Arrange — missed derivation is the most severe terminal state and dominates // Arrange — missed derivation is the most severe terminal state and dominates
val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum") val currency = createCurrency(id = "coin-eth", symbol = "ETH", networkName = "Ethereum")
val statuses = listOf( val statuses = listOf(
@ -252,7 +252,7 @@ internal class ForYouPortfolioReviewTokenRowConverterTest {
val converter = createConverter(totalFiatBalance = BigDecimal("1000")) val converter = createConverter(totalFiatBalance = BigDecimal("1000"))
// Act // Act
val result = converter.convertNetworkGroup(statuses) as TangemTokenRowUM.Content val result = converter.convert(statuses) as TangemTokenRowUM.Content
// Assert — top-end is a dash (no-address treatment), not an unreachable label // Assert — top-end is a dash (no-address treatment), not an unreachable label
val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content val topEnd = result.topEndContentUM as TangemTokenRowUM.EndContentUM.Content

View file

@ -137,7 +137,8 @@ internal class SetPortfolioReviewTransformerTest {
private fun contentPortfolioReview(): PortfolioReviewUM.Content = PortfolioReviewUM.Content( private fun contentPortfolioReview(): PortfolioReviewUM.Content = PortfolioReviewUM.Content(
tokenList = persistentListOf(), tokenList = persistentListOf(),
marketChartUM = MarketChartUM.NoData, marketChartUM = noDataChart(),
onAddFundsClick = null,
) )
private fun contentEarnOpportunities(): EarnOpportunitiesUM.Content = EarnOpportunitiesUM.Content( private fun contentEarnOpportunities(): EarnOpportunitiesUM.Content = EarnOpportunitiesUM.Content(
@ -148,6 +149,11 @@ internal class SetPortfolioReviewTransformerTest {
onAllEarnTokensClick = {}, onAllEarnTokensClick = {},
) )
private fun noDataChart(): MarketChartUM.NoData = MarketChartUM.NoData(
title = stringReference("No data"),
donutText = stringReference("No data"),
)
private fun accountStatusList(totalFiatBalance: TotalFiatBalance): AccountStatusList = mockk { private fun accountStatusList(totalFiatBalance: TotalFiatBalance): AccountStatusList = mockk {
every { this@mockk.totalFiatBalance } returns totalFiatBalance every { this@mockk.totalFiatBalance } returns totalFiatBalance
} }
@ -158,7 +164,7 @@ internal class SetPortfolioReviewTransformerTest {
private fun loadingState(): ForYouUM = ForYouUM( private fun loadingState(): ForYouUM = ForYouUM(
portfolioReviewUM = PortfolioReviewUM.Loading( portfolioReviewUM = PortfolioReviewUM.Loading(
tokenList = persistentListOf(), tokenList = persistentListOf(),
marketChartUM = MarketChartUM.NoData, marketChartUM = noDataChart(),
), ),
earnOpportunities = EarnOpportunitiesUM.Loading(tokenList = persistentListOf()), earnOpportunities = EarnOpportunitiesUM.Loading(tokenList = persistentListOf()),
notifications = persistentListOf(), notifications = persistentListOf(),