From a00ea0a438545c6ff5720fd66774112af07a7db6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Sep 2024 17:19:39 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../ui/notifications/NotificationsFactory.kt | 7 +- .../state/confirm/SendNotificationFactory.kt | 3 +- .../AddStakingNotificationsTransformer.kt | 3 +- .../swap/domain/models/domain/Warning.kt | 24 - .../swap/domain/models/ui/SwapState.kt | 5 +- .../feature/swap/domain/SwapInteractorImpl.kt | 193 ++---- .../feature/swap/models/SwapStateHolder.kt | 36 +- .../swap/models/states/SwapNotificationUM.kt | 205 ++++++ .../presentation/SwapNotificationsFactory.kt | 333 ++++++++++ .../tangem/feature/swap/ui/StateBuilder.kt | 596 ++---------------- .../feature/swap/ui/SwapScreenContent.kt | 112 ++-- .../swap/viewmodels/SwapProcessDataState.kt | 1 + .../feature/swap/viewmodels/SwapViewModel.kt | 16 +- .../com/tangem/lib/crypto/BlockchainUtils.kt | 5 + 14 files changed, 696 insertions(+), 843 deletions(-) delete mode 100644 features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt 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 index 9841d58e02..7eff241bef 100644 --- 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 @@ -1,7 +1,6 @@ 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 @@ -248,9 +247,9 @@ object NotificationsFactory { fun MutableList.addValidateTransactionNotifications( dustValue: BigDecimal, - fee: Fee?, validationError: Throwable?, cryptoCurrency: CryptoCurrency, + minAdaValue: BigDecimal?, // TODO revert to Fee, after swap TxFee refactored onReduceClick: ( reduceAmountTo: BigDecimal, notification: Class, @@ -266,11 +265,11 @@ object NotificationsFactory { error = validationError, onReduceClick = onReduceClick, ) - null -> (fee as? Fee.CardanoToken)?.let { + null -> minAdaValue?.let { add( NotificationUM.Cardano.MinAdaValueCharged( tokenName = cryptoCurrency.name, - minAdaValue = it.minAdaValue.parseBigDecimal(cryptoCurrency.decimals), + minAdaValue = minAdaValue.parseBigDecimal(cryptoCurrency.decimals), ), ) } diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt index 15e64cde68..4c92572d5b 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/confirm/SendNotificationFactory.kt @@ -1,6 +1,7 @@ package com.tangem.features.send.impl.presentation.state.confirm import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.common.ui.amountScreen.models.AmountState @@ -246,7 +247,7 @@ internal class SendNotificationFactory( ) addValidateTransactionNotifications( dustValue = currencyCheck.dustValue.orZero(), - fee = feeState.fee, + minAdaValue = (feeState.fee as? Fee.CardanoToken)?.minAdaValue, validationError = validationError, cryptoCurrency = currency, onReduceClick = clickIntents::onAmountReduceToClick, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index d96cda409d..f89e821f79 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers.notifications +import com.tangem.blockchain.common.transaction.Fee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification @@ -212,7 +213,7 @@ internal class AddStakingNotificationsTransformer( // blockchain specific addValidateTransactionNotifications( dustValue = currencyCheck.dustValue.orZero(), - fee = feeState?.fee, + minAdaValue = (feeState?.fee as? Fee.CardanoToken)?.minAdaValue, validationError = validatorError, cryptoCurrency = cryptoCurrency, onReduceClick = prevState.clickIntents::onAmountReduceToClick, diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt deleted file mode 100644 index 4d0d6904c4..0000000000 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.feature.swap.domain.models.domain - -import java.math.BigDecimal - -sealed class Warning { - - data class ExistentialDepositWarning( - val existentialDeposit: BigDecimal, - val minAvailableAmount: BigDecimal, - ) : Warning() - - data class MinAmountWarning(val dustValue: BigDecimal) : Warning() - - data class ReduceAmountWarning(val tezosFeeThreshold: BigDecimal) : Warning() - - sealed class Cardano : Warning() { - - data class MinAdaValueCharged(val tokenName: String, val minAdaValue: String) : Cardano() - - data object InsufficientBalanceToTransferCoin : Cardano() - - data class InsufficientBalanceToTransferToken(val tokenName: String) : Cardano() - } -} \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 6183efe111..a42a8db9a0 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* @@ -26,7 +27,9 @@ sealed interface SwapState { val permissionState: PermissionDataState = PermissionDataState.Empty, val swapDataModel: SwapDataModel? = null, val txFee: TxFeeState, - val warnings: List = emptyList(), + val currencyCheck: CryptoCurrencyCheck? = null, + val validationResult: Throwable? = null, + val minAdaValue: BigDecimal?, val swapProvider: SwapProvider, ) : SwapState diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 6ad0d0ff86..f42b692bf5 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -2,23 +2,23 @@ package com.tangem.feature.swap.domain import arrow.core.Either import arrow.core.getOrElse -import com.tangem.blockchain.common.* import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain.* +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.TransactionExtras import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.core.ui.utils.BigDecimalFormatter -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase +import com.tangem.domain.tokens.GetCurrencyCheckUseCase import com.tangem.domain.tokens.model.* -import com.tangem.domain.tokens.model.FeePaidCurrency +import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.QuotesRepository @@ -36,7 +36,6 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* -import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.ProxyAmount @@ -71,6 +70,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val validateTransactionUseCase: ValidateTransactionUseCase, private val estimateFeeUseCase: EstimateFeeUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, + private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, private val amountFormatter: AmountFormatter, @Assisted private val userWalletId: UserWalletId, ) : SwapInteractor { @@ -386,111 +386,30 @@ internal class SwapInteractorImpl @AssistedInject constructor( fromTokenStatus: CryptoCurrencyStatus, amount: SwapAmount, feeState: TxFeeState, - minAdaValue: BigDecimal?, - ): List { - val fromToken = fromTokenStatus.currency - val warnings = mutableListOf() - manageExistentialDepositWarning(warnings, userWalletId, amount, fromToken, feeState) - manageDustWarning(warnings, feeState, userWalletId, fromTokenStatus, amount) - manageReduceAmountWarning(warnings, fromTokenStatus, amount) - manageTransactionValidationWarnings( - warnings = warnings, - fromToken = fromToken, - amount = amount, - feeState = feeState, - userWalletId = userWalletId, - minAdaValue = minAdaValue, - ) - return warnings - } - - private suspend fun manageExistentialDepositWarning( - warnings: MutableList, - userWalletId: UserWalletId, - amount: SwapAmount, - fromToken: CryptoCurrency, - txFee: TxFeeState, - ) { - val existentialDeposit = currencyChecksRepository.getExistentialDeposit(userWalletId, fromToken.network) - if (existentialDeposit != null) { - val nativeBalance = userWalletManager.getNativeTokenBalance( - fromToken.network.backendId, - fromToken.network.derivationPath.value, - ) ?: ProxyAmount.empty() - // ignore if amount is bigger than balance - if (amount.value > nativeBalance.value) { - return - } - val fee = when (txFee) { - TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue - is TxFeeState.SingleFeeState -> txFee.fee.feeValue - } - val minAvailableAmount = nativeBalance.value - existentialDeposit - fee - if (nativeBalance.value.minus(amount.value + fee) < existentialDeposit) { - warnings.add(Warning.ExistentialDepositWarning(existentialDeposit, minAvailableAmount)) - } - } - } - - private suspend fun manageDustWarning( - warnings: MutableList, - feeState: TxFeeState, - userWalletId: UserWalletId, - fromTokenStatus: CryptoCurrencyStatus, - amount: SwapAmount, - ) { - if (BlockchainUtils.isCardano(fromTokenStatus.currency.network.id.value)) return - + ): CryptoCurrencyCheck { val fee = when (feeState) { TxFeeState.Empty -> BigDecimal.ZERO is TxFeeState.MultipleFeeState -> feeState.priorityFee.feeValue is TxFeeState.SingleFeeState -> feeState.fee.feeValue } - val dustValue = currencyChecksRepository.getDustValue(userWalletId, fromTokenStatus.currency.network) ?: return + val currencyCheck = getCurrencyCheckUseCase( + userWalletId = userWalletId, + currencyStatus = fromTokenStatus, + amount = amount.value, + fee = fee, + ) - val change = when (fromTokenStatus.currency) { - is CryptoCurrency.Coin -> { - val balance = fromTokenStatus.value.amount ?: BigDecimal.ZERO - balance - (fee + amount.value) - } - is CryptoCurrency.Token -> { - val nativeTokenBalance = userWalletManager.getNativeTokenBalance( - fromTokenStatus.currency.network.id.value, - fromTokenStatus.currency.network.derivationPath.value, - ) - - nativeTokenBalance?.value?.minus(fee) ?: BigDecimal.ZERO - } - } - - val isChangeLowerThanDust = change < dustValue && change > BigDecimal.ZERO - - if (amount.value < dustValue || isChangeLowerThanDust) { - warnings.add(Warning.MinAmountWarning(dustValue)) - } - } - - private fun manageReduceAmountWarning( - warnings: MutableList, - fromTokenStatus: CryptoCurrencyStatus, - amount: SwapAmount, - ) { - val isTezos = fromTokenStatus.currency.network.id.value == Blockchain.Tezos.id - if (isTezos && amount.value == fromTokenStatus.value.amount) { - warnings.add(Warning.ReduceAmountWarning(Blockchain.Tezos.minimalAmount())) - } + return currencyCheck } private suspend fun manageTransactionValidationWarnings( - warnings: MutableList, - fromToken: CryptoCurrency, + fromToken: CryptoCurrencyStatus, amount: SwapAmount, feeState: TxFeeState, userWalletId: UserWalletId, - minAdaValue: BigDecimal?, - ) { + ): Throwable? { + val currency = fromToken.currency val fee = Fee.Common( amount = Amount( value = when (feeState) { @@ -498,66 +417,20 @@ internal class SwapInteractorImpl @AssistedInject constructor( is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue is TxFeeState.SingleFeeState -> feeState.fee.feeValue }, - blockchain = Blockchain.fromId(fromToken.network.id.value), + blockchain = Blockchain.fromId(currency.network.id.value), ), ) - validateTransactionUseCase( - amount = amount.value.convertToSdkAmount(fromToken), + val result = validateTransactionUseCase( + amount = amount.value.convertToSdkAmount(currency), fee = fee, memo = null, - destination = getTokenAddress(fromToken), + destination = getTokenAddress(fromToken.currency), userWalletId = userWalletId, - network = fromToken.network, - ).fold( - ifLeft = { - addCardanoTransactionValidationError( - warnings = warnings, - error = it as? BlockchainSdkError.Cardano ?: return@fold, - fromToken = fromToken, - userWalletId = userWalletId, - ) - }, - ifRight = { - minAdaValue?.let { - warnings.add( - Warning.Cardano.MinAdaValueCharged( - tokenName = fromToken.name, - minAdaValue = minAdaValue.parseBigDecimal(fromToken.decimals), - ), - ) - } - }, - ) - } + network = currency.network, + ).leftOrNull() - private suspend fun addCardanoTransactionValidationError( - warnings: MutableList, - error: BlockchainSdkError.Cardano, - fromToken: CryptoCurrency, - userWalletId: UserWalletId, - ) { - when (error) { - BlockchainSdkError.Cardano.InsufficientMinAdaBalanceToSendToken -> { - Warning.Cardano.InsufficientBalanceToTransferToken(fromToken.name) - } - BlockchainSdkError.Cardano.InsufficientRemainingBalanceToWithdrawTokens -> { - when (fromToken) { - is CryptoCurrency.Coin -> Warning.Cardano.InsufficientBalanceToTransferCoin - is CryptoCurrency.Token -> { - Warning.Cardano.InsufficientBalanceToTransferToken(fromToken.name) - } - } - } - BlockchainSdkError.Cardano.InsufficientRemainingBalance, - BlockchainSdkError.Cardano.InsufficientSendingAdaAmount, - -> { - val dustValue = currencyChecksRepository.getDustValue(userWalletId, fromToken.network) ?: return - - Warning.MinAmountWarning(dustValue) - } - } - .let(warnings::add) // add warning to the list + return result } override suspend fun onSwap( @@ -1238,12 +1111,18 @@ internal class SwapInteractorImpl @AssistedInject constructor( txFeeState = txFee, provider = provider, ).copy( - warnings = manageWarnings( + currencyCheck = manageWarnings( fromTokenStatus = fromToken, amount = amount, feeState = txFee, - minAdaValue = (transactionFee?.normal as? Fee.CardanoToken)?.minAdaValue, ), + validationResult = manageTransactionValidationWarnings( + fromToken = fromToken, + amount = amount, + feeState = txFee, + userWalletId = userWalletId, + ), + minAdaValue = (transactionFee?.normal as? Fee.CardanoToken)?.minAdaValue, ) when (provider.type) { @@ -1478,11 +1357,16 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) swapState.copy( permissionState = PermissionDataState.Empty, - warnings = manageWarnings( + currencyCheck = manageWarnings( fromTokenStatus = fromToken, amount = amount, feeState = txFeeState, - minAdaValue = null, // no ADA in DEX + ), + validationResult = manageTransactionValidationWarnings( + fromToken = fromToken, + amount = amount, + feeState = txFeeState, + userWalletId = userWalletId, ), preparedSwapConfigState = preparedSwapConfigState, ) @@ -1578,6 +1462,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapDataModel = swapData, txFee = txFeeState, swapProvider = provider, + minAdaValue = null, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 8b21ec6919..50efc0c19f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -3,9 +3,9 @@ package com.tangem.feature.swap.models import androidx.annotation.DrawableRes import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference @@ -14,12 +14,15 @@ import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.models.states.events.SwapEvent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf internal data class SwapStateHolder( val sendCardData: SwapCardState, val receiveCardData: SwapCardState, val blockchainId: String, // not the same as networkId, its local id in app - val warnings: List = emptyList(), + val notifications: ImmutableList = persistentListOf(), + val isInsufficientFunds: Boolean, val event: StateEvent = consumedEvent(), val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED, val providerState: ProviderState, @@ -106,35 +109,6 @@ data class LegalState( val onClick: (String) -> Unit, ) -sealed interface SwapWarning { - data class PermissionNeeded(val notificationConfig: NotificationConfig) : SwapWarning - data object InsufficientFunds : SwapWarning - data class NoAvailableTokensToSwap(val notificationConfig: NotificationConfig) : SwapWarning - data class GenericWarning( - val title: TextReference? = null, - val message: TextReference? = null, - val onClick: () -> Unit, - ) : SwapWarning - - data class GeneralError(val notificationConfig: NotificationConfig) : SwapWarning - data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning - data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning - data class GeneralInformational(val notificationConfig: NotificationConfig) : SwapWarning - data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning - data class NeedReserveToCreateAccount(val notificationConfig: NotificationConfig) : SwapWarning - data class ReduceAmount(val notificationConfig: NotificationConfig) : SwapWarning - - sealed interface Cardano : SwapWarning { - val notificationConfig: NotificationConfig - - data class MinAdaValueCharged(override val notificationConfig: NotificationConfig) : Cardano - - data class InsufficientBalanceToTransferCoin(override val notificationConfig: NotificationConfig) : Cardano - - data class InsufficientBalanceToTransferToken(override val notificationConfig: NotificationConfig) : Cardano - } -} - enum class ChangeCardsButtonState { ENABLED, DISABLED, UPDATE_IN_PROGRESS } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt new file mode 100644 index 0000000000..7736b9a69d --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt @@ -0,0 +1,205 @@ +package com.tangem.feature.swap.models.states + +import com.tangem.common.ui.R +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.feature.swap.domain.models.ExpressDataError +import com.tangem.feature.swap.utils.getExpressErrorMessage +import com.tangem.feature.swap.utils.getExpressErrorTitle + +internal object SwapNotificationUM { + + sealed class Error( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.ic_alert_24, + buttonState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM.Error( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonState = buttonState, + onCloseClick = onCloseClick, + ) { + data class GenericError( + val title: TextReference = resourceReference(id = R.string.common_warning), + val subtitle: TextReference?, + val onConfirmClick: () -> Unit, + ) : Error( + title = title, + subtitle = subtitle ?: resourceReference(id = R.string.common_unknown_error), + buttonState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = resourceReference(R.string.common_ok), + onClick = onConfirmClick, + ), + ) + + data object ApprovalInProgressWarning : Error( + title = resourceReference(R.string.warning_express_approval_in_progress_title), + subtitle = resourceReference(R.string.warning_express_approval_in_progress_message), + ) + + data class TransactionInProgressWarning( + val currencySymbol: String, + ) : Error( + title = resourceReference(R.string.warning_express_active_transaction_title), + subtitle = resourceReference( + id = R.string.warning_express_active_transaction_message, + formatArgs = wrappedList(currencySymbol), + ), + ) + + data class UnableToCoverFeeWarning( + val fromToken: CryptoCurrency, + val currencyName: String, + val currencySymbol: String, + val feeCurrency: CryptoCurrency?, + val onConfirmClick: (CryptoCurrency) -> Unit, + ) : Error( + title = resourceReference( + R.string.warning_express_not_enough_fee_for_token_tx_title, + wrappedList(fromToken.network.name), + ), + subtitle = resourceReference( + R.string.warning_express_not_enough_fee_for_token_tx_description, + wrappedList(currencyName, currencySymbol), + ), + iconResId = fromToken.networkIconResId, + buttonState = feeCurrency?.let { + NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.common_buy_currency, wrappedList(currencySymbol)), + onClick = { onConfirmClick(it) }, + ) + }, + ) + + data class MinimalAmountError( + val amount: String, + ) : Error( + title = resourceReference( + id = R.string.warning_express_too_minimal_amount_title, + formatArgs = wrappedList(amount), + ), + subtitle = resourceReference(R.string.warning_express_wrong_amount_description), + ) + + data class MaximumAmountError( + val amount: String, + ) : Error( + title = resourceReference( + id = R.string.warning_express_too_maximum_amount_title, + formatArgs = wrappedList(amount), + ), + subtitle = resourceReference(R.string.warning_express_wrong_amount_description), + iconResId = R.drawable.ic_alert_circle_24, + ) + } + + sealed class Warning( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.img_attention_20, + buttonsState: NotificationConfig.ButtonsState? = null, + onCloseClick: (() -> Unit)? = null, + ) : NotificationUM.Warning( + title = title, + subtitle = subtitle, + iconResId = iconResId, + buttonsState = buttonsState, + onCloseClick = onCloseClick, + ) { + data class NoAvailableTokensToSwap( + val tokenName: String, + ) : Warning( + title = resourceReference( + com.tangem.feature.swap.presentation.R.string.warning_express_no_exchangeable_coins_title, + ), + subtitle = resourceReference( + id = com.tangem.feature.swap.presentation.R.string.warning_express_no_exchangeable_coins_description, + formatArgs = wrappedList(tokenName), + ), + ) + + data class NeedReserveToCreateAccount( + val amount: String, + val token: String, + ) : Warning( + title = resourceReference( + id = R.string.send_notification_invalid_reserve_amount_title, + formatArgs = wrappedList("$amount $token"), + ), + subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text), + ) + + data class ReduceAmount( + val currencyName: String, + val amount: String, + val onConfirmClick: () -> 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.xtz_withdrawal_message_reduce, + wrappedList(amount), + ), + onClick = onConfirmClick, + ), + ) + + data class ExpressError( + val expressDataError: ExpressDataError, + val onConfirmClick: () -> Unit, + ) : Warning( + title = getExpressErrorTitle(expressDataError), + subtitle = getExpressErrorMessage(expressDataError), + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = onConfirmClick, + ), + ) + + data class ExpressGeneralError( + val code: Int, + val onConfirmClick: () -> Unit, + ) : Warning( + title = TextReference.Res(R.string.warning_express_refresh_required_title), + subtitle = TextReference.Res(R.string.express_error_code, wrappedList(code)), + buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( + text = TextReference.Res(R.string.warning_button_refresh), + onClick = onConfirmClick, + ), + ) + } + + sealed class Info( + title: TextReference, + subtitle: TextReference, + iconResId: Int = R.drawable.ic_alert_circle_24, + ) : NotificationUM.Info( + title = title, + subtitle = subtitle, + iconResId = iconResId, + ) { + data class PermissionNeeded( + val providerName: String, + val fromTokenSymbol: String, + ) : Info( + title = resourceReference(R.string.express_provider_permission_needed), + subtitle = resourceReference( + id = R.string.give_permission_swap_subtitle, + formatArgs = wrappedList(providerName, fromTokenSymbol), + ), + iconResId = R.drawable.ic_locked_24, + ) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt new file mode 100644 index 0000000000..e5993fd356 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/presentation/SwapNotificationsFactory.kt @@ -0,0 +1,333 @@ +package com.tangem.feature.swap.presentation + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.domain.models.ExpressDataError +import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount +import com.tangem.feature.swap.domain.models.domain.SwapFeeState +import com.tangem.feature.swap.domain.models.ui.* +import com.tangem.feature.swap.models.UiActions +import com.tangem.feature.swap.models.states.FeeItemState +import com.tangem.feature.swap.models.states.SwapNotificationUM +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.lib.crypto.BlockchainUtils.getTezosThreshold +import com.tangem.lib.crypto.BlockchainUtils.isTezos +import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +internal class SwapNotificationsFactory( + private val actions: UiActions, +) { + + fun getInitialErrorStateNotifications(code: Int, onRefreshClick: () -> Unit): ImmutableList { + return persistentListOf( + SwapNotificationUM.Warning.ExpressGeneralError( + code = code, + onConfirmClick = onRefreshClick, + ), + ) + } + + fun getGeneralErrorStateNotifications( + notifications: ImmutableList, + message: TextReference?, + onClick: () -> Unit, + ): ImmutableList { + val updatedNotifications = notifications.toMutableList() + updatedNotifications.add( + SwapNotificationUM.Error.GenericError( + subtitle = message, + onConfirmClick = onClick, + ), + ) + + return updatedNotifications.toPersistentList() + } + + fun getNotAvailableStateNotifications(fromCurrencyName: String): ImmutableList { + return persistentListOf( + SwapNotificationUM.Warning.NoAvailableTokensToSwap(fromCurrencyName), + ) + } + + fun getQuotesErrorStateNotifications( + expressDataError: ExpressDataError, + fromToken: CryptoCurrency, + feeItem: FeeItemState, + includeFeeInAmount: IncludeFeeInAmount, + ): ImmutableList { + return buildList { + add(getWarningForError(expressDataError, fromToken, actions.onRetryClick)) + if (includeFeeInAmount is IncludeFeeInAmount.Included && feeItem is FeeItemState.Content) { + add( + NotificationUM.Warning.FeeCoverageNotification( + feeItem.amountCrypto, + feeItem.amountFiatFormatted, + ), + ) + } + }.toPersistentList() + } + + fun getApprovalInProgressStateNotification( + notifications: ImmutableList, + ): ImmutableList { + val updatedNotifications = notifications + .filterNot { it is SwapNotificationUM.Info.PermissionNeeded } + .toMutableList() + + updatedNotifications.add(0, SwapNotificationUM.Error.ApprovalInProgressWarning) + + return updatedNotifications.toPersistentList() + } + + fun getConfirmationStateNotifications( + quoteModel: SwapState.QuotesLoadedState, + fromToken: CryptoCurrency, + feeCryptoCurrencyStatus: CryptoCurrencyStatus?, + selectedFeeType: FeeType, + providerName: String, + ): ImmutableList { + val warnings = buildList { + maybeAddDomainWarnings(quoteModel, feeCryptoCurrencyStatus, selectedFeeType) + maybeAddNeedReserveToCreateAccountWarning(quoteModel) + maybeAddPermissionNeededWarning(quoteModel, fromToken, providerName) + maybeAddNetworkFeeCoverageWarning(quoteModel, selectedFeeType) + maybeAddUnableCoverFeeWarning(quoteModel, fromToken) + maybeAddTransactionInProgressWarning(quoteModel) + } + return warnings.toPersistentList() + } + + private fun MutableList.maybeAddTransactionInProgressWarning( + quoteModel: SwapState.QuotesLoadedState, + ) { + if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { + add(SwapNotificationUM.Error.ApprovalInProgressWarning) + } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { + add( + SwapNotificationUM.Error.TransactionInProgressWarning( + currencySymbol = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.network.currencySymbol, + ), + ) + } + } + + private fun MutableList.maybeAddDomainWarnings( + quoteModel: SwapState.QuotesLoadedState, + feeCryptoCurrencyStatus: CryptoCurrencyStatus?, + selectedFeeType: FeeType, + ) { + val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus + val includeFeeInAmount = quoteModel.preparedSwapConfigState.includeFeeInAmount + val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmount.Included) { + includeFeeInAmount.amountSubtractFee + } else { + quoteModel.fromTokenInfo.tokenAmount + } + val fee = when (val feeState = quoteModel.txFee) { + TxFeeState.Empty -> null + is TxFeeState.MultipleFeeState -> if (feeState.normalFee.feeType == selectedFeeType) { + feeState.normalFee + } else { + feeState.priorityFee + } + is TxFeeState.SingleFeeState -> feeState.fee + } + val isCardano = BlockchainUtils.isCardano(fromCurrencyStatus.currency.network.id.value) + // blockchain specific + + addExistentialWarningNotification( + existentialDeposit = quoteModel.currencyCheck?.existentialDeposit, + feeAmount = fee?.feeValue.orZero(), + receivedAmount = amountToRequest.value, + cryptoCurrencyStatus = fromCurrencyStatus, + onReduceClick = { _, reduceByDiff, _ -> + actions.onLeaveExistentialDeposit( + amountToRequest.copy( + value = amountToRequest.value.minus(reduceByDiff).minus(fee?.feeValue.orZero()), + ), + ) + }, + ) + addValidateTransactionNotifications( + dustValue = quoteModel.currencyCheck?.dustValue.orZero(), + validationError = quoteModel.validationResult, + cryptoCurrency = fromCurrencyStatus.currency, + minAdaValue = quoteModel.minAdaValue, + onReduceClick = { reduceTo, _ -> + actions.onLeaveExistentialDeposit(amountToRequest.copy(value = reduceTo)) + }, + ) + if (!isCardano) { + addDustWarningNotification( + dustValue = quoteModel.currencyCheck?.dustValue, + feeValue = fee?.feeValue.orZero(), + sendingAmount = amountToRequest.value, + cryptoCurrencyStatus = fromCurrencyStatus, + feeCurrencyStatus = feeCryptoCurrencyStatus, + ) + } + addReserveAmountErrorNotification( + reserveAmount = quoteModel.currencyCheck?.reserveAmount, + sendingAmount = amountToRequest.value, + cryptoCurrency = fromCurrencyStatus.currency, + isAccountFunded = false, + ) + addReduceAmountNotification( + cryptoCurrencyStatus = fromCurrencyStatus, + fromAmount = amountToRequest, + onReduceAmount = actions.onReduceAmount, + ) + } + + private fun MutableList.maybeAddNeedReserveToCreateAccountWarning( + quoteModel: SwapState.QuotesLoadedState, + ) { + val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value + if (status is CryptoCurrencyStatus.NoAccount) { + val amount = quoteModel.toTokenInfo.tokenAmount.value + val amountToCreateAccount = status.amountToCreateAccount + val currencyTo = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency + if (amount < amountToCreateAccount) { + add( + SwapNotificationUM.Warning.NeedReserveToCreateAccount( + status.amountToCreateAccount.parseBigDecimal(currencyTo.decimals), + currencyTo.symbol, + ), + ) + } + } + } + + private fun MutableList.maybeAddPermissionNeededWarning( + quoteModel: SwapState.QuotesLoadedState, + fromToken: CryptoCurrency, + providerName: String, + ) { + if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && + quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough && + quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest + ) { + add( + SwapNotificationUM.Info.PermissionNeeded( + providerName = fromToken.symbol, + fromTokenSymbol = providerName, + ), + ) + } + } + + private fun MutableList.maybeAddNetworkFeeCoverageWarning( + quoteModel: SwapState.QuotesLoadedState, + selectedFeeType: FeeType, + ) { + when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { + is IncludeFeeInAmount.Included -> { + val fee = selectFeeByType(selectedFeeType, quoteModel.txFee) ?: return + if (needShowNetworkFeeCoverageWarningShow(quoteModel)) { + add( + NotificationUM.Warning.FeeCoverageNotification( + fee.feeCryptoFormattedWithNative, + fee.feeFiatFormattedWithNative, + ), + ) + } + } + else -> Unit + } + } + + private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): TxFee? { + return when (txFeeState) { + TxFeeState.Empty -> null + is TxFeeState.SingleFeeState -> txFeeState.fee + is TxFeeState.MultipleFeeState -> when (feeType) { + FeeType.NORMAL -> txFeeState.normalFee + FeeType.PRIORITY -> txFeeState.priorityFee + } + } + } + + private fun MutableList.maybeAddUnableCoverFeeWarning( + quoteModel: SwapState.QuotesLoadedState, + fromToken: CryptoCurrency, + ) { + val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return + val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && + quoteModel.permissionState !is PermissionDataState.PermissionLoading && + feeEnoughState.feeCurrency != fromToken + if (needShowCoverWarning) { + add( + SwapNotificationUM.Error.UnableToCoverFeeWarning( + fromToken = fromToken, + feeCurrency = feeEnoughState.feeCurrency, + currencyName = feeEnoughState.currencyName ?: fromToken.network.name, + currencySymbol = feeEnoughState.currencySymbol ?: fromToken.network.currencySymbol, + onConfirmClick = actions.onBuyClick, + ), + ) + } + } + + private fun MutableList.addReduceAmountNotification( + cryptoCurrencyStatus: CryptoCurrencyStatus, + fromAmount: SwapAmount, + onReduceAmount: (SwapAmount) -> Unit, + ) { + val isTezos = isTezos(cryptoCurrencyStatus.currency.network.id.value) + if (isTezos && fromAmount.value == cryptoCurrencyStatus.value.amount) { + add( + SwapNotificationUM.Warning.ReduceAmount( + currencyName = cryptoCurrencyStatus.currency.name, + amount = getTezosThreshold().toPlainString(), + onConfirmClick = { + val patchedAmount = fromAmount.copy( + value = fromAmount.value - getTezosThreshold(), + ) + onReduceAmount(patchedAmount) + }, + ), + ) + } + } + + private fun getWarningForError( + expressDataError: ExpressDataError, + fromToken: CryptoCurrency, + onRetryClick: () -> Unit, + ): NotificationUM { + return when (expressDataError) { + is ExpressDataError.ExchangeTooSmallAmountError -> SwapNotificationUM.Error.MinimalAmountError( + expressDataError.amount.value.format { + crypto(fromToken.symbol, fromToken.decimals) + }, + ) + is ExpressDataError.ExchangeTooBigAmountError -> SwapNotificationUM.Error.MaximumAmountError( + expressDataError.amount.value.format { + crypto(fromToken.symbol, fromToken.decimals) + }, + ) + else -> SwapNotificationUM.Warning.ExpressError( + expressDataError, + onConfirmClick = onRetryClick, + ) + } + } + + private fun needShowNetworkFeeCoverageWarningShow(quoteModel: SwapState.QuotesLoadedState): Boolean { + return quoteModel.currencyCheck?.existentialDeposit == null + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index cc94ac10b4..73ff14265e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -4,13 +4,16 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.alerts.models.AlertDemoModeUM import com.tangem.common.ui.bottomsheet.permission.state.* +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.* +import com.tangem.core.ui.format.bigdecimal.anyDecimals +import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency @@ -20,21 +23,24 @@ import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.* +import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType +import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount +import com.tangem.feature.swap.domain.models.domain.NetworkInfo +import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.models.states.events.SwapEvent import com.tangem.feature.swap.presentation.R +import com.tangem.feature.swap.presentation.SwapNotificationsFactory import com.tangem.feature.swap.utils.formatToUIRepresentation -import com.tangem.feature.swap.utils.getExpressErrorMessage -import com.tangem.feature.swap.utils.getExpressErrorTitle import com.tangem.feature.swap.viewmodels.SwapProcessDataState import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.StringsSigns.PERCENT import com.tangem.utils.StringsSigns.TILDE_SIGN import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal import java.math.RoundingMode @@ -60,6 +66,10 @@ internal class StateBuilder( appCurrencyProvider = appCurrencyProvider, ) + private val notificationsFactory by lazy(LazyThreadSafetyMode.NONE) { + SwapNotificationsFactory(actions) + } + fun createInitialLoadingState( initialCurrencyFrom: CryptoCurrency, initialCurrencyTo: CryptoCurrency?, @@ -110,6 +120,7 @@ internal class StateBuilder( providerState = ProviderState.Empty(), shouldShowMaxAmount = false, priceImpact = PriceImpact.Empty(), + isInsufficientFunds = false, ) } @@ -141,18 +152,7 @@ internal class StateBuilder( ), canSelectAnotherToken = true, ), - warnings = listOf( - SwapWarning.NoAvailableTokensToSwap( - notificationConfig = NotificationConfig( - title = resourceReference(R.string.warning_express_no_exchangeable_coins_title), - subtitle = resourceReference( - id = R.string.warning_express_no_exchangeable_coins_description, - formatArgs = wrappedList(fromToken.currency.name), - ), - iconResId = R.drawable.img_attention_20, - ), - ), - ), + notifications = notificationsFactory.getNotAvailableStateNotifications(fromToken.currency.name), fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, @@ -211,7 +211,7 @@ internal class StateBuilder( networkIconRes = getActiveIconRes(toToken.network.id.value), isBalanceHidden = isBalanceHiddenProvider(), ), - warnings = emptyList(), + notifications = persistentListOf(), fee = FeeItemState.Empty, swapButton = SwapButton(enabled = false, onClick = {}), providerState = ProviderState.Loading(), @@ -235,6 +235,7 @@ internal class StateBuilder( uiStateHolder: SwapStateHolder, quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, + feeCryptoCurrencyStatus: CryptoCurrencyStatus?, swapProvider: SwapProvider, bestRatedProviderId: String, isNeedBestRateBadge: Boolean, @@ -243,9 +244,10 @@ internal class StateBuilder( ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder - val warnings = getWarningsForSuccessState( + val notifications = notificationsFactory.getConfirmationStateNotifications( quoteModel = quoteModel, fromToken = fromToken, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, selectedFeeType = selectedFeeType, providerName = swapProvider.name, ) @@ -304,7 +306,8 @@ internal class StateBuilder( balance = toCurrencyStatus.getFormattedAmount(isNeedSymbol = false), isBalanceHidden = isBalanceHiddenProvider(), ), - warnings = warnings, + isInsufficientFunds = isInsufficientFundsCondition(quoteModel), + notifications = notifications, permissionState = convertPermissionState( lastPermissionState = uiStateHolder.permissionState, permissionDataState = quoteModel.permissionState, @@ -314,7 +317,7 @@ internal class StateBuilder( ), fee = feeState, swapButton = SwapButton( - enabled = getSwapButtonEnabled(quoteModel), + enabled = getSwapButtonEnabled(notifications), onClick = actions.onSwapClick, ), changeCardsButtonState = if (isReverseSwapPossible) { @@ -363,281 +366,17 @@ internal class StateBuilder( ) } - @Suppress("LongMethod", "CyclomaticComplexMethod") - private fun getWarningsForSuccessState( - quoteModel: SwapState.QuotesLoadedState, - fromToken: CryptoCurrency, - selectedFeeType: FeeType, - providerName: String, - ): List { - val warnings = mutableListOf() - maybeAddDomainWarnings(quoteModel, warnings) - maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings) - maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken, providerName) - maybeAddNetworkFeeCoverageWarning(quoteModel, warnings, selectedFeeType) - maybeAddUnableCoverFeeWarning(quoteModel, fromToken, warnings) - maybeAddInsufficientFundsWarning(quoteModel, warnings) - maybeAddTransactionInProgressWarning(quoteModel, warnings) - return warnings - } - - private fun maybeAddTransactionInProgressWarning( - quoteModel: SwapState.QuotesLoadedState, - warnings: MutableList, - ) { - if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { - warnings.add( - SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.warning_express_approval_in_progress_title), - description = resourceReference(R.string.warning_express_approval_in_progress_message), - ), - ) - } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { - warnings.add( - SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.warning_express_active_transaction_title), - description = resourceReference( - id = R.string.warning_express_active_transaction_message, - formatArgs = wrappedList( - quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.network.currencySymbol, - ), - ), - ), - ) - } - } - - private fun maybeAddDomainWarnings(quoteModel: SwapState.QuotesLoadedState, warnings: MutableList) { - quoteModel.warnings.forEach { - when (it) { - is Warning.ExistentialDepositWarning -> { - warnings.add(createLeaveExistentialDepositWarning(quoteModel, it)) - } - is Warning.MinAmountWarning -> { - warnings.add( - SwapWarning.GeneralError( - NotificationConfig( - title = resourceReference(R.string.send_notification_invalid_amount_title), - subtitle = resourceReference( - R.string.warning_express_dust_message, - wrappedList( - it.dustValue.toPlainString(), - it.dustValue.toPlainString(), - ), - ), - iconResId = R.drawable.ic_alert_circle_24, - ), - ), - ) - } - is Warning.ReduceAmountWarning -> { - warnings.add( - SwapWarning.ReduceAmount( - notificationConfig = createReduceAmountNotificationConfig( - currencyName = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.name, - amount = it.tezosFeeThreshold.toPlainString(), - onConfirmClick = { - val fromAmount = quoteModel.fromTokenInfo.tokenAmount - val patchedAmount = fromAmount.copy( - value = fromAmount.value - it.tezosFeeThreshold, - ) - actions.onReduceAmount(patchedAmount) - }, - ), - ), - ) - } - Warning.Cardano.InsufficientBalanceToTransferCoin -> { - warnings.add(createInsufficientBalanceToTransferCoin()) - } - is Warning.Cardano.InsufficientBalanceToTransferToken -> { - warnings.add(createInsufficientBalanceToTransferToken(tokenName = it.tokenName)) - } - is Warning.Cardano.MinAdaValueCharged -> { - warnings.add(createMinAdaValueCharged(minAdaValue = it.minAdaValue, tokenName = it.tokenName)) - } - } - } - } - - private fun maybeAddNeedReserveToCreateAccountWarning( - quoteModel: SwapState.QuotesLoadedState, - warnings: MutableList, - ) { - val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value - if (status is CryptoCurrencyStatus.NoAccount) { - val amount = quoteModel.toTokenInfo.tokenAmount.value - val amountToCreateAccount = status.amountToCreateAccount - - if (amount < amountToCreateAccount) { - warnings.add( - SwapWarning.NeedReserveToCreateAccount( - notificationConfig = createActivateAccountNotificationConfig( - status.amountToCreateAccount, - quoteModel.toTokenInfo.cryptoCurrencyStatus.currency.symbol, - ), - ), - ) - } - } - } - - private fun createLeaveExistentialDepositWarning( - quoteModel: SwapState.QuotesLoadedState, - domainWarning: Warning.ExistentialDepositWarning, - ): SwapWarning { - val fromCurrency = quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency - val deposit = domainWarning.existentialDeposit.format { crypto(fromCurrency).uncapped() } - - return SwapWarning.GeneralError( - NotificationConfig( - title = resourceReference(R.string.send_notification_existential_deposit_title), - subtitle = resourceReference( - R.string.send_notification_existential_deposit_text, - wrappedList(deposit), - ), - iconResId = R.drawable.ic_alert_circle_24, - buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(R.string.common_ok), - onClick = { - actions.onLeaveExistentialDeposit( - SwapAmount( - domainWarning.minAvailableAmount, - fromCurrency.decimals, - ), - ) - }, - ), - ), - ) - } - - private fun maybeAddPermissionNeededWarning( - quoteModel: SwapState.QuotesLoadedState, - warnings: MutableList, - fromToken: CryptoCurrency, - providerName: String, - ) { - if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && - quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough && - quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest - ) { - warnings.add( - SwapWarning.PermissionNeeded( - createPermissionNotificationConfig(fromToken.symbol, providerName), - ), - ) - } - } - - private fun maybeAddNetworkFeeCoverageWarning( - quoteModel: SwapState.QuotesLoadedState, - warnings: MutableList, - selectedFeeType: FeeType, - ) { - when (quoteModel.preparedSwapConfigState.includeFeeInAmount) { - is IncludeFeeInAmount.Included -> { - val fee = selectFeeByType(selectedFeeType, quoteModel.txFee) ?: return - if (needShowNetworkFeeCoverageWarningShow(quoteModel)) { - warnings.add( - SwapWarning.GeneralWarning( - createNetworkFeeCoverageNotificationConfig( - fee.feeCryptoFormattedWithNative, - fee.feeFiatFormattedWithNative, - ), - ), - ) - } - } - else -> Unit - } - } - - private fun needShowNetworkFeeCoverageWarningShow(quoteModel: SwapState.QuotesLoadedState): Boolean { - return quoteModel.warnings.none { it is Warning.ExistentialDepositWarning } - } - - private fun selectFeeByType(feeType: FeeType, txFeeState: TxFeeState): TxFee? { - return when (txFeeState) { - TxFeeState.Empty -> null - is TxFeeState.SingleFeeState -> txFeeState.fee - is TxFeeState.MultipleFeeState -> when (feeType) { - FeeType.NORMAL -> txFeeState.normalFee - FeeType.PRIORITY -> txFeeState.priorityFee - } - } - } - - private fun maybeAddUnableCoverFeeWarning( - quoteModel: SwapState.QuotesLoadedState, - fromToken: CryptoCurrency, - warnings: MutableList, - ) { - val feeEnoughState = quoteModel.preparedSwapConfigState.feeState as? SwapFeeState.NotEnough ?: return - val needShowCoverWarning = quoteModel.preparedSwapConfigState.isBalanceEnough && - quoteModel.permissionState !is PermissionDataState.PermissionLoading && - feeEnoughState.feeCurrency != fromToken - if (needShowCoverWarning) { - warnings.add( - SwapWarning.UnableToCoverFeeWarning( - createUnableToCoverFeeNotificationConfig( - fromToken = fromToken, - feeCurrency = feeEnoughState.feeCurrency, - currencyName = feeEnoughState.currencyName ?: fromToken.network.name, - currencySymbol = feeEnoughState.currencySymbol ?: fromToken.network.currencySymbol, - ), - ), - ) - } - } - - private fun maybeAddInsufficientFundsWarning( - quoteModel: SwapState.QuotesLoadedState, - warnings: MutableList, - ) { - // check isBalanceEnough, but for dex includeFeeInAmount always Excluded - if (isInsufficientFundsCondition(quoteModel)) { - warnings.add(SwapWarning.InsufficientFunds) - } - } - private fun isInsufficientFundsCondition(quoteModel: SwapState.QuotesLoadedState): Boolean { return !quoteModel.preparedSwapConfigState.isBalanceEnough && quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included } - private fun getSwapButtonEnabled(quoteModel: SwapState.QuotesLoadedState): Boolean { - val status = quoteModel.toTokenInfo.cryptoCurrencyStatus.value - if (status is CryptoCurrencyStatus.NoAccount) { - val amount = quoteModel.toTokenInfo.tokenAmount.value - val amountToCreateAccount = status.amountToCreateAccount - - if (amount < amountToCreateAccount) { - return false - } - } - - val preparedSwapConfigState = quoteModel.preparedSwapConfigState - // check has has outgoing transaction - if (preparedSwapConfigState.hasOutgoingTransaction) return false - - // check has MinAmountWarning warning - val hasCriticalWarning = quoteModel.warnings.any { - it is Warning.MinAmountWarning || - it is Warning.Cardano.InsufficientBalanceToTransferCoin || - it is Warning.Cardano.InsufficientBalanceToTransferToken || - it is Warning.ExistentialDepositWarning - } - - if (hasCriticalWarning) return false - - return when (preparedSwapConfigState.includeFeeInAmount) { - IncludeFeeInAmount.BalanceNotEnough -> false - IncludeFeeInAmount.Excluded -> - preparedSwapConfigState.isAllowedToSpend && - preparedSwapConfigState.isBalanceEnough && - preparedSwapConfigState.feeState is SwapFeeState.Enough - is IncludeFeeInAmount.Included -> true + private fun getSwapButtonEnabled(notifications: ImmutableList): Boolean { + return notifications.none { + it is SwapNotificationUM.Error || it is NotificationUM.Error || + it is SwapNotificationUM.Warning.ExpressError || it is SwapNotificationUM.Warning.ExpressGeneralError || + it is SwapNotificationUM.Warning.NoAvailableTokensToSwap || + it is SwapNotificationUM.Warning.NeedReserveToCreateAccount } } @@ -653,15 +392,13 @@ internal class StateBuilder( ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder - val warnings = mutableListOf() - warnings.add(getWarningForError(expressDataError, fromToken.cryptoCurrencyStatus.currency)) - if (includeFeeInAmount is IncludeFeeInAmount.Included && uiStateHolder.fee is FeeItemState.Content) { - val feeCoverageNotification = createNetworkFeeCoverageNotificationConfig( - uiStateHolder.fee.amountCrypto, - uiStateHolder.fee.amountFiatFormatted, - ) - warnings.add(SwapWarning.GeneralWarning(feeCoverageNotification)) - } + val notifications = notificationsFactory.getQuotesErrorStateNotifications( + expressDataError = expressDataError, + fromToken = fromToken.cryptoCurrencyStatus.currency, + feeItem = uiStateHolder.fee, + includeFeeInAmount = includeFeeInAmount, + ) + val providerState = getProviderStateForError( swapProvider = swapProvider, fromToken = fromToken.cryptoCurrencyStatus.currency, @@ -700,7 +437,7 @@ internal class StateBuilder( balance = fromToken.cryptoCurrencyStatus.getFormattedAmount(isNeedSymbol = false), ), receiveCardData = receiveCardData, - warnings = warnings, + notifications = notifications, permissionState = GiveTxPermissionState.Empty, fee = FeeItemState.Empty, swapButton = SwapButton( @@ -754,57 +491,6 @@ internal class StateBuilder( } } - private fun getWarningForError(expressDataError: ExpressDataError, fromToken: CryptoCurrency): SwapWarning { - val providerErrorMessage = getExpressErrorMessage(expressDataError) - val providerErrorTitle = getExpressErrorTitle(expressDataError) - return when (expressDataError) { - is ExpressDataError.ExchangeTooSmallAmountError -> SwapWarning.GeneralError( - notificationConfig = NotificationConfig( - title = resourceReference( - id = R.string.warning_express_too_minimal_amount_title, - formatArgs = wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)), - ), - subtitle = resourceReference(R.string.warning_express_wrong_amount_description), - iconResId = R.drawable.ic_alert_circle_24, - ), - ) - is ExpressDataError.ExchangeTooBigAmountError -> SwapWarning.GeneralError( - notificationConfig = NotificationConfig( - title = resourceReference( - id = R.string.warning_express_too_maximum_amount_title, - formatArgs = wrappedList(expressDataError.amount.getFormattedCryptoAmount(fromToken)), - ), - subtitle = resourceReference(R.string.warning_express_wrong_amount_description), - iconResId = R.drawable.ic_alert_circle_24, - ), - ) - is ExpressDataError.ProviderDifferentAmountError -> SwapWarning.GeneralError( - notificationConfig = NotificationConfig( - title = resourceReference(id = R.string.common_error), - subtitle = resourceReference( - R.string.express_error_provider_amount_roundup, - formatArgs = wrappedList( - expressDataError.code, - expressDataError.fromProviderAmount.format { simple(decimals = expressDataError.decimals) }, - ), - ), - iconResId = R.drawable.ic_alert_circle_24, - ), - ) - else -> SwapWarning.GeneralWarning( - notificationConfig = NotificationConfig( - title = providerErrorTitle, - subtitle = providerErrorMessage, - iconResId = R.drawable.img_attention_20, - buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.warning_button_refresh), - onClick = actions.onRetryClick, - ), - ), - ) - } - } - fun createQuotesEmptyAmountState( uiStateHolder: SwapStateHolder, emptyAmountState: SwapState.EmptyAmountState, @@ -843,7 +529,8 @@ internal class StateBuilder( balance = toTokenStatus?.getFormattedAmount(isNeedSymbol = false) ?: DASH_SIGN, isBalanceHidden = isBalanceHiddenProvider(), ), - warnings = emptyList(), + notifications = persistentListOf(), + isInsufficientFunds = false, fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, @@ -1002,19 +689,8 @@ internal class StateBuilder( fun createInitialErrorState(uiState: SwapStateHolder, code: Int, onRefreshClick: () -> Unit): SwapStateHolder { return uiState.copy( - warnings = listOf( - SwapWarning.GeneralWarning( - notificationConfig = NotificationConfig( - title = TextReference.Res(R.string.warning_express_refresh_required_title), - subtitle = TextReference.Res(R.string.express_error_code, wrappedList(code)), - iconResId = R.drawable.ic_alert_triangle_20, - buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = TextReference.Res(R.string.warning_button_refresh), - onClick = onRefreshClick, - ), - ), - ), - ), + isInsufficientFunds = false, + notifications = notificationsFactory.getInitialErrorStateNotifications(code, onRefreshClick), ) } @@ -1051,17 +727,9 @@ internal class StateBuilder( } fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder { - val warnings = uiState.warnings.filterNot { it is SwapWarning.PermissionNeeded }.toMutableList() - warnings.add( - 0, - SwapWarning.TransactionInProgressWarning( - title = resourceReference(R.string.warning_express_approval_in_progress_title), - description = resourceReference(R.string.warning_express_approval_in_progress_message), - ), - ) return uiState.copy( permissionState = GiveTxPermissionState.InProgress, - warnings = warnings, + notifications = notificationsFactory.getApprovalInProgressStateNotification(uiState.notifications), ) } @@ -1211,17 +879,14 @@ internal class StateBuilder( fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(event = consumedEvent()) - fun addWarning(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder { - val renewWarnings = uiState.warnings.toMutableList() - renewWarnings.add( - SwapWarning.GenericWarning( + fun addNotification(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder { + return uiState.copy( + notifications = notificationsFactory.getGeneralErrorStateNotifications( + notifications = uiState.notifications, message = message, onClick = onClick, ), ) - return uiState.copy( - warnings = renewWarnings, - ) } private fun convertPermissionState( @@ -1272,17 +937,6 @@ internal class StateBuilder( } } - fun showWebViewBottomSheet(uiState: SwapStateHolder, url: String, onDismiss: () -> Unit): SwapStateHolder { - val config = WebViewBottomSheetConfig(url = url) - return uiState.copy( - bottomSheetConfig = TangemBottomSheetConfig( - isShow = true, - onDismissRequest = onDismiss, - content = config, - ), - ) - } - fun showPermissionBottomSheet(uiState: SwapStateHolder, onDismiss: () -> Unit): SwapStateHolder { val permissionState = uiState.permissionState if (permissionState is GiveTxPermissionState.ReadyForRequest) { @@ -1368,21 +1022,6 @@ internal class StateBuilder( } } - fun updateSelectedProvider(uiState: SwapStateHolder, selectedProviderId: String): SwapStateHolder { - val config = uiState.bottomSheetConfig?.content as? ChooseProviderBottomSheetConfig - return if (config != null) { - uiState.copy( - bottomSheetConfig = uiState.bottomSheetConfig.copy( - content = config.copy( - selectedProviderId = selectedProviderId, - ), - ), - ) - } else { - uiState - } - } - fun showSelectFeeBottomSheet( uiState: SwapStateHolder, selectedFee: FeeType, @@ -1420,21 +1059,6 @@ internal class StateBuilder( } } - fun updateSelectedFeeBottomSheet(uiState: SwapStateHolder, selectedFee: FeeType): SwapStateHolder { - val config = uiState.bottomSheetConfig?.content as? ChooseFeeBottomSheetConfig - return if (config != null) { - uiState.copy( - bottomSheetConfig = uiState.bottomSheetConfig.copy( - content = config.copy( - selectedFee = selectedFee, - ), - ), - ) - } else { - uiState - } - } - private fun TxFeeState.MultipleFeeState.toFeeItemState(): ImmutableList { return listOf( FeeItemState.Content( @@ -1483,122 +1107,6 @@ internal class StateBuilder( } } - // region warnings - private fun createPermissionNotificationConfig(fromTokenSymbol: String, providerName: String): NotificationConfig { - return NotificationConfig( - title = resourceReference(R.string.express_provider_permission_needed), - subtitle = resourceReference( - id = R.string.give_permission_swap_subtitle, - formatArgs = wrappedList(providerName, fromTokenSymbol), - ), - iconResId = R.drawable.ic_locked_24, - ) - } - - private fun createActivateAccountNotificationConfig(amount: BigDecimal, token: String): NotificationConfig { - return NotificationConfig( - title = resourceReference( - id = R.string.send_notification_invalid_reserve_amount_title, - formatArgs = wrappedList("$amount $token"), - ), - subtitle = resourceReference(R.string.send_notification_invalid_reserve_amount_text), - iconResId = R.drawable.img_attention_20, - ) - } - - private fun createReduceAmountNotificationConfig( - currencyName: String, - amount: String, - onConfirmClick: () -> Unit, - ): NotificationConfig { - return NotificationConfig( - title = resourceReference(R.string.send_notification_high_fee_title), - subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(currencyName, amount)), - iconResId = R.drawable.img_attention_20, - buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( - text = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)), - onClick = onConfirmClick, - ), - ) - } - - private fun createUnableToCoverFeeNotificationConfig( - fromToken: CryptoCurrency, - feeCurrency: CryptoCurrency?, - currencyName: String, - currencySymbol: String, - ): NotificationConfig { - val buttonState = feeCurrency?.let { - NotificationConfig.ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.common_buy_currency, wrappedList(currencySymbol)), - onClick = { actions.onBuyClick(it) }, - ) - } - return NotificationConfig( - title = resourceReference( - R.string.warning_express_not_enough_fee_for_token_tx_title, - wrappedList(fromToken.network.name), - ), - subtitle = resourceReference( - R.string.warning_express_not_enough_fee_for_token_tx_description, - wrappedList(currencyName, currencySymbol), - ), - iconResId = fromToken.networkIconResId, - buttonsState = buttonState, - ) - } - - private fun createNetworkFeeCoverageNotificationConfig( - cryptoAmount: String, - fiatAmount: String, - ): NotificationConfig { - return NotificationConfig( - title = resourceReference(R.string.send_network_fee_warning_title), - subtitle = resourceReference( - R.string.common_network_fee_warning_content, - wrappedList(cryptoAmount, fiatAmount), - ), - iconResId = R.drawable.img_attention_20, - ) - } - - private fun createMinAdaValueCharged(minAdaValue: String, tokenName: String): SwapWarning { - return SwapWarning.Cardano.MinAdaValueCharged( - NotificationConfig( - 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), - ), - iconResId = R.drawable.img_attention_20, - ), - ) - } - - private fun createInsufficientBalanceToTransferCoin(): SwapWarning { - return SwapWarning.Cardano.InsufficientBalanceToTransferCoin( - NotificationConfig( - title = resourceReference(id = R.string.cardano_max_amount_has_token_title), - subtitle = resourceReference(id = R.string.cardano_max_amount_has_token_description), - iconResId = R.drawable.ic_alert_circle_24, - ), - ) - } - - private fun createInsufficientBalanceToTransferToken(tokenName: String): SwapWarning { - return SwapWarning.Cardano.InsufficientBalanceToTransferToken( - NotificationConfig( - 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), - ), - iconResId = R.drawable.ic_alert_circle_24, - ), - ) - } - // end region - private fun getShortAddressValue(fullAddress: String): String { check(fullAddress.length > ADDRESS_MIN_LENGTH) { "Invalid address" } val firstAddressPart = fullAddress.substring(startIndex = 0, endIndex = ADDRESS_FIRST_PART_LENGTH) @@ -1712,14 +1220,6 @@ internal class StateBuilder( return amount.format { crypto(symbol, currency.decimals) } } - @Suppress("UnusedPrivateMember") - private fun CryptoCurrencyStatus.getFormattedFiatAmount(): String { - val fiatAmount = value.fiatAmount ?: return DASH_SIGN - val appCurrency = appCurrencyProvider() - - return BigDecimalFormatter.formatFiatAmount(fiatAmount, appCurrency.code, appCurrency.symbol) - } - private fun getFormattedFiatAmount(amount: BigDecimal?): String { val appCurrency = appCurrencyProvider() @@ -1735,10 +1235,6 @@ internal class StateBuilder( return this.divide(to, min(rateDecimals, MAX_DECIMALS_TO_SHOW), RoundingMode.HALF_UP) } - private fun toBigDecimalOrNull(text: String): BigDecimal? { - return text.replace(",", ".").toBigDecimalOrNull() - } - private fun getLocaleName(): String { return if (Locale.getDefault().language == "ru") { RU_LOCALE diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 54d6940303..1079427743 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -24,9 +24,9 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.* import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.getActiveIconResByCoinId import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference @@ -38,7 +38,9 @@ import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState +import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.presentation.R +import kotlinx.collections.immutable.persistentListOf @Suppress("LongMethod") @Composable @@ -69,7 +71,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi FeeItemBlock(state = state.fee) - if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings) + if (state.notifications.isNotEmpty()) SwapNotifications(notifications = state.notifications) MainButton(state = state, onPermissionWarningClick = state.onShowPermissionBottomSheet) @@ -302,69 +304,27 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable -private fun SwapWarnings(warnings: List) { +private fun SwapNotifications(notifications: List) { Column( modifier = Modifier .background(color = TangemTheme.colors.background.secondary) .fillMaxWidth(), ) { - warnings.forEach { warning -> - when (warning) { - is SwapWarning.PermissionNeeded -> { - Notification( - config = warning.notificationConfig, - ) - } - is SwapWarning.GenericWarning -> { - val message = warning.message?.resolveReference() - ?: stringResource(id = R.string.common_unknown_error) + notifications.forEach { notification -> + when (notification) { + is SwapNotificationUM.Error.GenericError -> { RefreshableWarningCard( - title = stringResource(id = R.string.common_warning), - description = message, - onClick = warning.onClick, + title = notification.title.resolveReference(), + description = notification.config.subtitle.resolveReference(), + onClick = notification.onConfirmClick, ) } - is SwapWarning.NoAvailableTokensToSwap -> { - Notification( - config = warning.notificationConfig, - ) - } - is SwapWarning.GeneralError -> { - Notification( - config = warning.notificationConfig, - iconTint = TangemTheme.colors.icon.warning, - ) - } - is SwapWarning.UnableToCoverFeeWarning -> { - Notification( - config = warning.notificationConfig, - ) - } - is SwapWarning.GeneralWarning -> { - Notification( - config = warning.notificationConfig, - ) - } - is SwapWarning.GeneralInformational -> { - Notification( - config = warning.notificationConfig, - iconTint = TangemTheme.colors.icon.accent, - ) - } - is SwapWarning.NeedReserveToCreateAccount -> { - Notification( - config = warning.notificationConfig, - ) - } - is SwapWarning.ReduceAmount -> { - Notification( - config = warning.notificationConfig, - ) - } - is SwapWarning.TransactionInProgressWarning -> { + is SwapNotificationUM.Error.ApprovalInProgressWarning, + is SwapNotificationUM.Error.TransactionInProgressWarning, + -> { CardWithIcon( - title = warning.title.resolveReference(), - description = warning.description.resolveReference(), + title = notification.config.title?.resolveReference().orEmpty(), + description = notification.config.subtitle.resolveReference(), icon = { CircularProgressIndicator( modifier = Modifier @@ -375,8 +335,20 @@ private fun SwapWarnings(warnings: List) { }, ) } - is SwapWarning.Cardano -> Notification(config = warning.notificationConfig) - SwapWarning.InsufficientFunds -> Unit + else -> { + Notification( + config = notification.config, + iconTint = when (notification) { + is SwapNotificationUM.Error.UnableToCoverFeeWarning, + is NotificationUM.Error.TokenExceedsBalance, + is NotificationUM.Error.ExceedsBalance, + is NotificationUM.Info, + is NotificationUM.Warning, + -> null + is NotificationUM.Error -> TangemTheme.colors.icon.warning + }, + ) + } } SpacerH8() } @@ -387,7 +359,7 @@ private fun SwapWarnings(warnings: List) { private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> Unit) { // order is important when { - state.warnings.any { it is SwapWarning.InsufficientFunds } -> { + state.isInsufficientFunds -> { PrimaryButton( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.swapping_insufficient_funds), @@ -395,7 +367,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U onClick = state.swapButton.onClick, ) } - state.warnings.any { it is SwapWarning.PermissionNeeded } -> { + state.notifications.any { it is SwapNotificationUM.Info.PermissionNeeded } -> { PrimaryButton( modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.give_permission_title), @@ -459,21 +431,12 @@ private val state = SwapStateHolder( isClickable = true, onClick = {}, ), - warnings = listOf( - SwapWarning.PermissionNeeded( - notificationConfig = NotificationConfig( - title = stringReference("Give Permission"), - subtitle = stringReference("To continue swapping you need to give permission to Tangem"), - iconResId = R.drawable.ic_locked_24, - ), - ), - SwapWarning.NoAvailableTokensToSwap( - notificationConfig = NotificationConfig( - title = stringReference("No tokens"), - subtitle = stringReference("Swap tokens not available"), - iconResId = R.drawable.img_attention_20, - ), + notifications = persistentListOf( + SwapNotificationUM.Info.PermissionNeeded( + providerName = "Provider", + fromTokenSymbol = "POL", ), + SwapNotificationUM.Warning.NoAvailableTokensToSwap("POLYGON"), ), swapButton = SwapButton(enabled = true, onClick = {}), onRefresh = {}, @@ -484,6 +447,7 @@ private val state = SwapStateHolder( providerState = ProviderState.Loading(), priceImpact = PriceImpact.Empty(), shouldShowMaxAmount = true, + isInsufficientFunds = false, tosState = TosState( tosLink = LegalState( title = stringReference("Terms of Use"), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index b35cb05405..65f4774f5f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -12,6 +12,7 @@ data class SwapProcessDataState( // Initial network id val fromCryptoCurrency: CryptoCurrencyStatus? = null, val toCryptoCurrency: CryptoCurrencyStatus? = null, + val feePaidCryptoCurrency: CryptoCurrencyStatus? = null, // Amount from input val amount: String? = null, val approveDataModel: RequestApproveStateData? = null, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 431017f917..d3dc6a591e 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -28,6 +28,7 @@ import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.GetMinimumTransactionAmountSyncUseCase +import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -44,8 +45,8 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.SwapStateHolder -import com.tangem.feature.swap.models.SwapWarning import com.tangem.feature.swap.models.UiActions +import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter @@ -81,6 +82,7 @@ internal class SwapViewModel @Inject constructor( private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, + private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getCardInfoUseCase: GetCardInfoUseCase, private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, @@ -400,7 +402,7 @@ internal class SwapViewModel @Inject constructor( }, onError = { Timber.e("Error when loading quotes: $it") - uiState = stateBuilder.addWarning(uiState, null) { startLoadingQuotesFromLastState() } + uiState = stateBuilder.addNotification(uiState, null) { startLoadingQuotesFromLastState() } }, ) } @@ -415,13 +417,14 @@ internal class SwapViewModel @Inject constructor( uiStateHolder = uiState, quoteModel = state, fromToken = fromToken.currency, + feeCryptoCurrencyStatus = dataState.feePaidCryptoCurrency, swapProvider = provider, bestRatedProviderId = bestRatedProviderId, isNeedBestRateBadge = dataState.lastLoadedSwapStates.consideredProvidersStates().size > 1, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, isReverseSwapPossible = isReverseSwapPossible(), ) - if (uiState.warnings.any { it is SwapWarning.UnableToCoverFeeWarning }) { + if (uiState.notifications.any { it is SwapNotificationUM.Error.UnableToCoverFeeWarning }) { analyticsEventHandler.send( SwapEvents.NoticeNotEnoughFee( token = initialCurrencyFrom.symbol, @@ -838,6 +841,13 @@ internal class SwapViewModel @Inject constructor( .onEach { Timber.d("${coin.id} balance is ${it.value.amount}") + dataState = dataState.copy( + feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = it, + ).getOrNull() ?: it, + ) + uiState = if (isFromCurrency) { dataState = dataState.copy(fromCryptoCurrency = it) stateBuilder.updateSendCurrencyBalance(uiState, it) 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 456959fddd..46a15a7443 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 @@ -5,8 +5,11 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.compatibility.l2BlockchainsList import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.isSupportedInApp +import com.tangem.blockchainsdk.utils.minimalAmount import com.tangem.lib.crypto.converter.XrpTaggedAddressConverter import com.tangem.lib.crypto.models.XrpTaggedAddress +import java.math.BigDecimal /** * !!!IMPORTANT!!! @@ -116,6 +119,8 @@ object BlockchainUtils { return l2BlockchainsList.contains(blockchain) } + fun getTezosThreshold(): BigDecimal = Blockchain.Tezos.minimalAmount() + private fun getNetworkStandardName(blockchain: Blockchain): String { return when (blockchain) { Blockchain.Ethereum, Blockchain.EthereumTestnet -> "ERC20"