Updated on 2026-08-14
This commit is contained in:
commit
b76e1bbc45
219 changed files with 4585 additions and 5271 deletions
|
|
@ -20,6 +20,7 @@ data class SwapStateHolder(
|
|||
val alert: SwapWarning.GenericWarning? = null,
|
||||
val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
val providerState: ProviderState,
|
||||
val reduceAmountIgnore: Boolean, // ignore warning about reducing XTZ amount by 0.01
|
||||
|
||||
val fee: FeeItemState = FeeItemState.Empty,
|
||||
val permissionState: SwapPermissionState = SwapPermissionState.Empty,
|
||||
|
|
@ -108,7 +109,6 @@ sealed interface SwapWarning {
|
|||
val title: TextReference? = null,
|
||||
val message: TextReference? = null,
|
||||
val type: GenericWarningType = GenericWarningType.OTHER,
|
||||
val shouldWrapMessage: Boolean = false,
|
||||
val onClick: () -> Unit,
|
||||
) : SwapWarning
|
||||
data class GeneralError(val notificationConfig: NotificationConfig) : SwapWarning
|
||||
|
|
@ -116,6 +116,8 @@ sealed interface 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
|
||||
}
|
||||
|
||||
enum class GenericWarningType {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.tangem.feature.swap.models
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.ui.TxFee
|
||||
|
||||
data class UiActions(
|
||||
|
|
@ -13,6 +14,8 @@ data class UiActions(
|
|||
val onChangeCardsClicked: () -> Unit,
|
||||
val onBackClicked: () -> Unit,
|
||||
val onMaxAmountSelected: () -> Unit,
|
||||
val onReduceAmount: (SwapAmount) -> Unit,
|
||||
val onReduceAmountIgnoreClick: () -> Unit,
|
||||
val openPermissionBottomSheet: () -> Unit,
|
||||
val onChangeApproveType: (ApproveType) -> Unit,
|
||||
// region new actions
|
||||
|
|
|
|||
|
|
@ -88,6 +88,7 @@ internal class StateBuilder(
|
|||
onShowPermissionBottomSheet = actions.openPermissionBottomSheet,
|
||||
providerState = ProviderState.Empty(),
|
||||
shouldShowMaxAmount = false,
|
||||
reduceAmountIgnore = false,
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
)
|
||||
}
|
||||
|
|
@ -213,7 +214,11 @@ internal class StateBuilder(
|
|||
): SwapStateHolder {
|
||||
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
|
||||
val warnings = getWarningsForSuccessState(quoteModel, fromToken)
|
||||
val warnings = getWarningsForSuccessState(
|
||||
quoteModel = quoteModel,
|
||||
fromToken = fromToken,
|
||||
ignoreAmountReduce = uiStateHolder.reduceAmountIgnore,
|
||||
)
|
||||
val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
|
||||
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
|
||||
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
|
||||
|
|
@ -311,41 +316,28 @@ internal class StateBuilder(
|
|||
private fun getWarningsForSuccessState(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
fromToken: CryptoCurrency,
|
||||
ignoreAmountReduce: Boolean,
|
||||
): List<SwapWarning> {
|
||||
val warnings = mutableListOf<SwapWarning>()
|
||||
addDomainWarnings(quoteModel, warnings)
|
||||
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
|
||||
quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough &&
|
||||
quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest
|
||||
) {
|
||||
warnings.add(
|
||||
SwapWarning.PermissionNeeded(
|
||||
createPermissionNotificationConfig(fromToken.symbol),
|
||||
),
|
||||
)
|
||||
}
|
||||
when (quoteModel.preparedSwapConfigState.includeFeeInAmount) {
|
||||
is IncludeFeeInAmount.Included ->
|
||||
warnings.add(
|
||||
SwapWarning.GeneralWarning(
|
||||
createNetworkFeeCoverageNotificationConfig(),
|
||||
),
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
addUnableCoverFeeWarning(quoteModel, fromToken, warnings)
|
||||
// check isBalanceEnough, but for dex includeFeeInAmount always Excluded
|
||||
if (!quoteModel.preparedSwapConfigState.isBalanceEnough &&
|
||||
quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included
|
||||
) {
|
||||
warnings.add(SwapWarning.InsufficientFunds)
|
||||
}
|
||||
maybeAddDomainWarnings(quoteModel, warnings, ignoreAmountReduce)
|
||||
maybeAddNeedReserveToCreateAccountWarning(quoteModel, warnings)
|
||||
maybeAddPermissionNeededWarning(quoteModel, warnings, fromToken)
|
||||
maybeAddNetworkFeeCoverageWarning(quoteModel, warnings)
|
||||
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.swapping_pending_transaction_title),
|
||||
description = resourceReference(R.string.swapping_pending_transaction_subtitle),
|
||||
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) {
|
||||
|
|
@ -361,10 +353,13 @@ internal class StateBuilder(
|
|||
),
|
||||
)
|
||||
}
|
||||
return warnings
|
||||
}
|
||||
|
||||
private fun addDomainWarnings(quoteModel: SwapState.QuotesLoadedState, warnings: MutableList<SwapWarning>) {
|
||||
private fun maybeAddDomainWarnings(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
warnings: MutableList<SwapWarning>,
|
||||
ignoreAmountReduce: Boolean,
|
||||
) {
|
||||
quoteModel.warnings.forEach {
|
||||
when (it) {
|
||||
is Warning.ExistentialDepositWarning -> {
|
||||
|
|
@ -390,7 +385,7 @@ internal class StateBuilder(
|
|||
NotificationConfig(
|
||||
title = resourceReference(R.string.send_notification_invalid_amount_title),
|
||||
subtitle = resourceReference(
|
||||
R.string.send_notification_invalid_minimum_amount_text,
|
||||
R.string.warning_express_dust_message,
|
||||
wrappedList(
|
||||
it.dustValue.toPlainString(),
|
||||
it.dustValue.toPlainString(),
|
||||
|
|
@ -401,11 +396,84 @@ internal class StateBuilder(
|
|||
),
|
||||
)
|
||||
}
|
||||
is Warning.ReduceAmountWarning -> {
|
||||
if (!ignoreAmountReduce) {
|
||||
warnings.add(
|
||||
SwapWarning.ReduceAmount(
|
||||
notificationConfig = createReduceAmountNotificationConfig(
|
||||
amount = it.tezosFeeThreshold.toPlainString(),
|
||||
onConfirmClick = {
|
||||
val fromAmount = quoteModel.fromTokenInfo.tokenAmount
|
||||
val patchedAmount = fromAmount.copy(
|
||||
value = fromAmount.value - it.tezosFeeThreshold,
|
||||
)
|
||||
actions.onReduceAmount(patchedAmount)
|
||||
},
|
||||
onDismissClick = actions.onReduceAmountIgnoreClick,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addUnableCoverFeeWarning(
|
||||
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.name,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeAddPermissionNeededWarning(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
warnings: MutableList<SwapWarning>,
|
||||
fromToken: CryptoCurrency,
|
||||
) {
|
||||
if (!quoteModel.preparedSwapConfigState.isAllowedToSpend &&
|
||||
quoteModel.preparedSwapConfigState.feeState is SwapFeeState.Enough &&
|
||||
quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest
|
||||
) {
|
||||
warnings.add(
|
||||
SwapWarning.PermissionNeeded(
|
||||
createPermissionNotificationConfig(fromToken.symbol),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeAddNetworkFeeCoverageWarning(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
warnings: MutableList<SwapWarning>,
|
||||
) {
|
||||
when (quoteModel.preparedSwapConfigState.includeFeeInAmount) {
|
||||
is IncludeFeeInAmount.Included ->
|
||||
warnings.add(
|
||||
SwapWarning.GeneralWarning(
|
||||
createNetworkFeeCoverageNotificationConfig(),
|
||||
),
|
||||
)
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
private fun maybeAddUnableCoverFeeWarning(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
fromToken: CryptoCurrency,
|
||||
warnings: MutableList<SwapWarning>,
|
||||
|
|
@ -428,7 +496,29 @@ internal class StateBuilder(
|
|||
}
|
||||
}
|
||||
|
||||
private fun maybeAddInsufficientFundsWarning(
|
||||
quoteModel: SwapState.QuotesLoadedState,
|
||||
warnings: MutableList<SwapWarning>,
|
||||
) {
|
||||
// check isBalanceEnough, but for dex includeFeeInAmount always Excluded
|
||||
if (!quoteModel.preparedSwapConfigState.isBalanceEnough &&
|
||||
quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included
|
||||
) {
|
||||
warnings.add(SwapWarning.InsufficientFunds)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -587,7 +677,7 @@ internal class StateBuilder(
|
|||
subtitle = if (dataError is DataError.UnknownError) {
|
||||
resourceReference(R.string.common_unknown_error)
|
||||
} else {
|
||||
resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString()))
|
||||
resourceReference(R.string.express_error_code, wrappedList(dataError.code.toString()))
|
||||
},
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
|
||||
|
|
@ -774,7 +864,7 @@ internal class StateBuilder(
|
|||
SwapWarning.GeneralWarning(
|
||||
notificationConfig = NotificationConfig(
|
||||
title = TextReference.Res(R.string.warning_express_refresh_required_title),
|
||||
subtitle = TextReference.Res(R.string.generic_error_code, wrappedList(code)),
|
||||
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),
|
||||
|
|
@ -823,8 +913,8 @@ internal class StateBuilder(
|
|||
warnings.add(
|
||||
0,
|
||||
SwapWarning.TransactionInProgressWarning(
|
||||
title = resourceReference(R.string.swapping_pending_transaction_title),
|
||||
description = resourceReference(R.string.swapping_pending_transaction_subtitle),
|
||||
title = resourceReference(R.string.warning_express_approval_in_progress_title),
|
||||
description = resourceReference(R.string.warning_express_approval_in_progress_message),
|
||||
),
|
||||
)
|
||||
return uiState.copy(
|
||||
|
|
@ -928,17 +1018,11 @@ internal class StateBuilder(
|
|||
|
||||
fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(alert = null)
|
||||
|
||||
fun addWarning(
|
||||
uiState: SwapStateHolder,
|
||||
message: TextReference?,
|
||||
shouldWrapMessage: Boolean = false,
|
||||
onClick: () -> Unit,
|
||||
): SwapStateHolder {
|
||||
fun addWarning(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder {
|
||||
val renewWarnings = uiState.warnings.filterNot { it is SwapWarning.GenericWarning }.toMutableList()
|
||||
renewWarnings.add(
|
||||
SwapWarning.GenericWarning(
|
||||
message = message,
|
||||
shouldWrapMessage = shouldWrapMessage,
|
||||
onClick = onClick,
|
||||
),
|
||||
)
|
||||
|
|
@ -1220,6 +1304,35 @@ internal class StateBuilder(
|
|||
)
|
||||
}
|
||||
|
||||
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(
|
||||
amount: String,
|
||||
onConfirmClick: () -> Unit,
|
||||
onDismissClick: () -> Unit,
|
||||
): NotificationConfig {
|
||||
return NotificationConfig(
|
||||
title = resourceReference(R.string.send_notification_high_fee_title),
|
||||
subtitle = resourceReference(R.string.send_notification_high_fee_text, wrappedList(amount)),
|
||||
iconResId = R.drawable.img_attention_20,
|
||||
buttonsState = NotificationConfig.ButtonsState.PairButtonsConfig(
|
||||
primaryText = resourceReference(R.string.xtz_withdrawal_message_reduce, wrappedList(amount)),
|
||||
onPrimaryClick = onConfirmClick,
|
||||
secondaryText = resourceReference(R.string.xtz_withdrawal_message_ignore),
|
||||
onSecondaryClick = onDismissClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun createUnableToCoverFeeNotificationConfig(
|
||||
fromToken: CryptoCurrency,
|
||||
feeCurrency: CryptoCurrency?,
|
||||
|
|
|
|||
|
|
@ -337,14 +337,9 @@ private fun SwapWarnings(warnings: List<SwapWarning>) {
|
|||
)
|
||||
}
|
||||
is SwapWarning.GenericWarning -> {
|
||||
val message = warning.message?.let {
|
||||
if (warning.shouldWrapMessage) {
|
||||
String.format(stringResource(id = R.string.swapping_error_wrapper), it.resolveReference())
|
||||
} else {
|
||||
it.resolveReference()
|
||||
}
|
||||
} ?: stringResource(id = R.string.common_unknown_error)
|
||||
RefreshableWaringCard(
|
||||
val message = warning.message?.resolveReference()
|
||||
?: stringResource(id = R.string.common_unknown_error)
|
||||
RefreshableWarningCard(
|
||||
title = stringResource(id = R.string.common_warning),
|
||||
description = message,
|
||||
onClick = warning.onClick,
|
||||
|
|
@ -377,6 +372,16 @@ private fun SwapWarnings(warnings: List<SwapWarning>) {
|
|||
iconTint = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
is SwapWarning.NeedReserveToCreateAccount -> {
|
||||
Notification(
|
||||
config = warning.notificationConfig,
|
||||
)
|
||||
}
|
||||
is SwapWarning.ReduceAmount -> {
|
||||
Notification(
|
||||
config = warning.notificationConfig,
|
||||
)
|
||||
}
|
||||
is SwapWarning.TransactionInProgressWarning -> {
|
||||
CardWithIcon(
|
||||
title = warning.title.resolveReference(),
|
||||
|
|
@ -499,6 +504,7 @@ private val state = SwapStateHolder(
|
|||
providerState = ProviderState.Loading(),
|
||||
priceImpact = PriceImpact.Empty(),
|
||||
shouldShowMaxAmount = true,
|
||||
reduceAmountIgnore = false,
|
||||
tosState = TosState(
|
||||
tosLink = LegalState(
|
||||
title = stringReference("Terms of Use"),
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import com.tangem.feature.swap.domain.BlockchainInteractor
|
|||
import com.tangem.feature.swap.domain.SwapInteractor
|
||||
import com.tangem.feature.swap.domain.models.DataError
|
||||
import com.tangem.feature.swap.domain.models.ExpressException
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.domain.*
|
||||
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
|
||||
import com.tangem.feature.swap.domain.models.ui.*
|
||||
|
|
@ -812,6 +813,10 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun onReduceAmountClicked(newAmount: SwapAmount) {
|
||||
onAmountChanged(newAmount.formatToUIRepresentation())
|
||||
}
|
||||
|
||||
@Suppress("UnusedPrivateMember")
|
||||
private fun onAmountSelected(selected: Boolean) {
|
||||
if (selected) {
|
||||
|
|
@ -874,7 +879,14 @@ internal class SwapViewModel @Inject constructor(
|
|||
}
|
||||
onSearchEntered("")
|
||||
},
|
||||
onMaxAmountSelected = { onMaxAmountClicked() },
|
||||
onMaxAmountSelected = ::onMaxAmountClicked,
|
||||
onReduceAmount = ::onReduceAmountClicked,
|
||||
onReduceAmountIgnoreClick = {
|
||||
uiState = uiState.copy(
|
||||
reduceAmountIgnore = true,
|
||||
warnings = uiState.warnings.filter { it !is SwapWarning.ReduceAmount },
|
||||
)
|
||||
},
|
||||
openPermissionBottomSheet = {
|
||||
singleTaskScheduler.cancelTask()
|
||||
analyticsEventHandler.send(SwapEvents.ButtonGivePermissionClicked)
|
||||
|
|
@ -1153,11 +1165,11 @@ internal class SwapViewModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val loggingTag = "SwapViewModel"
|
||||
private const val INITIAL_AMOUNT = ""
|
||||
private const val UPDATE_DELAY = 10000L
|
||||
private const val DEBOUNCE_AMOUNT_DELAY = 1000L
|
||||
private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
|
||||
private companion object {
|
||||
const val loggingTag = "SwapViewModel"
|
||||
const val INITIAL_AMOUNT = ""
|
||||
const val UPDATE_DELAY = 10000L
|
||||
const val DEBOUNCE_AMOUNT_DELAY = 1000L
|
||||
const val UPDATE_BALANCE_DELAY_MILLIS = 11000L
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue