From 33e5b99d978d76c123341d669aecdcb79741fb7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 19:14:03 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../common/ui/feeScreen/FeeCalculation.kt | 86 ------- .../presentation/state/StakingNotification.kt | 43 ++-- .../impl/presentation/state/StakingUiState.kt | 5 +- .../ConfirmationStatePreviewData.kt | 1 + .../state/stub/StakingClickIntentsStub.kt | 17 ++ .../AddStakingErrorTransformer.kt | 17 +- .../AddStakingNotificationsTransformer.kt | 25 +- ...issStakingNotificationsStateTransformer.kt | 25 ++ .../SetConfirmationStateLoadingTransformer.kt | 4 +- .../SetInitialDataStateTransformer.kt | 1 + .../state/utils/FeeCalculation.kt | 44 ++++ .../viewmodel/StakingClickIntents.kt | 18 ++ .../viewmodel/StakingViewModel.kt | 239 ++++++++++++------ 13 files changed, 327 insertions(+), 198 deletions(-) delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt b/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt deleted file mode 100644 index d1c3719c0a..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.common.ui.feeScreen - -import com.tangem.blockchain.common.transaction.TransactionFee -import com.tangem.common.extensions.isZero -import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import java.math.BigDecimal -import java.math.RoundingMode - -/** - * Check and calculates subtracted amount - */ -fun checkAndCalculateSubtractedAmount( - isAmountSubtractAvailable: Boolean, - cryptoCurrencyStatus: CryptoCurrencyStatus, - amountValue: BigDecimal, - feeValue: BigDecimal, - reduceAmountBy: BigDecimal, -): BigDecimal { - val balance = cryptoCurrencyStatus.value.amount ?: return amountValue - val isFeeCoverage = checkFeeCoverage( - isSubtractAvailable = isAmountSubtractAvailable, - balance = balance, - amountValue = amountValue, - feeValue = feeValue, - reduceAmountBy = reduceAmountBy, - ) - return if (isFeeCoverage) { - balance.minus(reduceAmountBy).minus(feeValue) - } else { - amountValue - } -} - -/** - * Checks if sending amount with fee is greater than balance - */ -fun checkFeeCoverage( - isSubtractAvailable: Boolean, - balance: BigDecimal, - amountValue: BigDecimal, - feeValue: BigDecimal, - reduceAmountBy: BigDecimal?, -): Boolean { - if (!isSubtractAvailable) return false - val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO) - return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue -} - -/** - * Check if custom fee is too low - */ -fun checkIfFeeTooLow(fee: TransactionFee, customValue: BigDecimal, isCustomSelected: Boolean): Boolean { - val multipleFees = fee as? TransactionFee.Choosable ?: return false - val minimumValue = multipleFees.minimum.amount.value ?: return false - - return isCustomSelected && minimumValue > customValue -} - -/** - * Check if custom fee is too high - */ -fun checkIfFeeTooHigh( - fee: TransactionFee, - customValue: BigDecimal, - isCustomSelected: Boolean, - onShow: (String) -> Unit, -): Boolean { - val multipleFees = fee as? TransactionFee.Choosable ?: return false - val highValue = multipleFees.priority.amount.value ?: return false - - val diff = customValue / highValue - val isShow = isCustomSelected && diff > FEE_MAX_DIFF - if (isShow) onShow(diff.parseBigDecimal(ZERO_DECIMALS, RoundingMode.HALF_UP)) - return isShow -} - -/** - * Checks if fee exceeds fee paid currency balance - */ -fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean { - return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance -} - -private val FEE_MAX_DIFF = BigDecimal("5") -private const val ZERO_DECIMALS = 0 \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt index f9da881678..682b777b25 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingNotification.kt @@ -1,61 +1,56 @@ package com.tangem.features.staking.impl.presentation.state +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.* import com.tangem.features.staking.impl.R -internal sealed class StakingNotification(val config: NotificationConfig) { +internal object StakingNotification { sealed class Error( title: TextReference, subtitle: TextReference, - iconResId: Int = R.drawable.ic_alert_24, buttonState: NotificationConfig.ButtonsState? = null, onCloseClick: (() -> Unit)? = null, - ) : StakingNotification( - config = NotificationConfig( - title = title, - subtitle = subtitle, - iconResId = iconResId, - buttonsState = buttonState, - onCloseClick = onCloseClick, - ), + ) : NotificationUM.Error( + title = title, + subtitle = subtitle, + iconResId = R.drawable.ic_alert_24, + buttonState = buttonState, + onCloseClick = onCloseClick, ) { - data class StakedPositionNotFoundError(val message: String) : Error( + data class StakedPositionNotFoundError(val message: String) : StakingNotification.Error( title = stringReference(message), subtitle = stringReference(message), ) - data class Common(val subtitle: TextReference) : Error( + data class Common(val subtitle: TextReference) : StakingNotification.Error( title = resourceReference(R.string.common_error), subtitle = subtitle, ) } - sealed class Warning( title: TextReference, subtitle: TextReference, buttonsState: NotificationConfig.ButtonsState? = null, onCloseClick: (() -> Unit)? = null, - ) : StakingNotification( - config = NotificationConfig( - title = title, - subtitle = subtitle, - iconResId = R.drawable.ic_alert_circle_24, - buttonsState = buttonsState, - onCloseClick = onCloseClick, - ), + ) : NotificationUM.Warning( + title = title, + subtitle = subtitle, + iconResId = R.drawable.ic_alert_circle_24, + buttonsState = buttonsState, + onCloseClick = onCloseClick, ) { data class EarnRewards( val subtitleText: TextReference, - ) : Warning( + ) : StakingNotification.Warning( title = resourceReference(R.string.staking_notification_earn_rewards_title), subtitle = subtitleText, ) data class Unstake( val cooldownPeriodDays: Int, - ) : Warning( + ) : StakingNotification.Warning( title = resourceReference(R.string.common_unstake), subtitle = resourceReference( R.string.staking_notification_unstake_text, @@ -72,6 +67,6 @@ internal sealed class StakingNotification(val config: NotificationConfig) { data class TransactionInProgress( val title: TextReference, val description: TextReference, - ) : Warning(title = title, subtitle = description) + ) : StakingNotification.Warning(title = title, subtitle = description) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index 55e576c90f..d06f0e1507 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButtonsState +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.list.RoundedListWithDividersItemData import com.tangem.core.ui.event.StateEvent @@ -13,6 +14,7 @@ import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents import kotlinx.collections.immutable.ImmutableList +import java.math.BigDecimal /** * Ui states of the staking screen @@ -84,10 +86,11 @@ internal sealed class StakingStates { val feeState: FeeState, val validatorState: ValidatorState, val pendingAction: PendingAction?, - val notifications: ImmutableList, + val notifications: ImmutableList, val footerText: String, val transactionDoneState: TransactionDoneState, val isApprovalNeeded: Boolean, + val reduceAmountBy: BigDecimal?, ) : ConfirmationState() data class Empty( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt index ef4234d37c..25879ce343 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/ConfirmationStatePreviewData.kt @@ -89,5 +89,6 @@ internal object ConfirmationStatePreviewData { transactionDoneState = TransactionDoneState.Empty, pendingAction = null, isApprovalNeeded = false, + reduceAmountBy = null, ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt index 8876bdabdb..b2edad1892 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/stub/StakingClickIntentsStub.kt @@ -1,11 +1,14 @@ package com.tangem.features.staking.impl.presentation.state.stub +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents +import java.math.BigDecimal @Suppress("TooManyFunctions") internal object StakingClickIntentsStub : StakingClickIntents { @@ -49,4 +52,18 @@ internal object StakingClickIntentsStub : StakingClickIntents { override fun onFailedTxEmailClick(errorMessage: String) {} override fun onActiveStake(activeStake: BalanceState) {} + + override fun getFee(pendingAction: PendingAction?) {} + + override fun onAmountReduceByClick( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) {} + + override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class) {} + + override fun onNotificationCancel(notification: Class) {} + + override fun openTokenDetails(cryptoCurrency: CryptoCurrency) {} } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt index 69278afeb6..5acae90ea3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingErrorTransformer.kt @@ -1,28 +1,37 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.staking.model.stakekit.StakingError -import com.tangem.features.staking.impl.presentation.state.* +import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingNotification +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList internal class AddStakingErrorTransformer( - private val error: StakingError, + private val error: StakingError? = null, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState + val notifications = buildList { + addAll(confirmationState.notifications) + error?.let { add(convertToNotification(it)) } + }.toPersistentList() + return prevState.copy( confirmationState = confirmationState.copy( - notifications = (confirmationState.notifications + convertToNotification(error)).toPersistentList(), + notifications = notifications, feeState = FeeState.Error, ), ) } - private fun convertToNotification(error: StakingError): StakingNotification { + private fun convertToNotification(error: StakingError): NotificationUM { return when (error) { is StakingError.StakedPositionNotFoundError -> StakingNotification.Error.StakedPositionNotFoundError( message = error.toString(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt index 2776bf6869..fd4fb47e43 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -1,8 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers import com.tangem.common.ui.amountScreen.models.AmountState -import com.tangem.common.ui.feeScreen.checkAndCalculateSubtractedAmount -import com.tangem.common.ui.feeScreen.checkFeeCoverage import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification import com.tangem.common.ui.notifications.NotificationsFactory.addExceedBalanceNotification @@ -14,13 +12,17 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountE import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError import com.tangem.features.staking.impl.presentation.state.FeeState +import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount +import com.tangem.features.staking.impl.presentation.state.utils.checkFeeCoverage import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero @@ -41,17 +43,17 @@ internal class AddStakingNotificationsTransformer( ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val appCurrency = appCurrencyProvider() 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 ?: return prevState + val feeState = confirmationState.feeState as? FeeState.Content val amountValue = amountState.amountTextField.cryptoAmount.value.orZero() - val feeValue = feeState.fee?.amount?.value.orZero() + val feeValue = feeState?.fee?.amount?.value.orZero() val reduceAmountBy = confirmationState.reduceAmountBy.orZero() + val isEnterAction = prevState.actionType == StakingActionCommonType.ENTER val isFeeCoverage = checkFeeCoverage( amountValue = amountValue, feeValue = feeValue, @@ -61,7 +63,7 @@ internal class AddStakingNotificationsTransformer( ) val sendingAmount = checkAndCalculateSubtractedAmount( isAmountSubtractAvailable = isSubtractAvailable, - cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + cryptoCurrencyStatus = cryptoCurrencyStatus, amountValue = amountValue, feeValue = feeValue, reduceAmountBy = reduceAmountBy, @@ -73,7 +75,7 @@ internal class AddStakingNotificationsTransformer( prevState = prevState, feeError = feeError, sendingAmount = sendingAmount, - onReload = { prevState.clickIntents.loadFee(confirmationState.pendingActions) }, + onReload = { prevState.clickIntents.getFee(confirmationState.pendingAction) }, feeValue = feeValue, ) // warnings @@ -82,7 +84,7 @@ internal class AddStakingNotificationsTransformer( amountState = amountState, feeState = feeState, sendingAmount = sendingAmount, - isFeeCoverage = isFeeCoverage, + isFeeCoverage = isFeeCoverage && isEnterAction, ) addAll(confirmationState.notifications) @@ -91,6 +93,7 @@ internal class AddStakingNotificationsTransformer( return prevState.copy( confirmationState = confirmationState.copy( notifications = notifications.toImmutableList(), + isPrimaryButtonEnabled = !notifications.any { it is StakingNotification.Error }, ), ) } @@ -151,7 +154,7 @@ internal class AddStakingNotificationsTransformer( private fun MutableList.addWarningNotifications( prevState: StakingUiState, amountState: AmountState.Data, - feeState: FeeState.Content, + feeState: FeeState.Content?, sendingAmount: BigDecimal, isFeeCoverage: Boolean, ) { @@ -161,7 +164,7 @@ internal class AddStakingNotificationsTransformer( addExistentialWarningNotification( existentialDeposit = currencyCheck.existentialDeposit, - feeAmount = feeState.fee?.amount?.value.orZero(), + feeAmount = feeState?.fee?.amount?.value.orZero(), receivedAmount = sendingAmount, cryptoCurrencyStatus = cryptoCurrencyStatus, onReduceClick = prevState.clickIntents::onAmountReduceByClick, @@ -177,7 +180,7 @@ internal class AddStakingNotificationsTransformer( // blockchain specific addValidateTransactionNotifications( dustValue = currencyCheck.dustValue.orZero(), - fee = feeState.fee, + fee = feeState?.fee, validationError = validatorError, cryptoCurrency = cryptoCurrency, onReduceClick = prevState.clickIntents::onAmountReduceToClick, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt new file mode 100644 index 0000000000..a5fc4d9bc5 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/DismissStakingNotificationsStateTransformer.kt @@ -0,0 +1,25 @@ +package com.tangem.features.staking.impl.presentation.state.transformers + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +internal class DismissStakingNotificationsStateTransformer( + private val notification: Class, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data + val updatedNotifications = confirmationState?.notifications + ?.filterNot { it::class == notification }?.toPersistentList() + ?: persistentListOf() + + return prevState.copy( + confirmationState = confirmationState?.copy( + notifications = updatedNotifications, + ) ?: StakingStates.ConfirmationState.Empty(), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt index 9e35b20442..e158860e89 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateLoadingTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.staking.model.stakekit.Yield @@ -34,11 +35,12 @@ internal class SetConfirmationStateLoadingTransformer( transactionDoneState = TransactionDoneState.Empty, pendingAction = null, isApprovalNeeded = false, + reduceAmountBy = null, ), ) } - private fun getNotifications(prevState: StakingUiState): ImmutableList { + private fun getNotifications(prevState: StakingUiState): ImmutableList { return persistentListOf( if (prevState.actionType == StakingActionCommonType.EXIT) { StakingNotification.Warning.Unstake( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 19f1c44544..7d873f5da1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -214,6 +214,7 @@ internal class SetInitialDataStateTransformer( transactionDoneState = TransactionDoneState.Empty, pendingAction = null, isApprovalNeeded = isApprovalNeeded, + reduceAmountBy = null, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt new file mode 100644 index 0000000000..21e95ed465 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/utils/FeeCalculation.kt @@ -0,0 +1,44 @@ +package com.tangem.features.staking.impl.presentation.state.utils + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import java.math.BigDecimal + +/** + * Check and calculates subtracted amount + */ +internal fun checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal, +): BigDecimal { + val balance = cryptoCurrencyStatus.value.amount ?: return amountValue + val isFeeCoverage = checkFeeCoverage( + isSubtractAvailable = isAmountSubtractAvailable, + balance = balance, + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + return if (isFeeCoverage) { + balance.minus(reduceAmountBy).minus(feeValue) + } else { + amountValue + } +} + +/** + * Checks if sending amount with fee is greater than balance + */ +internal fun checkFeeCoverage( + isSubtractAvailable: Boolean, + balance: BigDecimal, + amountValue: BigDecimal, + feeValue: BigDecimal, + reduceAmountBy: BigDecimal?, +): Boolean { + if (!isSubtractAvailable) return false + val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO) + return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt index ad7b9ff1c6..11910f51df 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingClickIntents.kt @@ -1,12 +1,16 @@ package com.tangem.features.staking.impl.presentation.viewmodel import com.tangem.common.ui.amountScreen.AmountScreenClickIntents +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType +import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.features.staking.impl.presentation.state.BalanceState import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType +import java.math.BigDecimal +@Suppress("TooManyFunctions") internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() @@ -21,6 +25,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onInfoClick(infoType: InfoType) + fun getFee(pendingAction: PendingAction?) + override fun onAmountNext() = onNextClick(actionType = null) fun openValidators() @@ -35,9 +41,21 @@ internal interface StakingClickIntents : AmountScreenClickIntents { fun onApprovalClick() + fun onAmountReduceByClick( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) + + fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class) + + fun onNotificationCancel(notification: Class) + fun onExploreClick() fun onShareClick() fun onFailedTxEmailClick(errorMessage: String) + + fun openTokenDetails(cryptoCurrency: CryptoCurrency) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt index 523380111a..1c18173837 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/viewmodel/StakingViewModel.kt @@ -12,7 +12,9 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.routing.AppRoute import com.tangem.common.routing.bundle.unbundle +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager @@ -32,16 +34,15 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType -import com.tangem.domain.tokens.FetchPendingTransactionsUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase +import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase +import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -50,10 +51,7 @@ import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory import com.tangem.features.staking.impl.presentation.state.transformers.* -import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer -import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer -import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer -import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer +import com.tangem.features.staking.impl.presentation.state.transformers.amount.* import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalBottomSheetInProgressTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetApprovalInProgressTransformer import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer @@ -103,6 +101,10 @@ internal class StakingViewModel @Inject constructor( private val feedbackManager: FeedbackManager, private val getCardInfoUseCase: GetCardInfoUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, + private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, + private val validateTransactionUseCase: ValidateTransactionUseCase, + private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, + private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, @DelayedWork private val coroutineScope: CoroutineScope, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents { @@ -140,6 +142,7 @@ internal class StakingViewModel @Inject constructor( ) private var stakingApproval: StakingApproval = StakingApproval.Empty + private var isAmountSubtractAvailable: Boolean = false private val allowanceTaskScheduler = SingleTaskScheduler() private val transactionsInProgress: CopyOnWriteArrayList = CopyOnWriteArrayList() @@ -171,6 +174,57 @@ internal class StakingViewModel @Inject constructor( stakingStateRouter.onNextClick() } + override fun getFee(pendingAction: PendingAction?) { + viewModelScope.launch { + stateController.update( + SetConfirmationStateLoadingTransformer( + yield = yield, + ), + ) + val cryptoCurrencyValue = cryptoCurrencyStatus.value + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + ?: error("No confirmation state") + val validatorState = confirmationState.validatorState as? ValidatorState.Content + ?: error("No validator provided") + + val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value + ?: error("No amount provided") + val sourceAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value + ?: error("No available address") + val validatorAddress = validatorState.chosenValidator.address + + val approval = stakingApproval as? StakingApproval.Needed + if (approval != null) { + val allowance = getAllowanceUseCase( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrencyStatus.currency, + spenderAddress = approval.spenderAddress, + ).getOrElse { BigDecimal.ZERO } + + if (allowance < amount) { + getApproveFee( + amount = amount, + validatorAddress = validatorAddress, + ) + } else { + estimateGas( + pendingAction = pendingAction, + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + ) + } + } else { + estimateGas( + pendingAction = pendingAction, + amount = amount, + sourceAddress = sourceAddress, + validatorAddress = validatorAddress, + ) + } + } + } + private fun handleOnNextConfirmationClick() { if (isAssentState()) { viewModelScope.launch { @@ -236,57 +290,6 @@ internal class StakingViewModel @Inject constructor( } } - private fun getFee(pendingAction: PendingAction?) { - viewModelScope.launch { - stateController.update( - SetConfirmationStateLoadingTransformer( - yield = yield, - ), - ) - val cryptoCurrencyValue = cryptoCurrencyStatus.value - val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data - ?: error("No confirmation state") - val validatorState = confirmationState.validatorState as? ValidatorState.Content - ?: error("No validator provided") - - val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value - ?: error("No amount provided") - val sourceAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value - ?: error("No available address") - val validatorAddress = validatorState.chosenValidator.address - - val approval = stakingApproval as? StakingApproval.Needed - if (approval != null) { - val allowance = getAllowanceUseCase( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrencyStatus.currency, - spenderAddress = approval.spenderAddress, - ).getOrElse { BigDecimal.ZERO } - - if (allowance < amount) { - getApproveFee( - amount = amount, - validatorAddress = validatorAddress, - ) - } else { - estimateGas( - pendingAction = pendingAction, - amount = amount, - sourceAddress = sourceAddress, - validatorAddress = validatorAddress, - ) - } - } else { - estimateGas( - pendingAction = pendingAction, - amount = amount, - sourceAddress = sourceAddress, - validatorAddress = validatorAddress, - ) - } - } - } - private suspend fun estimateGas( pendingAction: PendingAction?, amount: BigDecimal, @@ -310,7 +313,6 @@ internal class StakingViewModel @Inject constructor( stateController.update(AddStakingErrorTransformer(it)) return } - stateController.update( SetConfirmationStateAssentTransformer( appCurrencyProvider = Provider { appCurrency }, @@ -325,24 +327,30 @@ internal class StakingViewModel @Inject constructor( action = pendingAction, ), ) + updateNotifications() } private suspend fun getApproveFee(amount: BigDecimal, validatorAddress: String) { - val approvalFee = getFeeUseCase( + getFeeUseCase( amount = amount, destination = validatorAddress, userWallet = userWallet, cryptoCurrency = cryptoCurrencyStatus.currency, - ).getOrElse { - return stakingEventFactory.createGenericErrorAlert(it.toString()) - } - - stateController.update( - SetConfirmationStateAssentApprovalTransformer( - appCurrencyProvider = Provider { appCurrency }, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = approvalFee, - ), + ).fold( + ifRight = { fee -> + stateController.update( + SetConfirmationStateAssentApprovalTransformer( + appCurrencyProvider = Provider { appCurrency }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + fee = fee, + ), + ) + updateNotifications() + }, + ifLeft = { + stateController.update(AddStakingErrorTransformer()) + updateNotifications(it) + }, ) } @@ -499,6 +507,85 @@ internal class StakingViewModel @Inject constructor( }.saveIn(approvalJobHolder) } + private fun updateNotifications(feeError: GetFeeError? = null) { + viewModelScope.launch { + val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data + val feeState = confirmationState?.feeState as? FeeState.Content + val amountState = value.amountState as? AmountState.Data + + val amount = amountState?.amountTextField?.cryptoAmount?.value + val fee = feeState?.fee?.amount?.value + val currencyWarning = if (feeCryptoCurrencyStatus != null && fee != null) { + getBalanceNotEnoughForFeeWarningUseCase( + fee = fee, + userWalletId = userWalletId, + tokenStatus = cryptoCurrencyStatus, + coinStatus = feeCryptoCurrencyStatus ?: cryptoCurrencyStatus, + ).getOrNull() + } else { + null + } + val validation = amount?.let { + validateTransactionUseCase( + userWalletId = userWalletId, + amount = amount.convertToSdkAmount(cryptoCurrencyStatus.currency), + fee = feeState?.fee, + memo = null, + destination = "", + network = cryptoCurrencyStatus.currency.network, + ).leftOrNull() + } + + val currencyStatus = getCurrencyCheckUseCase( + userWalletId = userWalletId, + currencyStatus = cryptoCurrencyStatus, + amount = amount, + fee = fee, + ) + stateController.update( + AddStakingNotificationsTransformer( + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + appCurrencyProvider = Provider { appCurrency }, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + currencyWarning = currencyWarning, + validatorError = validation, + currencyCheck = currencyStatus, + isSubtractAvailable = isAmountSubtractAvailable, + feeError = feeError, + ), + ) + } + } + + override fun onAmountReduceByClick( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) { + AmountReduceByStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + value = AmountReduceByTransformer.ReduceByData( + reduceAmountBy = reduceAmountBy, + reduceAmountByDiff = reduceAmountByDiff, + ), + ) + onNotificationCancel(notification) + } + + override fun onAmountReduceToClick(reduceAmountTo: BigDecimal, notification: Class) { + stateController.update( + AmountReduceToStateTransformer( + cryptoCurrencyStatus = cryptoCurrencyStatus, + value = reduceAmountTo, + ), + ) + onNotificationCancel(notification) + } + + override fun onNotificationCancel(notification: Class) { + stateController.update(DismissStakingNotificationsStateTransformer(notification)) + } + private fun awaitForAllowance(pendingAction: PendingAction?) { val approval = stakingApproval as? StakingApproval.Needed ?: return allowanceTaskScheduler.scheduleTask( @@ -586,6 +673,10 @@ internal class StakingViewModel @Inject constructor( } } + override fun openTokenDetails(cryptoCurrency: CryptoCurrency) { + innerRouter.openTokenDetails(userWalletId, cryptoCurrency) + } + fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) { innerRouter = router this.stakingStateRouter = stateRouter @@ -612,6 +703,7 @@ internal class StakingViewModel @Inject constructor( cryptoCurrencyStatus = it setupApprovalNeeded() + checkIfSubtractAvailable() stateController.update( transformer = SetInitialDataStateTransformer( @@ -779,6 +871,11 @@ internal class StakingViewModel @Inject constructor( InnerConfirmationStakingState.ASSENT } + private suspend fun checkIfSubtractAvailable() { + isAmountSubtractAvailable = isAmountSubtractAvailableUseCase(userWalletId, cryptoCurrencyStatus.currency) + .getOrElse { false } + } + private data class FullTransactionData( val stakeKitTransaction: StakingTransaction, val tangemTransaction: TransactionData.Compiled,