Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-11 16:14:21 +05:00
parent 5dea3089f3
commit 6b8cb5604e
11 changed files with 146 additions and 203 deletions

View file

@ -139,16 +139,24 @@ internal object TokenDetailsPreviewData {
private val marketPriceLoading = MarketPriceBlockState.Loading(currencySymbol = "USDT")
private val stakingLoading = StakingBlocksState(
stakingAvailable = StakingAvailable.Loading(iconState),
stakingBalance = StakingBalance.Content(
cryptoValue = stringReference("5 SOL"),
fiatValue = stringReference("456.34 $"),
rewardValue = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")),
cryptoAmount = BigDecimal.ZERO,
fiatAmount = BigDecimal.ZERO,
onStakeClicked = {},
),
val stakingLoadingBlock = StakingBlockUM.Loading(iconState)
val stakingErrorBlock = StakingBlockUM.Error(iconState)
val stakingAvailableBlock = StakingBlockUM.StakeAvailable(
interestRate = "7.38",
periodInDays = 4,
tokenSymbol = "XLM",
iconState = iconState,
onStakeClicked = {},
)
val stakedBlock = StakingBlockUM.Staked(
cryptoValue = stringReference("5 SOL"),
fiatValue = stringReference("456.34 $"),
rewardValue = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")),
cryptoAmount = BigDecimal.ZERO,
fiatAmount = BigDecimal.ZERO,
onStakeClicked = {},
)
private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig(
@ -285,7 +293,7 @@ internal object TokenDetailsPreviewData {
tokenInfoBlockState = tokenInfoBlockState,
tokenBalanceBlockState = balanceLoading,
marketPriceBlockState = marketPriceLoading,
stakingBlocksState = stakingLoading,
stakingBlocksState = stakingLoadingBlock,
notifications = persistentListOf(),
txHistoryState = TxHistoryState.Content(
contentItems = MutableStateFlow(
@ -317,23 +325,7 @@ internal object TokenDetailsPreviewData {
type = PriceChangeType.UP,
),
),
stakingBlocksState = StakingBlocksState(
stakingAvailable = StakingAvailable.Content(
interestRate = "7.38",
periodInDays = 4,
tokenSymbol = "XLM",
iconState = iconState,
onStakeClicked = {},
),
stakingBalance = StakingBalance.Content(
cryptoValue = stringReference("5 SOL"),
fiatValue = stringReference("456.34 $"),
rewardValue = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")),
cryptoAmount = BigDecimal.ZERO,
fiatAmount = BigDecimal.ZERO,
onStakeClicked = {},
),
),
stakingBlocksState = stakingAvailableBlock,
notifications = persistentListOf(),
txHistoryState = TxHistoryState.NotSupported(
onExploreClick = {},
@ -356,5 +348,6 @@ internal object TokenDetailsPreviewData {
value = PagingData.from(txHistoryItems),
),
),
stakingBlocksState = stakedBlock,
)
}

View file

@ -1,41 +0,0 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import java.math.BigDecimal
internal data class StakingBlocksState(
val stakingAvailable: StakingAvailable,
val stakingBalance: StakingBalance,
)
@Immutable
internal sealed interface StakingAvailable {
val iconState: IconState
data class Error(override val iconState: IconState) : StakingAvailable
data class Loading(override val iconState: IconState) : StakingAvailable
data class Content(
override val iconState: IconState,
val interestRate: String,
val periodInDays: Int,
val tokenSymbol: String,
val onStakeClicked: () -> Unit,
) : StakingAvailable
}
@Immutable
sealed class StakingBalance {
data object Empty : StakingBalance()
data class Content(
val cryptoValue: TextReference,
val fiatValue: TextReference,
val rewardValue: TextReference,
val cryptoAmount: BigDecimal?,
val fiatAmount: BigDecimal?,
val onStakeClicked: () -> Unit,
) : StakingBalance()
}

View file

@ -0,0 +1,29 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.TextReference
import java.math.BigDecimal
@Stable
internal sealed interface StakingBlockUM {
data class Error(val iconState: IconState) : StakingBlockUM
data class Loading(val iconState: IconState) : StakingBlockUM
data class Staked(
val cryptoValue: TextReference,
val fiatValue: TextReference,
val rewardValue: TextReference,
val cryptoAmount: BigDecimal?,
val fiatAmount: BigDecimal?,
val onStakeClicked: () -> Unit,
) : StakingBlockUM
data class StakeAvailable(
val iconState: IconState,
val interestRate: String,
val periodInDays: Int,
val tokenSymbol: String,
val onStakeClicked: () -> Unit,
) : StakingBlockUM
}

View file

@ -17,7 +17,7 @@ internal data class TokenDetailsState(
val tokenInfoBlockState: TokenInfoBlockState,
val tokenBalanceBlockState: TokenDetailsBalanceBlockState,
val marketPriceBlockState: MarketPriceBlockState,
val stakingBlocksState: StakingBlocksState,
val stakingBlocksState: StakingBlockUM,
val notifications: ImmutableList<TokenDetailsNotification>,
val pendingTxs: PersistentList<TransactionState>,
val swapTxs: PersistentList<SwapTransactionsState>,

View file

@ -17,11 +17,11 @@ internal class TokenDetailsBalanceSelectStateConverter(
override fun convert(value: TokenBalanceSegmentedButtonConfig): TokenDetailsState {
return with(currentStateProvider()) {
if (stakingBlocksState.stakingBalance !is StakingBalance.Content) return this
if (stakingBlocksState !is StakingBlockUM.Staked) return this
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() ?: return this
val stakingCryptoAmount = stakingBlocksState.stakingBalance.cryptoAmount
val stakingFiatAmount = stakingBlocksState.stakingBalance.fiatAmount
val stakingCryptoAmount = stakingBlocksState.cryptoAmount
val stakingFiatAmount = stakingBlocksState.fiatAmount
copy(
tokenBalanceBlockState = if (tokenBalanceBlockState is TokenDetailsBalanceBlockState.Content) {

View file

@ -14,7 +14,10 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance
@ -131,17 +134,17 @@ internal class TokenDetailsLoadedBalanceConverter(
}
}
private fun getYieldBalance(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlocksState {
private fun getYieldBalance(status: CryptoCurrencyStatus, state: TokenDetailsState): StakingBlockUM {
val yieldBalance = status.value.yieldBalance as? YieldBalance.Data
val stakingCryptoAmount = yieldBalance?.getTotalStakingBalance()
val stakingRewardAmount = yieldBalance?.getRewardStakingBalance()
val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) }
val stakingBalance = if (stakingCryptoAmount.isNullOrZero()) {
StakingBalance.Empty
return if (stakingCryptoAmount.isNullOrZero()) {
StakingBlockUM.Loading(state.tokenInfoBlockState.iconState)
} else {
StakingBalance.Content(
StakingBlockUM.Staked(
cryptoAmount = stakingCryptoAmount,
fiatAmount = stakingFiatAmount,
cryptoValue = stringReference(
@ -167,10 +170,6 @@ internal class TokenDetailsLoadedBalanceConverter(
onStakeClicked = clickIntents::onStakeBannerClick,
)
}
return state.stakingBlocksState.copy(
stakingAvailable = state.stakingBlocksState.stakingAvailable,
stakingBalance = stakingBalance,
)
}
private fun getMarketPriceState(

View file

@ -54,10 +54,7 @@ internal class TokenDetailsSkeletonStateConverter(
selectedBalanceType = BalanceType.ALL,
),
marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol),
stakingBlocksState = StakingBlocksState(
stakingAvailable = StakingAvailable.Loading(iconState),
stakingBalance = StakingBalance.Empty,
),
stakingBlocksState = StakingBlockUM.Loading(iconState),
notifications = persistentListOf(),
pendingTxs = persistentListOf(),
swapTxs = persistentListOf(),

View file

@ -3,9 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import arrow.core.Either
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlocksState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
@ -14,31 +12,27 @@ import com.tangem.utils.converter.Converter
internal class TokenStakingStateConverter(
private val clickIntents: TokenDetailsClickIntents,
private val currentStateProvider: Provider<TokenDetailsState>,
) : Converter<Either<Throwable, StakingEntryInfo>, StakingBlocksState> {
) : Converter<Either<Throwable, StakingEntryInfo>, StakingBlockUM> {
override fun convert(value: Either<Throwable, StakingEntryInfo>): StakingBlocksState {
value.fold(
override fun convert(value: Either<Throwable, StakingEntryInfo>): StakingBlockUM {
val state = currentStateProvider()
if (state.stakingBlocksState is StakingBlockUM.Staked) return state.stakingBlocksState
val iconState = state.tokenInfoBlockState.iconState
return value.fold(
ifLeft = {
return StakingBlocksState(
stakingAvailable = StakingAvailable.Error(
iconState = currentStateProvider().tokenInfoBlockState.iconState,
),
stakingBalance = StakingBalance.Empty,
)
StakingBlockUM.Error(iconState = iconState)
},
ifRight = {
return StakingBlocksState(
stakingAvailable = StakingAvailable.Content(
interestRate = BigDecimalFormatter.formatPercent(
percent = it.interestRate,
useAbsoluteValue = true,
),
periodInDays = it.periodInDays,
tokenSymbol = it.tokenSymbol,
iconState = currentStateProvider().tokenInfoBlockState.iconState,
onStakeClicked = clickIntents::onStakeBannerClick,
StakingBlockUM.StakeAvailable(
interestRate = BigDecimalFormatter.formatPercent(
percent = it.interestRate,
useAbsoluteValue = true,
),
stakingBalance = StakingBalance.Empty,
periodInDays = it.periodInDays,
tokenSymbol = it.tokenSymbol,
iconState = iconState,
onStakeClicked = clickIntents::onStakeBannerClick,
)
},
)

View file

@ -41,8 +41,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
@ -52,7 +51,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.swapTransactionsItems
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.StakingBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
// TODO: Split to blocks [REDACTED_JIRA]
@ -141,25 +139,13 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
}
if (state.isStakingBlockShown) {
if (state.stakingBlocksState.stakingBalance is StakingBalance.Content) {
item(
key = StakingBalance::class.java,
contentType = StakingBalance::class.java,
content = {
StakingBalanceBlock(
state = state.stakingBlocksState.stakingBalance,
modifier = itemModifier,
)
},
)
}
item(
key = StakingAvailable::class.java,
contentType = StakingAvailable::class.java,
key = StakingBlockUM::class.java,
contentType = StakingBlockUM::class.java,
content = {
TokenStakingBlock(
modifier = itemModifier,
state = state.stakingBlocksState.stakingAvailable,
state = state.stakingBlocksState,
)
},
)

View file

@ -21,13 +21,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakedBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Strings
@Composable
fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modifier) {
internal fun StakingBalanceBlock(state: StakingBlockUM.Staked, modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
@ -87,21 +87,18 @@ fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modi
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun StakingBalanceBlock_Preview(
@PreviewParameter(StakingBalanceBlockPreviewProvider::class) data: StakingBalance,
@PreviewParameter(StakingBalanceBlockPreviewProvider::class) data: StakingBlockUM.Staked,
) {
TangemThemePreview {
StakingBalanceBlock(
state = data as StakingBalance.Content,
state = data,
modifier = Modifier.padding(TangemTheme.dimens.spacing16),
)
}
}
private class StakingBalanceBlockPreviewProvider : PreviewParameterProvider<StakingBalance> {
override val values: Sequence<StakingBalance>
get() = sequenceOf(
TokenDetailsPreviewData.tokenDetailsState_1.stakingBlocksState.stakingBalance,
TokenDetailsPreviewData.tokenDetailsState_2.stakingBlocksState.stakingBalance,
)
private class StakingBalanceBlockPreviewProvider : PreviewParameterProvider<StakingBlockUM.Staked> {
override val values: Sequence<StakingBlockUM.Staked>
get() = sequenceOf(stakedBlock)
}
// endregion

View file

@ -10,7 +10,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -23,8 +22,11 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA
import com.tangem.core.ui.utils.GrayscaleColorFilter
import com.tangem.core.ui.utils.NORMAL_ALPHA
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingAvailableBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingErrorBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData.stakingLoadingBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.CurrencyIcon
import com.tangem.features.tokendetails.impl.R
@ -35,7 +37,32 @@ import com.tangem.features.tokendetails.impl.R
* @param modifier modifier
*/
@Composable
internal fun TokenStakingBlock(state: StakingAvailable, modifier: Modifier = Modifier) {
internal fun TokenStakingBlock(state: StakingBlockUM, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = state,
contentAlignment = Alignment.CenterStart,
label = "Staking block animation",
) {
when (it) {
is StakingBlockUM.Error -> Row {} // TODO staking error
is StakingBlockUM.Loading -> StakingLoading(
iconState = it.iconState,
modifier = modifier,
)
is StakingBlockUM.Staked -> StakingBalanceBlock(
state = it,
modifier = modifier,
)
is StakingBlockUM.StakeAvailable -> StakingAvailableContent(
state = it,
modifier = modifier,
)
}
}
}
@Composable
private fun StakingAvailableContent(state: StakingBlockUM.StakeAvailable, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(
@ -43,46 +70,11 @@ internal fun TokenStakingBlock(state: StakingAvailable, modifier: Modifier = Mod
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size72)
.padding(all = TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
horizontalAlignment = Alignment.Start,
) {
Content(state = state)
}
}
@Composable
private fun Content(state: StakingAvailable, modifier: Modifier = Modifier) {
AnimatedContent(
modifier = modifier.heightIn(min = TangemTheme.dimens.size60),
targetState = state,
contentAlignment = Alignment.CenterStart,
label = "Update the content",
) { stakingBlockState ->
when (stakingBlockState) {
is StakingAvailable.Content -> {
StakingContent(
stakingBlockState = stakingBlockState,
iconState = stakingBlockState.iconState,
)
}
is StakingAvailable.Loading -> {
StakingLoading(
iconState = stakingBlockState.iconState,
)
}
is StakingAvailable.Error -> Row {} // TODO staking
}
}
}
@Composable
private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconState: IconState) {
Column {
Row {
val (alpha, colorFilter) = remember(iconState.isGrayscale) {
if (iconState.isGrayscale) {
val (alpha, colorFilter) = remember(state.iconState.isGrayscale) {
if (state.iconState.isGrayscale) {
GRAY_SCALE_ALPHA to GrayscaleColorFilter
} else {
NORMAL_ALPHA to null
@ -93,7 +85,7 @@ private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconStat
.size(TangemTheme.dimens.size20)
.clip(TangemTheme.shapes.roundedCorners8)
.align(Alignment.CenterVertically),
icon = iconState,
icon = state.iconState,
alpha = alpha,
colorFilter = colorFilter,
)
@ -102,7 +94,7 @@ private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconStat
Text(
text = stringResource(
R.string.token_details_staking_block_title,
stakingBlockState.interestRate,
state.interestRate,
),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle2,
@ -113,8 +105,8 @@ private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconStat
Text(
text = stringResource(
R.string.token_details_staking_block_subtitle,
stakingBlockState.tokenSymbol,
stakingBlockState.periodInDays,
state.tokenSymbol,
state.periodInDays,
),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
@ -126,14 +118,24 @@ private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconStat
SecondaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.common_stake),
onClick = stakingBlockState.onStakeClicked,
onClick = state.onStakeClicked,
)
}
}
@Composable
private fun StakingLoading(iconState: IconState) {
Column {
private fun StakingLoading(iconState: IconState, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(
color = TangemTheme.colors.background.primary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size72)
.padding(all = TangemTheme.dimens.spacing12),
) {
Row {
val (alpha, colorFilter) = remember(iconState.isGrayscale) {
if (iconState.isGrayscale) {
@ -181,31 +183,18 @@ private fun StakingLoading(iconState: IconState) {
@Composable
private fun Preview_TokenStakingBlock(
@PreviewParameter(StakingBlockStateProvider::class)
state: StakingAvailable,
state: StakingBlockUM,
) {
TangemThemePreview {
TokenStakingBlock(state = state)
}
}
private class StakingBlockStateProvider : CollectionPreviewParameterProvider<StakingAvailable>(
private class StakingBlockStateProvider : CollectionPreviewParameterProvider<StakingBlockUM>(
collection = listOf(
StakingAvailable.Content(
iconState = iconState,
interestRate = "10",
periodInDays = 4,
tokenSymbol = "SOL",
onStakeClicked = {},
),
StakingAvailable.Loading(iconState = iconState),
StakingAvailable.Error(iconState = iconState),
stakingLoadingBlock,
stakingAvailableBlock,
stakingErrorBlock,
),
)
private val iconState = IconState.TokenIcon(
url = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/stellar.png",
fallbackTint = Color.Cyan,
fallbackBackground = Color.Blue,
isGrayscale = false,
)
// endregion Preview