Updated on 2026-08-14
This commit is contained in:
parent
7e51309355
commit
f86441df6a
15 changed files with 343 additions and 160 deletions
|
|
@ -39,6 +39,8 @@ dependencies {
|
||||||
api(projects.domain.appCurrency)
|
api(projects.domain.appCurrency)
|
||||||
api(projects.domain.common)
|
api(projects.domain.common)
|
||||||
api(projects.domain.wallets)
|
api(projects.domain.wallets)
|
||||||
|
api(projects.domain.earn)
|
||||||
|
api(projects.domain.yieldSupply)
|
||||||
implementation(projects.domain.account)
|
implementation(projects.domain.account)
|
||||||
implementation(projects.domain.models)
|
implementation(projects.domain.models)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ internal data class ForYouUM(
|
||||||
val portfolioReviewUM: PortfolioReviewUM,
|
val portfolioReviewUM: PortfolioReviewUM,
|
||||||
val earnOpportunities: EarnOpportunitiesUM,
|
val earnOpportunities: EarnOpportunitiesUM,
|
||||||
val notifications: ImmutableList<ForYouNotification>,
|
val notifications: ImmutableList<ForYouNotification>,
|
||||||
|
val periodPickerUM: TangemSegmentedPickerUM,
|
||||||
|
val onPeriodClick: (tangemSegmentUM: TangemSegmentUM) -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|
@ -29,20 +31,32 @@ 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 periodPickerUM: TangemSegmentedPickerUM,
|
|
||||||
val onPeriodClick: (TangemSegmentUM) -> Unit,
|
|
||||||
) : PortfolioReviewUM
|
) : PortfolioReviewUM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State of the earn-opportunities section. [tokenList] holds either the user's earn-eligible holdings
|
||||||
|
* or top-earn suggestions, depending on which content state was picked (see
|
||||||
|
* `ForYouEarnOpportunitiesConverter` for the selection rules).
|
||||||
|
*/
|
||||||
@Immutable
|
@Immutable
|
||||||
internal sealed interface EarnOpportunitiesUM {
|
internal sealed interface EarnOpportunitiesUM {
|
||||||
|
|
||||||
val tokenList: ImmutableList<ForYouTokenListItemUM>
|
val tokenList: ImmutableList<ForYouTokenListItemUM>
|
||||||
|
|
||||||
|
/** Skeleton rows shown until the first real emission. */
|
||||||
data class Loading(
|
data class Loading(
|
||||||
override val tokenList: ImmutableList<ForYouTokenListItemUM>,
|
override val tokenList: ImmutableList<ForYouTokenListItemUM>,
|
||||||
) : EarnOpportunitiesUM
|
) : EarnOpportunitiesUM
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property subtitleRes section subtitle matching the picked state (nothing eligible / all active /
|
||||||
|
* potential rewards)
|
||||||
|
* @property potentialReward header value: the total projected yearly reward for eligible holdings,
|
||||||
|
* or the best suggestion's rate when the user holds nothing eligible; `null` when not applicable
|
||||||
|
* @property potentialRewardType label of [potentialReward]'s rate kind (APR/APY); only set alongside
|
||||||
|
* a rate-based reward
|
||||||
|
*/
|
||||||
data class Content(
|
data class Content(
|
||||||
override val tokenList: ImmutableList<ForYouTokenListItemUM>,
|
override val tokenList: ImmutableList<ForYouTokenListItemUM>,
|
||||||
@param:StringRes val subtitleRes: Int,
|
@param:StringRes val subtitleRes: Int,
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,39 @@ 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 arrow.core.left
|
||||||
|
import arrow.core.right
|
||||||
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.decompose.model.ParamsContainer
|
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.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.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.earn.EarnErrorResolver
|
||||||
|
import com.tangem.domain.earn.model.EarnTokensBatchingContext
|
||||||
|
import com.tangem.domain.earn.model.EarnTokensListConfig
|
||||||
|
import com.tangem.domain.earn.usecase.GetEarnTokensBatchFlowUseCase
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.domain.models.currency.CryptoCurrency
|
||||||
|
import com.tangem.domain.models.earn.EarnTopToken
|
||||||
import com.tangem.domain.models.wallet.UserWalletId
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase
|
||||||
|
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase
|
||||||
import com.tangem.features.foryou.ForYouComponent
|
import com.tangem.features.foryou.ForYouComponent
|
||||||
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.EarnOpportunitiesUM
|
import com.tangem.features.foryou.impl.entity.*
|
||||||
import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM
|
import com.tangem.features.foryou.impl.model.converter.TOP_EARN_TOKENS_BATCH_SIZE
|
||||||
import com.tangem.features.foryou.impl.entity.ForYouUM
|
import com.tangem.features.foryou.impl.model.converter.earnOpportunities.ForYouEarnOpportunitiesConverter
|
||||||
import com.tangem.features.foryou.impl.entity.PortfolioReviewUM
|
import com.tangem.features.foryou.impl.model.converter.portfolioReview.ForYouPortfolioReviewConverter
|
||||||
import com.tangem.features.foryou.impl.model.transformer.SetPortfolioReviewTransformer
|
import com.tangem.features.foryou.impl.model.transformer.SetPortfolioReviewTransformer
|
||||||
|
import com.tangem.pagination.BatchAction
|
||||||
|
import com.tangem.pagination.PaginationStatus
|
||||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||||
|
import com.tangem.utils.coroutines.combine6
|
||||||
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
|
||||||
|
|
@ -29,26 +43,34 @@ import javax.inject.Inject
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
@ModelScoped
|
@ModelScoped
|
||||||
|
@Suppress("LongParameterList")
|
||||||
internal class ForYouModel @Inject constructor(
|
internal class ForYouModel @Inject constructor(
|
||||||
paramsContainer: ParamsContainer,
|
paramsContainer: ParamsContainer,
|
||||||
userWalletsListRepository: UserWalletsListRepository,
|
userWalletsListRepository: UserWalletsListRepository,
|
||||||
multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
||||||
|
yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
|
||||||
override val dispatchers: CoroutineDispatcherProvider,
|
override val dispatchers: CoroutineDispatcherProvider,
|
||||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||||
|
private val getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase,
|
||||||
|
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
|
||||||
|
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||||
|
private val earnErrorResolver: EarnErrorResolver,
|
||||||
) : Model() {
|
) : Model() {
|
||||||
|
|
||||||
private val params = paramsContainer.require<ForYouComponent.Params>()
|
private val params = paramsContainer.require<ForYouComponent.Params>()
|
||||||
|
|
||||||
private val expandedAssetIds = MutableStateFlow<Set<String>>(value = emptySet())
|
private val expandedPortfolioReviewAssetIds = MutableStateFlow<Set<String>>(value = emptySet())
|
||||||
|
private val expandedEarnOpportunitiesAssetIds = 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(
|
||||||
notifications = persistentListOf(),
|
notifications = persistentListOf(),
|
||||||
|
periodPickerUM = TangemSegmentedPickerUM(persistentListOf()),
|
||||||
earnOpportunities = EarnOpportunitiesUM.Loading(
|
earnOpportunities = EarnOpportunitiesUM.Loading(
|
||||||
tokenList = buildList<ForYouTokenListItemUM> {
|
tokenList = buildList<ForYouTokenListItemUM> {
|
||||||
repeat(4) { index ->
|
repeat(5) { index ->
|
||||||
add(
|
add(
|
||||||
ForYouTokenListItemUM(
|
ForYouTokenListItemUM(
|
||||||
tokenRowUM = TangemTokenRowUM.Loading(
|
tokenRowUM = TangemTokenRowUM.Loading(
|
||||||
|
|
@ -62,6 +84,7 @@ internal class ForYouModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}.toPersistentList(),
|
}.toPersistentList(),
|
||||||
),
|
),
|
||||||
|
onPeriodClick = ::onPeriodClick,
|
||||||
portfolioReviewUM = PortfolioReviewUM.Loading(
|
portfolioReviewUM = PortfolioReviewUM.Loading(
|
||||||
marketChartUM = MarketChartUM.NoData,
|
marketChartUM = MarketChartUM.NoData,
|
||||||
tokenList = buildList<ForYouTokenListItemUM> {
|
tokenList = buildList<ForYouTokenListItemUM> {
|
||||||
|
|
@ -83,21 +106,51 @@ internal class ForYouModel @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
combine(
|
combine6(
|
||||||
flow = userWalletsListRepository.selectedUserWallet,
|
flow1 = userWalletsListRepository.selectedUserWallet,
|
||||||
flow2 = multiAccountStatusListSupplier.invokeAsMap(),
|
flow2 = multiAccountStatusListSupplier.invokeAsMap(),
|
||||||
flow3 = expandedAssetIds,
|
flow3 = expandedPortfolioReviewAssetIds,
|
||||||
) { globalSelectedWallet, accountStatusList, expanded ->
|
flow4 = expandedEarnOpportunitiesAssetIds,
|
||||||
|
flow5 = yieldSupplyApyFlowUseCase(),
|
||||||
|
flow6 = createTopEarnTokensFlow(),
|
||||||
|
) {
|
||||||
|
globalSelectedWallet, accountList,
|
||||||
|
expandedPortfolioReview, expandedEarnOpportunities,
|
||||||
|
yieldAvailability, topEarnTokens,
|
||||||
|
->
|
||||||
|
|
||||||
|
val stakingAvailability = accountList.flatMap { (userWalletId, accountStatusList) ->
|
||||||
|
stakingAvailabilityListUseCase.invokeSync(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
cryptoCurrencyList = accountStatusList.flattenCurrencies().map { it.currency },
|
||||||
|
).entries
|
||||||
|
}.associate { it.key to it.value }
|
||||||
|
|
||||||
// TODO For You add choose portfolio flow
|
// TODO For You add choose portfolio flow
|
||||||
val selectedWalletId = globalSelectedWallet?.walletId
|
val accountStatusList = accountList[globalSelectedWallet?.walletId]
|
||||||
|
|
||||||
|
val portfolioReviewUM = ForYouPortfolioReviewConverter(
|
||||||
|
appCurrency = selectedAppCurrencyFlow.value,
|
||||||
|
expandedAssetIds = expandedPortfolioReview,
|
||||||
|
expandClick = ::onExpandPortfolioReviewClick,
|
||||||
|
onTokenClick = ::onPortfolioReviewTokenClick,
|
||||||
|
).convert(accountStatusList)
|
||||||
|
|
||||||
|
val earnOpportunitiesUM = ForYouEarnOpportunitiesConverter(
|
||||||
|
appCurrency = selectedAppCurrencyFlow.value,
|
||||||
|
isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync(),
|
||||||
|
yieldSupplyAvailability = yieldAvailability,
|
||||||
|
yieldStakingAvailability = stakingAvailability,
|
||||||
|
topEarnTokens = topEarnTokens,
|
||||||
|
expandedAssetIds = expandedEarnOpportunities,
|
||||||
|
expandClick = ::onExpandEarnOpportunitiesClick,
|
||||||
|
).convert(accountStatusList)
|
||||||
|
|
||||||
uiState.update(
|
uiState.update(
|
||||||
SetPortfolioReviewTransformer(
|
SetPortfolioReviewTransformer(
|
||||||
accountStatusList = accountStatusList[selectedWalletId],
|
accountStatusList = accountStatusList,
|
||||||
appCurrency = selectedAppCurrencyFlow.value,
|
portfolioReviewUM = portfolioReviewUM,
|
||||||
expandedAssetIds = expanded,
|
earnOpportunitiesUM = earnOpportunitiesUM,
|
||||||
expandClick = ::onExpandClick,
|
|
||||||
onPeriodClick = ::onPeriodClick,
|
|
||||||
onTokenClick = { currency -> onTokenClick(selectedWalletId, currency) },
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -105,6 +158,40 @@ internal class ForYouModel @Inject constructor(
|
||||||
.launchIn(modelScope)
|
.launchIn(modelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the top-earn suggestions as a single batch of [TOP_EARN_TOKENS_BATCH_SIZE] tokens (a
|
||||||
|
* one-shot [BatchAction.Reload]; no further paging). Emits `null` while the initial load is in
|
||||||
|
* flight, a resolved error on failure, and the flattened token list on success — so the earn
|
||||||
|
* section can distinguish "not loaded yet" from "loaded empty".
|
||||||
|
*/
|
||||||
|
private fun createTopEarnTokensFlow(): Flow<EarnTopToken?> {
|
||||||
|
val actionsFlow = MutableSharedFlow<BatchAction<Int, EarnTokensListConfig, Nothing>>(replay = 1)
|
||||||
|
|
||||||
|
val batchFlow = getEarnTokensBatchFlowUseCase(
|
||||||
|
context = EarnTokensBatchingContext(
|
||||||
|
actionsFlow = actionsFlow,
|
||||||
|
coroutineScope = modelScope,
|
||||||
|
),
|
||||||
|
batchSize = TOP_EARN_TOKENS_BATCH_SIZE,
|
||||||
|
)
|
||||||
|
|
||||||
|
actionsFlow.tryEmit(
|
||||||
|
BatchAction.Reload(
|
||||||
|
requestParams = EarnTokensListConfig(type = null, networks = null, isForEarn = false),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return batchFlow.state.map { state ->
|
||||||
|
when (val status = state.status) {
|
||||||
|
is PaginationStatus.None,
|
||||||
|
is PaginationStatus.InitialLoading,
|
||||||
|
-> null
|
||||||
|
is PaginationStatus.InitialLoadingError -> earnErrorResolver.resolve(status.throwable).left()
|
||||||
|
else -> state.data.flatMap { batch -> batch.data }.right()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
private fun createSelectedAppCurrencyFlow(): StateFlow<AppCurrency> {
|
||||||
return getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
return getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
||||||
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
maybeAppCurrency.getOrElse { AppCurrency.Default }
|
||||||
|
|
@ -115,13 +202,19 @@ internal class ForYouModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onTokenClick(selectedWalletId: UserWalletId?, currency: CryptoCurrency) {
|
private fun onPortfolioReviewTokenClick(selectedWalletId: UserWalletId?, currency: CryptoCurrency) {
|
||||||
val walletId = selectedWalletId ?: return
|
val walletId = selectedWalletId ?: return
|
||||||
params.callbacks.onTokenClick(walletId, currency)
|
params.callbacks.onTokenClick(walletId, currency)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onExpandClick(assetId: String) {
|
private fun onExpandPortfolioReviewClick(assetId: String) {
|
||||||
expandedAssetIds.update { ids ->
|
expandedPortfolioReviewAssetIds.update { ids ->
|
||||||
|
if (assetId in ids) ids - assetId else ids + assetId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onExpandEarnOpportunitiesClick(assetId: String) {
|
||||||
|
expandedEarnOpportunitiesAssetIds.update { ids ->
|
||||||
if (assetId in ids) ids - assetId else ids + assetId
|
if (assetId in ids) ids - assetId else ids + assetId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -129,11 +222,9 @@ internal class ForYouModel @Inject constructor(
|
||||||
private fun onPeriodClick(tangemSegmentUM: TangemSegmentUM) {
|
private fun onPeriodClick(tangemSegmentUM: TangemSegmentUM) {
|
||||||
uiState.update { state ->
|
uiState.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
portfolioReviewUM = (state.portfolioReviewUM as? PortfolioReviewUM.Content)?.copy(
|
periodPickerUM = state.periodPickerUM.copy(
|
||||||
periodPickerUM = state.portfolioReviewUM.periodPickerUM.copy(
|
initialSelectedItem = tangemSegmentUM,
|
||||||
initialSelectedItem = tangemSegmentUM,
|
),
|
||||||
),
|
|
||||||
) ?: state.portfolioReviewUM,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,11 @@ import java.math.RoundingMode
|
||||||
/** Number of suggested top-earn tokens shown in the earn-opportunities block. */
|
/** Number of suggested top-earn tokens shown in the earn-opportunities block. */
|
||||||
internal const val FOR_YOU_TOP_EARN_TOKENS_COUNT = 5
|
internal const val FOR_YOU_TOP_EARN_TOKENS_COUNT = 5
|
||||||
|
|
||||||
/** Batch size for getting top earn tokens in single and only page */
|
/**
|
||||||
|
* Batch size for the top-earn-tokens request. Only the first batch is ever fetched (the section shows
|
||||||
|
* at most [FOR_YOU_TOP_EARN_TOKENS_COUNT] rows), but it is requested larger so that filtering out
|
||||||
|
* already-active tokens still leaves enough candidates to fill the list.
|
||||||
|
*/
|
||||||
internal const val TOP_EARN_TOKENS_BATCH_SIZE = 30
|
internal const val TOP_EARN_TOKENS_BATCH_SIZE = 30
|
||||||
|
|
||||||
/** Divisor converting backend percent values (5.5) to fractions (0.055). */
|
/** Divisor converting backend percent values (5.5) to fractions (0.055). */
|
||||||
|
|
@ -68,11 +72,11 @@ internal data class EarnApyInfo(
|
||||||
/**
|
/**
|
||||||
* Earn-eligible currencies of one account with their resolved rates.
|
* Earn-eligible currencies of one account with their resolved rates.
|
||||||
*
|
*
|
||||||
* @property accountPotentialReward sum of [EarnApyInfo.potentialRewards] over [earnCurrencues];
|
* @property accountPotentialReward sum of [EarnApyInfo.potentialRewards] over [earnCurrencies];
|
||||||
* accounts are ordered by it, descending
|
* accounts are ordered by it, descending
|
||||||
*/
|
*/
|
||||||
internal data class EarnOpportunities(
|
internal data class EarnOpportunities(
|
||||||
val account: Account.CryptoPortfolio,
|
val account: Account.CryptoPortfolio,
|
||||||
val earnCurrencues: Map<CryptoCurrencyStatus, EarnApyInfo>,
|
val earnCurrencies: Map<CryptoCurrencyStatus, EarnApyInfo>,
|
||||||
val accountPotentialReward: BigDecimal,
|
val accountPotentialReward: BigDecimal,
|
||||||
)
|
)
|
||||||
|
|
@ -75,7 +75,7 @@ internal class ForYouEarnOpportunitiesConverter(
|
||||||
|
|
||||||
EarnOpportunities(
|
EarnOpportunities(
|
||||||
account = cryptoAccountStatus.account,
|
account = cryptoAccountStatus.account,
|
||||||
earnCurrencues = tokenList.toMap(),
|
earnCurrencies = tokenList.toMap(),
|
||||||
accountPotentialReward = accountPotentialReward,
|
accountPotentialReward = accountPotentialReward,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -84,10 +84,14 @@ internal class ForYouEarnOpportunitiesConverter(
|
||||||
|
|
||||||
return when {
|
return when {
|
||||||
data.isEmpty() -> {
|
data.isEmpty() -> {
|
||||||
ForYouEarnOpportunitiesNoTokensConverter(topEarnTokens).convert(data)
|
ForYouEarnOpportunitiesNoTokensConverter(
|
||||||
|
topEarnTokens = topEarnTokens,
|
||||||
|
).convert(data)
|
||||||
}
|
}
|
||||||
data.all { earn -> earn.earnCurrencues.all { entry -> entry.value.isActive } } -> {
|
data.all { earn -> earn.earnCurrencies.all { entry -> entry.value.isActive } } -> {
|
||||||
ForYouEarnOpportunitiesTokensActiveConverter(topEarnTokens).convert(data)
|
ForYouEarnOpportunitiesTokensActiveConverter(
|
||||||
|
topEarnTokens = topEarnTokens,
|
||||||
|
).convert(data)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
ForYouEarnOpportunitiesPotentialRewardsConverter(
|
ForYouEarnOpportunitiesPotentialRewardsConverter(
|
||||||
|
|
@ -134,7 +138,7 @@ internal class ForYouEarnOpportunitiesConverter(
|
||||||
currencyStatus = cryptoCurrencyStatus,
|
currencyStatus = cryptoCurrencyStatus,
|
||||||
stakingApyMap = stakingApyMap,
|
stakingApyMap = stakingApyMap,
|
||||||
)
|
)
|
||||||
if (stakingInfo.rate != null) {
|
if (stakingInfo != null) {
|
||||||
return EarnApyInfo(
|
return EarnApyInfo(
|
||||||
isActive = stakingInfo.isActive,
|
isActive = stakingInfo.isActive,
|
||||||
apy = stakingInfo.rate,
|
apy = stakingInfo.rate,
|
||||||
|
|
@ -154,10 +158,9 @@ internal class ForYouEarnOpportunitiesConverter(
|
||||||
private fun findStakingRate(
|
private fun findStakingRate(
|
||||||
currencyStatus: CryptoCurrencyStatus,
|
currencyStatus: CryptoCurrencyStatus,
|
||||||
stakingApyMap: Map<CryptoCurrency, StakingAvailability>,
|
stakingApyMap: Map<CryptoCurrency, StakingAvailability>,
|
||||||
): StakingLocalInfo {
|
): StakingLocalInfo? {
|
||||||
val availability = stakingApyMap[currencyStatus.currency]
|
val availability = stakingApyMap[currencyStatus.currency]
|
||||||
val option = availability?.optionOrNull
|
val option = availability?.optionOrNull ?: return null
|
||||||
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
|
||||||
|
|
||||||
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
|
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
|
||||||
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
|
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
|
||||||
|
|
@ -166,7 +169,7 @@ internal class ForYouEarnOpportunitiesConverter(
|
||||||
|
|
||||||
// Full = no free capacity: show the badge only for tokens that already have a stake.
|
// Full = no free capacity: show the badge only for tokens that already have a stake.
|
||||||
if (availability is StakingAvailability.Full && !isActive) {
|
if (availability is StakingAvailability.Full && !isActive) {
|
||||||
return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
val rateInfo = when (option) {
|
val rateInfo = when (option) {
|
||||||
|
|
@ -196,18 +199,18 @@ internal class ForYouEarnOpportunitiesConverter(
|
||||||
}
|
}
|
||||||
.maxByOrNull { it.rate }
|
.maxByOrNull { it.rate }
|
||||||
}
|
}
|
||||||
}
|
} ?: return null
|
||||||
|
|
||||||
return StakingLocalInfo(
|
return StakingLocalInfo(
|
||||||
rate = rateInfo?.rate,
|
rate = rateInfo.rate,
|
||||||
isActive = isActive,
|
isActive = isActive,
|
||||||
rewardType = rateInfo?.type,
|
rewardType = rateInfo.type,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class StakingLocalInfo(
|
private data class StakingLocalInfo(
|
||||||
val rate: BigDecimal?,
|
val rate: BigDecimal,
|
||||||
val isActive: Boolean,
|
val isActive: Boolean,
|
||||||
val rewardType: RewardType?,
|
val rewardType: RewardType,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -20,6 +20,15 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Earn-opportunities state for a portfolio with tokens that could earn but don't yet: renders the
|
||||||
|
* user's earn-eligible holdings with their projected yearly rewards, headed by the total across
|
||||||
|
* accounts ([EarnOpportunitiesUM.Content.potentialReward]).
|
||||||
|
*
|
||||||
|
* With accounts mode on, each account becomes one expandable row (children delegated to
|
||||||
|
* [ForYouEarnOpportunitiesTokenRowConverter], expansion keyed by account id); with it off, the
|
||||||
|
* tokens are rendered as flat non-expandable rows.
|
||||||
|
*/
|
||||||
internal class ForYouEarnOpportunitiesPotentialRewardsConverter(
|
internal class ForYouEarnOpportunitiesPotentialRewardsConverter(
|
||||||
private val appCurrency: AppCurrency,
|
private val appCurrency: AppCurrency,
|
||||||
private val isAccountsModeEnabled: Boolean,
|
private val isAccountsModeEnabled: Boolean,
|
||||||
|
|
@ -51,16 +60,16 @@ internal class ForYouEarnOpportunitiesPotentialRewardsConverter(
|
||||||
tokenRowUM = createAssetRow(
|
tokenRowUM = createAssetRow(
|
||||||
account = earnData.account,
|
account = earnData.account,
|
||||||
potentialReward = earnData.accountPotentialReward,
|
potentialReward = earnData.accountPotentialReward,
|
||||||
tokenCount = earnData.earnCurrencues.size,
|
tokenCount = earnData.earnCurrencies.size,
|
||||||
),
|
),
|
||||||
tokenList = rowConverter.convertList(earnData.earnCurrencues.toList())
|
tokenList = rowConverter.convertList(earnData.earnCurrencies.toList())
|
||||||
.toPersistentList(),
|
.toPersistentList(),
|
||||||
isExpanded = earnData.account.accountId.value in expandedAssetIds,
|
isExpanded = earnData.account.accountId.value in expandedAssetIds,
|
||||||
isExpandable = true,
|
isExpandable = true,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
earnData.earnCurrencues.map { token ->
|
earnData.earnCurrencies.map { token ->
|
||||||
ForYouTokenListItemUM(
|
ForYouTokenListItemUM(
|
||||||
tokenRowUM = rowConverter.convert(token.toPair()),
|
tokenRowUM = rowConverter.convert(token.toPair()),
|
||||||
tokenList = persistentListOf(),
|
tokenList = persistentListOf(),
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,13 @@ import com.tangem.utils.extensions.orZero
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps one earn-eligible portfolio holding to a token row: network subtitle, projected yearly earn
|
||||||
|
* (`fiat balance * rate`) as the top end and the rate itself as the styled bottom end.
|
||||||
|
*
|
||||||
|
* Non-resolved statuses degrade the same way as in the portfolio review: loading → skeleton row,
|
||||||
|
* no-quote / no-address / unreachable → dashes, stale cache → error-sync icon on both ends.
|
||||||
|
*/
|
||||||
internal class ForYouEarnOpportunitiesTokenRowConverter(
|
internal class ForYouEarnOpportunitiesTokenRowConverter(
|
||||||
private val appCurrency: AppCurrency,
|
private val appCurrency: AppCurrency,
|
||||||
) : Converter<Pair<CryptoCurrencyStatus, EarnApyInfo>, TangemTokenRowUM> {
|
) : Converter<Pair<CryptoCurrencyStatus, EarnApyInfo>, TangemTokenRowUM> {
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ internal class ForYouEarnOpportunitiesTokensActiveConverter(
|
||||||
|
|
||||||
override fun convert(value: List<EarnOpportunities>): EarnOpportunitiesUM {
|
override fun convert(value: List<EarnOpportunities>): EarnOpportunitiesUM {
|
||||||
val activeAssetKeys = value
|
val activeAssetKeys = value
|
||||||
.flatMap { opportunities -> opportunities.earnCurrencues.keys }
|
.flatMap { opportunities -> opportunities.earnCurrencies.keys }
|
||||||
.map { status -> status.currency.forYouEarnAssetKey() }
|
.map { status -> status.currency.forYouEarnAssetKey() }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.tangem.features.foryou.impl.model.converter
|
package com.tangem.features.foryou.impl.model.converter.portfolioReview
|
||||||
|
|
||||||
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.components.currency.icon.CurrencyIconState
|
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||||
|
|
@ -8,14 +8,21 @@ import com.tangem.core.ui.extensions.*
|
||||||
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
|
||||||
import com.tangem.core.ui.format.bigdecimal.percent
|
import com.tangem.core.ui.format.bigdecimal.percent
|
||||||
|
import com.tangem.domain.account.models.AccountStatusList
|
||||||
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.currency.CryptoCurrency
|
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.features.foryou.impl.R
|
import com.tangem.features.foryou.impl.R
|
||||||
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.model.converter.forYouGroupKey
|
||||||
|
import com.tangem.features.foryou.impl.model.converter.forYouPlaceholderBadge
|
||||||
|
import com.tangem.features.foryou.impl.model.converter.toForYouPercent
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
|
import com.tangem.utils.extensions.isZero
|
||||||
import com.tangem.utils.extensions.orZero
|
import com.tangem.utils.extensions.orZero
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
@ -25,41 +32,78 @@ import java.math.BigDecimal
|
||||||
* (see [forYouGroupKey]) and maps each group to a [ForYouTokenListItemUM] — an aggregate asset row plus,
|
* (see [forYouGroupKey]) and maps each group to a [ForYouTokenListItemUM] — an aggregate asset row plus,
|
||||||
* when the asset spans more than one network, its per-network child rows.
|
* when the asset spans more than one network, its per-network child rows.
|
||||||
*
|
*
|
||||||
* The child rows are grouped by network (delegated to [ForYouTokenRowConverter]) so a network appears
|
* The child rows are grouped by network (delegated to [ForYouPortfolioReviewTokenRowConverter]) so a network appears
|
||||||
* once per asset even if the asset is held on it in several accounts;
|
* once per asset even if the asset is held on it in several accounts;
|
||||||
*
|
*
|
||||||
* Modelled on `TokenListStateConverter` (a list converter delegating to a per-item converter).
|
* Modelled on `TokenListStateConverter` (a list converter delegating to a per-item converter).
|
||||||
*/
|
*/
|
||||||
internal class ForYouTokenListConverter(
|
internal class ForYouPortfolioReviewConverter(
|
||||||
private val appCurrency: AppCurrency,
|
private val appCurrency: AppCurrency,
|
||||||
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 otherAssets: List<Pair<List<CryptoCurrencyStatus>, BigDecimal>>,
|
private val onTokenClick: (UserWalletId, CryptoCurrency) -> Unit,
|
||||||
private val onTokenClick: (CryptoCurrency) -> Unit,
|
) : Converter<AccountStatusList?, PortfolioReviewUM> {
|
||||||
) : Converter<List<CryptoCurrencyStatus>, ImmutableList<ForYouTokenListItemUM>> {
|
|
||||||
|
|
||||||
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
||||||
private val rowConverter = ForYouTokenRowConverter(
|
|
||||||
appCurrency = appCurrency,
|
|
||||||
totalFiatBalance = totalFiatBalance,
|
|
||||||
onTokenClick = onTokenClick,
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun convert(value: List<CryptoCurrencyStatus>): ImmutableList<ForYouTokenListItemUM> {
|
override fun convert(value: AccountStatusList?): PortfolioReviewUM {
|
||||||
val assetItems = value
|
val currencies = value?.flattenCurrencies().orEmpty()
|
||||||
|
val loadedBalance = value?.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
|
||||||
|
.filterNot { it.value.fiatAmount?.isZero() == true }
|
||||||
.groupBy { it.forYouGroupKey() }
|
.groupBy { it.forYouGroupKey() }
|
||||||
.map { (assetId, currencies) -> createListItem(assetId, currencies) }
|
.map { (_, networks) -> networks to networks.sumOf { it.value.fiatAmount.orZero() } }
|
||||||
|
.sortedByDescending { (_, assetBalance) -> assetBalance }
|
||||||
|
|
||||||
|
// The top assets are shown individually (each flattened back to its networks so the converter can
|
||||||
|
// regroup them by network); the remaining assets are collapsed into a single "Other" row.
|
||||||
|
val topAssets = rankedAssets.take(TOP_HOLDINGS_COUNT)
|
||||||
|
val otherAssets = rankedAssets.drop(TOP_HOLDINGS_COUNT)
|
||||||
|
val topCurrencies = topAssets.flatMap { (networks, _) -> networks }
|
||||||
|
|
||||||
|
val assetItems = topCurrencies
|
||||||
|
.groupBy { it.forYouGroupKey() }
|
||||||
|
.map { (assetId, currencies) ->
|
||||||
|
createListItem(
|
||||||
|
userWalletId = value?.userWalletId,
|
||||||
|
assetId = assetId,
|
||||||
|
currencies = currencies,
|
||||||
|
totalFiatBalance = totalFiatBalance,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 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 (otherAssets.count() > 0) {
|
val tokenList = if (otherAssets.count() > 0) {
|
||||||
assetItems + createOtherItem()
|
assetItems + createOtherItem(otherAssets, totalFiatBalance)
|
||||||
} else {
|
} else {
|
||||||
assetItems
|
assetItems
|
||||||
}.toPersistentList()
|
}.toPersistentList()
|
||||||
|
|
||||||
|
val marketChartUM = ForYouPortfolioReviewMarketChartConverter(
|
||||||
|
appCurrency = appCurrency,
|
||||||
|
topAssets = topAssets,
|
||||||
|
).convert(value?.totalFiatBalance)
|
||||||
|
|
||||||
|
return PortfolioReviewUM.Content(
|
||||||
|
tokenList = tokenList,
|
||||||
|
marketChartUM = marketChartUM,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createListItem(assetId: String, currencies: List<CryptoCurrencyStatus>): ForYouTokenListItemUM {
|
private fun createListItem(
|
||||||
|
userWalletId: UserWalletId?,
|
||||||
|
assetId: String,
|
||||||
|
currencies: List<CryptoCurrencyStatus>,
|
||||||
|
totalFiatBalance: BigDecimal,
|
||||||
|
): ForYouTokenListItemUM {
|
||||||
// Group the asset's holdings by blockchain (network.id.rawId, derivation-independent) so each
|
// Group the asset's holdings by blockchain (network.id.rawId, derivation-independent) so each
|
||||||
// network appears once even when the asset is held across several accounts/derivations on it,
|
// network appears once even when the asset is held across several accounts/derivations on it,
|
||||||
// summing those balances. Order by balance so the expanded breakdown reads top-down.
|
// summing those balances. Order by balance so the expanded breakdown reads top-down.
|
||||||
|
|
@ -67,11 +111,21 @@ internal class ForYouTokenListConverter(
|
||||||
.groupBy { it.currency.network.id.rawId }
|
.groupBy { it.currency.network.id.rawId }
|
||||||
.values
|
.values
|
||||||
.sortedByDescending { group -> group.sumOf { it.value.fiatAmount.orZero() } }
|
.sortedByDescending { group -> group.sumOf { it.value.fiatAmount.orZero() } }
|
||||||
|
|
||||||
|
val rowConverter = ForYouPortfolioReviewTokenRowConverter(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
appCurrency = appCurrency,
|
||||||
|
totalFiatBalance = totalFiatBalance,
|
||||||
|
onTokenClick = onTokenClick,
|
||||||
|
)
|
||||||
|
|
||||||
return ForYouTokenListItemUM(
|
return ForYouTokenListItemUM(
|
||||||
tokenRowUM = createAssetRow(
|
tokenRowUM = createAssetRow(
|
||||||
|
userWalletId = userWalletId,
|
||||||
assetId = assetId,
|
assetId = assetId,
|
||||||
currencies = currencies,
|
currencies = currencies,
|
||||||
networkCount = networkGroups.size,
|
networkCount = networkGroups.size,
|
||||||
|
totalFiatBalance = totalFiatBalance,
|
||||||
),
|
),
|
||||||
tokenList = networkGroups.map(rowConverter::convertNetworkGroup).toPersistentList(),
|
tokenList = networkGroups.map(rowConverter::convertNetworkGroup).toPersistentList(),
|
||||||
isExpanded = assetId in expandedAssetIds,
|
isExpanded = assetId in expandedAssetIds,
|
||||||
|
|
@ -81,8 +135,10 @@ internal class ForYouTokenListConverter(
|
||||||
|
|
||||||
private fun createAssetRow(
|
private fun createAssetRow(
|
||||||
assetId: String,
|
assetId: String,
|
||||||
|
userWalletId: UserWalletId?,
|
||||||
currencies: List<CryptoCurrencyStatus>,
|
currencies: List<CryptoCurrencyStatus>,
|
||||||
networkCount: Int,
|
networkCount: Int,
|
||||||
|
totalFiatBalance: BigDecimal,
|
||||||
): TangemTokenRowUM {
|
): TangemTokenRowUM {
|
||||||
if (currencies.all { it.value is CryptoCurrencyStatus.Loading }) {
|
if (currencies.all { it.value is CryptoCurrencyStatus.Loading }) {
|
||||||
return TangemTokenRowUM.Loading(id = assetId)
|
return TangemTokenRowUM.Loading(id = assetId)
|
||||||
|
|
@ -91,6 +147,12 @@ 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 rowConverter = ForYouPortfolioReviewTokenRowConverter(
|
||||||
|
userWalletId = userWalletId,
|
||||||
|
appCurrency = appCurrency,
|
||||||
|
totalFiatBalance = totalFiatBalance,
|
||||||
|
onTokenClick = onTokenClick,
|
||||||
|
)
|
||||||
val endContent = rowConverter.toEndContent(statuses = currencies, fiatAmount = assetFiatBalance)
|
val endContent = rowConverter.toEndContent(statuses = currencies, fiatAmount = assetFiatBalance)
|
||||||
|
|
||||||
val onlyCryptoCurrency = currencies.firstOrNull()?.currency
|
val onlyCryptoCurrency = currencies.firstOrNull()?.currency
|
||||||
|
|
@ -120,7 +182,10 @@ internal class ForYouTokenListConverter(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createOtherItem(): ForYouTokenListItemUM {
|
private fun createOtherItem(
|
||||||
|
otherAssets: List<Pair<List<CryptoCurrencyStatus>, BigDecimal>>,
|
||||||
|
totalFiatBalance: BigDecimal,
|
||||||
|
): ForYouTokenListItemUM {
|
||||||
val otherAssetsBalance = otherAssets.sumOf { (_, assetBalance) -> assetBalance }
|
val otherAssetsBalance = otherAssets.sumOf { (_, assetBalance) -> assetBalance }
|
||||||
return ForYouTokenListItemUM(
|
return ForYouTokenListItemUM(
|
||||||
tokenRowUM = TangemTokenRowUM.Content(
|
tokenRowUM = TangemTokenRowUM.Content(
|
||||||
|
|
@ -158,5 +223,6 @@ internal class ForYouTokenListConverter(
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val OTHER_ROW_ID = "for_you_other_assets"
|
const val OTHER_ROW_ID = "for_you_other_assets"
|
||||||
|
const val TOP_HOLDINGS_COUNT = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.tangem.features.foryou.impl.model.converter
|
package com.tangem.features.foryou.impl.model.converter.portfolioReview
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -7,13 +7,18 @@ 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.components.state.*
|
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.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.model.converter.toForYouPercent
|
||||||
import com.tangem.utils.converter.Converter
|
import com.tangem.utils.converter.Converter
|
||||||
import com.tangem.utils.extensions.orZero
|
import com.tangem.utils.extensions.orZero
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
|
|
||||||
internal class ForYouMarketChartConverter(
|
internal class ForYouPortfolioReviewMarketChartConverter(
|
||||||
private val appCurrency: AppCurrency,
|
private val appCurrency: AppCurrency,
|
||||||
private val topAssets: List<Pair<List<CryptoCurrencyStatus>, BigDecimal>>,
|
private val topAssets: List<Pair<List<CryptoCurrencyStatus>, BigDecimal>>,
|
||||||
) : Converter<TotalFiatBalance?, MarketChartUM> {
|
) : Converter<TotalFiatBalance?, MarketChartUM> {
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package com.tangem.features.foryou.impl.model.converter
|
package com.tangem.features.foryou.impl.model.converter.portfolioReview
|
||||||
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
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
|
||||||
|
|
@ -16,6 +16,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency
|
||||||
import com.tangem.domain.models.StatusSource
|
import com.tangem.domain.models.StatusSource
|
||||||
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
|
||||||
|
import com.tangem.domain.models.wallet.UserWalletId
|
||||||
|
import com.tangem.features.foryou.impl.model.converter.forYouPlaceholderBadge
|
||||||
|
import com.tangem.features.foryou.impl.model.converter.toForYouPercent
|
||||||
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.persistentListOf
|
||||||
|
|
@ -39,10 +42,11 @@ import java.math.BigDecimal
|
||||||
* classification (they contribute nothing yet). The cache/flicker indicators derive from the most
|
* classification (they contribute nothing yet). The cache/flicker indicators derive from the most
|
||||||
* conservative [CryptoCurrencyStatus.Sources.total] across the contributing statuses.
|
* conservative [CryptoCurrencyStatus.Sources.total] across the contributing statuses.
|
||||||
*/
|
*/
|
||||||
internal class ForYouTokenRowConverter(
|
internal class ForYouPortfolioReviewTokenRowConverter(
|
||||||
private val appCurrency: AppCurrency,
|
private val appCurrency: AppCurrency,
|
||||||
|
private val userWalletId: UserWalletId?,
|
||||||
private val totalFiatBalance: BigDecimal,
|
private val totalFiatBalance: BigDecimal,
|
||||||
private val onTokenClick: (CryptoCurrency) -> Unit,
|
private val onTokenClick: (UserWalletId, CryptoCurrency) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
||||||
|
|
@ -66,7 +70,7 @@ internal class ForYouTokenRowConverter(
|
||||||
subtitleUM = toRowSubtitle(state, currency, cryptoAmount),
|
subtitleUM = toRowSubtitle(state, currency, cryptoAmount),
|
||||||
topEndContentUM = toRowTopEnd(state, fiatAmount),
|
topEndContentUM = toRowTopEnd(state, fiatAmount),
|
||||||
bottomEndContentUM = toRowBottomEnd(state, fiatAmount),
|
bottomEndContentUM = toRowBottomEnd(state, fiatAmount),
|
||||||
onItemClick = { onTokenClick(currency) },
|
onItemClick = { if (userWalletId != null) onTokenClick(userWalletId, currency) },
|
||||||
onItemLongClick = null,
|
onItemLongClick = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -88,9 +92,9 @@ internal class ForYouTokenRowConverter(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Title: For You always shows the asset symbol with the placeholder price-change badge. */
|
/** Title: For You always shows the asset name with the placeholder price-change badge. */
|
||||||
private fun toRowTitle(currency: CryptoCurrency): TangemTokenRowUM.TitleUM = TangemTokenRowUM.TitleUM.Content(
|
private fun toRowTitle(currency: CryptoCurrency): TangemTokenRowUM.TitleUM = TangemTokenRowUM.TitleUM.Content(
|
||||||
text = stringReference(currency.symbol),
|
text = stringReference(currency.name),
|
||||||
badge = forYouPlaceholderBadge(),
|
badge = forYouPlaceholderBadge(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -4,94 +4,48 @@ 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.domain.account.models.AccountStatusList
|
import com.tangem.domain.account.models.AccountStatusList
|
||||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
|
||||||
import com.tangem.domain.models.StatusSource
|
import com.tangem.domain.models.StatusSource
|
||||||
import com.tangem.domain.models.TotalFiatBalance
|
import com.tangem.domain.models.TotalFiatBalance
|
||||||
import com.tangem.domain.models.currency.CryptoCurrency
|
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
||||||
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.ForYouNotification
|
||||||
import com.tangem.features.foryou.impl.model.converter.ForYouMarketChartConverter
|
import com.tangem.features.foryou.impl.model.converter.portfolioReview.ForYouPortfolioReviewConverter
|
||||||
import com.tangem.features.foryou.impl.model.converter.ForYouTokenListConverter
|
|
||||||
import com.tangem.features.foryou.impl.model.converter.forYouGroupKey
|
|
||||||
import com.tangem.utils.extensions.isZero
|
|
||||||
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
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the [ForYouUM] state for the For You screen: the outdated-data notifications plus the portfolio
|
* Applies one combined emission to the [ForYouUM] state: sets the pre-built portfolio-review and
|
||||||
* review (market chart, period picker and the grouped token list).
|
* earn-opportunities sections (see [ForYouPortfolioReviewConverter] and
|
||||||
|
* `ForYouEarnOpportunitiesConverter`) and derives the outdated-data notification from
|
||||||
|
* [accountStatusList]'s total-balance source.
|
||||||
*
|
*
|
||||||
* The token list is delegated to [ForYouTokenListConverter] and the market chart to
|
|
||||||
* [ForYouMarketChartConverter]; the period picker selection is carried over from the previous state so
|
* subsequent refreshes the previous picker is carried over so the user's selection is not reset.
|
||||||
* 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).
|
* section construction to dedicated converters).
|
||||||
*/
|
*/
|
||||||
@Suppress("LongParameterList")
|
|
||||||
internal class SetPortfolioReviewTransformer(
|
internal class SetPortfolioReviewTransformer(
|
||||||
private val accountStatusList: AccountStatusList?,
|
private val accountStatusList: AccountStatusList?,
|
||||||
private val appCurrency: AppCurrency,
|
private val portfolioReviewUM: PortfolioReviewUM,
|
||||||
private val expandedAssetIds: Set<String>,
|
private val earnOpportunitiesUM: EarnOpportunitiesUM,
|
||||||
private val expandClick: (assetId: String) -> Unit,
|
|
||||||
private val onPeriodClick: (TangemSegmentUM) -> Unit,
|
|
||||||
private val onTokenClick: (CryptoCurrency) -> Unit,
|
|
||||||
) : Transformer<ForYouUM> {
|
) : Transformer<ForYouUM> {
|
||||||
|
|
||||||
override fun transform(prevState: ForYouUM): ForYouUM {
|
override fun transform(prevState: ForYouUM): ForYouUM {
|
||||||
val currencies = accountStatusList?.flattenCurrencies().orEmpty()
|
|
||||||
val loadedBalance = accountStatusList?.totalFiatBalance as? TotalFiatBalance.Loaded
|
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
|
|
||||||
.filterNot { it.value.fiatAmount?.isZero() == true }
|
|
||||||
.groupBy { it.forYouGroupKey() }
|
|
||||||
.map { (_, networks) -> networks to networks.sumOf { it.value.fiatAmount.orZero() } }
|
|
||||||
.sortedByDescending { (_, assetBalance) -> assetBalance }
|
|
||||||
|
|
||||||
// The top assets are shown individually (each flattened back to its networks so the converter can
|
|
||||||
// regroup them by network); the remaining assets are collapsed into a single "Other" row.
|
|
||||||
val topAssets = rankedAssets.take(TOP_HOLDINGS_COUNT)
|
|
||||||
val otherAssets = rankedAssets.drop(TOP_HOLDINGS_COUNT)
|
|
||||||
val topCurrencies = topAssets.flatMap { (networks, _) -> networks }
|
|
||||||
|
|
||||||
val tokenList = ForYouTokenListConverter(
|
|
||||||
appCurrency = appCurrency,
|
|
||||||
totalFiatBalance = totalFiatBalance,
|
|
||||||
expandedAssetIds = expandedAssetIds,
|
|
||||||
expandClick = expandClick,
|
|
||||||
otherAssets = otherAssets,
|
|
||||||
onTokenClick = onTokenClick,
|
|
||||||
).convert(topCurrencies)
|
|
||||||
|
|
||||||
val marketChartUM = ForYouMarketChartConverter(
|
|
||||||
appCurrency = appCurrency,
|
|
||||||
topAssets = topAssets,
|
|
||||||
).convert(accountStatusList?.totalFiatBalance)
|
|
||||||
|
|
||||||
return prevState.copy(
|
return prevState.copy(
|
||||||
notifications = if (loadedBalance?.source == StatusSource.ONLY_CACHE) {
|
notifications = if (loadedBalance?.source == StatusSource.ONLY_CACHE) {
|
||||||
persistentListOf(ForYouNotification.UsedOutdatedData)
|
persistentListOf(ForYouNotification.UsedOutdatedData)
|
||||||
} else {
|
} else {
|
||||||
persistentListOf()
|
persistentListOf()
|
||||||
},
|
},
|
||||||
portfolioReviewUM = PortfolioReviewUM.Content(
|
earnOpportunities = earnOpportunitiesUM,
|
||||||
periodPickerUM = when (prevState.portfolioReviewUM) {
|
portfolioReviewUM = portfolioReviewUM,
|
||||||
is PortfolioReviewUM.Content -> prevState.portfolioReviewUM.periodPickerUM
|
periodPickerUM = when (prevState.portfolioReviewUM) {
|
||||||
is PortfolioReviewUM.Loading -> createPeriodPicker()
|
is PortfolioReviewUM.Loading -> createPeriodPicker()
|
||||||
},
|
is PortfolioReviewUM.Content -> prevState.periodPickerUM
|
||||||
tokenList = tokenList,
|
},
|
||||||
marketChartUM = marketChartUM,
|
|
||||||
onPeriodClick = onPeriodClick,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,8 +63,4 @@ internal class SetPortfolioReviewTransformer(
|
||||||
isAltSurface = true,
|
isAltSurface = true,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val TOP_HOLDINGS_COUNT = 4
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -19,8 +19,11 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastForEachIndexed
|
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.tabs.TangemSegmentUM
|
||||||
|
import com.tangem.core.ui.ds.tabs.TangemSegmentedPickerUM
|
||||||
import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner
|
import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner
|
||||||
import com.tangem.core.ui.extensions.conditional
|
import com.tangem.core.ui.extensions.conditional
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
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
|
||||||
|
|
@ -78,6 +81,15 @@ internal fun ForYouContent(
|
||||||
|
|
||||||
ForYouPortfolioReview(
|
ForYouPortfolioReview(
|
||||||
portfolioReviewUM = forYouUM.portfolioReviewUM,
|
portfolioReviewUM = forYouUM.portfolioReviewUM,
|
||||||
|
periodPickerUM = forYouUM.periodPickerUM,
|
||||||
|
onPeriodClick = forYouUM.onPeriodClick,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
|
||||||
|
SpacerH(48.dp)
|
||||||
|
|
||||||
|
ForYouEarnOpportunities(
|
||||||
|
earnOpportunitiesUM = forYouUM.earnOpportunities,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -114,6 +126,17 @@ private class ForYouContentPreviewProvider : PreviewParameterProvider<ForYouUM>
|
||||||
notifications = persistentListOf(ForYouNotification.UsedOutdatedData),
|
notifications = persistentListOf(ForYouNotification.UsedOutdatedData),
|
||||||
earnOpportunities = ForYouEarnOpportunitiesPreviewData.tokensRewards,
|
earnOpportunities = ForYouEarnOpportunitiesPreviewData.tokensRewards,
|
||||||
portfolioReviewUM = ForYouPortfolioReviewPreviewData.reviewContent,
|
portfolioReviewUM = ForYouPortfolioReviewPreviewData.reviewContent,
|
||||||
|
periodPickerUM = TangemSegmentedPickerUM(
|
||||||
|
items = persistentListOf(
|
||||||
|
TangemSegmentUM(id = "0", title = stringReference("Day")),
|
||||||
|
TangemSegmentUM(id = "1", title = stringReference("Week")),
|
||||||
|
TangemSegmentUM(id = "2", title = stringReference("Month")),
|
||||||
|
),
|
||||||
|
initialSelectedItem = TangemSegmentUM(id = "0", title = stringReference("Day")),
|
||||||
|
isFixed = true,
|
||||||
|
isAltSurface = true,
|
||||||
|
),
|
||||||
|
onPeriodClick = {},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@ 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.row.token.TangemTokenRowUM
|
||||||
|
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.ds2.badge.TangemBadge
|
import com.tangem.core.ui.ds2.badge.TangemBadge
|
||||||
import com.tangem.core.ui.ds2.shimmers.TangemShimmer
|
import com.tangem.core.ui.ds2.shimmers.TangemShimmer
|
||||||
import com.tangem.core.ui.extensions.stringReference
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
|
@ -34,7 +36,12 @@ import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toPersistentList
|
import kotlinx.collections.immutable.toPersistentList
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ForYouPortfolioReview(portfolioReviewUM: PortfolioReviewUM, modifier: Modifier = Modifier) {
|
internal fun ForYouPortfolioReview(
|
||||||
|
periodPickerUM: TangemSegmentedPickerUM,
|
||||||
|
onPeriodClick: (TangemSegmentUM) -> Unit,
|
||||||
|
portfolioReviewUM: PortfolioReviewUM,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
|
@ -64,8 +71,8 @@ internal fun ForYouPortfolioReview(portfolioReviewUM: PortfolioReviewUM, modifie
|
||||||
when (portfolioReviewUM) {
|
when (portfolioReviewUM) {
|
||||||
is PortfolioReviewUM.Content -> {
|
is PortfolioReviewUM.Content -> {
|
||||||
TangemSegmentedPicker(
|
TangemSegmentedPicker(
|
||||||
tangemSegmentedPickerUM = portfolioReviewUM.periodPickerUM,
|
tangemSegmentedPickerUM = periodPickerUM,
|
||||||
onClick = portfolioReviewUM.onPeriodClick,
|
onClick = onPeriodClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is PortfolioReviewUM.Loading -> TangemShimmer(
|
is PortfolioReviewUM.Loading -> TangemShimmer(
|
||||||
|
|
@ -91,6 +98,17 @@ private fun ForYouPortfolioReview_Review(
|
||||||
ForYouPortfolioReview(
|
ForYouPortfolioReview(
|
||||||
portfolioReviewUM = params,
|
portfolioReviewUM = params,
|
||||||
modifier = Modifier.background(TangemTheme.colors3.bg.primary),
|
modifier = Modifier.background(TangemTheme.colors3.bg.primary),
|
||||||
|
periodPickerUM = TangemSegmentedPickerUM(
|
||||||
|
items = persistentListOf(
|
||||||
|
TangemSegmentUM(id = "0", title = stringReference("Day")),
|
||||||
|
TangemSegmentUM(id = "1", title = stringReference("Week")),
|
||||||
|
TangemSegmentUM(id = "2", title = stringReference("Month")),
|
||||||
|
),
|
||||||
|
initialSelectedItem = TangemSegmentUM(id = "0", title = stringReference("Day")),
|
||||||
|
isFixed = true,
|
||||||
|
isAltSurface = true,
|
||||||
|
),
|
||||||
|
onPeriodClick = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ 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.ds.tabs.TangemSegmentUM
|
|
||||||
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.DonutChartUM
|
||||||
import com.tangem.features.foryou.impl.components.state.DonutSegmentColor
|
import com.tangem.features.foryou.impl.components.state.DonutSegmentColor
|
||||||
|
|
@ -23,17 +21,6 @@ import java.math.BigDecimal
|
||||||
internal object ForYouPortfolioReviewPreviewData {
|
internal object ForYouPortfolioReviewPreviewData {
|
||||||
|
|
||||||
val reviewContent = PortfolioReviewUM.Content(
|
val reviewContent = PortfolioReviewUM.Content(
|
||||||
periodPickerUM = TangemSegmentedPickerUM(
|
|
||||||
items = persistentListOf(
|
|
||||||
TangemSegmentUM(id = "0", title = stringReference("Day")),
|
|
||||||
TangemSegmentUM(id = "1", title = stringReference("Week")),
|
|
||||||
TangemSegmentUM(id = "2", title = stringReference("Month")),
|
|
||||||
),
|
|
||||||
initialSelectedItem = TangemSegmentUM(id = "0", title = stringReference("Day")),
|
|
||||||
isFixed = true,
|
|
||||||
isAltSurface = true,
|
|
||||||
),
|
|
||||||
onPeriodClick = {},
|
|
||||||
marketChartUM = MarketChartUM.Loaded(
|
marketChartUM = MarketChartUM.Loaded(
|
||||||
donutChart = DonutChartUM.Loaded(
|
donutChart = DonutChartUM.Loaded(
|
||||||
totalAmount = "10000$",
|
totalAmount = "10000$",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue