Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-22 12:07:07 +03:00
parent c6fc619530
commit c922665d8b
7 changed files with 203 additions and 81 deletions

View file

@ -76,4 +76,6 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun onActivateTonAccountNotificationShow()
fun onActivateTonAccountClick()
fun onAmountReduceByFeeClick(reduceAmount: BigDecimal, notification: Class<out NotificationUM>)
}

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.routing.AppRouter
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer
import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
@ -1081,6 +1082,17 @@ internal class StakingModel @Inject constructor(
}
}
override fun onAmountReduceByFeeClick(reduceAmount: BigDecimal, notification: Class<out NotificationUM>) {
stateController.update(
AmountReduceByStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
value = ReduceByData(reduceAmount, reduceAmount),
minimumTransactionAmount = null,
),
)
onNotificationCancel(notification)
}
private suspend fun setupApprovalNeeded() {
val approval = StakingIntegrationID.create(currencyId = cryptoCurrencyStatus.currency.id)?.approval
?: StakingApproval.Empty

View file

@ -89,9 +89,20 @@ internal object StakingNotification {
subtitle = subtitleText,
)
data object StakeEntireBalance : Info(
data class StakeEntireBalance(
private val reduceAmountValue: TextReference?,
private val onReduceClick: () -> Unit,
) : Info(
title = resourceReference(R.string.common_network_fee_title),
subtitle = resourceReference(R.string.staking_notification_stake_entire_balance_text),
buttonsState = if (reduceAmountValue != null) {
NotificationConfig.ButtonsState.PrimaryButtonConfig(
text = reduceAmountValue,
onClick = onReduceClick,
)
} else {
null
},
)
data class Unstake(

View file

@ -84,4 +84,6 @@ internal object StakingClickIntentsStub : StakingClickIntents {
override fun onActivateTonAccountNotificationShow() {}
override fun onActivateTonAccountClick() {}
override fun onAmountReduceByFeeClick(reduceAmount: BigDecimal, notification: Class<out NotificationUM>) {}
}

View file

@ -33,6 +33,7 @@ import com.tangem.lib.crypto.BlockchainUtils.isTon
import com.tangem.utils.Provider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
@ -56,29 +57,44 @@ internal class AddStakingNotificationsTransformer(
isSubtractAvailable = isSubtractAvailable,
)
@Suppress("LongMethod")
override fun transform(prevState: StakingUiState): StakingUiState {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val balance = cryptoCurrencyStatus.value.amount.orZero()
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
val amountState = prevState.amountState as? AmountState.Data ?: return prevState
val feeState = confirmationState.feeState as? FeeState.Content
val amountValue = amountState.amountTextField.cryptoAmount.value.orZero()
val feeValue = feeState?.fee?.amount?.value.orZero()
val reduceAmountBy = confirmationState.reduceAmountBy.orZero()
val isEnterAction = prevState.actionType is StakingActionCommonType.Enter
val isFeeCoverage = checkFeeCoverage(
amountValue = amountValue,
feeValue = feeValue,
balance = balance,
isSubtractAvailable = isSubtractAvailable,
reduceAmountBy = reduceAmountBy,
val sendingAmount = calculateSendingAmount(
prevState = prevState,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountState = amountState,
confirmationState = confirmationState,
)
val minimumRequirement = integration.enterMinimumAmount.orZero()
val sendingAmount = if (isEnterAction) {
val notifications = buildNotifications(
prevState = prevState,
amountState = amountState,
confirmationState = confirmationState,
sendingAmount = sendingAmount,
)
val isActualSources = areSourcesActual(cryptoCurrencyStatus)
return prevState.copy(
confirmationState = confirmationState.copy(
notifications = notifications,
isPrimaryButtonEnabled = isPrimaryButtonEnabled(notifications, isActualSources),
),
)
}
private fun calculateSendingAmount(
prevState: StakingUiState,
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountState: AmountState.Data,
confirmationState: StakingStates.ConfirmationState.Data,
): BigDecimal {
val isEnterAction = prevState.actionType is StakingActionCommonType.Enter
return if (isEnterAction) {
val amountValue = amountState.amountTextField.cryptoAmount.value.orZero()
val feeValue = (confirmationState.feeState as? FeeState.Content)?.fee?.amount?.value.orZero()
val reduceAmountBy = confirmationState.reduceAmountBy.orZero()
checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
@ -87,63 +103,89 @@ internal class AddStakingNotificationsTransformer(
reduceAmountBy = reduceAmountBy,
)
} else {
// No amount is taken from account balance on exit or pending actions
BigDecimal.ZERO
}
val notifications = if (isAccountInitializedProvider.invoke()) {
buildList {
// errors
addErrorNotifications(
prevState = prevState,
feeError = feeError,
sendingAmount = sendingAmount,
onReload = prevState.clickIntents::getFee,
feeValue = feeValue,
)
addStakingErrorNotifications(stakingError = stakingError, onReload = prevState.clickIntents::getFee)
// warnings
addWarningNotifications(
prevState = prevState,
amountState = amountState,
feeState = feeState,
sendingAmount = sendingAmount,
isFeeCoverage = isFeeCoverage && isEnterAction && !sendingAmount.equals(minimumRequirement),
)
stakingInfoNotificationsFactory.addInfoNotifications(
notifications = this,
prevState = prevState,
sendingAmount = sendingAmount,
actionAmount = amountValue,
feeValue = feeValue,
tonBalanceExtraFeeThreshold = TON_BALANCE_EXTRA_FEE_THRESHOLD,
)
}.toImmutableList()
} else {
buildList {
addTonInitializeAccountNotification(prevState)
}.toImmutableList()
}
val isActualSources = with(cryptoCurrencyStatus.value) {
sources.stakingBalanceSource.isActual() && sources.networkSource.isActual()
}
return prevState.copy(
confirmationState = confirmationState.copy(
notifications = notifications.toImmutableList(),
isPrimaryButtonEnabled = notifications.none {
it is StakingNotification.Error ||
it is NotificationUM.Error ||
it is NotificationUM.Warning.NetworkFeeUnreachable ||
it is StakingNotification.Warning.TransactionInProgress ||
it is StakingNotification.Warning.InitializeTonAccount
} && isActualSources,
),
)
}
private fun buildNotifications(
prevState: StakingUiState,
amountState: AmountState.Data,
confirmationState: StakingStates.ConfirmationState.Data,
sendingAmount: BigDecimal,
) = if (isAccountInitializedProvider.invoke()) {
buildInitializedAccountNotifications(
prevState = prevState,
amountState = amountState,
confirmationState = confirmationState,
sendingAmount = sendingAmount,
)
} else {
buildList { addTonInitializeAccountNotification(prevState) }.toImmutableList()
}
private fun buildInitializedAccountNotifications(
prevState: StakingUiState,
amountState: AmountState.Data,
confirmationState: StakingStates.ConfirmationState.Data,
sendingAmount: BigDecimal,
) = buildList {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val balance = cryptoCurrencyStatus.value.amount.orZero()
val feeState = confirmationState.feeState as? FeeState.Content
val amountValue = amountState.amountTextField.cryptoAmount.value.orZero()
val feeValue = feeState?.fee?.amount?.value.orZero()
val reduceAmountBy = confirmationState.reduceAmountBy.orZero()
val isEnterAction = prevState.actionType is StakingActionCommonType.Enter
val minimumRequirement = integration.enterMinimumAmount.orZero()
val isFeeCoverage = checkFeeCoverage(
amountValue = amountValue,
feeValue = feeValue,
balance = balance,
isSubtractAvailable = isSubtractAvailable,
reduceAmountBy = reduceAmountBy,
)
addErrorNotifications(
prevState = prevState,
feeError = feeError,
sendingAmount = sendingAmount,
onReload = prevState.clickIntents::getFee,
feeValue = feeValue,
)
addStakingErrorNotifications(stakingError = stakingError, onReload = prevState.clickIntents::getFee)
addWarningNotifications(
prevState = prevState,
amountState = amountState,
feeState = feeState,
sendingAmount = sendingAmount,
isFeeCoverage = isFeeCoverage && isEnterAction && !sendingAmount.equals(minimumRequirement),
)
stakingInfoNotificationsFactory.addInfoNotifications(
notifications = this,
prevState = prevState,
sendingAmount = sendingAmount,
actionAmount = amountValue,
feeAmount = feeState?.fee?.amount,
isFeeApproximate = feeState?.isFeeApproximate == true,
onAmountReduceByFeeClick = prevState.clickIntents::onAmountReduceByFeeClick,
tonBalanceExtraFeeThreshold = TON_BALANCE_EXTRA_FEE_THRESHOLD,
)
}.toImmutableList()
private fun areSourcesActual(cryptoCurrencyStatus: CryptoCurrencyStatus) = with(cryptoCurrencyStatus.value) {
sources.stakingBalanceSource.isActual() && sources.networkSource.isActual()
}
private fun isPrimaryButtonEnabled(notifications: ImmutableList<NotificationUM>, isActualSources: Boolean) =
notifications.none {
it is StakingNotification.Error ||
it is NotificationUM.Error ||
it is NotificationUM.Warning.NetworkFeeUnreachable ||
it is StakingNotification.Warning.TransactionInProgress ||
it is StakingNotification.Warning.InitializeTonAccount
} && isActualSources
private fun MutableList<NotificationUM>.addStakingErrorNotifications(
stakingError: StakingError?,
onReload: () -> Unit,

View file

@ -13,7 +13,7 @@ internal class DismissStakingNotificationsStateTransformer(
override fun transform(prevState: StakingUiState): StakingUiState {
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
val updatedNotifications = confirmationState?.notifications
?.filterNot { it::class == notification }?.toPersistentList()
?.filterNot { it::class.java == notification }?.toPersistentList()
?: persistentListOf()
return prevState.copy(

View file

@ -1,9 +1,13 @@
package com.tangem.features.staking.impl.presentation.state.transformers.notifications
import com.tangem.blockchain.common.Amount
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.extensions.pluralReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fee
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.BalanceType
import com.tangem.domain.models.staking.StakingBalance
@ -35,7 +39,10 @@ internal class StakingInfoNotificationsFactory(
* @param prevState current screen state to update
* @param sendingAmount amount being transferred from user account
* @param actionAmount any amount being transferred or used action
* @param feeValue fee amount payed from user account
* @param feeAmount fee amount payed from user account
* @param tonBalanceExtraFeeThreshold threshold for TON extra fee notification
* @param isFeeApproximate whether fee is approximate
* @param onAmountReduceByFeeClick callback to reduce amount by fee
*/
@Suppress("LongParameterList")
fun addInfoNotifications(
@ -43,17 +50,24 @@ internal class StakingInfoNotificationsFactory(
prevState: StakingUiState,
sendingAmount: BigDecimal,
actionAmount: BigDecimal,
feeValue: BigDecimal,
feeAmount: Amount?,
tonBalanceExtraFeeThreshold: BigDecimal,
isFeeApproximate: Boolean,
onAmountReduceByFeeClick: (BigDecimal, notification: Class<out NotificationUM>) -> Unit,
) = with(notifications) {
addStakingLowBalanceNotification(prevState, actionAmount)
addTonExtraFeeInfoNotification(tonBalanceExtraFeeThreshold)
when (prevState.actionType) {
is StakingActionCommonType.Enter -> addEnterInfoNotifications(sendingAmount, feeValue)
is StakingActionCommonType.Enter -> addEnterInfoNotifications(
sendingAmount = sendingAmount,
feeAmount = feeAmount,
isFeeApproximate = isFeeApproximate,
onAmountReduceByFeeClick = onAmountReduceByFeeClick,
)
is StakingActionCommonType.Exit -> addExitInfoNotifications(prevState)
is StakingActionCommonType.Pending -> {
addCardanoRestakeMinimumAmountNotification(feeValue)
addCardanoRestakeMinimumAmountNotification(feeAmount?.value.orZero())
addPendingInfoNotifications(prevState)
addTonHaveToUnstakeAllNotification(prevState)
}
@ -67,12 +81,19 @@ internal class StakingInfoNotificationsFactory(
private fun MutableList<NotificationUM>.addEnterInfoNotifications(
sendingAmount: BigDecimal,
feeValue: BigDecimal,
feeAmount: Amount?,
isFeeApproximate: Boolean,
onAmountReduceByFeeClick: (BigDecimal, notification: Class<out NotificationUM>) -> Unit,
) {
addCardanoStakeMinimumAmountNotification(feeValue)
addCardanoStakeMinimumAmountNotification(feeAmount?.value.orZero())
addTronRevoteNotification()
addCardanoStakeNotification()
addStakingEntireBalanceNotification(sendingAmount, feeValue)
addStakingEntireBalanceNotification(
sendingAmount = sendingAmount,
feeAmount = feeAmount,
isFeeApproximate = isFeeApproximate,
onAmountReduceByFeeClick = onAmountReduceByFeeClick,
)
}
private fun MutableList<NotificationUM>.addPendingInfoNotifications(prevState: StakingUiState) {
@ -194,15 +215,46 @@ internal class StakingInfoNotificationsFactory(
private fun MutableList<NotificationUM>.addStakingEntireBalanceNotification(
sendingAmount: BigDecimal,
feeValue: BigDecimal,
feeAmount: Amount?,
isFeeApproximate: Boolean,
onAmountReduceByFeeClick: (BigDecimal, notification: Class<out NotificationUM>) -> Unit,
) {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val balance = cryptoCurrencyStatus.value.amount.orZero()
val feeValue = feeAmount?.value.orZero()
val isEntireBalance = sendingAmount.plus(feeValue) == balance
if (isEntireBalance && isSubtractAvailable && !isCardano(cryptoCurrencyStatus.currency.network.rawId)) {
add(StakingNotification.Info.StakeEntireBalance)
val value = feeValue.multiply(FEE_DECIMALS_MULTIPLIER)
val reduceAmountValue = feeAmount
?.takeIf { feeValue != BigDecimal.ZERO }
?.let { amount ->
resourceReference(
R.string.send_notification_reduce_by,
wrappedList(
value.format {
crypto(
symbol = amount.currencySymbol,
decimals = amount.decimals,
).fee(canBeLower = isFeeApproximate)
},
),
)
}
add(
StakingNotification.Info.StakeEntireBalance(
reduceAmountValue,
{
onAmountReduceByFeeClick.invoke(
value,
StakingNotification.Info.StakeEntireBalance::class.java,
)
},
),
)
}
}
@ -286,5 +338,6 @@ internal class StakingInfoNotificationsFactory(
private companion object {
val MINIMUM_STAKE_BALANCE = "5".toBigDecimal()
val MINIMUM_RESTAKE_BALANCE = "3".toBigDecimal()
val FEE_DECIMALS_MULTIPLIER = "3".toBigDecimal()
}
}