diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index bfafdfeadf..c98a1a7c48 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -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, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt index e66862cd6f..1bdd520d50 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImpl.kt @@ -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, diff --git a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt index f67cfeb9b2..24e768bb02 100644 --- a/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt +++ b/features/swap/domain/src/test/kotlin/com/tangem/feature/swap/domain/transfer/SwapTransferInteractorImplTest.kt @@ -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 diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt index fa52e874f8..14ab1bb126 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilder.kt @@ -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 { diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt index 5e3196c4a2..2a254964f3 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferStateBuilderTest.kt @@ -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,