Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-03 18:14:36 +05:00
parent d61801a6d4
commit 10d959b0de
18 changed files with 467 additions and 162 deletions

View file

@ -90,6 +90,7 @@ fun InputRowDefault(
bottom = TangemTheme.dimens.spacing10,
)
.clickable(
enabled = onIconClick != null,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) { onIconClick?.invoke() },

View file

@ -36,13 +36,13 @@ import com.tangem.core.ui.res.TangemThemePreview
@Suppress("LongParameterList")
@Composable
fun InputRowImageInfo(
title: TextReference?,
subtitle: TextReference,
caption: TextReference,
infoTitle: TextReference,
infoSubtitle: TextReference,
imageUrl: String,
modifier: Modifier = Modifier,
title: TextReference? = null,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
) {

View file

@ -4,4 +4,5 @@ object Strings {
const val STARS = "\u2217\u2217\u2217"
const val DOT = ""
const val PLUS = "+"
}

View file

@ -1,8 +1,8 @@
package com.tangem.features.staking.impl.presentation.state
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.staking.model.BalanceType
import com.tangem.domain.staking.model.Yield
import kotlinx.collections.immutable.ImmutableList
sealed class InnerYieldBalanceState {
data class Data(
@ -16,15 +16,21 @@ sealed class InnerYieldBalanceState {
}
data class BalanceGroupedState(
val items: List<BalanceState>,
val type: BalanceType,
val items: ImmutableList<BalanceState>,
val footer: TextReference?,
val title: TextReference,
)
data class BalanceState(
val validator: Yield.Validator,
val cryptoValue: String,
val cryptoAmount: TextReference,
val fiatAmount: TextReference,
val rawCurrencyId: String?,
)
)
enum class BalanceGroupType {
ACTIVE,
UNSTAKED,
UNKNOWN,
}

View file

@ -39,6 +39,7 @@ internal class StakingStateController @Inject constructor() {
currentStep = StakingStep.InitialInfo,
initialInfoState = StakingStates.InitialInfoState.Empty(),
amountState = AmountState.Empty(),
rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(),
confirmStakingState = StakingStates.ConfirmStakingState.Empty(),
isBalanceHidden = false,
event = consumedEvent(),

View file

@ -28,6 +28,7 @@ internal class StakingStateRouter(
fun onNextClick() {
when (stateController.uiState.value.currentStep) {
StakingStep.InitialInfo -> showAmount()
StakingStep.RewardsValidators,
StakingStep.Validators,
StakingStep.Amount,
-> showConfirm()
@ -50,6 +51,10 @@ internal class StakingStateRouter(
stateController.update { it.copy(currentStep = StakingStep.InitialInfo) }
}
fun showRewardsValidators() {
stateController.update { it.copy(currentStep = StakingStep.RewardsValidators) }
}
fun showAmount() {
stateController.update { it.copy(currentStep = StakingStep.Amount) }
}

View file

@ -19,6 +19,7 @@ internal data class StakingUiState(
val currentStep: StakingStep,
val initialInfoState: StakingStates.InitialInfoState,
val amountState: AmountState,
val rewardsValidatorsState: StakingStates.RewardsValidatorsState,
val confirmStakingState: StakingStates.ConfirmStakingState,
val isBalanceHidden: Boolean,
val bottomSheetConfig: TangemBottomSheetConfig?,
@ -61,6 +62,18 @@ internal sealed class StakingStates {
) : InitialInfoState()
}
/** Select validator to claim rewards state */
sealed class RewardsValidatorsState : StakingStates() {
data class Data(
override val isPrimaryButtonEnabled: Boolean,
val rewards: ImmutableList<BalanceState>,
) : RewardsValidatorsState()
data class Empty(
override val isPrimaryButtonEnabled: Boolean = false,
) : RewardsValidatorsState()
}
/** Confirm state */
sealed class ConfirmStakingState : StakingStates() {
data class Data(
@ -81,6 +94,7 @@ internal sealed class StakingStates {
enum class StakingStep {
InitialInfo,
RewardsValidators,
Amount,
Confirm,
Validators,

View file

@ -0,0 +1,100 @@
package com.tangem.features.staking.impl.presentation.state.converters
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.BalanceItem
import com.tangem.domain.staking.model.BalanceType
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.utils.Provider
import com.tangem.utils.Strings.PLUS
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.addOrReplace
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
internal class RewardsValidatorStateConverter(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val yield: Yield,
) : Converter<Unit, StakingStates.RewardsValidatorsState> {
override fun convert(value: Unit): StakingStates.RewardsValidatorsState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance
return if (yieldBalance is YieldBalance.Data) {
val balances = yieldBalance.balance.items
StakingStates.RewardsValidatorsState.Data(
isPrimaryButtonEnabled = true,
rewards = balances
// todo remove when real data is available
.addOrReplace(
item = balances.first().copy(
type = BalanceType.REWARDS,
),
predicate = { true },
)
.filter { it.type == BalanceType.REWARDS }
.mapRewardBalances(cryptoCurrencyStatus)
.toPersistentList(),
)
} else {
StakingStates.RewardsValidatorsState.Empty()
}
}
private fun List<BalanceItem>.mapRewardBalances(cryptoCurrencyStatus: CryptoCurrencyStatus) =
this.mapNotNull { balance ->
val validator = yield.validators.firstOrNull {
it.address.contains(balance.validatorAddress.orEmpty(), ignoreCase = true)
}
val cryptoValue = balance.amount.times(balance.pricePerShare)
val fiatValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoValue)
validator?.toBalanceState(
cryptoCurrencyStatus = cryptoCurrencyStatus,
cryptoValue = cryptoValue,
fiatValue = fiatValue,
)
}
private fun Yield.Validator.toBalanceState(
cryptoCurrencyStatus: CryptoCurrencyStatus,
cryptoValue: BigDecimal,
fiatValue: BigDecimal?,
): BalanceState {
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
val cryptoAmount = stringReference(
BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = cryptoValue,
cryptoCurrency = cryptoCurrency,
),
)
val fiatAmount = combinedReference(
stringReference(PLUS),
stringReference(
BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatValue,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
),
)
return BalanceState(
validator = this,
cryptoValue = cryptoValue.parseBigDecimal(cryptoCurrency.decimals),
cryptoAmount = cryptoAmount,
fiatAmount = fiatAmount,
rawCurrencyId = cryptoCurrency.id.rawCurrencyId,
)
}
}

View file

@ -0,0 +1,138 @@
package com.tangem.features.staking.impl.presentation.state.converters
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.*
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceGroupType
import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.addOrReplace
import com.tangem.utils.isNullOrZero
import kotlinx.collections.immutable.toPersistentList
internal class YieldBalancesConverter(
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val yield: Yield,
) : Converter<Unit, InnerYieldBalanceState> {
override fun convert(value: Unit): InnerYieldBalanceState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance
return if (yieldBalance is YieldBalance.Data) {
val cryptoRewardsValue = yieldBalance.getRewardStakingBalance()
val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue)
val groupedBalances = getGroupedBalance(yieldBalance.balance)
InnerYieldBalanceState.Data(
rewardsCrypto = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = cryptoRewardsValue,
cryptoCurrency = cryptoCurrency,
),
rewardsFiat = BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatRewardsValue,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
isRewardsToClaim = !cryptoRewardsValue.isNullOrZero(),
balance = groupedBalances,
)
} else {
InnerYieldBalanceState.Empty
}
}
private fun getGroupedBalance(balance: YieldBalanceItem) = balance.items
// todo remove when real data is available
.addOrReplace(
item = balance.items.first().copy(
type = BalanceType.REWARDS,
),
predicate = { true },
)
.groupBy { it.type.toGroup() }
.mapNotNull { item ->
val (title, footer) = getGroupTitle(item.key)
title?.let {
BalanceGroupedState(
items = item.value.mapBalances().toPersistentList(),
footer = footer,
title = it,
)
}
}
private fun List<BalanceItem>.mapBalances(): List<BalanceState> {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
return this.mapNotNull { balance ->
val validator = yield.validators.firstOrNull {
balance.validatorAddress?.contains(it.address, ignoreCase = true) == true
}
val cryptoAmount = balance.amount * balance.pricePerShare
val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount)
validator?.let {
BalanceState(
validator = validator,
cryptoValue = cryptoAmount.parseBigDecimal(cryptoCurrency.decimals),
cryptoAmount = stringReference(
BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = cryptoAmount,
cryptoCurrency = cryptoCurrency,
),
),
fiatAmount = stringReference(
BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatAmount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
),
rawCurrencyId = balance.rawCurrencyId,
)
}
}
}
private fun BalanceType.toGroup() = when (this) {
BalanceType.PREPARING,
BalanceType.STAKED,
BalanceType.REWARDS,
BalanceType.AVAILABLE,
BalanceType.LOCKED,
-> BalanceGroupType.ACTIVE
BalanceType.UNSTAKING,
BalanceType.UNLOCKING,
BalanceType.UNSTAKED,
-> BalanceGroupType.UNSTAKED
BalanceType.UNKNOWN,
-> BalanceGroupType.UNKNOWN
}
private fun getGroupTitle(type: BalanceGroupType) = when (type) {
BalanceGroupType.ACTIVE -> resourceReference(
R.string.staking_active,
) to resourceReference(
R.string.staking_active_footer,
)
BalanceGroupType.UNSTAKED -> resourceReference(
R.string.staking_unstaked,
) to resourceReference(
R.string.staking_unstaked_footer,
)
BalanceGroupType.UNKNOWN -> null to null
}
}

View file

@ -1,12 +1,12 @@
package com.tangem.features.staking.impl.presentation.state.previewdata
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.staking.model.BalanceType
import com.tangem.domain.staking.model.Yield
import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import kotlinx.collections.immutable.persistentListOf
internal object InitialStakingStatePreview {
val defaultState = StakingStates.InitialInfoState.Data(
@ -30,11 +30,11 @@ internal object InitialStakingStatePreview {
isRewardsToClaim = false,
balance = listOf(
BalanceGroupedState(
type = BalanceType.STAKED,
title = stringReference("Staked"),
footer = null,
items = listOf(
items = persistentListOf(
BalanceState(
cryptoValue = "100",
cryptoAmount = stringReference("100 SOL"),
fiatAmount = stringReference("100 $"),
rawCurrencyId = null,

View file

@ -27,4 +27,8 @@ object StakingClickIntentsStub : StakingClickIntents {
override fun openValidators() {}
override fun onValidatorSelect(validator: Yield.Validator) {}
override fun openRewardsValidators() {}
override fun selectRewardValidator(rewardValue: String) {}
}

View file

@ -9,19 +9,21 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.BalanceType
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.staking.model.YieldBalanceItem
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingStep
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import com.tangem.features.staking.impl.presentation.state.converters.RewardsValidatorStateConverter
import com.tangem.features.staking.impl.presentation.state.converters.YieldBalancesConverter
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal
@ -45,6 +47,14 @@ internal class SetInitialDataStateTransformer(
)
}
private val rewardsValidatorStateConverter by lazy(LazyThreadSafetyMode.NONE) {
RewardsValidatorStateConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield)
}
private val yieldBalancesConverter by lazy(LazyThreadSafetyMode.NONE) {
YieldBalancesConverter(cryptoCurrencyStatusProvider, appCurrencyProvider, yield)
}
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
clickIntents = clickIntents,
@ -52,6 +62,8 @@ internal class SetInitialDataStateTransformer(
initialInfoState = createInitialInfoState(),
amountState = createInitialAmountState(),
confirmStakingState = createInitialConfirmationState(),
rewardsValidatorsState = rewardsValidatorStateConverter.convert(Unit),
bottomSheetConfig = null,
)
}
@ -78,7 +90,7 @@ internal class SetInitialDataStateTransformer(
warmupPeriod = yield.metadata.warmupPeriod.days.toString(),
rewardSchedule = yield.metadata.rewardSchedule,
onInfoClick = clickIntents::onInfoClick,
yieldBalance = getStakedBalances(cryptoCurrencyStatus),
yieldBalance = yieldBalancesConverter.convert(Unit),
)
}
@ -116,96 +128,6 @@ internal class SetInitialDataStateTransformer(
return resourceReference(R.string.common_range, wrappedList(formattedMinApr, formattedMaxApr))
}
private fun getStakedBalances(cryptoCurrencyStatus: CryptoCurrencyStatus): InnerYieldBalanceState {
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance
return if (yieldBalance is YieldBalance.Data) {
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
val cryptoRewardsValue = yieldBalance.getRewardStakingBalance()
val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue)
val groupedBalances = getGroupedBalance(yieldBalance.balance, cryptoCurrencyStatus, appCurrency)
InnerYieldBalanceState.Data(
rewardsCrypto = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = cryptoRewardsValue,
cryptoCurrency = cryptoCurrency,
),
rewardsFiat = BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatRewardsValue,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
isRewardsToClaim = !cryptoRewardsValue.isNullOrZero(),
balance = groupedBalances,
)
} else {
InnerYieldBalanceState.Empty
}
}
private fun getGroupedBalance(
balance: YieldBalanceItem,
cryptoCurrencyStatus: CryptoCurrencyStatus,
appCurrency: AppCurrency,
) = balance.items
.sortedByDescending { it.type }
.groupBy { it.type }
.mapNotNull { item ->
val (title, footer) = getValidatorBalanceInto(item.key)
val balances = item.value.mapNotNull { balance ->
val validator = yield.validators.firstOrNull {
balance.validatorAddress?.contains(it.address, ignoreCase = true) == true
}
val cryptoAmount = balance.amount * balance.pricePerShare
val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount)
validator?.let {
BalanceState(
validator = validator,
cryptoAmount = stringReference(
BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = balance.amount,
cryptoCurrency = cryptoCurrencyStatus.currency,
),
),
fiatAmount = stringReference(
BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatAmount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
),
rawCurrencyId = balance.rawCurrencyId,
)
}
}
title?.let {
BalanceGroupedState(
items = balances,
type = item.key,
footer = footer,
title = it,
)
}
}
private fun getValidatorBalanceInto(type: BalanceType) = when (type) {
BalanceType.PREPARING,
BalanceType.STAKED,
-> resourceReference(R.string.staking_active) to resourceReference(R.string.staking_active_footer)
BalanceType.UNSTAKING,
BalanceType.UNLOCKING,
BalanceType.UNSTAKED,
-> resourceReference(R.string.staking_unstaked) to resourceReference(R.string.staking_unstaked_footer)
BalanceType.AVAILABLE,
BalanceType.LOCKED,
BalanceType.UNKNOWN,
BalanceType.REWARDS,
-> null to null
}
companion object {
private val EQUALITY_THRESHOLD = BigDecimal(1E-10)
}

View file

@ -0,0 +1,73 @@
package com.tangem.features.staking.impl.presentation.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import com.tangem.core.ui.components.inputrow.InputRowImageInfo
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import com.tangem.utils.extensions.orZero
@Composable
internal fun StakingClaimRewardsValidatorContent(
state: StakingStates.RewardsValidatorsState,
clickIntents: StakingClickIntents,
modifier: Modifier = Modifier,
) {
if (state !is StakingStates.RewardsValidatorsState.Data) return
Column(
modifier = Modifier // Do not put fillMaxSize() in here
.background(TangemTheme.colors.background.tertiary)
.padding(horizontal = TangemTheme.dimens.spacing12)
.verticalScroll(rememberScrollState()),
) {
state.rewards.forEachIndexed { index, item ->
key(item.validator.address) {
InputRowImageInfo(
subtitle = stringReference(item.validator.name),
caption = combinedReference(
resourceReference(R.string.staking_details_apr),
annotatedReference(
buildAnnotatedString {
appendSpace()
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
append(
BigDecimalFormatter.formatPercent(
percent = item.validator.apr.orZero(),
useAbsoluteValue = true,
),
)
}
},
),
),
infoTitle = item.fiatAmount,
infoSubtitle = item.cryptoAmount,
imageUrl = item.validator.image.orEmpty(),
modifier = modifier
.roundedShapeItemDecoration(index, state.rewards.lastIndex, false)
.background(TangemTheme.colors.background.action)
.clickable(
onClick = {
clickIntents.selectRewardValidator(item.cryptoValue)
},
),
)
}
}
}
}

View file

@ -4,13 +4,18 @@ import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -34,12 +39,14 @@ import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import com.tangem.utils.Strings.DOT
import com.tangem.utils.extensions.orZero
@Composable
internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) {
internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, clickIntents: StakingClickIntents) {
if (state !is StakingStates.InitialInfoState.Data) return
Column(
@ -63,6 +70,7 @@ internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) {
rewardCrypto = it.rewardsCrypto,
rewardFiat = it.rewardsFiat,
isRewardsToClaim = it.isRewardsToClaim,
onRewardsClick = clickIntents::openRewardsValidators,
)
}
}
@ -188,7 +196,12 @@ internal fun StakingDetailsRows(state: StakingStates.InitialInfoState.Data) {
}
@Composable
private fun StakingRewardBlock(rewardCrypto: String, rewardFiat: String, isRewardsToClaim: Boolean) {
private fun StakingRewardBlock(
rewardCrypto: String,
rewardFiat: String,
isRewardsToClaim: Boolean,
onRewardsClick: () -> Unit,
) {
val (text, textColor) = if (isRewardsToClaim) {
annotatedReference(
buildAnnotatedString {
@ -211,37 +224,46 @@ private fun StakingRewardBlock(rewardCrypto: String, rewardFiat: String, isRewar
textColor = textColor,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
.background(TangemTheme.colors.background.action)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
onClick = onRewardsClick,
),
)
}
@Composable
private fun ActiveStakingBlock(groupes: List<BalanceGroupedState>) {
private fun ActiveStakingBlock(groups: List<BalanceGroupedState>) {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
groupes.forEach { group ->
FooterContainer(
footer = group.footer?.resolveReference(),
modifier = Modifier,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
groups.forEach { group ->
key(group.title) {
FooterContainer(
footer = group.footer?.resolveReference(),
modifier = Modifier,
) {
group.items.forEachIndexed { index, balance ->
InputRowImageInfo(
title = group.title.takeIf { index == 0 },
subtitle = stringReference(balance.validator.name),
caption = stringReference(
BigDecimalFormatter.formatPercent(balance.validator.apr.orZero(), true),
),
infoTitle = balance.fiatAmount,
infoSubtitle = balance.cryptoAmount,
imageUrl = balance.validator.image.orEmpty(),
)
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
) {
group.items.forEachIndexed { index, balance ->
key(balance.validator.address) {
InputRowImageInfo(
title = group.title.takeIf { index == 0 },
subtitle = stringReference(balance.validator.name),
caption = stringReference(
BigDecimalFormatter.formatPercent(balance.validator.apr.orZero(), true),
),
infoTitle = balance.fiatAmount,
infoSubtitle = balance.cryptoAmount,
imageUrl = balance.validator.image.orEmpty(),
)
}
}
}
}
}
@ -278,6 +300,7 @@ private fun StakingInitialInfoContent_Preview(
TangemThemePreview {
StakingInitialInfoContent(
state = feeState,
clickIntents = StakingClickIntentsStub,
)
}
}

View file

@ -1,8 +1,6 @@
package com.tangem.features.staking.impl.presentation.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@ -84,22 +82,29 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier
SpacerW12()
}
}
TangemButton(
text = stringResource(buttonTextId),
icon = buttonIcon,
enabled = isButtonEnabled,
onClick = {
if (isStakingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
buttonClick()
},
showProgress = false,
modifier = Modifier.fillMaxWidth(),
colors = TangemButtonsDefaults.primaryButtonColors,
)
AnimatedVisibility(
visible = buttonClick != null,
enter = fadeIn(),
exit = fadeOut(),
) {
TangemButton(
text = stringResource(buttonTextId),
icon = buttonIcon,
enabled = isButtonEnabled && buttonClick != null,
onClick = {
if (isStakingState) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
if (buttonClick != null) buttonClick()
},
showProgress = false,
modifier = Modifier.fillMaxWidth(),
colors = TangemButtonsDefaults.primaryButtonColors,
)
}
}
}
private fun getButtonData(currentState: StakingUiState): Pair<Int, () -> Unit> {
private fun getButtonData(currentState: StakingUiState): Pair<Int, (() -> Unit)?> {
return when (currentState.currentStep) {
StakingStep.InitialInfo -> {
val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data
@ -114,11 +119,13 @@ private fun getButtonData(currentState: StakingUiState): Pair<Int, () -> Unit> {
StakingStep.Confirm -> R.string.common_stake to currentState.clickIntents::onNextClick
StakingStep.Validators -> R.string.common_continue to currentState.clickIntents::onNextClick
StakingStep.Success -> R.string.common_close to currentState.clickIntents::onBackClick
else -> R.string.common_next to null
}
}
private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) {
StakingStep.InitialInfo,
StakingStep.RewardsValidators,
StakingStep.Confirm,
StakingStep.Success,
-> false
@ -132,6 +139,7 @@ private fun isButtonEnabled(uiState: StakingUiState): Boolean {
StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled
StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled
StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled
StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled
StakingStep.Success -> true
StakingStep.Validators -> true
}

View file

@ -65,6 +65,7 @@ private fun SendAppBar(uiState: StakingUiState) {
val titleRes = when (uiState.currentStep) {
StakingStep.Amount -> stringResource(id = R.string.send_amount_label)
StakingStep.InitialInfo,
StakingStep.RewardsValidators,
StakingStep.Validators,
StakingStep.Confirm,
-> stringResource(id = R.string.common_stake)
@ -78,7 +79,9 @@ private fun SendAppBar(uiState: StakingUiState) {
-> {
R.drawable.ic_close_24
}
StakingStep.InitialInfo -> {
StakingStep.RewardsValidators,
StakingStep.InitialInfo,
-> {
R.drawable.ic_back_24
}
}
@ -137,7 +140,14 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M
when (state) {
StakingStep.InitialInfo -> StakingInitialInfoContent(
state = uiState.initialInfoState,
clickIntents = uiState.clickIntents,
)
StakingStep.RewardsValidators -> {
StakingClaimRewardsValidatorContent(
state = uiState.rewardsValidatorsState,
clickIntents = uiState.clickIntents,
)
}
StakingStep.Amount -> AmountScreenContent(
amountState = uiState.amountState,
isBalanceHiding = uiState.isBalanceHidden,

View file

@ -19,4 +19,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun openValidators()
fun onValidatorSelect(validator: Yield.Validator)
fun openRewardsValidators()
fun selectRewardValidator(rewardValue: String)
}

View file

@ -21,9 +21,9 @@ import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.staking.impl.navigation.InnerStakingRouter
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.StakingStateController
import com.tangem.features.staking.impl.presentation.state.StakingStateRouter
import com.tangem.features.staking.impl.presentation.state.StakingStep
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.transformers.*
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer
@ -89,22 +89,8 @@ internal class StakingViewModel @Inject constructor(
override fun onNextClick() {
stakingStateRouter.onNextClick()
when (value.currentStep) {
StakingStep.Confirm -> {
initStaking()
}
StakingStep.InitialInfo -> {
// TODO staking
}
StakingStep.Amount -> {
// TODO staking
}
StakingStep.Success -> {
// TODO staking
}
StakingStep.Validators -> {
// TODO staking
}
if (value.currentStep == StakingStep.Confirm) {
initStaking()
}
}
@ -175,6 +161,15 @@ internal class StakingViewModel @Inject constructor(
stateController.update(ValidatorSelectChangeTransformer(validator))
}
override fun openRewardsValidators() {
stakingStateRouter.showRewardsValidators()
}
override fun selectRewardValidator(rewardValue: String) {
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, rewardValue))
stakingStateRouter.onNextClick()
}
fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) {
innerRouter = router
this.stakingStateRouter = stateRouter