Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-12 20:32:25 +01:00
parent 7e42240fc6
commit bcd4838105
5 changed files with 138 additions and 15 deletions

View file

@ -43,7 +43,7 @@ sealed interface SwapState {
val userWallet: UserWallet,
val fromTokenInfo: TokenSwapInfo,
val toTokenInfo: TokenSwapInfo,
val txFee: TxFeeState,
val isInsufficientBalance: Boolean,
val appCurrency: AppCurrency,
val isBalanceHidden: Boolean,
val isAccountsMode: Boolean,

View file

@ -14,7 +14,6 @@ import com.tangem.domain.swap.models.SwapCurrencyStatus
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.domain.models.ui.TxFeeState
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.flow.first
@ -40,6 +39,7 @@ class SwapTransferInteractorImpl @Inject constructor(
val isAccountsMode = isAccountsModeEnabledUseCase.invokeSync()
val fromTokenAmountValue = fromTokenAmount.parseBigDecimalOrNull() ?: return createEmptyAmountState(appCurrency)
val fromTokenAmountFiat = fromSwapCurrencyStatus.status.value.fiatRate.orZero() * fromTokenAmountValue
val fromTokenBalance = fromSwapCurrencyStatus.status.value.amount.orZero()
val fromTokenInfo = TokenSwapInfo(
tokenAmount = SwapAmount(fromTokenAmountValue, fromToken.decimals),
@ -56,7 +56,7 @@ class SwapTransferInteractorImpl @Inject constructor(
userWallet = toSwapCurrencyStatus.userWallet,
fromTokenInfo = fromTokenInfo,
toTokenInfo = toTokenInfo,
txFee = TxFeeState.Empty, // todo Will be implemented in [REDACTED_TASK_KEY]
isInsufficientBalance = fromTokenAmountValue > fromTokenBalance,
appCurrency = appCurrency,
isBalanceHidden = isBalanceHidden,
isAccountsMode = isAccountsMode,

View file

@ -14,7 +14,6 @@ import com.tangem.domain.swap.models.SwapCurrencyStatus
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.domain.models.ui.TxFeeState
import com.tangem.features.swap.SwapFeatureToggles
import io.mockk.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -85,6 +84,7 @@ internal class SwapTransferInteractorImplTest {
rawCurrencyId = FROM_RAW_CURRENCY_ID,
decimals = FROM_DECIMALS,
fiatRate = BigDecimal.TEN,
amount = BigDecimal("1.6"),
)
val toCurrencyStatus = buildCurrencyStatus(
rawCurrencyId = TO_RAW_CURRENCY_ID,
@ -115,7 +115,7 @@ internal class SwapTransferInteractorImplTest {
swapCurrencyStatus = toCurrencyStatus,
amountFiat = expectedFiat,
),
txFee = TxFeeState.Empty,
isInsufficientBalance = false,
appCurrency = appCurrency,
isBalanceHidden = true,
isAccountsMode = true,
@ -125,6 +125,55 @@ internal class SwapTransferInteractorImplTest {
verify { getBalanceHidingSettingsUseCase.isBalanceHidden() }
}
@Test
fun `GIVEN insufficient amount WHEN updateTransfer THEN return state with insufficient amount`() = runTest {
val appCurrency = AppCurrency(code = "USD", name = "US Dollar", symbol = "$")
val userWallet: UserWallet = mockk()
val fromCurrencyStatus = buildCurrencyStatus(
rawCurrencyId = FROM_RAW_CURRENCY_ID,
decimals = FROM_DECIMALS,
fiatRate = BigDecimal.TEN,
amount = BigDecimal("1.4"),
)
val toCurrencyStatus = buildCurrencyStatus(
rawCurrencyId = TO_RAW_CURRENCY_ID,
decimals = TO_DECIMALS,
userWallet = userWallet,
)
every { getSelectedAppCurrencyUseCase() } returns flowOf(appCurrency.right())
every { getBalanceHidingSettingsUseCase.isBalanceHidden() } returns flowOf(true)
coEvery { isAccountsModeEnabledUseCase.invokeSync() } returns true
val result = sut.updateTransfer(
fromSwapCurrencyStatus = fromCurrencyStatus,
toSwapCurrencyStatus = toCurrencyStatus,
fromTokenAmount = "1,5",
)
val expectedAmount = BigDecimal("1.5")
val expectedFiat = BigDecimal("15.0")
val expected = SwapState.Transfer(
userWallet = userWallet,
fromTokenInfo = TokenSwapInfo(
tokenAmount = SwapAmount(expectedAmount, FROM_DECIMALS),
swapCurrencyStatus = fromCurrencyStatus,
amountFiat = expectedFiat,
),
toTokenInfo = TokenSwapInfo(
tokenAmount = SwapAmount(expectedAmount, TO_DECIMALS),
swapCurrencyStatus = toCurrencyStatus,
amountFiat = expectedFiat,
),
isInsufficientBalance = true,
appCurrency = appCurrency,
isBalanceHidden = true,
isAccountsMode = true,
)
assertThat(result).isEqualTo(expected)
coVerify { isAccountsModeEnabledUseCase.invokeSync() }
verify { getBalanceHidingSettingsUseCase.isBalanceHidden() }
}
// endregion
// region shouldTransferInsteadOfSwap
@ -249,6 +298,7 @@ internal class SwapTransferInteractorImplTest {
rawCurrencyId: CryptoCurrency.RawID?,
decimals: Int,
fiatRate: BigDecimal = BigDecimal.ZERO,
amount: BigDecimal = BigDecimal.ZERO,
userWallet: UserWallet = mockk(),
): SwapCurrencyStatus {
val currencyId: CryptoCurrency.ID = mockk {
@ -260,6 +310,7 @@ internal class SwapTransferInteractorImplTest {
}
val currencyValue: CryptoCurrencyStatus.Value = mockk {
every { this@mockk.fiatRate } returns fiatRate
every { this@mockk.amount } returns amount
}
val status: CryptoCurrencyStatus = mockk {
every { this@mockk.value } returns currencyValue

View file

@ -38,6 +38,7 @@ internal class SwapTransferStateBuilder @Inject constructor() {
): SwapStateHolder {
val fromTokenSwapInfo = transferState.fromTokenInfo
val toTokenSwapInfo = transferState.toTokenInfo
val isInsufficientBalance = transferState.isInsufficientBalance
return uiStateHolder.copy(
sendCardData = createSendSwapCardState(
actions = actions,
@ -46,6 +47,7 @@ internal class SwapTransferStateBuilder @Inject constructor() {
isAccountsMode = transferState.isAccountsMode,
isFromCard = true,
isBalanceHidden = transferState.isBalanceHidden,
isInsufficientBalance = isInsufficientBalance,
),
receiveCardData = createSendSwapCardState(
actions = actions,
@ -54,10 +56,12 @@ internal class SwapTransferStateBuilder @Inject constructor() {
isAccountsMode = transferState.isAccountsMode,
isFromCard = false,
isBalanceHidden = transferState.isBalanceHidden,
isInsufficientBalance = isInsufficientBalance,
),
isInsufficientFunds = isInsufficientBalance,
swapButton = SwapButton(
walletInteractionIcon = walletInterationIcon(transferState.userWallet),
isEnabled = false,
isEnabled = !isInsufficientBalance,
mode = SwapButton.Mode.TRANSFER,
onClick = actions.onTransferClick,
),
@ -72,6 +76,7 @@ internal class SwapTransferStateBuilder @Inject constructor() {
isAccountsMode: Boolean,
isFromCard: Boolean,
isBalanceHidden: Boolean,
isInsufficientBalance: Boolean,
): SwapCardState {
val swapCurrencyStatus = tokenSwapInfo.swapCurrencyStatus
val formattedSwapAmount = tokenSwapInfo.tokenAmount.formatToUIRepresentation()
@ -82,6 +87,7 @@ internal class SwapTransferStateBuilder @Inject constructor() {
swapCurrencyStatus = tokenSwapInfo.swapCurrencyStatus,
isAccountsMode = isAccountsMode,
isFromCard = isFromCard,
isInsufficientBalance = isInsufficientBalance,
),
currencyIconState = iconConverter.convert(
value = swapCurrencyStatus.status,
@ -105,17 +111,27 @@ internal class SwapTransferStateBuilder @Inject constructor() {
swapCurrencyStatus: SwapCurrencyStatus,
isAccountsMode: Boolean,
isFromCard: Boolean,
isInsufficientBalance: Boolean,
): TransactionCardType {
val type = if (isFromCard) {
TransactionCardType.Inputtable(
onAmountChanged = actions.onAmountChanged,
onFocusChanged = actions.onAmountSelected,
inputError = TransactionCardType.InputError.Empty,
accountTitleUM = getCardAccountTitle(
val accountTitleUM = if (isInsufficientBalance) {
AccountTitleUM.Text(resourceReference(R.string.swapping_insufficient_funds))
} else {
getCardAccountTitle(
account = swapCurrencyStatus.account,
isAccountsMode = isAccountsMode,
isFromCard = true,
),
)
}
TransactionCardType.Inputtable(
onAmountChanged = actions.onAmountChanged,
onFocusChanged = actions.onAmountSelected,
inputError = if (isInsufficientBalance) {
TransactionCardType.InputError.InsufficientFunds
} else {
TransactionCardType.InputError.Empty
},
accountTitleUM = accountTitleUM,
isEnabled = true,
)
} else {

View file

@ -19,7 +19,6 @@ import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.ui.PriceImpact
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
import com.tangem.feature.swap.domain.models.ui.TxFeeState
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.presentation.R
@ -105,6 +104,62 @@ internal class SwapTransferStateBuilderTest {
)
}
@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,
)
val result = sut.createTransferState(actions, transferState, baseStateHolder())
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)
}
@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,
)
val result = sut.createTransferState(actions, transferState, baseStateHolder())
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()
}
private fun assertSharedCardShape(
result: SwapStateHolder,
transferState: SwapState.Transfer,
@ -128,7 +183,7 @@ internal class SwapTransferStateBuilderTest {
assertThat(result.swapButton).isEqualTo(
SwapButton(
walletInteractionIcon = walletInterationIcon(transferState.userWallet),
isEnabled = false,
isEnabled = !transferState.isInsufficientBalance,
mode = SwapButton.Mode.TRANSFER,
onClick = actions.onTransferClick,
),
@ -139,6 +194,7 @@ internal class SwapTransferStateBuilderTest {
fromAmount: BigDecimal,
toAmount: BigDecimal,
isAccountsMode: Boolean,
isInsufficientBalance: Boolean = false,
): SwapState.Transfer {
val fromInfo = TokenSwapInfo(
tokenAmount = SwapAmount(value = fromAmount, decimals = fromCurrencyStatus.currency.decimals),
@ -154,7 +210,7 @@ internal class SwapTransferStateBuilderTest {
userWallet = coldWallet,
fromTokenInfo = fromInfo,
toTokenInfo = toInfo,
txFee = TxFeeState.Empty,
isInsufficientBalance = isInsufficientBalance,
appCurrency = AppCurrency.Default,
isBalanceHidden = false,
isAccountsMode = isAccountsMode,