Updated on 2026-08-14

This commit is contained in:
Tangem 2025-03-11 11:56:37 +05:00
parent 3eb843a05f
commit 9c6d5fc4fa
15 changed files with 192 additions and 73 deletions

View file

@ -16,6 +16,8 @@ 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.navigation.share.ShareManager
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -508,19 +510,34 @@ internal class StakingModel @Inject constructor(
analyticsEventHandler.send(StakingAnalyticsEvent.ButtonRewards)
val rewardsValidators =
stateController.value.rewardsValidatorsState as? StakingStates.RewardsValidatorsState.Data
val rewards = rewardsValidators?.rewards
if (rewards != null && rewards.isSingleItem()) {
onActiveStake(rewards.first())
} else {
analyticsEventHandler.send(
StakingAnalyticsEvent.ButtonValidator(
source = StakeScreenSource.Info,
),
val initialInfoState = stateController.value.initialInfoState as? StakingStates.InitialInfoState.Data
val yieldBalance = initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data
val rewardBlockType = yieldBalance?.reward?.rewardBlockType
val rewardPendingActionConstraints = yieldBalance?.reward?.rewardConstraints
if (rewardBlockType == RewardBlockType.RewardsRequirementsError) {
stakingEventFactory.createStakingRewardsMinimumRequirementsErrorAlert(
cryptoCurrencyName = cryptoCurrencyStatus.currency.name,
cryptoAmountValue = rewardPendingActionConstraints?.amountArg?.minimum?.format {
crypto(cryptoCurrencyStatus.currency)
}.orEmpty(),
)
stateController.update {
value.copy(actionType = StakingActionCommonType.Pending.Rewards)
} else {
val rewards = rewardsValidators?.rewards
if (rewards != null && rewards.isSingleItem()) {
onActiveStake(rewards.first())
} else {
analyticsEventHandler.send(
StakingAnalyticsEvent.ButtonValidator(
source = StakeScreenSource.Info,
),
)
stateController.update {
value.copy(actionType = StakingActionCommonType.Pending.Rewards)
}
stakingStateRouter.showRewardsValidators()
}
stakingStateRouter.showRewardsValidators()
}
}

View file

@ -2,18 +2,14 @@ package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.RewardBlockType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.*
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
@Immutable
internal sealed class InnerYieldBalanceState {
data class Data(
val rewardsCrypto: String,
val rewardsFiat: String,
val rewardBlockType: RewardBlockType,
val reward: YieldReward,
val isActionable: Boolean,
val balances: ImmutableList<BalanceState>,
) : InnerYieldBalanceState()
@ -37,4 +33,12 @@ internal data class BalanceState(
val validator: Yield.Validator?,
val pendingActions: ImmutableList<PendingAction>,
val isPending: Boolean,
)
@Immutable
internal data class YieldReward(
val rewardsCrypto: String,
val rewardsFiat: String,
val rewardBlockType: RewardBlockType,
val rewardConstraints: PendingActionConstraints?,
)

View file

@ -6,8 +6,10 @@ import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.YieldReward
import com.tangem.lib.crypto.BlockchainUtils.isBSC
import com.tangem.lib.crypto.BlockchainUtils.isSolana
import com.tangem.utils.Provider
@ -33,7 +35,8 @@ internal class YieldBalancesConverter(
val balanceToShowItems = balancesToShowProvider()
return if (yieldBalance is YieldBalance.Data || balanceToShowItems.any { it.isPending }) {
val cryptoRewardsValue = (yieldBalance as? YieldBalance.Data)?.getRewardStakingBalance()
val yieldBalance = yieldBalance as? YieldBalance.Data
val cryptoRewardsValue = yieldBalance?.getRewardStakingBalance()
val fiatRate = cryptoCurrencyStatus.value.fiatRate
val fiatRewardsValue = if (fiatRate != null && cryptoRewardsValue != null) {
@ -41,17 +44,25 @@ internal class YieldBalancesConverter(
} else {
null
}
val (type, isActionable) = getRewardBlockType()
val type = getRewardBlockType()
val pendingRewardsConstraints = yieldBalance?.balance?.items
?.firstOrNull { it.type == BalanceType.REWARDS }
?.pendingActionsConstraints
?.firstOrNull { it.type == StakingActionType.CLAIM_REWARDS }
InnerYieldBalanceState.Data(
rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) },
rewardsFiat = fiatRewardsValue.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
rewardBlockType = type,
isActionable = isActionable,
reward = YieldReward(
rewardsCrypto = cryptoRewardsValue.format { crypto(cryptoCurrency) },
rewardsFiat = fiatRewardsValue.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
rewardBlockType = type,
rewardConstraints = pendingRewardsConstraints,
),
isActionable = type.isActionable,
balances = balanceToShowItems.mapBalances(),
)
} else {
@ -66,7 +77,7 @@ internal class YieldBalancesConverter(
.sortedBy { it.type.order }
.toPersistentList()
private fun getRewardBlockType(): Pair<RewardBlockType, Boolean> {
private fun getRewardBlockType(): RewardBlockType {
val blockchainId = cryptoCurrencyStatus.currency.network.id.value
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance as? YieldBalance.Data
val rewards = yieldBalance?.balance?.items
@ -76,9 +87,10 @@ internal class YieldBalancesConverter(
val isRewardsClaimable = rewards?.isNotEmpty() == true
return when {
isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable to false
isRewardsClaimable -> RewardBlockType.Rewards to isActionable
else -> RewardBlockType.NoRewards to false
isSolana(blockchainId) || isBSC(blockchainId) -> RewardBlockType.RewardUnavailable
isRewardsClaimable && isActionable -> RewardBlockType.Rewards
isRewardsClaimable && !isActionable -> RewardBlockType.RewardsRequirementsError
else -> RewardBlockType.NoRewards
}
}
}

View file

@ -68,4 +68,17 @@ internal sealed class StakingAlertUM : AlertUM {
override val message: TextReference = resourceReference(id = R.string.staking_notification_ton_activate_account)
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
}
data class RewardsMinimumRequirementsError(
val cryptoCurrencyName: String,
val cryptoAmountValue: String,
) : StakingAlertUM() {
override val onConfirmClick: (() -> Unit)? = null
override val title: TextReference? = null
override val message: TextReference = resourceReference(
id = R.string.staking_details_min_rewards_notification,
formatArgs = wrappedList(cryptoCurrencyName, cryptoAmountValue),
)
override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok)
}
}

View file

@ -51,4 +51,15 @@ internal class StakingEventFactory(
val alert = StakingEvent.ShowAlert(alert = StakingAlertUM.InitializeAccount)
stateController.updateEvent(alert)
}
fun createStakingRewardsMinimumRequirementsErrorAlert(cryptoCurrencyName: String, cryptoAmountValue: String) {
stateController.updateEvent(
StakingEvent.ShowAlert(
alert = StakingAlertUM.RewardsMinimumRequirementsError(
cryptoCurrencyName = cryptoCurrencyName,
cryptoAmountValue = cryptoAmountValue,
),
),
)
}
}

View file

@ -1,9 +1,9 @@
package com.tangem.features.staking.impl.presentation.state.previewdata
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.RewardBlockType
import com.tangem.domain.staking.model.stakekit.Yield
@ -11,6 +11,7 @@ import com.tangem.features.staking.impl.R
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 com.tangem.features.staking.impl.presentation.state.YieldReward
import kotlinx.collections.immutable.persistentListOf
internal object InitialStakingStatePreview {
@ -63,9 +64,12 @@ internal object InitialStakingStatePreview {
val stateWithYield = defaultState.copy(
yieldBalance = InnerYieldBalanceState.Data(
rewardsFiat = "100 $",
rewardsCrypto = "100 SOL",
rewardBlockType = RewardBlockType.RewardUnavailable,
reward = YieldReward(
rewardsFiat = "100 $",
rewardsCrypto = "100 SOL",
rewardBlockType = RewardBlockType.RewardUnavailable,
rewardConstraints = null,
),
isActionable = true,
balances = persistentListOf(
BalanceState(

View file

@ -43,28 +43,7 @@ internal class SetConfirmationStateInitTransformer(
@Suppress("CyclomaticComplexMethod")
override fun transform(prevState: StakingUiState): StakingUiState {
val actionType = when {
isEnter -> StakingActionCommonType.Enter(
skipEnterAmount = yieldArgs.enter.isPartialAmountDisabled,
)
isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit(isPartiallyUnstakeDisabled(prevState))
else -> when (pendingAction?.type) {
StakingActionType.STAKE -> StakingActionCommonType.Pending.Stake(
skipEnterAmount = yieldArgs.enter.isPartialAmountDisabled,
)
StakingActionType.UNSTAKE -> StakingActionCommonType.Exit(
partiallyUnstakeDisabled = isPartiallyUnstakeDisabled(prevState),
)
StakingActionType.CLAIM_REWARDS,
StakingActionType.RESTAKE_REWARDS,
-> StakingActionCommonType.Pending.Rewards
StakingActionType.VOTE_LOCKED,
StakingActionType.RESTAKE,
-> StakingActionCommonType.Pending.Restake
else -> StakingActionCommonType.Pending.Other
}
}
val actionType = getActionType(prevState)
return prevState.copy(
actionType = actionType,
balanceState = balanceState,
@ -88,6 +67,26 @@ internal class SetConfirmationStateInitTransformer(
)
}
private fun getActionType(prevState: StakingUiState): StakingActionCommonType {
val isPartialEnterAmountDisabled = yieldArgs.enter.isPartialAmountDisabled
val isPartialExitAmountDisabled = isPartiallyUnstakeDisabled(prevState)
return when {
isEnter -> StakingActionCommonType.Enter(isPartialEnterAmountDisabled)
isImplicitExit || isExplicitExit -> StakingActionCommonType.Exit(isPartialExitAmountDisabled)
else -> when (pendingAction?.type) {
StakingActionType.STAKE -> StakingActionCommonType.Pending.Stake(isPartialEnterAmountDisabled)
StakingActionType.UNSTAKE -> StakingActionCommonType.Exit(isPartialExitAmountDisabled)
StakingActionType.CLAIM_REWARDS,
StakingActionType.RESTAKE_REWARDS,
-> StakingActionCommonType.Pending.Rewards
StakingActionType.VOTE_LOCKED,
StakingActionType.RESTAKE,
-> StakingActionCommonType.Pending.Restake
else -> StakingActionCommonType.Pending.Other
}
}
}
private fun isPartiallyUnstakeDisabled(state: StakingUiState): Boolean {
val isSolana = BlockchainUtils.isSolana(state.cryptoCurrencyBlockchainId)
val isValidatorPreferred = balanceState?.validator?.preferred == true

View file

@ -98,9 +98,7 @@ internal class AddStakingNotificationsTransformer(
onReload = prevState.clickIntents::getFee,
feeValue = feeValue,
)
addStakingErrorNotifications(
stakingError = stakingError,
)
addStakingErrorNotifications(stakingError = stakingError)
// warnings
addWarningNotifications(
prevState = prevState,

View file

@ -31,6 +31,7 @@ import androidx.compose.ui.unit.Density
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.components.inputrow.InputRowImageInfo
import com.tangem.core.ui.components.list.roundedListWithDividersItems
@ -38,19 +39,18 @@ import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.RewardBlockType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.model.StakingClickIntents
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 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.model.StakingClickIntents
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.extensions.orZero
@ -214,14 +214,17 @@ private fun StakingRewardBlock(
onRewardsClick: () -> Unit,
isBalanceHidden: Boolean,
) {
val (text, textColor) = when (yieldBalanceState.rewardBlockType) {
RewardBlockType.Rewards -> {
val reward = yieldBalanceState.reward
val (text, textColor) = when (yieldBalanceState.reward.rewardBlockType) {
RewardBlockType.RewardsRequirementsError,
RewardBlockType.Rewards,
-> {
annotatedReference {
append(yieldBalanceState.rewardsFiat.orMaskWithStars(isBalanceHidden))
append(reward.rewardsFiat.orMaskWithStars(isBalanceHidden))
appendSpace()
append(DOT)
appendSpace()
append(yieldBalanceState.rewardsCrypto.orMaskWithStars(isBalanceHidden))
append(reward.rewardsCrypto.orMaskWithStars(isBalanceHidden))
} to TangemTheme.colors.text.primary1
}
RewardBlockType.RewardUnavailable -> {
@ -232,7 +235,7 @@ private fun StakingRewardBlock(
resourceReference(R.string.staking_details_no_rewards_to_claim) to TangemTheme.colors.text.tertiary
}
}
val isShowIcon = yieldBalanceState.rewardBlockType == RewardBlockType.Rewards && yieldBalanceState.isActionable
val isShowIcon = reward.rewardBlockType == RewardBlockType.Rewards && yieldBalanceState.isActionable
InputRowDefault(
title = resourceReference(R.string.staking_rewards),
text = text,