From c10f747de8d8ec3247d86a47f301822a89e242c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 18:05:46 +0500 Subject: [PATCH 1/3] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 1 + .../common/ui/notifications/NotificationUM.kt | 251 ++++++++++++ .../ui/notifications/NotificationsFactory.kt | 362 ++++++++++++++++++ .../domain/transaction/error/GetFeeError.kt | 0 .../ui/block/NotificationsBlock.kt | 12 +- .../com/tangem/lib/crypto/BlockchainUtils.kt | 5 + 6 files changed, 629 insertions(+), 2 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt rename domain/transaction/{src/main/java => models/src/main/kotlin}/com/tangem/domain/transaction/error/GetFeeError.kt (100%) diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 9380922c04..315574334e 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(projects.domain.transaction.models) implementation(projects.domain.wallets.models) + implementation(deps.tangem.card.core) implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt new file mode 100644 index 0000000000..20dad94e6b --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -0,0 +1,251 @@ +package com.tangem.common.ui.notifications + +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.ui.R +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.BigDecimalFormatter +import java.math.BigDecimal + +sealed class NotificationUM(val config: NotificationConfig) { + + open class Error( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.ic_alert_24, + buttonState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonState, + onCloseClick = onCloseClick, + ), + ) { + + data object TotalExceedsBalance : Error( + title = resourceReference(R.string.send_notification_exceed_balance_title), + subtitle = resourceReference(R.string.send_notification_exceed_balance_text), + ) + + data object InvalidAmount : Error( + title = resourceReference(R.string.send_notification_invalid_amount_title), + subtitle = resourceReference(R.string.send_notification_invalid_amount_text), + ) + + data class MinimumAmountError(val amount: String) : Error( + title = resourceReference(R.string.send_notification_invalid_amount_title), + subtitle = resourceReference( + R.string.send_notification_invalid_minimum_amount_text, + wrappedList(amount, amount), + ), + ) + + data class TransactionLimitError( + val cryptoCurrency: String, + val utxoLimit: String, + val amountLimit: String, + val onConfirmClick: () -> Unit, + ) : Error( + title = resourceReference(R.string.send_notification_transaction_limit_title), + subtitle = resourceReference( + R.string.send_notification_transaction_limit_text, + wrappedList(cryptoCurrency, utxoLimit, amountLimit), + ), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_leave_button, wrappedList(amountLimit)), + onClick = onConfirmClick, + ), + ) + + data class ExceedsBalance( + val networkIconId: Int, + val currencyName: String, + val feeName: String, + val feeSymbol: String, + val networkName: String, + val mergeFeeNetworkName: Boolean = false, + val onClick: (() -> Unit)? = null, + ) : Error( + title = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_title, + wrappedList(feeName), + ), + subtitle = resourceReference( + id = R.string.warning_send_blocked_funds_for_fee_message, + formatArgs = wrappedList(currencyName, networkName, currencyName, feeName, feeSymbol), + ), + iconResId = networkIconId, + buttonState = onClick?.let { + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference( + R.string.common_buy_currency, + wrappedList( + if (mergeFeeNetworkName) { + "$currencyName ($feeSymbol)" + } else { + feeName + }, + ), + ), + onClick = onClick, + ) + }, + ) + + data class ExistentialDeposit(val deposit: String, val onConfirmClick: () -> Unit) : Error( + title = resourceReference(R.string.send_notification_existential_deposit_title), + subtitle = resourceReference(R.string.send_notification_existential_deposit_text, wrappedList(deposit)), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_leave_button, wrappedList(deposit)), + onClick = onConfirmClick, + ), + ) + + data class ReserveAmount(val amount: String) : Error( + title = resourceReference( + id = R.string.send_notification_invalid_reserve_amount_title, + wrappedList(amount), + ), + subtitle = resourceReference(id = R.string.send_notification_invalid_reserve_amount_text), + ) + } + + open class Warning( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.img_attention_20, + buttonsState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM( + config = NotificationConfig( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonsState, + onCloseClick = onCloseClick, + ), + ) { + data class HighFeeError( + val currencyName: String, + val amount: String, + val onConfirmClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : Warning( + title = resourceReference(R.string.send_notification_high_fee_title), + subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)), + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_reduce_by, wrappedList(amount)), + onClick = onConfirmClick, + ), + onCloseClick = onCloseClick, + ) + + data object FeeTooLow : Warning( + title = resourceReference(id = R.string.send_notification_transaction_delay_title), + subtitle = resourceReference(id = R.string.send_notification_transaction_delay_text), + ) + + data class TooHigh( + val value: String, + ) : Warning( + title = resourceReference(id = R.string.send_notification_fee_too_high_title), + subtitle = resourceReference(id = R.string.send_notification_fee_too_high_text, wrappedList(value)), + ) + + data class NetworkFeeUnreachable(val onRefresh: () -> Unit) : Warning( + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference(R.string.send_fee_unreachable_error_text), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = onRefresh, + ), + ) + + data class TronAccountNotActivated(val tokenName: String) : Warning( + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference( + R.string.send_tron_account_activation_error, + wrappedList(tokenName), + ), + ) + + data class FeeCoverageNotification(val cryptoAmount: String, val fiatAmount: String) : Warning( + title = resourceReference(R.string.send_network_fee_warning_title), + subtitle = resourceReference( + R.string.common_network_fee_warning_content, + wrappedList(cryptoAmount, fiatAmount), + ), + ) + } + + sealed interface Cardano { + + data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Warning( + title = resourceReference(id = R.string.cardano_coin_will_be_send_with_token_title), + subtitle = resourceReference( + id = R.string.cardano_coin_will_be_send_with_token_description, + formatArgs = wrappedList(minAdaValue, tokenName), + ), + ) + + data object InsufficientBalanceToTransferCoin : Error( + title = resourceReference(id = R.string.cardano_max_amount_has_token_title), + subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description), + ) + + data class InsufficientBalanceToTransferToken(val tokenName: String) : Error( + title = resourceReference(id = R.string.cardano_insufficient_balance_to_send_token_title), + subtitle = resourceReference( + id = R.string.cardano_insufficient_balance_to_send_token_description, + formatArgs = wrappedList(tokenName), + ), + ) + } + + sealed interface Koinos { + data class InsufficientRecoverableMana( + val mana: BigDecimal, + val maxMana: BigDecimal, + ) : Error( + title = resourceReference(R.string.koinos_insufficient_mana_to_send_koin_title), + subtitle = resourceReference( + R.string.koinos_insufficient_mana_to_send_koin_description, + formatArgs = wrappedList( + BigDecimalFormatter.formatCryptoAmountShorted(mana, "", Blockchain.Koinos.decimals()), + BigDecimalFormatter.formatCryptoAmountShorted(maxMana, "", Blockchain.Koinos.decimals()), + ), + ), + ) + + data object InsufficientBalance : Error( + title = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_title), + subtitle = resourceReference(R.string.koinos_insufficient_balance_to_send_koin_description), + ) + + data class ManaExceedsBalance( + val availableKoinForTransfer: BigDecimal, + val onReduceClick: () -> Unit, + ) : Error( + title = resourceReference(R.string.koinos_mana_exceeds_koin_balance_title), + subtitle = resourceReference( + R.string.koinos_mana_exceeds_koin_balance_description, + formatArgs = wrappedList( + BigDecimalFormatter.formatCryptoAmount( + availableKoinForTransfer, + Blockchain.Koinos.currency, + Blockchain.Koinos.decimals(), + ), + ), + ), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.send_notification_reduce_to, wrappedList(availableKoinForTransfer)), + onClick = onReduceClick, + ), + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt new file mode 100644 index 0000000000..526007b989 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationsFactory.kt @@ -0,0 +1,362 @@ +package com.tangem.common.ui.notifications + +import com.tangem.blockchain.common.BlockchainSdkError +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.common.ui.R +import com.tangem.common.ui.amountScreen.models.AmountFieldModel +import com.tangem.common.ui.amountScreen.utils.getFiatString +import com.tangem.core.ui.extensions.networkIconResId +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.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning +import com.tangem.domain.transaction.error.GetFeeError +import java.math.BigDecimal + +@Suppress("LargeClass") +object NotificationsFactory { + + fun MutableList.addFeeUnreachableNotification( + feeError: GetFeeError, + tokenName: String, + onReload: () -> Unit, + ) { + when (feeError) { + is GetFeeError.BlockchainErrors.TronActivationError -> add( + NotificationUM.Warning.TronAccountNotActivated(tokenName), + ) + is GetFeeError.DataError, + is GetFeeError.UnknownError, + -> add( + NotificationUM.Warning.NetworkFeeUnreachable(onReload), + ) + else -> { + /* do nothing */ + } + } + } + + fun MutableList.addExceedBalanceNotification( + feeAmount: BigDecimal, + sendingAmount: BigDecimal, + isSubtractionAvailable: Boolean, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ) { + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + + if (!isSubtractionAvailable) return + + val showNotification = sendingAmount + feeAmount > balance + if (showNotification) { + add(NotificationUM.Error.TotalExceedsBalance) + } + } + + fun MutableList.addReserveAmountErrorNotification( + reserveAmount: BigDecimal?, + sendingAmount: BigDecimal, + cryptoCurrency: CryptoCurrency, + isAccountFunded: Boolean, + ) { + if (!isAccountFunded && reserveAmount != null && reserveAmount > sendingAmount) { + add( + NotificationUM.Error.ReserveAmount( + BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = sendingAmount, + cryptoCurrency = cryptoCurrency, + ), + ), + ) + } + } + + fun MutableList.addTransactionLimitErrorNotification( + utxoLimit: UtxoAmountLimit?, + cryptoCurrency: CryptoCurrency, + onReduceClick: ( + reduceAmountTo: BigDecimal, + notification: Class, + ) -> Unit, + ) { + if (utxoLimit != null) { + add( + NotificationUM.Error.TransactionLimitError( + cryptoCurrency = cryptoCurrency.name, + utxoLimit = utxoLimit.maxLimit.toPlainString(), + amountLimit = BigDecimalFormatter.formatCryptoAmount( + cryptoAmount = utxoLimit.maxAmount, + cryptoCurrency = cryptoCurrency, + ), + onConfirmClick = { + onReduceClick( + utxoLimit.maxAmount, + NotificationUM.Error.TransactionLimitError::class.java, + ) + }, + ), + ) + } + } + + fun MutableList.addExistentialWarningNotification( + existentialDeposit: BigDecimal?, + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + onReduceClick: ( + reduceAmountBy: BigDecimal, + reduceAmountByDiff: BigDecimal, + notification: Class, + ) -> Unit, + ) { + val cryptoCurrency = cryptoCurrencyStatus.currency + val balance = cryptoCurrencyStatus.value.amount ?: return + val spendingAmount = if (cryptoCurrency is CryptoCurrency.Token) { + feeAmount + } else { + receivedAmount + } + val diff = balance.minus(spendingAmount) + if (existentialDeposit != null && diff >= BigDecimal.ZERO && existentialDeposit > diff) { + add( + NotificationUM.Error.ExistentialDeposit( + deposit = BigDecimalFormatter.formatCryptoAmountUncapped( + cryptoAmount = existentialDeposit, + cryptoCurrency = cryptoCurrency, + ), + onConfirmClick = { + onReduceClick( + existentialDeposit, + existentialDeposit.minus(diff), + NotificationUM.Error.ExistentialDeposit::class.java, + ) + }, + ), + ) + } + } + + fun MutableList.addFeeCoverageNotification( + isFeeCoverage: Boolean, + amountField: AmountFieldModel, + sendingValue: BigDecimal, + appCurrency: AppCurrency, + cryptoCurrencyStatus: CryptoCurrencyStatus, + ) { + val cryptoCurrency = cryptoCurrencyStatus.currency + val fiatRate = cryptoCurrencyStatus.value.fiatRate + val amountValue = amountField.cryptoAmount.value ?: return + + val cryptoDiff = amountValue.minus(sendingValue) + if (isFeeCoverage) { + add( + NotificationUM.Warning.FeeCoverageNotification( + cryptoAmount = BigDecimalFormatter.formatCryptoAmountUncapped( + cryptoAmount = cryptoDiff, + cryptoCurrency = cryptoCurrency, + ), + fiatAmount = getFiatString( + value = cryptoDiff, + rate = fiatRate, + appCurrency = appCurrency, + ), + ), + ) + } + } + + fun MutableList.addDustWarningNotification( + dustValue: BigDecimal?, + feeValue: BigDecimal, + sendingAmount: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + ) { + if (dustValue == null) return + val isExceedsLimit = checkDustLimits( + feeAmount = feeValue, + receivedAmount = sendingAmount, + dustValue = dustValue, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + ) + if (isExceedsLimit) { + add( + NotificationUM.Error.MinimumAmountError( + amount = dustValue.parseBigDecimal(cryptoCurrencyStatus.currency.decimals), + ), + ) + } + } + + fun MutableList.addExceedsBalanceNotification( + cryptoCurrencyWarning: CryptoCurrencyWarning?, + cryptoCurrencyStatus: CryptoCurrencyStatus, + shouldMergeFeeNetworkName: Boolean, + onClick: (CryptoCurrency) -> Unit, + onAnalyticsEvent: (CryptoCurrency) -> Unit, + ) { + when (cryptoCurrencyWarning) { + is CryptoCurrencyWarning.BalanceNotEnoughForFee -> { + add( + NotificationUM.Error.ExceedsBalance( + networkIconId = cryptoCurrencyWarning.coinCurrency.networkIconResId, + networkName = cryptoCurrencyWarning.coinCurrency.name, + currencyName = cryptoCurrencyStatus.currency.name, + feeName = cryptoCurrencyWarning.coinCurrency.name, + feeSymbol = cryptoCurrencyWarning.coinCurrency.symbol, + mergeFeeNetworkName = shouldMergeFeeNetworkName, + onClick = { + onClick(cryptoCurrencyWarning.coinCurrency) + }, + ), + ) + onAnalyticsEvent(cryptoCurrencyStatus.currency) + } + is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> { + val currency = cryptoCurrencyWarning.feeCurrency + add( + NotificationUM.Error.ExceedsBalance( + networkIconId = currency?.networkIconResId ?: R.drawable.ic_alert_24, + currencyName = cryptoCurrencyWarning.currency.name, + feeName = cryptoCurrencyWarning.feeCurrencyName, + feeSymbol = cryptoCurrencyWarning.feeCurrencySymbol, + networkName = cryptoCurrencyWarning.networkName, + mergeFeeNetworkName = shouldMergeFeeNetworkName, + onClick = { + currency?.let { + onClick(currency) + } + }, + ), + ) + onAnalyticsEvent(cryptoCurrencyWarning.currency) + } + else -> Unit + } + } + + fun MutableList.addValidateTransactionNotifications( + dustValue: BigDecimal, + fee: Fee?, + validationError: Throwable?, + cryptoCurrency: CryptoCurrency, + onReduceClick: ( + reduceAmountTo: BigDecimal, + notification: Class, + ) -> Unit, + ) { + when (validationError) { + is BlockchainSdkError.Cardano -> addCardanoTransactionValidationError( + error = validationError, + sendingCurrency = cryptoCurrency, + dustValue = dustValue, + ) + is BlockchainSdkError.Koinos -> addKoinosTransactionValidationError( + error = validationError, + onReduceClick = onReduceClick, + ) + null -> (fee as? Fee.CardanoToken)?.let { + add( + NotificationUM.Cardano.MinAdaValueCharged( + tokenName = cryptoCurrency.name, + minAdaValue = it.minAdaValue.parseBigDecimal(cryptoCurrency.decimals), + ), + ) + } + else -> return + } + } + + private fun MutableList.addCardanoTransactionValidationError( + error: BlockchainSdkError.Cardano, + sendingCurrency: CryptoCurrency, + dustValue: BigDecimal?, + ) { + when (error) { + BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> { + add(NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name)) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> { + when (sendingCurrency) { + is CryptoCurrency.Coin -> NotificationUM.Cardano.InsufficientBalanceToTransferCoin + is CryptoCurrency.Token -> { + NotificationUM.Cardano.InsufficientBalanceToTransferToken(sendingCurrency.name) + } + }.let(::add) + } + BlockchainSdkError.Cardano.InsufficientRemainingBalance, + BlockchainSdkError.Cardano.InsufficientSendingAdaAmount, + -> { + dustValue?.let { + add( + NotificationUM.Error.MinimumAmountError( + amount = it.parseBigDecimal(sendingCurrency.decimals), + ), + ) + } + } + } + } + + private fun MutableList.addKoinosTransactionValidationError( + error: BlockchainSdkError.Koinos, + onReduceClick: ( + reduceAmountTo: BigDecimal, + notification: Class, + ) -> Unit, + ) { + when (error) { + is BlockchainSdkError.Koinos.InsufficientBalance -> { + add(NotificationUM.Koinos.InsufficientBalance) + } + is BlockchainSdkError.Koinos.InsufficientMana -> { + add( + NotificationUM.Koinos.InsufficientRecoverableMana( + mana = error.manaBalance ?: BigDecimal.ZERO, + maxMana = error.maxMana ?: BigDecimal.ZERO, + ), + ) + } + is BlockchainSdkError.Koinos.ManaFeeExceedsBalance -> { + add( + NotificationUM.Koinos.ManaExceedsBalance( + availableKoinForTransfer = error.availableKoinForTransfer, + onReduceClick = { + onReduceClick( + error.availableKoinForTransfer, + NotificationUM.Koinos.InsufficientRecoverableMana::class.java, + ) + }, + ), + ) + } + else -> {} + } + } + + private fun checkDustLimits( + feeAmount: BigDecimal, + receivedAmount: BigDecimal, + dustValue: BigDecimal, + cryptoCurrencyStatus: CryptoCurrencyStatus, + feeCurrencyStatus: CryptoCurrencyStatus?, + ): Boolean { + val change = when (cryptoCurrencyStatus.currency) { + is CryptoCurrency.Coin -> { + val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + balance - (feeAmount + receivedAmount) + } + is CryptoCurrency.Token -> { + val balance = feeCurrencyStatus?.value?.amount ?: BigDecimal.ZERO + balance - feeAmount + } + } + + val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO + return receivedAmount < dustValue || isChangeLowerThanDust + } +} \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt b/domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/GetFeeError.kt similarity index 100% rename from domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt rename to domain/transaction/models/src/main/kotlin/com/tangem/domain/transaction/error/GetFeeError.kt diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt index a9dcad67cf..dca4eefe62 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/NotificationsBlock.kt @@ -5,6 +5,7 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.CardWithIcon import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.extensions.resolveReference @@ -13,7 +14,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingNotification import kotlinx.collections.immutable.ImmutableList @Composable -internal fun NotificationsBlock(notifications: ImmutableList) { +internal fun NotificationsBlock(notifications: ImmutableList) { notifications.forEach { notification -> key(notification) { if (notification is StakingNotification.Warning.TransactionInProgress) { @@ -32,8 +33,15 @@ internal fun NotificationsBlock(notifications: ImmutableList TangemTheme.colors.icon.warning is StakingNotification.Warning -> TangemTheme.colors.icon.accent + + is NotificationUM.Error.ExceedsBalance, + is NotificationUM.Warning, + -> null + + is StakingNotification.Error, + is NotificationUM.Error, + -> TangemTheme.colors.icon.warning }, ) } diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 830677086d..89f796a6b7 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -56,6 +56,11 @@ object BlockchainUtils { return Blockchain.fromNetworkId(networkId)?.isSupportedInApp() ?: false } + fun isArbitrum(networkId: String): Boolean { + val blockchain = Blockchain.fromId(networkId) + return blockchain == Blockchain.Arbitrum + } + data class BlockchainInfo( val blockchainId: String, val name: String, From 52ea08c979029df6bd3ed651e225a8313ea6e272 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 18:08:43 +0500 Subject: [PATCH 2/3] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 6 + .../common/ui/feeScreen/FeeCalculation.kt | 86 ++++++++ .../model/warnings/CryptoCurrencyCheck.kt | 11 ++ .../domain/tokens/GetCurrencyCheckUseCase.kt | 41 ++++ .../staking/impl/di/StakingRouterModule.kt | 6 +- .../impl/navigation/DefaultStakingRouter.kt | 18 ++ .../impl/navigation/InnerStakingRouter.kt | 4 + .../AddStakingNotificationsTransformer.kt | 186 ++++++++++++++++++ .../amount/AmountReduceByStateTransformer.kt | 19 ++ .../amount/AmountReduceToStateTransformer.kt | 18 ++ 10 files changed, 393 insertions(+), 2 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt create mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 078e47c07e..5169a4b83a 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -436,4 +436,10 @@ internal object TokensDomainModule { fun provideCheckHasLinkedTokensUseCase(currenciesRepository: CurrenciesRepository): CheckHasLinkedTokensUseCase { return CheckHasLinkedTokensUseCase(currenciesRepository) } + + @Provides + @Singleton + fun provideGetCurrencyCheckUseCase(currencyChecksRepository: CurrencyChecksRepository): GetCurrencyCheckUseCase { + return GetCurrencyCheckUseCase(currencyChecksRepository) + } } \ No newline at end of file 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 new file mode 100644 index 0000000000..d1c3719c0a --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/feeScreen/FeeCalculation.kt @@ -0,0 +1,86 @@ +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/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt new file mode 100644 index 0000000000..04018ed4aa --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyCheck.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.tokens.model.warnings + +import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit +import java.math.BigDecimal + +data class CryptoCurrencyCheck( + val dustValue: BigDecimal?, + val reserveAmount: BigDecimal?, + val existentialDeposit: BigDecimal?, + val utxoAmountLimit: UtxoAmountLimit?, +) \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt new file mode 100644 index 0000000000..4e74a21591 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyCheckUseCase.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.tokens + +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck +import com.tangem.domain.tokens.repository.CurrencyChecksRepository +import com.tangem.domain.wallets.models.UserWalletId +import java.math.BigDecimal + +class GetCurrencyCheckUseCase( + private val currencyChecksRepository: CurrencyChecksRepository, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + currencyStatus: CryptoCurrencyStatus, + amount: BigDecimal?, + fee: BigDecimal?, + ): CryptoCurrencyCheck { + val network = currencyStatus.currency.network + val dustValue = currencyChecksRepository.getDustValue(userWalletId, network) + val reserveAmount = currencyChecksRepository.getReserveAmount(userWalletId, network) + val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, network) + val utxoAmountLimit = if (amount != null && fee != null) { + currencyChecksRepository.checkUtxoAmountLimit( + userWalletId = userWalletId, + network = network, + amount = amount, + fee = fee, + ) + } else { + null + } + + return CryptoCurrencyCheck( + dustValue = dustValue, + reserveAmount = reserveAmount, + existentialDeposit = existentialDeposit, + utxoAmountLimit = utxoAmountLimit, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt index c64b0dd4f2..1e01cf0b3d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/di/StakingRouterModule.kt @@ -1,8 +1,9 @@ package com.tangem.features.staking.impl.di +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener -import com.tangem.features.staking.impl.navigation.DefaultStakingRouter import com.tangem.features.staking.api.navigation.StakingRouter +import com.tangem.features.staking.impl.navigation.DefaultStakingRouter import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,9 +19,10 @@ internal object StakingRouterModule { @Provides @ActivityScoped - fun provideStakingRouter(urlOpener: UrlOpener): StakingRouter { + fun provideStakingRouter(urlOpener: UrlOpener, router: AppRouter): StakingRouter { return DefaultStakingRouter( urlOpener = urlOpener, + router = router, ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt index 464d28694e..6acad0658b 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/DefaultStakingRouter.kt @@ -1,15 +1,33 @@ package com.tangem.features.staking.impl.navigation import androidx.fragment.app.Fragment +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.navigation.url.UrlOpener +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.impl.presentation.StakingFragment internal class DefaultStakingRouter( private val urlOpener: UrlOpener, + private val router: AppRouter, ) : InnerStakingRouter { override fun getEntryFragment(): Fragment = StakingFragment.create() override fun openUrl(url: String) { urlOpener.openUrl(url) } + + override fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) { + router.pop { isSuccess -> + if (isSuccess) { + router.push( + AppRoute.CurrencyDetails( + userWalletId = userWalletId, + currency = currency, + ), + ) + } + } + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt index d4197489fb..e1fec4bab1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/navigation/InnerStakingRouter.kt @@ -1,8 +1,12 @@ package com.tangem.features.staking.impl.navigation +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.staking.api.navigation.StakingRouter interface InnerStakingRouter : StakingRouter { fun openUrl(url: String) + + fun openTokenDetails(userWalletId: UserWalletId, currency: CryptoCurrency) } \ No newline at end of file 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 new file mode 100644 index 0000000000..2776bf6869 --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/AddStakingNotificationsTransformer.kt @@ -0,0 +1,186 @@ +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 +import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification +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.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.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.Provider +import com.tangem.utils.extensions.orZero +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal + +@Suppress("LongParameterList") +internal class AddStakingNotificationsTransformer( + private val cryptoCurrencyStatusProvider: Provider, + private val appCurrencyProvider: Provider, + private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?, + private val currencyWarning: CryptoCurrencyWarning?, + private val validatorError: Throwable?, + private val feeError: GetFeeError?, + private val currencyCheck: CryptoCurrencyCheck, + private val isSubtractAvailable: Boolean, +) : 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 amountValue = amountState.amountTextField.cryptoAmount.value.orZero() + val feeValue = feeState.fee?.amount?.value.orZero() + val reduceAmountBy = confirmationState.reduceAmountBy.orZero() + + val isFeeCoverage = checkFeeCoverage( + amountValue = amountValue, + feeValue = feeValue, + balance = balance, + isSubtractAvailable = isSubtractAvailable, + reduceAmountBy = reduceAmountBy, + ) + val sendingAmount = checkAndCalculateSubtractedAmount( + isAmountSubtractAvailable = isSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + amountValue = amountValue, + feeValue = feeValue, + reduceAmountBy = reduceAmountBy, + ) + + val notifications = buildList { + // errors + addErrorNotifications( + prevState = prevState, + feeError = feeError, + sendingAmount = sendingAmount, + onReload = { prevState.clickIntents.loadFee(confirmationState.pendingActions) }, + feeValue = feeValue, + ) + // warnings + addWarningNotifications( + prevState = prevState, + amountState = amountState, + feeState = feeState, + sendingAmount = sendingAmount, + isFeeCoverage = isFeeCoverage, + ) + + addAll(confirmationState.notifications) + }.toImmutableList() + + return prevState.copy( + confirmationState = confirmationState.copy( + notifications = notifications.toImmutableList(), + ), + ) + } + + private fun MutableList.addErrorNotifications( + prevState: StakingUiState, + onReload: () -> Unit, + feeError: GetFeeError?, + sendingAmount: BigDecimal, + feeValue: BigDecimal, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + val network = cryptoCurrency.network + + if (feeError != null) { + addFeeUnreachableNotification( + feeError = feeError, + tokenName = cryptoCurrencyStatusProvider().currency.name, + onReload = onReload, + ) + } + addExceedBalanceNotification( + feeAmount = feeValue, + sendingAmount = sendingAmount, + isSubtractionAvailable = isSubtractAvailable, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + addExceedsBalanceNotification( + cryptoCurrencyWarning = currencyWarning, + cryptoCurrencyStatus = cryptoCurrencyStatus, + shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId), + onClick = prevState.clickIntents::openTokenDetails, + onAnalyticsEvent = { /* [REDACTED_TODO_COMMENT] */ }, + ) + if (!BlockchainUtils.isCardano(network.id.value)) { + addDustWarningNotification( + dustValue = currencyCheck.dustValue, + feeValue = feeValue, + sendingAmount = sendingAmount, + cryptoCurrencyStatus = cryptoCurrencyStatus, + feeCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + addTransactionLimitErrorNotification( + utxoLimit = currencyCheck.utxoAmountLimit, + cryptoCurrency = cryptoCurrency, + onReduceClick = prevState.clickIntents::onAmountReduceToClick, + ) + addReserveAmountErrorNotification( + reserveAmount = currencyCheck.reserveAmount, + sendingAmount = sendingAmount, + cryptoCurrency = cryptoCurrency, + isAccountFunded = false, + ) + } + + private fun MutableList.addWarningNotifications( + prevState: StakingUiState, + amountState: AmountState.Data, + feeState: FeeState.Content, + sendingAmount: BigDecimal, + isFeeCoverage: Boolean, + ) { + val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() + val appCurrency = appCurrencyProvider() + val cryptoCurrency = cryptoCurrencyStatus.currency + + addExistentialWarningNotification( + existentialDeposit = currencyCheck.existentialDeposit, + feeAmount = feeState.fee?.amount?.value.orZero(), + receivedAmount = sendingAmount, + cryptoCurrencyStatus = cryptoCurrencyStatus, + onReduceClick = prevState.clickIntents::onAmountReduceByClick, + ) + addFeeCoverageNotification( + isFeeCoverage = isFeeCoverage, + amountField = amountState.amountTextField, + sendingValue = sendingAmount, + appCurrency = appCurrency, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) + + // blockchain specific + addValidateTransactionNotifications( + dustValue = currencyCheck.dustValue.orZero(), + fee = feeState.fee, + validationError = validatorError, + cryptoCurrency = cryptoCurrency, + onReduceClick = prevState.clickIntents::onAmountReduceToClick, + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt new file mode 100644 index 0000000000..91468a21bd --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceByStateTransformer.kt @@ -0,0 +1,19 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer +import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer + +internal class AmountReduceByStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: ReduceByData, +) : Transformer { + + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountReduceByTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + ) + } +} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt new file mode 100644 index 0000000000..b826aae59e --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountReduceToStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.staking.impl.presentation.state.transformers.amount + +import com.tangem.common.ui.amountScreen.converters.AmountReduceToTransformer +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.features.staking.impl.presentation.state.StakingUiState +import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal + +internal class AmountReduceToStateTransformer( + private val cryptoCurrencyStatus: CryptoCurrencyStatus, + private val value: BigDecimal, +) : Transformer { + override fun transform(prevState: StakingUiState): StakingUiState { + return prevState.copy( + amountState = AmountReduceToTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + ) + } +} \ No newline at end of file From 33e5b99d978d76c123341d669aecdcb79741fb7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Aug 2024 19:14:03 +0500 Subject: [PATCH 3/3] 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,