From 535f2ce1b3e632e693c93db34729daa343c580f7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 26 Jun 2026 17:52:48 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../swap/domain/models/ui/SwapState.kt | 1 + .../transfer/SwapTransferInteractorImpl.kt | 17 ++- .../SwapTransferInteractorImplTest.kt | 2 + .../tangem/feature/swap/model/SwapModel.kt | 5 +- .../SwapTransferNotificationsFactory.kt | 47 +++++- .../ui/transfer/SwapTransferStateBuilder.kt | 11 +- .../SwapTransferNotificationsFactoryTest.kt | 139 +++++++++++++----- .../transfer/SwapTransferStateBuilderTest.kt | 74 +++++----- 8 files changed, 201 insertions(+), 95 deletions(-) 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 209108a216..3884207e3c 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 @@ -55,6 +55,7 @@ sealed interface SwapState { val isFeeCoverage: Boolean, val sendingAmount: BigDecimal, val tronFeeNotificationShowCount: Int, + val isAmountSubtractAvailable: Boolean, val isSendingAmountLoading: Boolean = false, val currencyCheck: CryptoCurrencyCheck? = null, val validationResult: Throwable? = null, 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 ac06572551..e6def26c9f 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 @@ -110,9 +110,14 @@ class SwapTransferInteractorImpl @Inject constructor( fee = warningsFee, feeCurrencyBalanceAfterTransaction = null, ) + val isAmountSubtractAvailable = isAmountSubtractAvailable( + userWalletId = userWallet.walletId, + currency = fromTokenInfo.swapCurrencyStatus.currency, + fee = fee, + ) val coverageState = getCoverageState( fromTokenInfo = fromTokenInfo, - userWallet = userWallet, + isAmountSubtractAvailable = isAmountSubtractAvailable, fee = fee, currencyCheck = currencyCheck, ) @@ -137,6 +142,7 @@ class SwapTransferInteractorImpl @Inject constructor( isFeeCoverage = coverageState.isFeeCoverage, sendingAmount = coverageState.sendingAmount, tronFeeNotificationShowCount = tronFeeNotificationShowCount, + isAmountSubtractAvailable = isAmountSubtractAvailable, isSendingAmountLoading = coverageState.isSendingAmountLoading, currencyCheck = currencyCheck, ) @@ -156,18 +162,13 @@ class SwapTransferInteractorImpl @Inject constructor( ).getOrNull() } - private suspend fun getCoverageState( + private fun getCoverageState( fromTokenInfo: TokenSwapInfo, - userWallet: UserWallet, + isAmountSubtractAvailable: Boolean, fee: Fee?, currencyCheck: CryptoCurrencyCheck, ): CoverageState { val swapCurrencyStatus = fromTokenInfo.swapCurrencyStatus - val isAmountSubtractAvailable = isAmountSubtractAvailable( - userWalletId = userWallet.walletId, - currency = swapCurrencyStatus.currency, - fee = fee, - ) val balance = swapCurrencyStatus.status.value.amount ?: BigDecimal.ZERO val reduceAmountBy = currencyCheck.existentialDeposit.orZero() val amount = fromTokenInfo.tokenAmount 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 b140e07270..6ff5e5270d 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 @@ -187,6 +187,7 @@ internal class SwapTransferInteractorImplTest { isFeeCoverage = false, sendingAmount = expectedAmount, tronFeeNotificationShowCount = 0, + isAmountSubtractAvailable = false, currencyCheck = currencyCheck, ) assertThat(result).isEqualTo(expected) @@ -259,6 +260,7 @@ internal class SwapTransferInteractorImplTest { isFeeCoverage = false, sendingAmount = expectedAmount, tronFeeNotificationShowCount = 0, + isAmountSubtractAvailable = false, currencyCheck = currencyCheck, ) assertThat(result).isEqualTo(expected) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index fde708ab4b..1929007a12 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -822,8 +822,7 @@ internal class SwapModel @Inject constructor( transferState = swapState, uiStateHolder = uiState, feePaidCryptoCurrencyStatus = feePaidCryptoCurrency, - fee = selectedFee, - feeError = feeSelectorRepository.state.value as? FeeSelectorUM.Error, + feeSelectorUM = feeSelectorRepository.state.value, ) when { uiState.successState != null -> Unit @@ -871,7 +870,7 @@ internal class SwapModel @Inject constructor( feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatus ?: dataState.feePaidCryptoCurrency, fee = fee, isTangemPayWithdrawal = isTangemPayWithdrawal(), - feeError = feeSelectorRepository.state.value as? FeeSelectorUM.Error, + feeSelectorUM = feeSelectorRepository.state.value, ) } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt index 9508fd1e4b..2a79befcf2 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactory.kt @@ -1,8 +1,8 @@ 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.addExceedBalanceNotification import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification @@ -18,6 +18,8 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.states.SwapNotificationUM +import com.tangem.features.send.api.entity.FeeSelectorUM +import com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils import com.tangem.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.getTezosThreshold import com.tangem.lib.crypto.BlockchainUtils.isTezos @@ -33,22 +35,30 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { @Suppress("LongParameterList") fun getNotifications( transferState: SwapState.Transfer, + feeSelectorUM: FeeSelectorUM?, feeCryptoCurrencyStatus: CryptoCurrencyStatus?, - fee: Fee?, actions: UiActions, - getFeeError: GetFeeError?, ): ImmutableList { + // The fee selector exposes a single sealed state; narrow it here so call sites pass the raw + // FeeSelectorUM and this factory owns the Content/Error/Loading discrimination. + val feeContent = feeSelectorUM + val getFeeError = (feeSelectorUM as? FeeSelectorUM.Error)?.error return buildList { maybeAddRentExemptionError(transferState) maybeAddDomainWarnings( state = transferState, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = fee, + feeSelectorUM = feeContent, onReduceByAmount = actions.onReduceByAmount, onReduceToAmount = actions.onReduceToAmount, ) maybeAddNeedReserveToCreateAccountWarning(transferState) - maybeAddExceedsBalanceNotification(transferState, onBuyClick = actions.openTokenDetailsScreen) + maybeAddExceedsBalanceNotifications( + transferState = transferState, + feeSelectorUM = feeContent, + onBuyClick = actions.openTokenDetailsScreen, + ) + maybeAddTooHighOrTooLowNotification(feeContent) addTronNetworkFeesNotification( cryptoCurrencyStatus = transferState.fromTokenInfo.swapCurrencyStatus.status, transferState = transferState, @@ -71,13 +81,14 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { private fun MutableList.maybeAddDomainWarnings( state: SwapState.Transfer, feeCryptoCurrencyStatus: CryptoCurrencyStatus?, - fee: Fee?, + feeSelectorUM: FeeSelectorUM?, 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 fee = (feeSelectorUM as? FeeSelectorUM.Content)?.selectedFeeItem?.fee val feeValue = fee?.amount?.value.orZero() val isCardano = BlockchainUtils.isCardano(swapCurrencyStatus.currency.network.rawId) addExistentialWarningNotification( @@ -191,8 +202,9 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { } } - private fun MutableList.maybeAddExceedsBalanceNotification( + private fun MutableList.maybeAddExceedsBalanceNotifications( transferState: SwapState.Transfer, + feeSelectorUM: FeeSelectorUM?, onBuyClick: (CryptoCurrency) -> Unit, ) { val cryptoCurrencyStatus = transferState.fromTokenInfo.swapCurrencyStatus.status @@ -206,6 +218,27 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { onAnalyticsEvent = {}, onResetAnalyticsEvent = {}, ) + val feeAmount = (feeSelectorUM as? FeeSelectorUM.Content)?.selectedFeeItem?.fee?.amount?.value + if (feeAmount != null) { + addExceedBalanceNotification( + feeAmount = feeAmount, + sendingAmount = transferState.sendingAmount, + isSubtractionAvailable = transferState.isAmountSubtractAvailable, + cryptoCurrencyStatus = transferState.fromTokenInfo.swapCurrencyStatus.status, + ) + } + } + + @Suppress("CanBeNonNullable") + private fun MutableList.maybeAddTooHighOrTooLowNotification(feeSelectorUM: FeeSelectorUM?) { + val content = feeSelectorUM as? FeeSelectorUM.Content ?: return + val (isFeeTooHigh, diff) = FeeCalculationUtils.checkIfCustomFeeTooHigh(feeSelectorUM = content) + if (isFeeTooHigh) { + add(NotificationUM.Warning.TooHigh(diff)) + } + if (FeeCalculationUtils.checkIfCustomFeeTooLow(feeSelectorUM = content)) { + add(NotificationUM.Warning.FeeTooLow) + } } private fun MutableList.addTronNetworkFeesNotification( 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 96b938cd3b..e292f5585f 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 @@ -59,8 +59,7 @@ internal class SwapTransferStateBuilder @Inject constructor( transferState: SwapState.Transfer, uiStateHolder: SwapStateHolder, feePaidCryptoCurrencyStatus: CryptoCurrencyStatus?, - fee: Fee?, - feeError: FeeSelectorUM.Error?, + feeSelectorUM: FeeSelectorUM?, ): SwapStateHolder { val fromTokenSwapInfo = transferState.fromTokenInfo val isInsufficientBalance = transferState.isInsufficientBalance @@ -68,10 +67,9 @@ internal class SwapTransferStateBuilder @Inject constructor( val prevAmountField = prevSendCard?.amountField val notifications = notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = feeSelectorUM, feeCryptoCurrencyStatus = feePaidCryptoCurrencyStatus, - fee = fee, actions = actions, - getFeeError = feeError?.error, ) return uiStateHolder.copy( sendCardData = createSendSwapCardState( @@ -344,14 +342,13 @@ internal class SwapTransferStateBuilder @Inject constructor( feePaidCryptoCurrencyStatus: CryptoCurrencyStatus?, fee: Fee?, isTangemPayWithdrawal: Boolean, - feeError: FeeSelectorUM.Error?, + feeSelectorUM: FeeSelectorUM?, ): SwapStateHolder { val notifications = notificationsFactory.getNotifications( transferState = transferState, feeCryptoCurrencyStatus = feePaidCryptoCurrencyStatus, - fee = fee, + feeSelectorUM = feeSelectorUM, actions = actions, - getFeeError = feeError?.error, ) return uiStateHolder.copy( notifications = notifications, diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt index f415d9db37..a8f09a8351 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/transfer/SwapTransferNotificationsFactoryTest.kt @@ -2,6 +2,7 @@ package com.tangem.feature.swap.ui.transfer import com.google.common.truth.Truth.assertThat import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.notifications.NotificationUM import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account @@ -18,8 +19,12 @@ import com.tangem.feature.swap.domain.models.ui.SwapState import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.states.SwapNotificationUM +import com.tangem.features.send.api.entity.CustomFeeFieldUM +import com.tangem.features.send.api.entity.FeeItem +import com.tangem.features.send.api.entity.FeeSelectorUM 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 @@ -43,10 +48,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result).isEmpty() @@ -65,10 +69,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -85,16 +88,13 @@ internal class SwapTransferNotificationsFactoryTest { ), currencyCheck = buildCurrencyCheck(existentialDeposit = BigDecimal("0.5")), ) - val fee: Fee = mockk(relaxed = true) { - every { amount.value } returns BigDecimal("0.4") - } + val feeSelectorUM = contentWithFee(feeValue = BigDecimal("0.4")) val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = feeSelectorUM, feeCryptoCurrencyStatus = null, - fee = fee, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -113,10 +113,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -131,10 +130,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -154,10 +152,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -176,10 +173,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) val reserve = result.filterIsInstance() @@ -199,10 +195,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -220,10 +215,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -242,10 +236,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -264,10 +257,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).isEmpty() @@ -285,10 +277,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).isEmpty() @@ -302,10 +293,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = errorSelector(GetFeeError.UnknownError), feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = null, actions = actions, - getFeeError = GetFeeError.UnknownError, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -319,10 +309,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = errorSelector(GetFeeError.BlockchainErrors.TronActivationError), feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = null, actions = actions, - getFeeError = GetFeeError.BlockchainErrors.TronActivationError, ) val notifications = result.filterIsInstance() @@ -337,10 +326,9 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = errorSelector(GetFeeError.UnknownError), feeCryptoCurrencyStatus = null, - fee = null, actions = actions, - getFeeError = GetFeeError.UnknownError, ) assertThat(result.filterIsInstance()).isEmpty() @@ -353,15 +341,94 @@ internal class SwapTransferNotificationsFactoryTest { val result = sut.getNotifications( transferState = transferState, + feeSelectorUM = null, feeCryptoCurrencyStatus = buildCoinStatus().status, - fee = null, actions = actions, - getFeeError = null, ) assertThat(result.filterIsInstance()).isEmpty() } + @Test + fun `GIVEN custom fee below network minimum WHEN getNotifications THEN FeeTooLow is added`() = runTest { + val transferState = buildTransferState() + val feeSelectorUM = contentWithCustomFeeBelowMinimum( + customFeeValue = "0.0001", + minimumFeeValue = BigDecimal("0.001"), + ) + + val result = sut.getNotifications( + transferState = transferState, + feeSelectorUM = feeSelectorUM, + feeCryptoCurrencyStatus = null, + actions = actions, + ) + + assertThat(result.filterIsInstance()).hasSize(1) + } + + @Test + fun `GIVEN custom fee at network minimum WHEN getNotifications THEN no FeeTooLow`() = runTest { + val transferState = buildTransferState() + val feeSelectorUM = contentWithCustomFeeBelowMinimum( + customFeeValue = "0.001", + minimumFeeValue = BigDecimal("0.001"), + ) + + val result = sut.getNotifications( + transferState = transferState, + feeSelectorUM = feeSelectorUM, + feeCryptoCurrencyStatus = null, + actions = actions, + ) + + assertThat(result.filterIsInstance()).isEmpty() + } + + /** + * Builds a [FeeSelectorUM.Content] with a Custom fee whose [customFeeValue] is below the choosable + * [minimumFeeValue], so + * [com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkIfCustomFeeTooLow] + * reports the fee as too low. The choosable `priority` is left unstubbed (null) so the sibling + * `checkIfCustomFeeTooHigh` short-circuits and does not add a spurious TooHigh notification. + */ + private fun contentWithCustomFeeBelowMinimum( + customFeeValue: String, + minimumFeeValue: BigDecimal, + decimals: Int = 8, + ): FeeSelectorUM.Content { + val customField: CustomFeeFieldUM = mockk(relaxed = true) { + every { value } returns customFeeValue + every { this@mockk.decimals } returns decimals + } + val customFeeItem: FeeItem.Custom = mockk(relaxed = true) { + every { customValues } returns persistentListOf(customField) + } + val choosableFees: TransactionFee.Choosable = mockk(relaxed = true) { + every { minimum.amount.value } returns minimumFeeValue + } + return mockk(relaxed = true) { + every { selectedFeeItem } returns customFeeItem + every { fees } returns choosableFees + } + } + + /** + * Builds a [FeeSelectorUM.Content] whose selected fee carries [feeValue]. A non-Custom fee item is used so + * [com.tangem.features.send.api.subcomponents.feeSelector.utils.FeeCalculationUtils.checkIfCustomFeeTooHigh] + * short-circuits and does not add a spurious TooHigh notification. + */ + private fun contentWithFee(feeValue: BigDecimal): FeeSelectorUM.Content { + val fee: Fee = mockk(relaxed = true) { + every { amount.value } returns feeValue + } + return mockk(relaxed = true) { + every { selectedFeeItem } returns FeeItem.Market(fee = fee) + } + } + + private fun errorSelector(error: GetFeeError): FeeSelectorUM.Error = FeeSelectorUM.Error(error = error) + @Suppress("LongParameterList") private fun buildTransferState( fromTokenInfo: TokenSwapInfo = buildTokenInfo(buildCoinStatus()), @@ -373,6 +440,7 @@ internal class SwapTransferNotificationsFactoryTest { isFeeCoverage: Boolean = false, sendingAmount: BigDecimal = fromTokenInfo.tokenAmount.value, tronFeeNotificationShowCount: Int = 0, + isAmountSubtractAvailable: Boolean = false, ): SwapState.Transfer = SwapState.Transfer( userWallet = coldWallet, fromTokenInfo = fromTokenInfo, @@ -385,6 +453,7 @@ internal class SwapTransferNotificationsFactoryTest { isFeeCoverage = isFeeCoverage, sendingAmount = sendingAmount, tronFeeNotificationShowCount = tronFeeNotificationShowCount, + isAmountSubtractAvailable = isAmountSubtractAvailable, currencyCheck = currencyCheck, validationResult = validationResult, minAdaValue = minAdaValue, 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 64861e80a5..9b0c42f5dd 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 @@ -39,12 +39,14 @@ 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 io.mockk.clearMocks 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.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import java.math.BigDecimal @@ -57,10 +59,9 @@ internal class SwapTransferStateBuilderTest { coEvery { getNotifications( transferState = any(), + feeSelectorUM = any(), feeCryptoCurrencyStatus = any(), - fee = any(), actions = any(), - getFeeError = any(), ) } returns persistentListOf() } @@ -72,6 +73,21 @@ internal class SwapTransferStateBuilderTest { isFeeApproximateUseCase = isFeeApproximateUseCase, ) + // PER_CLASS reuses the notificationsFactory mock across tests, so clear its recorded calls (and re-stub) + // before each test to keep coVerify(exactly = 1) scoped to the current test. + @BeforeEach + fun resetMocks() { + clearMocks(notificationsFactory) + coEvery { + notificationsFactory.getNotifications( + transferState = any(), + feeSelectorUM = any(), + feeCryptoCurrencyStatus = any(), + actions = any(), + ) + } returns persistentListOf() + } + private val userWalletId = UserWalletId(stringValue = "deadbeef") private val coldWallet: UserWallet.Cold = mockk(relaxed = true) { every { walletId } returns userWalletId @@ -123,8 +139,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = uiState, feePaidCryptoCurrencyStatus = null, - fee = null, - feeError = null, + feeSelectorUM = null, ) val portfolioAccount = fromCurrencyStatus.account as Account.CryptoPortfolio @@ -154,10 +169,9 @@ internal class SwapTransferStateBuilderTest { coVerify(exactly = 1) { notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = any(), feeCryptoCurrencyStatus = null, - fee = null, actions = any(), - getFeeError = any(), ) } } @@ -177,8 +191,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = uiState, feePaidCryptoCurrencyStatus = null, - fee = null, - feeError = null, + feeSelectorUM = null, ) val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable @@ -197,10 +210,9 @@ internal class SwapTransferStateBuilderTest { coVerify(exactly = 1) { notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = any(), feeCryptoCurrencyStatus = null, - fee = null, actions = any(), - getFeeError = any(), ) } } @@ -221,8 +233,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = uiState, feePaidCryptoCurrencyStatus = null, - fee = null, - feeError = null, + feeSelectorUM = null, ) val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable @@ -241,10 +252,9 @@ internal class SwapTransferStateBuilderTest { coVerify(exactly = 1) { notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = any(), feeCryptoCurrencyStatus = null, - fee = null, actions = any(), - getFeeError = any(), ) } } @@ -265,8 +275,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = uiState, feePaidCryptoCurrencyStatus = null, - fee = null, - feeError = null, + feeSelectorUM = null, ) val portfolioAccount = toCurrencyStatus.account as Account.CryptoPortfolio @@ -291,10 +300,9 @@ internal class SwapTransferStateBuilderTest { coVerify(exactly = 1) { notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = any(), feeCryptoCurrencyStatus = null, - fee = null, actions = any(), - getFeeError = any(), ) } } @@ -338,10 +346,9 @@ internal class SwapTransferStateBuilderTest { coEvery { notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = any(), feeCryptoCurrencyStatus = null, - fee = fee, actions = any(), - getFeeError = any(), ) } returns persistentListOf() @@ -353,7 +360,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = fee, isTangemPayWithdrawal = false, - feeError = null, + feeSelectorUM = null, ) assertThat(result.swapButton.isEnabled).isTrue() @@ -362,10 +369,9 @@ internal class SwapTransferStateBuilderTest { coVerify(exactly = 1) { notificationsFactory.getNotifications( transferState = transferState, + feeSelectorUM = any(), feeCryptoCurrencyStatus = null, - fee = fee, actions = any(), - getFeeError = any(), ) } } @@ -387,8 +393,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = baseStateHolder(), feePaidCryptoCurrencyStatus = null, - fee = mockk(relaxed = true), - feeError = null, + feeSelectorUM = null, ) val sendCard = result.sendCardData as SwapCardState.SwapCardData @@ -422,8 +427,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = baseStateHolder(), feePaidCryptoCurrencyStatus = null, - fee = null, - feeError = null, + feeSelectorUM = null, ) val receiveCard = result.receiveCardData as SwapCardState.SwapCardData @@ -450,8 +454,7 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, uiStateHolder = baseStateHolder(), feePaidCryptoCurrencyStatus = null, - fee = null, - feeError = null, + feeSelectorUM = null, ) val receiveCard = result.receiveCardData as SwapCardState.SwapCardData @@ -482,7 +485,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = mockk(relaxed = true), isTangemPayWithdrawal = false, - feeError = null, + feeSelectorUM = null, ) val receiveCard = result.receiveCardData as SwapCardState.SwapCardData @@ -517,7 +520,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = fee, isTangemPayWithdrawal = false, - feeError = null, + feeSelectorUM = null, ) assertThat(result.transferFooter).isInstanceOf(TextReference.Combined::class.java) @@ -576,7 +579,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = fee, isTangemPayWithdrawal = false, - feeError = null, + feeSelectorUM = null, ) assertThat(result.transferFooter).isEqualTo( @@ -622,7 +625,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = fee, isTangemPayWithdrawal = false, - feeError = null, + feeSelectorUM = null, ) assertThat(result.transferFooter).isEqualTo( @@ -753,7 +756,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = null, isTangemPayWithdrawal = true, - feeError = null, + feeSelectorUM = null, ) assertThat(result.swapButton.isEnabled).isTrue() @@ -789,7 +792,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = null, isTangemPayWithdrawal = false, - feeError = null, + feeSelectorUM = null, ) assertThat(result.swapButton.isEnabled).isFalse() @@ -939,6 +942,7 @@ internal class SwapTransferStateBuilderTest { isFeeCoverage = isFeeCoverage, sendingAmount = toAmount, tronFeeNotificationShowCount = 0, + isAmountSubtractAvailable = false, isSendingAmountLoading = isSendingAmountLoading, ) }