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.common)
|
||||
api(projects.domain.wallets)
|
||||
api(projects.domain.earn)
|
||||
api(projects.domain.yieldSupply)
|
||||
implementation(projects.domain.account)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ internal data class ForYouUM(
|
|||
val portfolioReviewUM: PortfolioReviewUM,
|
||||
val earnOpportunities: EarnOpportunitiesUM,
|
||||
val notifications: ImmutableList<ForYouNotification>,
|
||||
val periodPickerUM: TangemSegmentedPickerUM,
|
||||
val onPeriodClick: (tangemSegmentUM: TangemSegmentUM) -> Unit,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
|
|
@ -29,20 +31,32 @@ internal sealed interface PortfolioReviewUM {
|
|||
data class Content(
|
||||
override val tokenList: ImmutableList<ForYouTokenListItemUM>,
|
||||
override val marketChartUM: MarketChartUM,
|
||||
val periodPickerUM: TangemSegmentedPickerUM,
|
||||
val onPeriodClick: (TangemSegmentUM) -> Unit,
|
||||
) : 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
|
||||
internal sealed interface EarnOpportunitiesUM {
|
||||
|
||||
val tokenList: ImmutableList<ForYouTokenListItemUM>
|
||||
|
||||
/** Skeleton rows shown until the first real emission. */
|
||||
data class Loading(
|
||||
override val tokenList: ImmutableList<ForYouTokenListItemUM>,
|
||||
) : 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(
|
||||
override val tokenList: ImmutableList<ForYouTokenListItemUM>,
|
||||
@param:StringRes val subtitleRes: Int,
|
||||
|
|
|
|||
|
|
@ -2,25 +2,39 @@ package com.tangem.features.foryou.impl.model
|
|||
|
||||
import androidx.compose.runtime.Stable
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import arrow.core.right
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.core.decompose.model.Model
|
||||
import com.tangem.core.decompose.model.ParamsContainer
|
||||
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
|
||||
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.usecase.IsAccountsModeEnabledUseCase
|
||||
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
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.earn.EarnTopToken
|
||||
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.impl.components.state.MarketChartUM
|
||||
import com.tangem.features.foryou.impl.entity.EarnOpportunitiesUM
|
||||
import com.tangem.features.foryou.impl.entity.ForYouTokenListItemUM
|
||||
import com.tangem.features.foryou.impl.entity.ForYouUM
|
||||
import com.tangem.features.foryou.impl.entity.PortfolioReviewUM
|
||||
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.earnOpportunities.ForYouEarnOpportunitiesConverter
|
||||
import com.tangem.features.foryou.impl.model.converter.portfolioReview.ForYouPortfolioReviewConverter
|
||||
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.combine6
|
||||
import com.tangem.utils.transformer.update
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
|
@ -29,26 +43,34 @@ import javax.inject.Inject
|
|||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class ForYouModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
|
||||
yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
|
||||
private val getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase,
|
||||
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val earnErrorResolver: EarnErrorResolver,
|
||||
) : Model() {
|
||||
|
||||
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()
|
||||
|
||||
val uiState: StateFlow<ForYouUM>
|
||||
field = MutableStateFlow<ForYouUM>(
|
||||
ForYouUM(
|
||||
notifications = persistentListOf(),
|
||||
periodPickerUM = TangemSegmentedPickerUM(persistentListOf()),
|
||||
earnOpportunities = EarnOpportunitiesUM.Loading(
|
||||
tokenList = buildList<ForYouTokenListItemUM> {
|
||||
repeat(4) { index ->
|
||||
repeat(5) { index ->
|
||||
add(
|
||||
ForYouTokenListItemUM(
|
||||
tokenRowUM = TangemTokenRowUM.Loading(
|
||||
|
|
@ -62,6 +84,7 @@ internal class ForYouModel @Inject constructor(
|
|||
}
|
||||
}.toPersistentList(),
|
||||
),
|
||||
onPeriodClick = ::onPeriodClick,
|
||||
portfolioReviewUM = PortfolioReviewUM.Loading(
|
||||
marketChartUM = MarketChartUM.NoData,
|
||||
tokenList = buildList<ForYouTokenListItemUM> {
|
||||
|
|
@ -83,21 +106,51 @@ internal class ForYouModel @Inject constructor(
|
|||
)
|
||||
|
||||
init {
|
||||
combine(
|
||||
flow = userWalletsListRepository.selectedUserWallet,
|
||||
combine6(
|
||||
flow1 = userWalletsListRepository.selectedUserWallet,
|
||||
flow2 = multiAccountStatusListSupplier.invokeAsMap(),
|
||||
flow3 = expandedAssetIds,
|
||||
) { globalSelectedWallet, accountStatusList, expanded ->
|
||||
flow3 = expandedPortfolioReviewAssetIds,
|
||||
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
|
||||
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(
|
||||
SetPortfolioReviewTransformer(
|
||||
accountStatusList = accountStatusList[selectedWalletId],
|
||||
appCurrency = selectedAppCurrencyFlow.value,
|
||||
expandedAssetIds = expanded,
|
||||
expandClick = ::onExpandClick,
|
||||
onPeriodClick = ::onPeriodClick,
|
||||
onTokenClick = { currency -> onTokenClick(selectedWalletId, currency) },
|
||||
accountStatusList = accountStatusList,
|
||||
portfolioReviewUM = portfolioReviewUM,
|
||||
earnOpportunitiesUM = earnOpportunitiesUM,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -105,6 +158,40 @@ internal class ForYouModel @Inject constructor(
|
|||
.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> {
|
||||
return getSelectedAppCurrencyUseCase().map { maybeAppCurrency ->
|
||||
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
|
||||
params.callbacks.onTokenClick(walletId, currency)
|
||||
}
|
||||
|
||||
private fun onExpandClick(assetId: String) {
|
||||
expandedAssetIds.update { ids ->
|
||||
private fun onExpandPortfolioReviewClick(assetId: String) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -129,11 +222,9 @@ internal class ForYouModel @Inject constructor(
|
|||
private fun onPeriodClick(tangemSegmentUM: TangemSegmentUM) {
|
||||
uiState.update { state ->
|
||||
state.copy(
|
||||
portfolioReviewUM = (state.portfolioReviewUM as? PortfolioReviewUM.Content)?.copy(
|
||||
periodPickerUM = state.portfolioReviewUM.periodPickerUM.copy(
|
||||
periodPickerUM = state.periodPickerUM.copy(
|
||||
initialSelectedItem = tangemSegmentUM,
|
||||
),
|
||||
) ?: state.portfolioReviewUM,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,11 @@ import java.math.RoundingMode
|
|||
/** Number of suggested top-earn tokens shown in the earn-opportunities block. */
|
||||
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
|
||||
|
||||
/** 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.
|
||||
*
|
||||
* @property accountPotentialReward sum of [EarnApyInfo.potentialRewards] over [earnCurrencues];
|
||||
* @property accountPotentialReward sum of [EarnApyInfo.potentialRewards] over [earnCurrencies];
|
||||
* accounts are ordered by it, descending
|
||||
*/
|
||||
internal data class EarnOpportunities(
|
||||
val account: Account.CryptoPortfolio,
|
||||
val earnCurrencues: Map<CryptoCurrencyStatus, EarnApyInfo>,
|
||||
val earnCurrencies: Map<CryptoCurrencyStatus, EarnApyInfo>,
|
||||
val accountPotentialReward: BigDecimal,
|
||||
)
|
||||
|
|
@ -75,7 +75,7 @@ internal class ForYouEarnOpportunitiesConverter(
|
|||
|
||||
EarnOpportunities(
|
||||
account = cryptoAccountStatus.account,
|
||||
earnCurrencues = tokenList.toMap(),
|
||||
earnCurrencies = tokenList.toMap(),
|
||||
accountPotentialReward = accountPotentialReward,
|
||||
)
|
||||
}
|
||||
|
|
@ -84,10 +84,14 @@ internal class ForYouEarnOpportunitiesConverter(
|
|||
|
||||
return when {
|
||||
data.isEmpty() -> {
|
||||
ForYouEarnOpportunitiesNoTokensConverter(topEarnTokens).convert(data)
|
||||
ForYouEarnOpportunitiesNoTokensConverter(
|
||||
topEarnTokens = topEarnTokens,
|
||||
).convert(data)
|
||||
}
|
||||
data.all { earn -> earn.earnCurrencues.all { entry -> entry.value.isActive } } -> {
|
||||
ForYouEarnOpportunitiesTokensActiveConverter(topEarnTokens).convert(data)
|
||||
data.all { earn -> earn.earnCurrencies.all { entry -> entry.value.isActive } } -> {
|
||||
ForYouEarnOpportunitiesTokensActiveConverter(
|
||||
topEarnTokens = topEarnTokens,
|
||||
).convert(data)
|
||||
}
|
||||
else -> {
|
||||
ForYouEarnOpportunitiesPotentialRewardsConverter(
|
||||
|
|
@ -134,7 +138,7 @@ internal class ForYouEarnOpportunitiesConverter(
|
|||
currencyStatus = cryptoCurrencyStatus,
|
||||
stakingApyMap = stakingApyMap,
|
||||
)
|
||||
if (stakingInfo.rate != null) {
|
||||
if (stakingInfo != null) {
|
||||
return EarnApyInfo(
|
||||
isActive = stakingInfo.isActive,
|
||||
apy = stakingInfo.rate,
|
||||
|
|
@ -154,10 +158,9 @@ internal class ForYouEarnOpportunitiesConverter(
|
|||
private fun findStakingRate(
|
||||
currencyStatus: CryptoCurrencyStatus,
|
||||
stakingApyMap: Map<CryptoCurrency, StakingAvailability>,
|
||||
): StakingLocalInfo {
|
||||
): StakingLocalInfo? {
|
||||
val availability = stakingApyMap[currencyStatus.currency]
|
||||
val option = availability?.optionOrNull
|
||||
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
||||
val option = availability?.optionOrNull ?: return null
|
||||
|
||||
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
|
||||
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.
|
||||
if (availability is StakingAvailability.Full && !isActive) {
|
||||
return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
|
||||
return null
|
||||
}
|
||||
|
||||
val rateInfo = when (option) {
|
||||
|
|
@ -196,18 +199,18 @@ internal class ForYouEarnOpportunitiesConverter(
|
|||
}
|
||||
.maxByOrNull { it.rate }
|
||||
}
|
||||
}
|
||||
} ?: return null
|
||||
|
||||
return StakingLocalInfo(
|
||||
rate = rateInfo?.rate,
|
||||
rate = rateInfo.rate,
|
||||
isActive = isActive,
|
||||
rewardType = rateInfo?.type,
|
||||
rewardType = rateInfo.type,
|
||||
)
|
||||
}
|
||||
|
||||
private data class StakingLocalInfo(
|
||||
val rate: BigDecimal?,
|
||||
val rate: BigDecimal,
|
||||
val isActive: Boolean,
|
||||
val rewardType: RewardType?,
|
||||
val rewardType: RewardType,
|
||||
)
|
||||
}
|
||||
|
|
@ -20,6 +20,15 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
import kotlinx.collections.immutable.toPersistentList
|
||||
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(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val isAccountsModeEnabled: Boolean,
|
||||
|
|
@ -51,16 +60,16 @@ internal class ForYouEarnOpportunitiesPotentialRewardsConverter(
|
|||
tokenRowUM = createAssetRow(
|
||||
account = earnData.account,
|
||||
potentialReward = earnData.accountPotentialReward,
|
||||
tokenCount = earnData.earnCurrencues.size,
|
||||
tokenCount = earnData.earnCurrencies.size,
|
||||
),
|
||||
tokenList = rowConverter.convertList(earnData.earnCurrencues.toList())
|
||||
tokenList = rowConverter.convertList(earnData.earnCurrencies.toList())
|
||||
.toPersistentList(),
|
||||
isExpanded = earnData.account.accountId.value in expandedAssetIds,
|
||||
isExpandable = true,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
earnData.earnCurrencues.map { token ->
|
||||
earnData.earnCurrencies.map { token ->
|
||||
ForYouTokenListItemUM(
|
||||
tokenRowUM = rowConverter.convert(token.toPair()),
|
||||
tokenList = persistentListOf(),
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ import com.tangem.utils.extensions.orZero
|
|||
import kotlinx.collections.immutable.toImmutableList
|
||||
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(
|
||||
private val appCurrency: AppCurrency,
|
||||
) : Converter<Pair<CryptoCurrencyStatus, EarnApyInfo>, TangemTokenRowUM> {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ internal class ForYouEarnOpportunitiesTokensActiveConverter(
|
|||
|
||||
override fun convert(value: List<EarnOpportunities>): EarnOpportunitiesUM {
|
||||
val activeAssetKeys = value
|
||||
.flatMap { opportunities -> opportunities.earnCurrencues.keys }
|
||||
.flatMap { opportunities -> opportunities.earnCurrencies.keys }
|
||||
.map { status -> status.currency.forYouEarnAssetKey() }
|
||||
.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.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.format
|
||||
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.models.TotalFiatBalance
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
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.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.extensions.isZero
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
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,
|
||||
* 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;
|
||||
*
|
||||
* Modelled on `TokenListStateConverter` (a list converter delegating to a per-item converter).
|
||||
*/
|
||||
internal class ForYouTokenListConverter(
|
||||
internal class ForYouPortfolioReviewConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val totalFiatBalance: BigDecimal,
|
||||
private val expandedAssetIds: Set<String>,
|
||||
private val expandClick: (assetId: String) -> Unit,
|
||||
private val otherAssets: List<Pair<List<CryptoCurrencyStatus>, BigDecimal>>,
|
||||
private val onTokenClick: (CryptoCurrency) -> Unit,
|
||||
) : Converter<List<CryptoCurrencyStatus>, ImmutableList<ForYouTokenListItemUM>> {
|
||||
private val onTokenClick: (UserWalletId, CryptoCurrency) -> Unit,
|
||||
) : Converter<AccountStatusList?, PortfolioReviewUM> {
|
||||
|
||||
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
||||
private val rowConverter = ForYouTokenRowConverter(
|
||||
appCurrency = appCurrency,
|
||||
totalFiatBalance = totalFiatBalance,
|
||||
onTokenClick = onTokenClick,
|
||||
)
|
||||
|
||||
override fun convert(value: List<CryptoCurrencyStatus>): ImmutableList<ForYouTokenListItemUM> {
|
||||
val assetItems = value
|
||||
override fun convert(value: AccountStatusList?): PortfolioReviewUM {
|
||||
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() }
|
||||
.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.
|
||||
return if (otherAssets.count() > 0) {
|
||||
assetItems + createOtherItem()
|
||||
val tokenList = if (otherAssets.count() > 0) {
|
||||
assetItems + createOtherItem(otherAssets, totalFiatBalance)
|
||||
} else {
|
||||
assetItems
|
||||
}.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
|
||||
// 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.
|
||||
|
|
@ -67,11 +111,21 @@ internal class ForYouTokenListConverter(
|
|||
.groupBy { it.currency.network.id.rawId }
|
||||
.values
|
||||
.sortedByDescending { group -> group.sumOf { it.value.fiatAmount.orZero() } }
|
||||
|
||||
val rowConverter = ForYouPortfolioReviewTokenRowConverter(
|
||||
userWalletId = userWalletId,
|
||||
appCurrency = appCurrency,
|
||||
totalFiatBalance = totalFiatBalance,
|
||||
onTokenClick = onTokenClick,
|
||||
)
|
||||
|
||||
return ForYouTokenListItemUM(
|
||||
tokenRowUM = createAssetRow(
|
||||
userWalletId = userWalletId,
|
||||
assetId = assetId,
|
||||
currencies = currencies,
|
||||
networkCount = networkGroups.size,
|
||||
totalFiatBalance = totalFiatBalance,
|
||||
),
|
||||
tokenList = networkGroups.map(rowConverter::convertNetworkGroup).toPersistentList(),
|
||||
isExpanded = assetId in expandedAssetIds,
|
||||
|
|
@ -81,8 +135,10 @@ internal class ForYouTokenListConverter(
|
|||
|
||||
private fun createAssetRow(
|
||||
assetId: String,
|
||||
userWalletId: UserWalletId?,
|
||||
currencies: List<CryptoCurrencyStatus>,
|
||||
networkCount: Int,
|
||||
totalFiatBalance: BigDecimal,
|
||||
): TangemTokenRowUM {
|
||||
if (currencies.all { it.value is CryptoCurrencyStatus.Loading }) {
|
||||
return TangemTokenRowUM.Loading(id = assetId)
|
||||
|
|
@ -91,6 +147,12 @@ internal class ForYouTokenListConverter(
|
|||
val asset = currencies.first()
|
||||
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 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 }
|
||||
return ForYouTokenListItemUM(
|
||||
tokenRowUM = TangemTokenRowUM.Content(
|
||||
|
|
@ -158,5 +223,6 @@ internal class ForYouTokenListConverter(
|
|||
|
||||
private companion object {
|
||||
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.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.models.TotalFiatBalance
|
||||
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.extensions.orZero
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class ForYouMarketChartConverter(
|
||||
internal class ForYouPortfolioReviewMarketChartConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val topAssets: List<Pair<List<CryptoCurrencyStatus>, BigDecimal>>,
|
||||
) : 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 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.currency.CryptoCurrency
|
||||
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.extensions.orZero
|
||||
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
|
||||
* conservative [CryptoCurrencyStatus.Sources.total] across the contributing statuses.
|
||||
*/
|
||||
internal class ForYouTokenRowConverter(
|
||||
internal class ForYouPortfolioReviewTokenRowConverter(
|
||||
private val appCurrency: AppCurrency,
|
||||
private val userWalletId: UserWalletId?,
|
||||
private val totalFiatBalance: BigDecimal,
|
||||
private val onTokenClick: (CryptoCurrency) -> Unit,
|
||||
private val onTokenClick: (UserWalletId, CryptoCurrency) -> Unit,
|
||||
) {
|
||||
|
||||
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
||||
|
|
@ -66,7 +70,7 @@ internal class ForYouTokenRowConverter(
|
|||
subtitleUM = toRowSubtitle(state, currency, cryptoAmount),
|
||||
topEndContentUM = toRowTopEnd(state, fiatAmount),
|
||||
bottomEndContentUM = toRowBottomEnd(state, fiatAmount),
|
||||
onItemClick = { onTokenClick(currency) },
|
||||
onItemClick = { if (userWalletId != null) onTokenClick(userWalletId, currency) },
|
||||
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(
|
||||
text = stringReference(currency.symbol),
|
||||
text = stringReference(currency.name),
|
||||
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.extensions.stringReference
|
||||
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.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.PortfolioReviewUM
|
||||
import com.tangem.features.foryou.impl.model.ForYouNotification
|
||||
import com.tangem.features.foryou.impl.model.converter.ForYouMarketChartConverter
|
||||
import com.tangem.features.foryou.impl.model.converter.ForYouTokenListConverter
|
||||
import com.tangem.features.foryou.impl.model.converter.forYouGroupKey
|
||||
import com.tangem.utils.extensions.isZero
|
||||
import com.tangem.utils.extensions.orZero
|
||||
import com.tangem.features.foryou.impl.model.converter.portfolioReview.ForYouPortfolioReviewConverter
|
||||
import com.tangem.utils.transformer.Transformer
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
* Builds the [ForYouUM] state for the For You screen: the outdated-data notifications plus the portfolio
|
||||
* review (market chart, period picker and the grouped token list).
|
||||
* Applies one combined emission to the [ForYouUM] state: sets the pre-built portfolio-review and
|
||||
* 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
|
||||
* it is not reset on every balance refresh.
|
||||
|
||||
* subsequent refreshes the previous picker is carried over so the user's selection is not reset.
|
||||
*
|
||||
* 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(
|
||||
private val accountStatusList: AccountStatusList?,
|
||||
private val appCurrency: AppCurrency,
|
||||
private val expandedAssetIds: Set<String>,
|
||||
private val expandClick: (assetId: String) -> Unit,
|
||||
private val onPeriodClick: (TangemSegmentUM) -> Unit,
|
||||
private val onTokenClick: (CryptoCurrency) -> Unit,
|
||||
private val portfolioReviewUM: PortfolioReviewUM,
|
||||
private val earnOpportunitiesUM: EarnOpportunitiesUM,
|
||||
) : Transformer<ForYouUM> {
|
||||
|
||||
override fun transform(prevState: ForYouUM): ForYouUM {
|
||||
val currencies = accountStatusList?.flattenCurrencies().orEmpty()
|
||||
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(
|
||||
notifications = if (loadedBalance?.source == StatusSource.ONLY_CACHE) {
|
||||
persistentListOf(ForYouNotification.UsedOutdatedData)
|
||||
} else {
|
||||
persistentListOf()
|
||||
},
|
||||
portfolioReviewUM = PortfolioReviewUM.Content(
|
||||
earnOpportunities = earnOpportunitiesUM,
|
||||
portfolioReviewUM = portfolioReviewUM,
|
||||
periodPickerUM = when (prevState.portfolioReviewUM) {
|
||||
is PortfolioReviewUM.Content -> prevState.portfolioReviewUM.periodPickerUM
|
||||
is PortfolioReviewUM.Loading -> createPeriodPicker()
|
||||
is PortfolioReviewUM.Content -> prevState.periodPickerUM
|
||||
},
|
||||
tokenList = tokenList,
|
||||
marketChartUM = marketChartUM,
|
||||
onPeriodClick = onPeriodClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -109,8 +63,4 @@ internal class SetPortfolioReviewTransformer(
|
|||
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 com.tangem.core.ui.components.SpacerH
|
||||
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.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.LocalMainBottomSheetColor
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
|
@ -78,6 +81,15 @@ internal fun ForYouContent(
|
|||
|
||||
ForYouPortfolioReview(
|
||||
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),
|
||||
)
|
||||
|
||||
|
|
@ -114,6 +126,17 @@ private class ForYouContentPreviewProvider : PreviewParameterProvider<ForYouUM>
|
|||
notifications = persistentListOf(ForYouNotification.UsedOutdatedData),
|
||||
earnOpportunities = ForYouEarnOpportunitiesPreviewData.tokensRewards,
|
||||
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.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.TangemSegmentedPicker
|
||||
import com.tangem.core.ui.ds.tabs.TangemSegmentedPickerUM
|
||||
import com.tangem.core.ui.ds2.badge.TangemBadge
|
||||
import com.tangem.core.ui.ds2.shimmers.TangemShimmer
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
|
|
@ -34,7 +36,12 @@ import kotlinx.collections.immutable.persistentListOf
|
|||
import kotlinx.collections.immutable.toPersistentList
|
||||
|
||||
@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) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
|
|
@ -64,8 +71,8 @@ internal fun ForYouPortfolioReview(portfolioReviewUM: PortfolioReviewUM, modifie
|
|||
when (portfolioReviewUM) {
|
||||
is PortfolioReviewUM.Content -> {
|
||||
TangemSegmentedPicker(
|
||||
tangemSegmentedPickerUM = portfolioReviewUM.periodPickerUM,
|
||||
onClick = portfolioReviewUM.onPeriodClick,
|
||||
tangemSegmentedPickerUM = periodPickerUM,
|
||||
onClick = onPeriodClick,
|
||||
)
|
||||
}
|
||||
is PortfolioReviewUM.Loading -> TangemShimmer(
|
||||
|
|
@ -91,6 +98,17 @@ private fun ForYouPortfolioReview_Review(
|
|||
ForYouPortfolioReview(
|
||||
portfolioReviewUM = params,
|
||||
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.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.TangemSegmentedPickerUM
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.features.foryou.impl.components.state.DonutChartUM
|
||||
import com.tangem.features.foryou.impl.components.state.DonutSegmentColor
|
||||
|
|
@ -23,17 +21,6 @@ import java.math.BigDecimal
|
|||
internal object ForYouPortfolioReviewPreviewData {
|
||||
|
||||
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(
|
||||
donutChart = DonutChartUM.Loaded(
|
||||
totalAmount = "10000$",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue