Updated on 2026-08-14
This commit is contained in:
parent
cf9e84b748
commit
44cae7ffd8
11 changed files with 1048 additions and 179 deletions
|
|
@ -10,6 +10,7 @@ import com.arkivanov.decompose.router.slot.SlotNavigation
|
|||
import com.arkivanov.decompose.router.slot.activate
|
||||
import com.arkivanov.decompose.router.slot.dismiss
|
||||
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.fromNetworkId
|
||||
import com.tangem.common.routing.AppRoute
|
||||
|
|
@ -205,6 +206,7 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
|
||||
private val amountDebouncer = Debouncer()
|
||||
private val transferModeDebouncer = Debouncer()
|
||||
private val singleTaskScheduler = SingleTaskScheduler<Map<SwapProvider, SwapState>>()
|
||||
private val performanceTracker = SwapQuotePerformanceTracker()
|
||||
|
||||
|
|
@ -682,13 +684,18 @@ internal class SwapModel @Inject constructor(
|
|||
fromSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
fromTokenAmount: String,
|
||||
forceUpdate: Boolean = true,
|
||||
): Boolean {
|
||||
val shouldTransferInsteadOfSwap = swapTransferInteractor.shouldTransferInsteadOfSwap(
|
||||
fromSwapCurrencyStatus.currency,
|
||||
toSwapCurrencyStatus.currency,
|
||||
)
|
||||
if (shouldTransferInsteadOfSwap) {
|
||||
modelScope.launch {
|
||||
transferModeDebouncer.debounce(
|
||||
coroutineScope = modelScope,
|
||||
waitMs = DEBOUNCE_AMOUNT_DELAY,
|
||||
forceUpdate = forceUpdate,
|
||||
) {
|
||||
singleTaskScheduler.destroyTask()
|
||||
swapPairsJobHolder.cancel()
|
||||
updateTransferUIState(fromSwapCurrencyStatus, toSwapCurrencyStatus, fromTokenAmount)
|
||||
|
|
@ -702,19 +709,28 @@ internal class SwapModel @Inject constructor(
|
|||
toSwapCurrencyStatus: SwapCurrencyStatus,
|
||||
fromTokenAmount: String,
|
||||
) {
|
||||
val feePaidCryptoCurrency = dataState.feePaidCryptoCurrency
|
||||
val selectedFee = getSelectedSwapFee()?.fee
|
||||
val swapState = swapTransferInteractor.updateTransfer(
|
||||
fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus,
|
||||
fromTokenAmount,
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
fromTokenAmount = fromTokenAmount,
|
||||
feePaidCurrencyStatus = feePaidCryptoCurrency,
|
||||
fee = selectedFee,
|
||||
)
|
||||
when (swapState) {
|
||||
is SwapState.EmptyAmountState -> setupEmptyAmountUiState(swapState, fromSwapCurrencyStatus)
|
||||
is SwapState.Transfer -> {
|
||||
dataState = dataState.copy(amount = fromTokenAmount)
|
||||
dataState = dataState.copy(
|
||||
amount = fromTokenAmount,
|
||||
currentTransferState = swapState,
|
||||
)
|
||||
uiState = swapTransferStateBuilder.createTransferState(
|
||||
actions = actions,
|
||||
transferState = swapState,
|
||||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = feePaidCryptoCurrency,
|
||||
fee = selectedFee,
|
||||
)
|
||||
feeSelectorRepository.state.value = FeeSelectorUM.Loading
|
||||
feeSelectorReloadTrigger.triggerUpdate()
|
||||
|
|
@ -723,11 +739,36 @@ internal class SwapModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun refreshTransferUIStateAfterFeeUpdate() {
|
||||
private fun refreshTransferUIStateAfterFeeUpdateIfNeeded(
|
||||
feePaidCryptoCurrencyStatus: CryptoCurrencyStatus? = null,
|
||||
fee: Fee? = null,
|
||||
) {
|
||||
val from = dataState.fromSwapCurrencyStatus ?: return
|
||||
val to = dataState.toSwapCurrencyStatus ?: return
|
||||
if (!swapTransferInteractor.shouldTransferInsteadOfSwap(from.currency, to.currency)) return
|
||||
// todo notification check should be triggered (will be implemented in [REDACTED_TASK_KEY])
|
||||
val currentTransferState = dataState.currentTransferState ?: return
|
||||
val amount = dataState.amount ?: return
|
||||
modelScope.launch {
|
||||
// The cached currentTransferState may have been built when the fee selector
|
||||
// had not loaded yet (fee=null). Recompute it with the freshly-loaded fee so
|
||||
// isFeeCoverage and sendingAmount reflect the actual fee, otherwise the fee
|
||||
// coverage notification stays hidden on first Max click.
|
||||
val refreshed = swapTransferInteractor.updateTransfer(
|
||||
fromSwapCurrencyStatus = from,
|
||||
toSwapCurrencyStatus = to,
|
||||
fromTokenAmount = amount,
|
||||
feePaidCurrencyStatus = feePaidCryptoCurrencyStatus,
|
||||
fee = fee,
|
||||
) as? SwapState.Transfer ?: currentTransferState
|
||||
dataState = dataState.copy(currentTransferState = refreshed)
|
||||
uiState = swapTransferStateBuilder.updateTransferButtonEnableState(
|
||||
transferState = refreshed,
|
||||
actions = actions,
|
||||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatus,
|
||||
fee = fee,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun retrySwapPairs(fromSwapCurrencyStatus: SwapCurrencyStatus, toSwapCurrencyStatus: SwapCurrencyStatus) {
|
||||
|
|
@ -1243,12 +1284,13 @@ internal class SwapModel @Inject constructor(
|
|||
showAlert()
|
||||
return
|
||||
}
|
||||
val transferState = dataState.currentTransferState ?: return
|
||||
uiState = swapTransferStateBuilder.createTransferInProgressState(uiState)
|
||||
modelScope.launch(dispatchers.main) {
|
||||
swapTransferInteractor.sendTransfer(
|
||||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
fromTokenAmount = lastAmount.value,
|
||||
sendingAmount = transferState.sendingAmount,
|
||||
fee = fee,
|
||||
transactionFeeResult = requireNotNull(getSelectedSwapFee()?.transactionFeeResult) {
|
||||
"It should be not null at this stage"
|
||||
|
|
@ -1256,7 +1298,6 @@ internal class SwapModel @Inject constructor(
|
|||
).fold(
|
||||
ifLeft = { error ->
|
||||
TangemLogger.e("onTransferClick: transfer failed: ${error.getAnalyticsDescription()}")
|
||||
refreshTransferUIStateAfterFeeUpdate()
|
||||
showAlert()
|
||||
},
|
||||
ifRight = { txHash ->
|
||||
|
|
@ -1486,6 +1527,7 @@ internal class SwapModel @Inject constructor(
|
|||
fromSwapCurrencyStatus = fromSwapCurrencyStatus,
|
||||
toSwapCurrencyStatus = toSwapCurrencyStatus,
|
||||
fromTokenAmount = lastAmount.value,
|
||||
forceUpdate = forceQuotesUpdate,
|
||||
)
|
||||
if (isUpdatedToTransferMode) return@launch
|
||||
if (toSwapCurrencyStatus.status.value.amount != null) {
|
||||
|
|
@ -2224,21 +2266,22 @@ internal class SwapModel @Inject constructor(
|
|||
override fun onResult(newState: FeeSelectorUM) {
|
||||
state.value = newState
|
||||
|
||||
val quoteState = dataState.getCurrentLoadedSwapState() ?: return
|
||||
|
||||
if (newState is FeeSelectorUM.Error) {
|
||||
TangemLogger.e("loadFee: ${newState.error}, isHidden = true")
|
||||
refreshTransferUIStateAfterFeeUpdateIfNeeded()
|
||||
uiState = stateBuilder.createFeeErrorState(
|
||||
uiStateHolder = uiState,
|
||||
quoteModel = quoteState,
|
||||
quoteModel = dataState.getCurrentLoadedSwapState() ?: return,
|
||||
feeCryptoCurrencyStatus = dataState.feePaidCryptoCurrency,
|
||||
feeError = newState.error,
|
||||
)
|
||||
modelScope.launch { forceUpdateState.emit(newState.copy(isHidden = true)) }
|
||||
refreshTransferUIStateAfterFeeUpdate()
|
||||
return
|
||||
}
|
||||
refreshTransferUIStateAfterFeeUpdate()
|
||||
refreshTransferUIStateAfterFeeUpdateIfNeeded(
|
||||
feePaidCryptoCurrencyStatus = dataState.feePaidCryptoCurrency,
|
||||
fee = (newState as? FeeSelectorUM.Content)?.selectedFeeItem?.fee,
|
||||
)
|
||||
|
||||
val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus
|
||||
val toSwapCurrencyStatus = dataState.toSwapCurrencyStatus
|
||||
|
|
@ -2249,6 +2292,7 @@ internal class SwapModel @Inject constructor(
|
|||
)
|
||||
if (shouldTransferInsteadOfSwap) return
|
||||
|
||||
val quoteState = dataState.getCurrentLoadedSwapState() ?: return
|
||||
val swapFee = getSelectedSwapFee() ?: return
|
||||
|
||||
modelScope.launch(dispatchers.default) {
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ data class SwapProcessDataState(
|
|||
val selectedPairProviders: List<SwapProvider> = emptyList(),
|
||||
val selectedProvider: SwapProvider? = null,
|
||||
val lastLoadedSwapStates: Map<SwapProvider, SwapState> = emptyMap(),
|
||||
val currentTransferState: SwapState.Transfer? = null,
|
||||
|
||||
// Amount from input
|
||||
val amount: String? = null,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
package com.tangem.feature.swap.ui.transfer
|
||||
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
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.addFeeCoverageNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification
|
||||
import com.tangem.common.ui.notifications.NotificationsFactory.addValidateTransactionNotifications
|
||||
import com.tangem.core.ui.utils.parseBigDecimal
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
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.toPersistentList
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SwapTransferNotificationsFactory @Inject constructor() {
|
||||
|
||||
fun getNotifications(
|
||||
transferState: SwapState.Transfer,
|
||||
feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
fee: Fee?,
|
||||
onReduceByAmount: (SwapAmount, BigDecimal) -> Unit,
|
||||
onReduceToAmount: (SwapAmount) -> Unit,
|
||||
): ImmutableList<NotificationUM> {
|
||||
return buildList {
|
||||
maybeAddRentExemptionError(transferState)
|
||||
maybeAddDomainWarnings(
|
||||
state = transferState,
|
||||
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
fee = fee,
|
||||
onReduceByAmount = onReduceByAmount,
|
||||
onReduceToAmount = onReduceToAmount,
|
||||
)
|
||||
maybeAddNeedReserveToCreateAccountWarning(transferState)
|
||||
}.toPersistentList()
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.maybeAddRentExemptionError(state: SwapState.Transfer) {
|
||||
state.currencyCheck?.rentWarning?.let {
|
||||
add(NotificationUM.Solana.RentInfo(it))
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.maybeAddDomainWarnings(
|
||||
state: SwapState.Transfer,
|
||||
feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
fee: Fee?,
|
||||
onReduceByAmount: (SwapAmount, BigDecimal) -> Unit,
|
||||
onReduceToAmount: (SwapAmount) -> Unit,
|
||||
) {
|
||||
val swapCurrencyStatus = state.fromTokenInfo.swapCurrencyStatus
|
||||
val amount = state.fromTokenInfo.tokenAmount
|
||||
val balance = swapCurrencyStatus.status.value.amount ?: BigDecimal.ZERO
|
||||
val feeValue = fee?.amount?.value.orZero()
|
||||
val isCardano = BlockchainUtils.isCardano(swapCurrencyStatus.currency.network.rawId)
|
||||
addExistentialWarningNotification(
|
||||
existentialDeposit = state.currencyCheck?.existentialDeposit,
|
||||
feeAmount = feeValue,
|
||||
sendingAmount = amount.value,
|
||||
cryptoCurrencyStatus = swapCurrencyStatus.status,
|
||||
onReduceClick = { reduceBy, reduceByDiff, _ ->
|
||||
onReduceByAmount(
|
||||
amount.copy(value = amount.value.minus(reduceByDiff)),
|
||||
reduceBy,
|
||||
)
|
||||
},
|
||||
)
|
||||
addValidateTransactionNotifications(
|
||||
dustValue = state.currencyCheck?.dustValue.orZero(),
|
||||
validationError = state.validationResult,
|
||||
cryptoCurrency = swapCurrencyStatus.currency,
|
||||
minAdaValue = state.minAdaValue,
|
||||
onReduceClick = { reduceTo, _ ->
|
||||
onReduceToAmount(amount.copy(value = reduceTo))
|
||||
},
|
||||
)
|
||||
if (!isCardano) {
|
||||
addDustWarningNotification(
|
||||
dustValue = state.currencyCheck?.dustValue,
|
||||
feeValue = feeValue,
|
||||
sendingAmount = amount.value,
|
||||
cryptoCurrencyStatus = swapCurrencyStatus.status,
|
||||
feeCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
)
|
||||
}
|
||||
addReserveAmountErrorNotification(
|
||||
reserveAmount = state.currencyCheck?.reserveAmount,
|
||||
sendingAmount = amount.value,
|
||||
cryptoCurrency = swapCurrencyStatus.currency,
|
||||
feeCryptoCurrency = feeCryptoCurrencyStatus?.currency,
|
||||
isAccountFunded = true,
|
||||
)
|
||||
addReduceAmountNotification(
|
||||
cryptoCurrencyStatus = swapCurrencyStatus.status,
|
||||
fromAmount = state.fromTokenInfo.tokenAmount,
|
||||
balance = balance,
|
||||
onReduceByAmount = onReduceByAmount,
|
||||
)
|
||||
addTransactionLimitErrorNotification(
|
||||
currencyCheck = state.currencyCheck,
|
||||
sendingAmount = amount.value,
|
||||
cryptoCurrencyStatus = swapCurrencyStatus.status,
|
||||
feeCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
feeValue = feeValue,
|
||||
onReduceClick = { reduceTo, _ ->
|
||||
onReduceToAmount(amount.copy(value = reduceTo))
|
||||
},
|
||||
)
|
||||
maybeAddFeeCoverageNotification(state = state, amount = amount)
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.maybeAddFeeCoverageNotification(
|
||||
state: SwapState.Transfer,
|
||||
amount: SwapAmount,
|
||||
) {
|
||||
addFeeCoverageNotification(
|
||||
isFeeCoverage = state.isFeeCoverage,
|
||||
enteredAmountValue = amount.value,
|
||||
sendingValue = state.sendingAmount,
|
||||
appCurrency = state.appCurrency,
|
||||
cryptoCurrencyStatus = state.fromTokenInfo.swapCurrencyStatus.status,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.maybeAddNeedReserveToCreateAccountWarning(state: SwapState.Transfer) {
|
||||
val status = state.toTokenInfo.swapCurrencyStatus.status.value
|
||||
if (status is CryptoCurrencyStatus.NoAccount) {
|
||||
val amount = state.toTokenInfo.tokenAmount.value
|
||||
val amountToCreateAccount = status.amountToCreateAccount
|
||||
val currencyTo = state.toTokenInfo.swapCurrencyStatus.currency
|
||||
if (amount < amountToCreateAccount) {
|
||||
add(
|
||||
SwapNotificationUM.Warning.NeedReserveToCreateAccount(
|
||||
receiveAmount = status.amountToCreateAccount.parseBigDecimal(currencyTo.decimals),
|
||||
receiveToken = currencyTo.symbol,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<NotificationUM>.addReduceAmountNotification(
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
fromAmount: SwapAmount,
|
||||
balance: BigDecimal,
|
||||
onReduceByAmount: (SwapAmount, BigDecimal) -> Unit,
|
||||
) {
|
||||
val isTezos = isTezos(cryptoCurrencyStatus.currency.network.rawId)
|
||||
val threshold = getTezosThreshold()
|
||||
val isTotalBalance = fromAmount.value >= balance && balance > threshold
|
||||
if (isTezos && isTotalBalance) {
|
||||
add(
|
||||
SwapNotificationUM.Warning.ReduceAmount(
|
||||
currencyName = cryptoCurrencyStatus.currency.name,
|
||||
amount = threshold.toPlainString(),
|
||||
onConfirmClick = {
|
||||
val patchedAmount = fromAmount.copy(
|
||||
value = fromAmount.value - threshold,
|
||||
)
|
||||
onReduceByAmount(patchedAmount, threshold)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,13 @@
|
|||
package com.tangem.feature.swap.ui.transfer
|
||||
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.account.AccountIconUM
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
|
|
@ -24,13 +25,16 @@ import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
|
|||
import com.tangem.feature.swap.model.SwapProcessDataState
|
||||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.models.SwapButton.Mode
|
||||
import com.tangem.feature.swap.models.states.SwapNotificationUM
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.feature.swap.utils.formatToUIRepresentation
|
||||
import com.tangem.utils.StringsSigns.DASH_SIGN
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import java.math.BigDecimal
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class SwapTransferStateBuilder @Inject constructor() {
|
||||
internal class SwapTransferStateBuilder @Inject constructor(
|
||||
private val notificationsFactory: SwapTransferNotificationsFactory,
|
||||
) {
|
||||
|
||||
private val iconConverter by lazy(::CryptoCurrencyToIconStateConverter)
|
||||
|
||||
|
|
@ -38,13 +42,24 @@ internal class SwapTransferStateBuilder @Inject constructor() {
|
|||
actions: UiActions,
|
||||
transferState: SwapState.Transfer,
|
||||
uiStateHolder: SwapStateHolder,
|
||||
feePaidCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
fee: Fee?,
|
||||
): SwapStateHolder {
|
||||
val fromTokenSwapInfo = transferState.fromTokenInfo
|
||||
val toTokenSwapInfo = transferState.toTokenInfo
|
||||
val isInsufficientBalance = transferState.isInsufficientBalance
|
||||
val amountTextFieldValue = (uiStateHolder.sendCardData as? SwapCardState.SwapCardData)?.amountTextFieldValue
|
||||
val notifications = notificationsFactory.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = feePaidCryptoCurrencyStatus,
|
||||
fee = fee,
|
||||
onReduceByAmount = actions.onReduceByAmount,
|
||||
onReduceToAmount = actions.onReduceToAmount,
|
||||
)
|
||||
return uiStateHolder.copy(
|
||||
sendCardData = createSendSwapCardState(
|
||||
actions = actions,
|
||||
amountTextFieldValue = amountTextFieldValue,
|
||||
tokenSwapInfo = fromTokenSwapInfo,
|
||||
appCurrency = transferState.appCurrency,
|
||||
isAccountsMode = transferState.isAccountsMode,
|
||||
|
|
@ -54,6 +69,7 @@ internal class SwapTransferStateBuilder @Inject constructor() {
|
|||
),
|
||||
receiveCardData = createSendSwapCardState(
|
||||
actions = actions,
|
||||
amountTextFieldValue = amountTextFieldValue,
|
||||
tokenSwapInfo = toTokenSwapInfo,
|
||||
appCurrency = transferState.appCurrency,
|
||||
isAccountsMode = transferState.isAccountsMode,
|
||||
|
|
@ -69,12 +85,14 @@ internal class SwapTransferStateBuilder @Inject constructor() {
|
|||
onClick = actions.onTransferClick,
|
||||
),
|
||||
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
|
||||
notifications = notifications,
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun createSendSwapCardState(
|
||||
actions: UiActions,
|
||||
amountTextFieldValue: TextFieldValue?,
|
||||
tokenSwapInfo: TokenSwapInfo,
|
||||
appCurrency: AppCurrency,
|
||||
isAccountsMode: Boolean,
|
||||
|
|
@ -83,7 +101,6 @@ internal class SwapTransferStateBuilder @Inject constructor() {
|
|||
isInsufficientBalance: Boolean,
|
||||
): SwapCardState {
|
||||
val swapCurrencyStatus = tokenSwapInfo.swapCurrencyStatus
|
||||
val formattedSwapAmount = tokenSwapInfo.tokenAmount.formatToUIRepresentation()
|
||||
|
||||
return SwapCardState.SwapCardData(
|
||||
type = createSendTransactionCardType(
|
||||
|
|
@ -101,10 +118,7 @@ internal class SwapTransferStateBuilder @Inject constructor() {
|
|||
appCurrency = appCurrency,
|
||||
amount = tokenSwapInfo.amountFiat,
|
||||
),
|
||||
amountTextFieldValue = TextFieldValue(
|
||||
text = formattedSwapAmount,
|
||||
selection = TextRange(index = formattedSwapAmount.length),
|
||||
),
|
||||
amountTextFieldValue = amountTextFieldValue,
|
||||
balance = swapCurrencyStatus.status.getFormattedAmount(),
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
)
|
||||
|
|
@ -190,6 +204,40 @@ internal class SwapTransferStateBuilder @Inject constructor() {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateTransferButtonEnableState(
|
||||
transferState: SwapState.Transfer,
|
||||
actions: UiActions,
|
||||
uiStateHolder: SwapStateHolder,
|
||||
feePaidCryptoCurrencyStatus: CryptoCurrencyStatus?,
|
||||
fee: Fee?,
|
||||
): SwapStateHolder {
|
||||
val notifications = notificationsFactory.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = feePaidCryptoCurrencyStatus,
|
||||
fee = fee,
|
||||
onReduceByAmount = actions.onReduceByAmount,
|
||||
onReduceToAmount = actions.onReduceToAmount,
|
||||
)
|
||||
return uiStateHolder.copy(
|
||||
notifications = notifications,
|
||||
swapButton = uiStateHolder.swapButton.copy(
|
||||
isEnabled = getTransferButtonEnabled(notifications, fee),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun getTransferButtonEnabled(notifications: ImmutableList<NotificationUM>, fee: Fee?): Boolean {
|
||||
return fee != null && notifications.none { notification ->
|
||||
notification is SwapNotificationUM.Error || notification is NotificationUM.Error ||
|
||||
notification is SwapNotificationUM.Warning.ExpressErrorWarning ||
|
||||
notification is SwapNotificationUM.Warning.ExpressGeneralError ||
|
||||
notification is SwapNotificationUM.Warning.NoAvailableTokensToSwap ||
|
||||
notification is SwapNotificationUM.Warning.SwapNotSupported ||
|
||||
notification is SwapNotificationUM.Warning.NeedReserveToCreateAccount ||
|
||||
notification is SwapNotificationUM.Info.PermissionNeeded
|
||||
}
|
||||
}
|
||||
|
||||
fun createTransferInProgressState(uiState: SwapStateHolder): SwapStateHolder {
|
||||
return uiState.copy(
|
||||
swapButton = uiState.swapButton.copy(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,297 @@
|
|||
package com.tangem.feature.swap.ui.transfer
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.notifications.NotificationUM
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
|
||||
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.ui.SwapState
|
||||
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
|
||||
import com.tangem.feature.swap.models.states.SwapNotificationUM
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
internal class SwapTransferNotificationsFactoryTest {
|
||||
|
||||
private val sut = SwapTransferNotificationsFactory()
|
||||
|
||||
private val userWalletId = UserWalletId(stringValue = "deadbeef")
|
||||
private val coldWallet: UserWallet.Cold = mockk(relaxed = true) {
|
||||
every { walletId } returns userWalletId
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN clean state WHEN getNotifications THEN list is empty`() = runTest {
|
||||
val transferState = buildTransferState()
|
||||
|
||||
val result = sut.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
)
|
||||
|
||||
assertThat(result).isEmpty()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN currencyCheck with rentWarning WHEN getNotifications THEN Solana RentInfo is added`() = runTest {
|
||||
val rentWarning = CryptoCurrencyWarning.Rent(
|
||||
rent = BigDecimal("0.01"),
|
||||
exemptionAmount = BigDecimal("1.0"),
|
||||
cryptoCurrency = buildCoin(),
|
||||
)
|
||||
val transferState = buildTransferState(
|
||||
currencyCheck = buildCurrencyCheck(rentWarning = rentWarning),
|
||||
)
|
||||
|
||||
val result = sut.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<NotificationUM.Solana.RentInfo>()).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN existential deposit greater than diff WHEN getNotifications THEN ExistentialDeposit is added`() =
|
||||
runTest {
|
||||
val fromStatus = buildCoinStatus(balance = BigDecimal("1.0"))
|
||||
val transferState = buildTransferState(
|
||||
fromTokenInfo = buildTokenInfo(
|
||||
swapCurrencyStatus = fromStatus,
|
||||
amount = BigDecimal("0.5"),
|
||||
),
|
||||
currencyCheck = buildCurrencyCheck(existentialDeposit = BigDecimal("0.5")),
|
||||
)
|
||||
val fee: Fee = mockk(relaxed = true) {
|
||||
every { amount.value } returns BigDecimal("0.4")
|
||||
}
|
||||
|
||||
val result = sut.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<NotificationUM.Error.ExistentialDeposit>()).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN dust limit exceeded for coin WHEN getNotifications THEN MinimumAmountError is added`() = runTest {
|
||||
val fromStatus = buildCoinStatus(balance = BigDecimal("1.0"))
|
||||
val transferState = buildTransferState(
|
||||
fromTokenInfo = buildTokenInfo(
|
||||
swapCurrencyStatus = fromStatus,
|
||||
amount = BigDecimal("0.0001"),
|
||||
),
|
||||
currencyCheck = buildCurrencyCheck(dustValue = BigDecimal("0.01")),
|
||||
)
|
||||
|
||||
val result = sut.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<NotificationUM.Error.MinimumAmountError>()).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN minAdaValue and no validationResult WHEN getNotifications THEN MinAdaValueCharged is added`() =
|
||||
runTest {
|
||||
val transferState = buildTransferState(
|
||||
minAdaValue = BigDecimal("1500000"),
|
||||
)
|
||||
|
||||
val result = sut.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<NotificationUM.Cardano.MinAdaValueCharged>()).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN transferState with isFeeCoverage true WHEN getNotifications THEN FeeCoverage is added`() = runTest {
|
||||
val fromStatus = buildCoinStatus(balance = BigDecimal("1.5"))
|
||||
val transferState = buildTransferState(
|
||||
fromTokenInfo = buildTokenInfo(
|
||||
swapCurrencyStatus = fromStatus,
|
||||
amount = BigDecimal("1.0"),
|
||||
),
|
||||
isFeeCoverage = true,
|
||||
sendingAmount = BigDecimal("0.5"),
|
||||
)
|
||||
|
||||
val result = sut.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<NotificationUM.Warning.FeeCoverageNotification>()).hasSize(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN toToken has NoAccount status with reserve gap WHEN getNotifications THEN NeedReserveToCreateAccount is added`() =
|
||||
runTest {
|
||||
val toStatus = buildNoAccountStatus(amountToCreateAccount = BigDecimal("2.0"))
|
||||
val transferState = buildTransferState(
|
||||
toTokenInfo = buildTokenInfo(
|
||||
swapCurrencyStatus = toStatus,
|
||||
amount = BigDecimal("0.5"),
|
||||
),
|
||||
)
|
||||
|
||||
val result = sut.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
)
|
||||
|
||||
val reserve = result.filterIsInstance<SwapNotificationUM.Warning.NeedReserveToCreateAccount>()
|
||||
assertThat(reserve).hasSize(1)
|
||||
assertThat(reserve.first().receiveToken).isEqualTo(toStatus.currency.symbol)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN Tezos network with total balance amount WHEN getNotifications THEN ReduceAmount is added`() = runTest {
|
||||
val fromStatus = buildCoinStatus(rawNetworkId = "tezos", balance = BigDecimal("1.0"))
|
||||
val transferState = buildTransferState(
|
||||
fromTokenInfo = buildTokenInfo(
|
||||
swapCurrencyStatus = fromStatus,
|
||||
amount = BigDecimal("1.0"),
|
||||
),
|
||||
)
|
||||
|
||||
val result = sut.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
onReduceByAmount = { _, _ -> },
|
||||
onReduceToAmount = {},
|
||||
)
|
||||
|
||||
assertThat(result.filterIsInstance<SwapNotificationUM.Warning.ReduceAmount>()).hasSize(1)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
private fun buildTransferState(
|
||||
fromTokenInfo: TokenSwapInfo = buildTokenInfo(buildCoinStatus()),
|
||||
toTokenInfo: TokenSwapInfo = buildTokenInfo(buildCoinStatus()),
|
||||
currencyCheck: CryptoCurrencyCheck? = null,
|
||||
validationResult: Throwable? = null,
|
||||
minAdaValue: BigDecimal? = null,
|
||||
isFeeCoverage: Boolean = false,
|
||||
sendingAmount: BigDecimal = fromTokenInfo.tokenAmount.value,
|
||||
): SwapState.Transfer = SwapState.Transfer(
|
||||
userWallet = coldWallet,
|
||||
fromTokenInfo = fromTokenInfo,
|
||||
toTokenInfo = toTokenInfo,
|
||||
isInsufficientBalance = false,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isBalanceHidden = false,
|
||||
isAccountsMode = false,
|
||||
isFeeCoverage = isFeeCoverage,
|
||||
sendingAmount = sendingAmount,
|
||||
currencyCheck = currencyCheck,
|
||||
validationResult = validationResult,
|
||||
minAdaValue = minAdaValue,
|
||||
)
|
||||
|
||||
private fun buildTokenInfo(
|
||||
swapCurrencyStatus: SwapCurrencyStatus,
|
||||
amount: BigDecimal = BigDecimal("0.1"),
|
||||
): TokenSwapInfo = TokenSwapInfo(
|
||||
tokenAmount = SwapAmount(value = amount, decimals = swapCurrencyStatus.currency.decimals),
|
||||
amountFiat = amount * BigDecimal("2000"),
|
||||
swapCurrencyStatus = swapCurrencyStatus,
|
||||
)
|
||||
|
||||
private fun buildCurrencyCheck(
|
||||
existentialDeposit: BigDecimal? = null,
|
||||
dustValue: BigDecimal? = null,
|
||||
reserveAmount: BigDecimal? = null,
|
||||
rentWarning: CryptoCurrencyWarning.Rent? = null,
|
||||
): CryptoCurrencyCheck = CryptoCurrencyCheck(
|
||||
dustValue = dustValue,
|
||||
reserveAmount = reserveAmount,
|
||||
minimumSendAmount = null,
|
||||
existentialDeposit = existentialDeposit,
|
||||
utxoAmountLimit = null,
|
||||
isAccountFunded = true,
|
||||
rentWarning = rentWarning,
|
||||
)
|
||||
|
||||
private fun buildCoinStatus(
|
||||
rawNetworkId: String = "ethereum",
|
||||
balance: BigDecimal = BigDecimal("1.0"),
|
||||
fiatRate: BigDecimal = BigDecimal("2000"),
|
||||
): SwapCurrencyStatus {
|
||||
val coin = buildCoin(rawNetworkId = rawNetworkId)
|
||||
val statusValue: CryptoCurrencyStatus.Loaded = mockk(relaxed = true) {
|
||||
every { amount } returns balance
|
||||
every { this@mockk.fiatRate } returns fiatRate
|
||||
every { fiatAmount } returns balance.multiply(fiatRate)
|
||||
}
|
||||
return SwapCurrencyStatus(
|
||||
userWallet = coldWallet,
|
||||
status = CryptoCurrencyStatus(currency = coin, value = statusValue),
|
||||
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildNoAccountStatus(amountToCreateAccount: BigDecimal): SwapCurrencyStatus {
|
||||
val coin = buildCoin()
|
||||
val statusValue: CryptoCurrencyStatus.NoAccount = mockk(relaxed = true) {
|
||||
every { this@mockk.amountToCreateAccount } returns amountToCreateAccount
|
||||
}
|
||||
return SwapCurrencyStatus(
|
||||
userWallet = coldWallet,
|
||||
status = CryptoCurrencyStatus(currency = coin, value = statusValue),
|
||||
account = Account.CryptoPortfolio.createMainAccount(userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildCoin(rawNetworkId: String = "ethereum"): CryptoCurrency.Coin {
|
||||
return mockk(relaxed = true) {
|
||||
every { id } returns mockk(relaxed = true)
|
||||
every { network } returns mockk(relaxed = true) {
|
||||
every { rawId } returns rawNetworkId
|
||||
every { name } returns "Test Network"
|
||||
}
|
||||
every { name } returns "Test Coin"
|
||||
every { symbol } returns "TST"
|
||||
every { decimals } returns 18
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,19 +3,20 @@ package com.tangem.feature.swap.ui.transfer
|
|||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.common.ui.account.AccountTitleUM
|
||||
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
|
||||
import com.tangem.common.ui.account.toUM
|
||||
import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
|
||||
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.account.Account
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.swap.models.SwapCurrencyStatus
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.feature.swap.buildSwapCurrencyStatus
|
||||
import com.tangem.feature.swap.domain.models.SwapAmount
|
||||
import com.tangem.feature.swap.domain.models.ui.PriceImpact
|
||||
|
|
@ -25,9 +26,12 @@ import com.tangem.feature.swap.model.SwapProcessDataState
|
|||
import com.tangem.feature.swap.models.*
|
||||
import com.tangem.feature.swap.models.states.ProviderState
|
||||
import com.tangem.feature.swap.presentation.R
|
||||
import com.tangem.feature.swap.utils.formatToUIRepresentation
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
import java.math.BigDecimal
|
||||
|
|
@ -36,7 +40,18 @@ import java.math.BigDecimal
|
|||
internal class SwapTransferStateBuilderTest {
|
||||
|
||||
private val actions: UiActions = mockk(relaxed = true)
|
||||
private val sut = SwapTransferStateBuilder()
|
||||
private val notificationsFactory: SwapTransferNotificationsFactory = mockk(relaxed = true) {
|
||||
coEvery {
|
||||
getNotifications(
|
||||
transferState = any(),
|
||||
feeCryptoCurrencyStatus = any(),
|
||||
fee = any(),
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
)
|
||||
} returns persistentListOf()
|
||||
}
|
||||
private val sut = SwapTransferStateBuilder(notificationsFactory = notificationsFactory)
|
||||
|
||||
private val userWalletId = UserWalletId(stringValue = "deadbeef")
|
||||
private val coldWallet: UserWallet.Cold = mockk(relaxed = true) {
|
||||
|
|
@ -47,121 +62,193 @@ internal class SwapTransferStateBuilderTest {
|
|||
private val iconConverter = CryptoCurrencyToIconStateConverter()
|
||||
private val fromIcon = iconConverter.convert(fromCurrencyStatus.status)
|
||||
private val toIcon = iconConverter.convert(toCurrencyStatus.status)
|
||||
private val initialAmountTextFieldValue = TextFieldValue(
|
||||
text = "0.5",
|
||||
selection = TextRange(index = 3),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN accounts mode enabled WHEN createTransferState THEN cards expose Account titles for from and to`() {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("1.5"),
|
||||
toAmount = BigDecimal("1.5"),
|
||||
isAccountsMode = true,
|
||||
)
|
||||
fun `GIVEN accounts mode enabled WHEN createTransferState THEN cards expose Account titles for from and to`() =
|
||||
runTest {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("1.5"),
|
||||
toAmount = BigDecimal("1.5"),
|
||||
isAccountsMode = true,
|
||||
)
|
||||
val uiState = baseStateHolder()
|
||||
|
||||
val result = sut.createTransferState(actions, transferState, baseStateHolder())
|
||||
val result = sut.createTransferState(
|
||||
actions = actions,
|
||||
transferState = transferState,
|
||||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
)
|
||||
|
||||
val portfolioAccount = fromCurrencyStatus.account as Account.CryptoPortfolio
|
||||
val expectedAccountIcon = CryptoPortfolioIconConverter.convert(portfolioAccount.icon)
|
||||
val expectedAccountName = portfolioAccount.accountName.toUM().value
|
||||
val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
|
||||
val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
|
||||
assertThat(sendType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.swapping_from_account_title),
|
||||
name = expectedAccountName,
|
||||
icon = expectedAccountIcon,
|
||||
),
|
||||
)
|
||||
assertThat(receiveType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.swapping_to_account_title),
|
||||
name = expectedAccountName,
|
||||
icon = expectedAccountIcon,
|
||||
),
|
||||
)
|
||||
assertSharedCardShape(
|
||||
result = result,
|
||||
transferState = transferState,
|
||||
)
|
||||
}
|
||||
val portfolioAccount = fromCurrencyStatus.account as Account.CryptoPortfolio
|
||||
val expectedAccountIcon = CryptoPortfolioIconConverter.convert(portfolioAccount.icon)
|
||||
val expectedAccountName = portfolioAccount.accountName.toUM().value
|
||||
val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
|
||||
val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
|
||||
assertThat(sendType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.swapping_from_account_title),
|
||||
name = expectedAccountName,
|
||||
icon = expectedAccountIcon,
|
||||
),
|
||||
)
|
||||
assertThat(receiveType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.swapping_to_account_title),
|
||||
name = expectedAccountName,
|
||||
icon = expectedAccountIcon,
|
||||
),
|
||||
)
|
||||
assertSharedCardShape(
|
||||
result = result,
|
||||
transferState = transferState,
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
notificationsFactory.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN accounts mode disabled WHEN createTransferState THEN cards fall back to Text titles for from and to`() {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("2"),
|
||||
toAmount = BigDecimal("2"),
|
||||
isAccountsMode = false,
|
||||
)
|
||||
fun `GIVEN accounts mode disabled WHEN createTransferState THEN cards fall back to Text titles for from and to`() =
|
||||
runTest {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("2"),
|
||||
toAmount = BigDecimal("2"),
|
||||
isAccountsMode = false,
|
||||
)
|
||||
val uiState = baseStateHolder()
|
||||
|
||||
val result = sut.createTransferState(actions, transferState, baseStateHolder())
|
||||
val result = sut.createTransferState(
|
||||
actions = actions,
|
||||
transferState = transferState,
|
||||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
)
|
||||
|
||||
val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
|
||||
val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
|
||||
assertThat(sendType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_from_title_v2)),
|
||||
)
|
||||
assertThat(receiveType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_to_title)),
|
||||
)
|
||||
assertSharedCardShape(
|
||||
result = result,
|
||||
transferState = transferState,
|
||||
)
|
||||
}
|
||||
val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
|
||||
val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
|
||||
assertThat(sendType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_from_title_v2)),
|
||||
)
|
||||
assertThat(receiveType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_to_title)),
|
||||
)
|
||||
assertSharedCardShape(
|
||||
result = result,
|
||||
transferState = transferState,
|
||||
)
|
||||
coVerify(exactly = 1) {
|
||||
notificationsFactory.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN insufficient balance and accounts mode disabled WHEN createTransferState THEN from card shows insufficient funds title and error and swap is disabled`() {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("10"),
|
||||
toAmount = BigDecimal("10"),
|
||||
isAccountsMode = false,
|
||||
isInsufficientBalance = true,
|
||||
)
|
||||
fun `GIVEN insufficient balance and accounts mode disabled WHEN createTransferState THEN from card shows insufficient funds title and error and swap is disabled`() =
|
||||
runTest {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("10"),
|
||||
toAmount = BigDecimal("10"),
|
||||
isAccountsMode = false,
|
||||
isInsufficientBalance = true,
|
||||
)
|
||||
val uiState = baseStateHolder()
|
||||
|
||||
val result = sut.createTransferState(actions, transferState, baseStateHolder())
|
||||
val result = sut.createTransferState(
|
||||
actions = actions,
|
||||
transferState = transferState,
|
||||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
)
|
||||
|
||||
val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
|
||||
val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
|
||||
assertThat(sendType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_insufficient_funds)),
|
||||
)
|
||||
assertThat(sendType.inputError).isEqualTo(TransactionCardType.InputError.InsufficientFunds)
|
||||
assertThat(receiveType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_to_title)),
|
||||
)
|
||||
assertThat(result.isInsufficientFunds).isTrue()
|
||||
assertThat(result.swapButton.isEnabled).isFalse()
|
||||
assertThat(result.swapButton.mode).isEqualTo(SwapButton.Mode.TRANSFER)
|
||||
}
|
||||
val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
|
||||
val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
|
||||
assertThat(sendType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_insufficient_funds)),
|
||||
)
|
||||
assertThat(sendType.inputError).isEqualTo(TransactionCardType.InputError.InsufficientFunds)
|
||||
assertThat(receiveType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_to_title)),
|
||||
)
|
||||
assertThat(result.isInsufficientFunds).isTrue()
|
||||
assertThat(result.swapButton.isEnabled).isFalse()
|
||||
assertThat(result.swapButton.mode).isEqualTo(SwapButton.Mode.TRANSFER)
|
||||
coVerify(exactly = 1) {
|
||||
notificationsFactory.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN insufficient balance and accounts mode enabled WHEN createTransferState THEN from card overrides Account title with insufficient funds text`() {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("10"),
|
||||
toAmount = BigDecimal("10"),
|
||||
isAccountsMode = true,
|
||||
isInsufficientBalance = true,
|
||||
)
|
||||
fun `GIVEN insufficient balance and accounts mode enabled WHEN createTransferState THEN from card overrides Account title with insufficient funds text`() =
|
||||
runTest {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("10"),
|
||||
toAmount = BigDecimal("10"),
|
||||
isAccountsMode = true,
|
||||
isInsufficientBalance = true,
|
||||
)
|
||||
val uiState = baseStateHolder()
|
||||
|
||||
val result = sut.createTransferState(actions, transferState, baseStateHolder())
|
||||
val result = sut.createTransferState(
|
||||
actions = actions,
|
||||
transferState = transferState,
|
||||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
)
|
||||
|
||||
val portfolioAccount = toCurrencyStatus.account as Account.CryptoPortfolio
|
||||
val expectedAccountIcon = CryptoPortfolioIconConverter.convert(portfolioAccount.icon)
|
||||
val expectedAccountName = portfolioAccount.accountName.toUM().value
|
||||
val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
|
||||
val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
|
||||
assertThat(sendType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_insufficient_funds)),
|
||||
)
|
||||
assertThat(sendType.inputError).isEqualTo(TransactionCardType.InputError.InsufficientFunds)
|
||||
assertThat(receiveType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.swapping_to_account_title),
|
||||
name = expectedAccountName,
|
||||
icon = expectedAccountIcon,
|
||||
),
|
||||
)
|
||||
assertThat(result.isInsufficientFunds).isTrue()
|
||||
assertThat(result.swapButton.isEnabled).isFalse()
|
||||
}
|
||||
val portfolioAccount = toCurrencyStatus.account as Account.CryptoPortfolio
|
||||
val expectedAccountIcon = CryptoPortfolioIconConverter.convert(portfolioAccount.icon)
|
||||
val expectedAccountName = portfolioAccount.accountName.toUM().value
|
||||
val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable
|
||||
val receiveType = (result.receiveCardData as SwapCardState.SwapCardData).type as TransactionCardType.ReadOnly
|
||||
assertThat(sendType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Text(resourceReference(R.string.swapping_insufficient_funds)),
|
||||
)
|
||||
assertThat(sendType.inputError).isEqualTo(TransactionCardType.InputError.InsufficientFunds)
|
||||
assertThat(receiveType.accountTitleUM).isEqualTo(
|
||||
AccountTitleUM.Account(
|
||||
prefixText = resourceReference(R.string.swapping_to_account_title),
|
||||
name = expectedAccountName,
|
||||
icon = expectedAccountIcon,
|
||||
),
|
||||
)
|
||||
assertThat(result.isInsufficientFunds).isTrue()
|
||||
assertThat(result.swapButton.isEnabled).isFalse()
|
||||
coVerify(exactly = 1) {
|
||||
notificationsFactory.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = null,
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN content uiState WHEN createTransferInProgressState THEN swap button is disabled in TRANSFER_PROGRESSING mode`() {
|
||||
|
|
@ -181,6 +268,55 @@ internal class SwapTransferStateBuilderTest {
|
|||
assertThat(result.swapButton.onClick).isEqualTo(initialButton.onClick)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN no blocking notifications and non-null fee WHEN updateTransferButtonEnableState THEN swap button becomes enabled`() =
|
||||
runTest {
|
||||
val transferState = buildTransferState(
|
||||
fromAmount = BigDecimal("1"),
|
||||
toAmount = BigDecimal("1"),
|
||||
isAccountsMode = false,
|
||||
)
|
||||
val fee: Fee = mockk(relaxed = true)
|
||||
val uiState = baseStateHolder().copy(
|
||||
swapButton = SwapButton(
|
||||
walletInteractionIcon = null,
|
||||
isEnabled = false,
|
||||
mode = SwapButton.Mode.TRANSFER,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
coEvery {
|
||||
notificationsFactory.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
)
|
||||
} returns persistentListOf()
|
||||
|
||||
val result = sut.updateTransferButtonEnableState(
|
||||
transferState = transferState,
|
||||
actions = actions,
|
||||
uiStateHolder = uiState,
|
||||
feePaidCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
)
|
||||
|
||||
assertThat(result.swapButton.isEnabled).isTrue()
|
||||
assertThat(result.swapButton.mode).isEqualTo(SwapButton.Mode.TRANSFER)
|
||||
assertThat(result.notifications).isEmpty()
|
||||
coVerify(exactly = 1) {
|
||||
notificationsFactory.getNotifications(
|
||||
transferState = transferState,
|
||||
feeCryptoCurrencyStatus = null,
|
||||
fee = fee,
|
||||
onReduceByAmount = any(),
|
||||
onReduceToAmount = any(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN dataState with from-to currencies WHEN createSuccessState THEN success holder is built in transfer mode with given fee and txUrl`() {
|
||||
val appCurrency = AppCurrency(code = "USD", name = "US Dollar", symbol = "$")
|
||||
|
|
@ -242,14 +378,8 @@ internal class SwapTransferStateBuilderTest {
|
|||
) {
|
||||
val sendCard = result.sendCardData as SwapCardState.SwapCardData
|
||||
val receiveCard = result.receiveCardData as SwapCardState.SwapCardData
|
||||
val expectedFromText = transferState.fromTokenInfo.tokenAmount.formatToUIRepresentation()
|
||||
val expectedToText = transferState.toTokenInfo.tokenAmount.formatToUIRepresentation()
|
||||
assertThat(sendCard.amountTextFieldValue).isEqualTo(
|
||||
TextFieldValue(text = expectedFromText, selection = TextRange(index = expectedFromText.length)),
|
||||
)
|
||||
assertThat(receiveCard.amountTextFieldValue).isEqualTo(
|
||||
TextFieldValue(text = expectedToText, selection = TextRange(index = expectedToText.length)),
|
||||
)
|
||||
assertThat(sendCard.amountTextFieldValue).isEqualTo(initialAmountTextFieldValue)
|
||||
assertThat(receiveCard.amountTextFieldValue).isEqualTo(initialAmountTextFieldValue)
|
||||
assertThat(sendCard.currencyIconState).isEqualTo(fromIcon)
|
||||
assertThat(receiveCard.currencyIconState).isEqualTo(toIcon)
|
||||
assertThat(sendCard.isBalanceHidden).isEqualTo(transferState.isBalanceHidden)
|
||||
|
|
@ -290,14 +420,26 @@ internal class SwapTransferStateBuilderTest {
|
|||
appCurrency = AppCurrency.Default,
|
||||
isBalanceHidden = false,
|
||||
isAccountsMode = isAccountsMode,
|
||||
isFeeCoverage = false,
|
||||
sendingAmount = fromAmount,
|
||||
)
|
||||
}
|
||||
|
||||
private fun baseStateHolder(): SwapStateHolder = SwapStateHolder(
|
||||
sendCardData = SwapCardState.Loading(
|
||||
type = TransactionCardType.ReadOnly(
|
||||
sendCardData = SwapCardState.SwapCardData(
|
||||
type = TransactionCardType.Inputtable(
|
||||
onAmountChanged = {},
|
||||
onFocusChanged = {},
|
||||
inputError = TransactionCardType.InputError.Empty,
|
||||
accountTitleUM = AccountTitleUM.Text(resourceReference(R.string.swapping_from_title_v2)),
|
||||
isEnabled = true,
|
||||
),
|
||||
currencyIconState = fromIcon,
|
||||
tokenSymbol = stringReference(""),
|
||||
amountEquivalent = TextReference.EMPTY,
|
||||
amountTextFieldValue = initialAmountTextFieldValue,
|
||||
balance = "",
|
||||
isBalanceHidden = false,
|
||||
),
|
||||
receiveCardData = SwapCardState.Loading(
|
||||
type = TransactionCardType.ReadOnly(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue