Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-30 17:19:39 +05:00
parent a107ddd36e
commit a00ea0a438
14 changed files with 696 additions and 843 deletions

View file

@ -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<NotificationUM>.addValidateTransactionNotifications(
dustValue: BigDecimal,
fee: Fee?,
validationError: Throwable?,
cryptoCurrency: CryptoCurrency,
minAdaValue: BigDecimal?, // TODO revert to Fee, after swap TxFee refactored
onReduceClick: (
reduceAmountTo: BigDecimal,
notification: Class<out NotificationUM>,
@ -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),
),
)
}

View file

@ -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,

View file

@ -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,

View file

@ -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()
}
}

View file

@ -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<Warning> = emptyList(),
val currencyCheck: CryptoCurrencyCheck? = null,
val validationResult: Throwable? = null,
val minAdaValue: BigDecimal?,
val swapProvider: SwapProvider,
) : SwapState

View file

@ -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<Warning> {
val fromToken = fromTokenStatus.currency
val warnings = mutableListOf<Warning>()
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<Warning>,
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<Warning>,
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 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,
val currencyCheck = getCurrencyCheckUseCase(
userWalletId = userWalletId,
currencyStatus = fromTokenStatus,
amount = amount.value,
fee = fee,
)
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<Warning>,
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<Warning>,
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<Warning>,
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,
)
}

View file

@ -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<SwapWarning> = emptyList(),
val notifications: ImmutableList<NotificationUM> = persistentListOf(),
val isInsufficientFunds: Boolean,
val event: StateEvent<SwapEvent> = 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
}

View file

@ -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,
)
}
}

View file

@ -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<NotificationUM> {
return persistentListOf(
SwapNotificationUM.Warning.ExpressGeneralError(
code = code,
onConfirmClick = onRefreshClick,
),
)
}
fun getGeneralErrorStateNotifications(
notifications: ImmutableList<NotificationUM>,
message: TextReference?,
onClick: () -> Unit,
): ImmutableList<NotificationUM> {
val updatedNotifications = notifications.toMutableList()
updatedNotifications.add(
SwapNotificationUM.Error.GenericError(
subtitle = message,
onConfirmClick = onClick,
),
)
return updatedNotifications.toPersistentList()
}
fun getNotAvailableStateNotifications(fromCurrencyName: String): ImmutableList<NotificationUM> {
return persistentListOf(
SwapNotificationUM.Warning.NoAvailableTokensToSwap(fromCurrencyName),
)
}
fun getQuotesErrorStateNotifications(
expressDataError: ExpressDataError,
fromToken: CryptoCurrency,
feeItem: FeeItemState,
includeFeeInAmount: IncludeFeeInAmount,
): ImmutableList<NotificationUM> {
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<NotificationUM>,
): ImmutableList<NotificationUM> {
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<NotificationUM> {
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<NotificationUM>.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<NotificationUM>.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<NotificationUM>.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<NotificationUM>.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<NotificationUM>.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<NotificationUM>.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<NotificationUM>.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
}
}

View file

@ -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<SwapWarning> {
val warnings = mutableListOf<SwapWarning>()
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<SwapWarning>,
) {
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<SwapWarning>) {
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<SwapWarning>,
) {
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<SwapWarning>,
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<SwapWarning>,
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<SwapWarning>,
) {
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<SwapWarning>,
) {
// 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<NotificationUM>): 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<SwapWarning>()
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,
val notifications = notificationsFactory.getQuotesErrorStateNotifications(
expressDataError = expressDataError,
fromToken = fromToken.cryptoCurrencyStatus.currency,
feeItem = uiStateHolder.fee,
includeFeeInAmount = includeFeeInAmount,
)
warnings.add(SwapWarning.GeneralWarning(feeCoverageNotification))
}
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<FeeItemState.Content> {
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

View file

@ -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<SwapWarning>) {
private fun SwapNotifications(notifications: List<NotificationUM>) {
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<SwapWarning>) {
},
)
}
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<SwapWarning>) {
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"),

View file

@ -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,

View file

@ -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)

View file

@ -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"