From e338fb8fdcadb97ef6c66d273735185fb86d26e4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 25 Jun 2026 11:54:27 +0100 Subject: [PATCH] Updated on 2026-08-14 --- features/swap/domain/build.gradle.kts | 1 + .../swap/domain/models/ui/SwapState.kt | 1 + .../domain/transfer/SwapTransferInteractor.kt | 2 + .../transfer/SwapTransferInteractorImpl.kt | 12 + .../SwapTransferInteractorImplTest.kt | 8 + .../tangem/feature/swap/model/SwapModel.kt | 15 +- .../feature/swap/models/SwapStateHolder.kt | 1 + .../tangem/feature/swap/models/UiActions.kt | 1 + .../swap/models/states/SwapNotificationUM.kt | 5 + .../tangem/feature/swap/ui/StateBuilder.kt | 1 + .../feature/swap/ui/SwapScreenContent.kt | 15 +- .../SwapTransferNotificationsFactory.kt | 60 ++++- .../ui/transfer/SwapTransferStateBuilder.kt | 22 +- .../feature/swap/StateBuilderQuotesTest.kt | 0 .../feature/swap/StateBuilderSwapDataTest.kt | 0 .../ui/SwapAmountScreenClickIntentsTest.kt | 1 + .../SwapTransferNotificationsFactoryTest.kt | 211 +++++++++++++++--- .../transfer/SwapTransferStateBuilderTest.kt | 63 ++++-- 18 files changed, 351 insertions(+), 68 deletions(-) delete mode 100644 features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderQuotesTest.kt delete mode 100644 features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index 13530ac93f..d8f2337725 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -47,6 +47,7 @@ dependencies { implementation(projects.domain.visa.models) implementation(projects.domain.balanceHiding) implementation(projects.domain.yieldSupply) + implementation(projects.domain.notifications) /** Common modules */ implementation(projects.common.ui) 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 a1759b9e86..209108a216 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 @@ -54,6 +54,7 @@ sealed interface SwapState { val isAccountsMode: Boolean, val isFeeCoverage: Boolean, val sendingAmount: BigDecimal, + val tronFeeNotificationShowCount: Int, 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/SwapTransferInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractor.kt index fc42b91521..8c4476d24c 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/transfer/SwapTransferInteractor.kt @@ -53,4 +53,6 @@ interface SwapTransferInteractor { cryptoAmount: BigDecimal, toSwapCurrencyStatus: SwapCurrencyStatus, ): Either + + suspend fun incrementTronTokenFeeShowCount(cryptoCurrencyStatus: CryptoCurrencyStatus?) } \ No newline at end of file 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 03f6d275be..ac06572551 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 @@ -19,6 +19,8 @@ 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.notifications.GetTronFeeNotificationShowCountUseCase +import com.tangem.domain.notifications.IncrementNotificationsShowCountUseCase import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.swap.models.SwapCurrencyStatus import com.tangem.domain.tangempay.TangemPayWithdrawUseCase @@ -62,6 +64,8 @@ class SwapTransferInteractorImpl @Inject constructor( private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase, private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase, + private val getTronFeeNotificationShowCountUseCase: GetTronFeeNotificationShowCountUseCase, + private val incrementNotificationsShowCountUseCase: IncrementNotificationsShowCountUseCase, ) : SwapTransferInteractor { override suspend fun updateTransfer( @@ -120,6 +124,7 @@ class SwapTransferInteractorImpl @Inject constructor( feeStatus = feeStatus, ) } + val tronFeeNotificationShowCount = getTronFeeNotificationShowCountUseCase() return SwapState.Transfer( userWallet = userWallet, fromTokenInfo = fromTokenInfo, @@ -131,6 +136,7 @@ class SwapTransferInteractorImpl @Inject constructor( isAccountsMode = isAccountsMode, isFeeCoverage = coverageState.isFeeCoverage, sendingAmount = coverageState.sendingAmount, + tronFeeNotificationShowCount = tronFeeNotificationShowCount, isSendingAmountLoading = coverageState.isSendingAmountLoading, currencyCheck = currencyCheck, ) @@ -390,4 +396,10 @@ class SwapTransferInteractorImpl @Inject constructor( private fun SwapCurrencyStatus.destinationAddress(): String? { return status.value.networkAddress?.defaultAddress?.value } + + override suspend fun incrementTronTokenFeeShowCount(cryptoCurrencyStatus: CryptoCurrencyStatus?) { + cryptoCurrencyStatus?.currency?.let { cryptoCurrency -> + incrementNotificationsShowCountUseCase(cryptoCurrency) + } + } } \ No newline at end of file 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 cdecb5a391..b140e07270 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 @@ -16,6 +16,8 @@ import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.notifications.GetTronFeeNotificationShowCountUseCase +import com.tangem.domain.notifications.IncrementNotificationsShowCountUseCase import com.tangem.domain.pay.WithdrawalResult import com.tangem.domain.swap.models.SwapCurrencyStatus import com.tangem.domain.tangempay.TangemPayWithdrawUseCase @@ -61,6 +63,8 @@ internal class SwapTransferInteractorImplTest { private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase = mockk() private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase = mockk() private val getBalanceNotEnoughForFeeWarningUseCase: GetBalanceNotEnoughForFeeWarningUseCase = mockk(relaxed = true) + private val getTronFeeNotificationShowCountUseCase: GetTronFeeNotificationShowCountUseCase = mockk(relaxed = true) + private val incrementNotificationsShowCountUseCase: IncrementNotificationsShowCountUseCase = mockk(relaxed = true) private val sut = SwapTransferInteractorImpl( swapFeatureToggles = swapFeatureToggles, @@ -76,6 +80,8 @@ internal class SwapTransferInteractorImplTest { isAmountSubtractAvailableUseCase = isAmountSubtractAvailableUseCase, tangemPayWithdrawUseCase = tangemPayWithdrawUseCase, getBalanceNotEnoughForFeeWarningUseCase = getBalanceNotEnoughForFeeWarningUseCase, + getTronFeeNotificationShowCountUseCase = getTronFeeNotificationShowCountUseCase, + incrementNotificationsShowCountUseCase = incrementNotificationsShowCountUseCase, ) @AfterEach @@ -180,6 +186,7 @@ internal class SwapTransferInteractorImplTest { isAccountsMode = true, isFeeCoverage = false, sendingAmount = expectedAmount, + tronFeeNotificationShowCount = 0, currencyCheck = currencyCheck, ) assertThat(result).isEqualTo(expected) @@ -251,6 +258,7 @@ internal class SwapTransferInteractorImplTest { isAccountsMode = true, isFeeCoverage = false, sendingAmount = expectedAmount, + tronFeeNotificationShowCount = 0, 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 c1b113f3c2..ddd139e87f 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,6 +822,7 @@ internal class SwapModel @Inject constructor( uiStateHolder = uiState, feePaidCryptoCurrencyStatus = feePaidCryptoCurrency, fee = selectedFee, + feeError = feeSelectorRepository.state.value as? FeeSelectorUM.Error, ) when { uiState.successState != null -> Unit @@ -866,9 +867,10 @@ internal class SwapModel @Inject constructor( transferState = refreshed, actions = actions, uiStateHolder = uiState, - feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatus, + feePaidCryptoCurrencyStatus = feePaidCryptoCurrencyStatus ?: dataState.feePaidCryptoCurrency, fee = fee, isTangemPayWithdrawal = isTangemPayWithdrawal(), + feeError = feeSelectorRepository.state.value as? FeeSelectorUM.Error, ) } } @@ -2105,6 +2107,7 @@ internal class SwapModel @Inject constructor( }, onSwapUIModeChange = ::onSwapUIModeChange, onSwapTypeMenuOpened = ::onSwapTypeMenuOpened, + onTronBannerShown = ::incrementTronTokenFeeShowCount, ) } @@ -2132,6 +2135,16 @@ internal class SwapModel @Inject constructor( ) } + private fun incrementTronTokenFeeShowCount() { + // Fired once per banner appearance from the UI (tied to the banner's composition lifecycle), + // so the show-count advances per appearance rather than on every transfer-state rebuild. + modelScope.launch { + swapTransferInteractor.incrementTronTokenFeeShowCount( + cryptoCurrencyStatus = dataState.fromSwapCurrencyStatus?.status, + ) + } + } + private fun selectWalletInSelector( fromSwapCurrencyStatus: SwapCurrencyStatus?, toSwapCurrencyStatus: SwapCurrencyStatus?, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 65ae024ca4..ad200c6f48 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -49,6 +49,7 @@ internal data class SwapStateHolder( val onShowPermissionBottomSheet: () -> Unit = {}, val onSwapUIModeChange: (SwapUIMode) -> Unit = {}, val onSwapTypeMenuOpened: () -> Unit = {}, + val onTronBannerShown: () -> Unit = {}, ) @Immutable diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 97fe358356..1e01c3a4aa 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -34,4 +34,5 @@ internal data class UiActions( val onReceiveCardWarningClick: () -> Unit, val onSwapUIModeChange: (SwapUIMode) -> Unit, val onSwapTypeMenuOpened: () -> Unit, + val onTronBannerShown: () -> Unit, ) \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt index 0769f7b5b7..a195f9bd4a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt @@ -251,5 +251,10 @@ internal object SwapNotificationUM { onClick = onApproveClick, ), ) + + data object TronTokenFee : Info( + title = resourceReference(R.string.tron_will_be_send_token_fee_title), + subtitle = resourceReference(R.string.tron_will_be_send_token_fee_description), + ) } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index a7fe90f04a..726e1c5a5b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -122,6 +122,7 @@ internal class StateBuilder( swapUIMode = swapUIMode, onSwapUIModeChange = actions.onSwapUIModeChange, onSwapTypeMenuOpened = actions.onSwapTypeMenuOpened, + onTronBannerShown = actions.onTronBannerShown, shouldShowAbMenu = swapFeatureToggles.isSwapAbEnabled, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index de4b81df32..4f15adb4cd 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -94,7 +94,12 @@ internal fun SwapScreenContent( feeBlock?.invoke(Modifier.fillMaxWidth()) - if (state.notifications.isNotEmpty()) SwapNotifications(notifications = state.notifications) + if (state.notifications.isNotEmpty()) { + SwapNotifications( + notifications = state.notifications, + onTronBannerShown = state.onTronBannerShown, + ) + } SpacerHMax() @@ -342,7 +347,13 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable -private fun SwapNotifications(notifications: List) { +private fun SwapNotifications(notifications: List, onTronBannerShown: () -> Unit) { + // The Tron token-fee banner's show-count is an "impression": tied to actual on-screen visibility. + // LaunchedEffect re-arms only when the boolean flips, so it fires once per hidden -> shown appearance. + val isTronBannerShown = notifications.any { it is SwapNotificationUM.Info.TronTokenFee } + LaunchedEffect(isTronBannerShown) { + if (isTronBannerShown) onTronBannerShown() + } Column( modifier = Modifier .background(color = TangemTheme.colors.background.secondary) 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 5ae7859709..9508fd1e4b 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 @@ -6,18 +6,22 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addDustWarningNot import com.tangem.common.ui.notifications.NotificationsFactory.addExceedsBalanceNotification import com.tangem.common.ui.notifications.NotificationsFactory.addExistentialWarningNotification import com.tangem.common.ui.notifications.NotificationsFactory.addFeeCoverageNotification +import com.tangem.common.ui.notifications.NotificationsFactory.addFeeUnreachableNotification 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.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError 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.lib.crypto.BlockchainUtils import com.tangem.lib.crypto.BlockchainUtils.getTezosThreshold import com.tangem.lib.crypto.BlockchainUtils.isTezos +import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList @@ -31,9 +35,8 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { transferState: SwapState.Transfer, feeCryptoCurrencyStatus: CryptoCurrencyStatus?, fee: Fee?, - onReduceByAmount: (SwapAmount, BigDecimal) -> Unit, - onReduceToAmount: (SwapAmount) -> Unit, - onBuyClick: (CryptoCurrency) -> Unit, + actions: UiActions, + getFeeError: GetFeeError?, ): ImmutableList { return buildList { maybeAddRentExemptionError(transferState) @@ -41,11 +44,21 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { state = transferState, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = fee, - onReduceByAmount = onReduceByAmount, - onReduceToAmount = onReduceToAmount, + onReduceByAmount = actions.onReduceByAmount, + onReduceToAmount = actions.onReduceToAmount, ) maybeAddNeedReserveToCreateAccountWarning(transferState) - maybeAddExceedsBalanceNotification(transferState, onBuyClick) + maybeAddExceedsBalanceNotification(transferState, onBuyClick = actions.openTokenDetailsScreen) + addTronNetworkFeesNotification( + cryptoCurrencyStatus = transferState.fromTokenInfo.swapCurrencyStatus.status, + transferState = transferState, + ) + maybeAddFeeUnreachableNotification( + transferState = transferState, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + feeError = getFeeError, + actions = actions, + ) }.toPersistentList() } @@ -194,4 +207,39 @@ internal class SwapTransferNotificationsFactory @Inject constructor() { onResetAnalyticsEvent = {}, ) } + + private fun MutableList.addTronNetworkFeesNotification( + cryptoCurrencyStatus: CryptoCurrencyStatus, + transferState: SwapState.Transfer, + ) { + val cryptoCurrency = cryptoCurrencyStatus.currency + val isTronToken = cryptoCurrency is CryptoCurrency.Token && isTron(cryptoCurrency.network.rawId) + val isVisible = isTronToken && + transferState.tronFeeNotificationShowCount <= TRON_FEE_NOTIFICATION_MAX_SHOW_COUNT + + if (isVisible) { + add(SwapNotificationUM.Info.TronTokenFee) + } + } + + private fun MutableList.maybeAddFeeUnreachableNotification( + transferState: SwapState.Transfer, + feeCryptoCurrencyStatus: CryptoCurrencyStatus?, + feeError: GetFeeError?, + actions: UiActions, + ) { + feeCryptoCurrencyStatus ?: return + addFeeUnreachableNotification( + tokenStatus = transferState.fromTokenInfo.swapCurrencyStatus.status, + coinStatus = feeCryptoCurrencyStatus, + feeError = feeError, + dustValue = transferState.currencyCheck?.dustValue, + onReload = actions.onRetryClick, + onClick = actions.openTokenDetailsScreen, + ) + } + + companion object { + private const val TRON_FEE_NOTIFICATION_MAX_SHOW_COUNT = 3 + } } \ No newline at end of file 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 10a023634f..96b938cd3b 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 @@ -37,6 +37,7 @@ import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.ui.SwapAmountScreenClickIntents import com.tangem.feature.swap.ui.swapSuccessNavigation +import com.tangem.features.send.api.entity.FeeSelectorUM import com.tangem.features.send.api.utils.formatFooterFiatFee import com.tangem.features.send.api.utils.getTronTokenFeeSendingText import com.tangem.utils.extensions.orZero @@ -52,12 +53,14 @@ internal class SwapTransferStateBuilder @Inject constructor( private val iconConverter by lazy(::CryptoCurrencyToIconStateConverter) + @Suppress("LongParameterList") fun createTransferState( actions: UiActions, transferState: SwapState.Transfer, uiStateHolder: SwapStateHolder, feePaidCryptoCurrencyStatus: CryptoCurrencyStatus?, fee: Fee?, + feeError: FeeSelectorUM.Error?, ): SwapStateHolder { val fromTokenSwapInfo = transferState.fromTokenInfo val isInsufficientBalance = transferState.isInsufficientBalance @@ -67,9 +70,8 @@ internal class SwapTransferStateBuilder @Inject constructor( transferState = transferState, feeCryptoCurrencyStatus = feePaidCryptoCurrencyStatus, fee = fee, - onBuyClick = actions.openTokenDetailsScreen, - onReduceByAmount = actions.onReduceByAmount, - onReduceToAmount = actions.onReduceToAmount, + actions = actions, + getFeeError = feeError?.error, ) return uiStateHolder.copy( sendCardData = createSendSwapCardState( @@ -342,14 +344,14 @@ internal class SwapTransferStateBuilder @Inject constructor( feePaidCryptoCurrencyStatus: CryptoCurrencyStatus?, fee: Fee?, isTangemPayWithdrawal: Boolean, + feeError: FeeSelectorUM.Error?, ): SwapStateHolder { val notifications = notificationsFactory.getNotifications( transferState = transferState, feeCryptoCurrencyStatus = feePaidCryptoCurrencyStatus, fee = fee, - onBuyClick = actions.openTokenDetailsScreen, - onReduceByAmount = actions.onReduceByAmount, - onReduceToAmount = actions.onReduceToAmount, + actions = actions, + getFeeError = feeError?.error, ) return uiStateHolder.copy( notifications = notifications, @@ -395,7 +397,8 @@ internal class SwapTransferStateBuilder @Inject constructor( val fiatAmountValue = tokenSwapInfo.amountFiat val status = dataState.fromSwapCurrencyStatus?.status ?: return null - val fiatFeeValue = fee.amount.value + val value = dataState.feePaidCryptoCurrency?.value + val fiatFeeValue = value?.fiatRate?.multiply(fee.amount.value) val isFeeConvertibleToFiat = status.currency.network.hasFiatFeeRate val fiatSendingValue = if (isFeeConvertibleToFiat) { @@ -412,8 +415,11 @@ internal class SwapTransferStateBuilder @Inject constructor( } val networkId = status.currency.network.id + // When the fee is convertible to fiat, show the fiat-converted value; otherwise keep the raw + // crypto fee amount — formatFooterFiatFee renders amount.value as crypto in the non-fiat case. + val feeAmount = if (isFeeConvertibleToFiat) fee.amount.copy(value = fiatFeeValue) else fee.amount val fiatFee = formatFooterFiatFee( - amount = fee.amount.copy(value = fiatFeeValue), + amount = feeAmount, isFeeConvertibleToFiat = isFeeConvertibleToFiat, isFeeApproximate = isFeeApproximateUseCase(networkId = networkId, amountType = fee.amount.type), appCurrency = appCurrency, diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderQuotesTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderQuotesTest.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/SwapAmountScreenClickIntentsTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/SwapAmountScreenClickIntentsTest.kt index 90da2f7079..ffaf87dce5 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/SwapAmountScreenClickIntentsTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/ui/SwapAmountScreenClickIntentsTest.kt @@ -37,6 +37,7 @@ internal class SwapAmountScreenClickIntentsTest { onReceiveCardWarningClick = {}, onSwapUIModeChange = {}, onSwapTypeMenuOpened = {}, + onTronBannerShown = {}, ) private val sut = SwapAmountScreenClickIntents(actions) 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 f541df7896..f415d9db37 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 @@ -12,9 +12,11 @@ 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.domain.transaction.error.GetFeeError 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.UiActions import com.tangem.feature.swap.models.states.SwapNotificationUM import io.mockk.every import io.mockk.mockk @@ -28,6 +30,8 @@ internal class SwapTransferNotificationsFactoryTest { private val sut = SwapTransferNotificationsFactory() + private val actions: UiActions = mockk(relaxed = true) + private val userWalletId = UserWalletId(stringValue = "deadbeef") private val coldWallet: UserWallet.Cold = mockk(relaxed = true) { every { walletId } returns userWalletId @@ -41,9 +45,8 @@ internal class SwapTransferNotificationsFactoryTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = null, - onReduceByAmount = { _, _ -> }, - onReduceToAmount = {}, - onBuyClick = {}, + actions = actions, + getFeeError = null, ) assertThat(result).isEmpty() @@ -64,9 +67,8 @@ internal class SwapTransferNotificationsFactoryTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = null, - onReduceByAmount = { _, _ -> }, - onReduceToAmount = {}, - onBuyClick = {}, + actions = actions, + getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -91,9 +93,8 @@ internal class SwapTransferNotificationsFactoryTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = fee, - onReduceByAmount = { _, _ -> }, - onReduceToAmount = {}, - onBuyClick = {}, + actions = actions, + getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -114,9 +115,8 @@ internal class SwapTransferNotificationsFactoryTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = null, - onReduceByAmount = { _, _ -> }, - onReduceToAmount = {}, - onBuyClick = {}, + actions = actions, + getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -133,9 +133,8 @@ internal class SwapTransferNotificationsFactoryTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = null, - onReduceByAmount = { _, _ -> }, - onReduceToAmount = {}, - onBuyClick = {}, + actions = actions, + getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -157,9 +156,8 @@ internal class SwapTransferNotificationsFactoryTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = null, - onReduceByAmount = { _, _ -> }, - onReduceToAmount = {}, - onBuyClick = {}, + actions = actions, + getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -180,9 +178,8 @@ internal class SwapTransferNotificationsFactoryTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = null, - onReduceByAmount = { _, _ -> }, - onReduceToAmount = {}, - onBuyClick = {}, + actions = actions, + getFeeError = null, ) val reserve = result.filterIsInstance() @@ -204,9 +201,8 @@ internal class SwapTransferNotificationsFactoryTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = null, - onReduceByAmount = { _, _ -> }, - onReduceToAmount = {}, - onBuyClick = {}, + actions = actions, + getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) @@ -226,14 +222,146 @@ internal class SwapTransferNotificationsFactoryTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = null, - onReduceByAmount = { _, _ -> }, - onReduceToAmount = {}, - onBuyClick = {}, + actions = actions, + getFeeError = null, ) assertThat(result.filterIsInstance()).hasSize(1) } + @Test + fun `GIVEN Tron token and show count within limit WHEN getNotifications THEN Tron network fees Info is added`() = + runTest { + val transferState = buildTransferState( + fromTokenInfo = buildTokenInfo( + swapCurrencyStatus = buildTronTokenStatus(), + amount = BigDecimal("10"), + ), + tronFeeNotificationShowCount = TRON_FEE_NOTIFICATION_MAX_SHOW_COUNT, + ) + + val result = sut.getNotifications( + transferState = transferState, + feeCryptoCurrencyStatus = null, + fee = null, + actions = actions, + getFeeError = null, + ) + + assertThat(result.filterIsInstance()).hasSize(1) + } + + @Test + fun `GIVEN Tron token but show count exceeds limit WHEN getNotifications THEN no Tron network fees Info`() = + runTest { + val transferState = buildTransferState( + fromTokenInfo = buildTokenInfo( + swapCurrencyStatus = buildTronTokenStatus(), + amount = BigDecimal("10"), + ), + tronFeeNotificationShowCount = TRON_FEE_NOTIFICATION_MAX_SHOW_COUNT + 1, + ) + + val result = sut.getNotifications( + transferState = transferState, + feeCryptoCurrencyStatus = null, + fee = null, + actions = actions, + getFeeError = null, + ) + + assertThat(result.filterIsInstance()).isEmpty() + } + + @Test + fun `GIVEN Tron coin (not token) WHEN getNotifications THEN no Tron network fees Info`() = runTest { + val transferState = buildTransferState( + fromTokenInfo = buildTokenInfo( + swapCurrencyStatus = buildCoinStatus(rawNetworkId = "tron"), + amount = BigDecimal("10"), + ), + tronFeeNotificationShowCount = 0, + ) + + val result = sut.getNotifications( + transferState = transferState, + feeCryptoCurrencyStatus = null, + fee = null, + actions = actions, + getFeeError = null, + ) + + assertThat(result.filterIsInstance()).isEmpty() + } + + @Test + fun `GIVEN UnknownError fee error and fee currency status WHEN getNotifications THEN NetworkFeeUnreachable is added`() = + runTest { + val transferState = buildTransferState() + val feeCryptoCurrencyStatus = buildCoinStatus().status + + val result = sut.getNotifications( + transferState = transferState, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + fee = null, + actions = actions, + getFeeError = GetFeeError.UnknownError, + ) + + assertThat(result.filterIsInstance()).hasSize(1) + } + + @Test + fun `GIVEN TronActivationError fee error and fee currency status WHEN getNotifications THEN TronAccountNotActivated is added`() = + runTest { + val transferState = buildTransferState() + val feeCryptoCurrencyStatus = buildCoinStatus().status + + val result = sut.getNotifications( + transferState = transferState, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + fee = null, + actions = actions, + getFeeError = GetFeeError.BlockchainErrors.TronActivationError, + ) + + val notifications = result.filterIsInstance() + assertThat(notifications).hasSize(1) + assertThat(notifications.first().tokenName).isEqualTo(feeCryptoCurrencyStatus.currency.name) + } + + @Test + fun `GIVEN fee error but null fee currency status WHEN getNotifications THEN no fee unreachable notification`() = + runTest { + val transferState = buildTransferState() + + val result = sut.getNotifications( + transferState = transferState, + feeCryptoCurrencyStatus = null, + fee = null, + actions = actions, + getFeeError = GetFeeError.UnknownError, + ) + + assertThat(result.filterIsInstance()).isEmpty() + } + + @Test + fun `GIVEN null fee error and fee currency status WHEN getNotifications THEN no fee unreachable notification`() = + runTest { + val transferState = buildTransferState() + + val result = sut.getNotifications( + transferState = transferState, + feeCryptoCurrencyStatus = buildCoinStatus().status, + fee = null, + actions = actions, + getFeeError = null, + ) + + assertThat(result.filterIsInstance()).isEmpty() + } + @Suppress("LongParameterList") private fun buildTransferState( fromTokenInfo: TokenSwapInfo = buildTokenInfo(buildCoinStatus()), @@ -244,6 +372,7 @@ internal class SwapTransferNotificationsFactoryTest { minAdaValue: BigDecimal? = null, isFeeCoverage: Boolean = false, sendingAmount: BigDecimal = fromTokenInfo.tokenAmount.value, + tronFeeNotificationShowCount: Int = 0, ): SwapState.Transfer = SwapState.Transfer( userWallet = coldWallet, fromTokenInfo = fromTokenInfo, @@ -255,6 +384,7 @@ internal class SwapTransferNotificationsFactoryTest { isAccountsMode = false, isFeeCoverage = isFeeCoverage, sendingAmount = sendingAmount, + tronFeeNotificationShowCount = tronFeeNotificationShowCount, currencyCheck = currencyCheck, validationResult = validationResult, minAdaValue = minAdaValue, @@ -326,4 +456,31 @@ internal class SwapTransferNotificationsFactoryTest { every { decimals } returns 18 } } + + private fun buildTronTokenStatus(): SwapCurrencyStatus { + val token = mockk(relaxed = true) { + every { id } returns mockk(relaxed = true) + every { network } returns mockk(relaxed = true) { + every { rawId } returns "tron" + every { name } returns "Tron" + } + every { name } returns "Tether" + every { symbol } returns "USDT" + every { decimals } returns 6 + } + val statusValue: CryptoCurrencyStatus.Loaded = mockk(relaxed = true) { + every { amount } returns BigDecimal("100") + every { fiatRate } returns BigDecimal.ONE + every { fiatAmount } returns BigDecimal("100") + } + return SwapCurrencyStatus( + userWallet = coldWallet, + status = CryptoCurrencyStatus(currency = token, value = statusValue), + account = Account.CryptoPortfolio.createMainAccount(userWalletId), + ) + } + + private companion object { + const val TRON_FEE_NOTIFICATION_MAX_SHOW_COUNT = 3 + } } \ No newline at end of file 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 a5d4203e98..64861e80a5 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 @@ -59,9 +59,8 @@ internal class SwapTransferStateBuilderTest { transferState = any(), feeCryptoCurrencyStatus = any(), fee = any(), - onReduceByAmount = any(), - onReduceToAmount = any(), - onBuyClick = any(), + actions = any(), + getFeeError = any(), ) } returns persistentListOf() } @@ -125,6 +124,7 @@ internal class SwapTransferStateBuilderTest { uiStateHolder = uiState, feePaidCryptoCurrencyStatus = null, fee = null, + feeError = null, ) val portfolioAccount = fromCurrencyStatus.account as Account.CryptoPortfolio @@ -156,9 +156,8 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = null, - onReduceByAmount = any(), - onReduceToAmount = any(), - onBuyClick = any(), + actions = any(), + getFeeError = any(), ) } } @@ -179,6 +178,7 @@ internal class SwapTransferStateBuilderTest { uiStateHolder = uiState, feePaidCryptoCurrencyStatus = null, fee = null, + feeError = null, ) val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable @@ -199,9 +199,8 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = null, - onReduceByAmount = any(), - onReduceToAmount = any(), - onBuyClick = any(), + actions = any(), + getFeeError = any(), ) } } @@ -223,6 +222,7 @@ internal class SwapTransferStateBuilderTest { uiStateHolder = uiState, feePaidCryptoCurrencyStatus = null, fee = null, + feeError = null, ) val sendType = (result.sendCardData as SwapCardState.SwapCardData).type as TransactionCardType.Inputtable @@ -243,9 +243,8 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = null, - onReduceByAmount = any(), - onReduceToAmount = any(), - onBuyClick = any(), + actions = any(), + getFeeError = any(), ) } } @@ -267,6 +266,7 @@ internal class SwapTransferStateBuilderTest { uiStateHolder = uiState, feePaidCryptoCurrencyStatus = null, fee = null, + feeError = null, ) val portfolioAccount = toCurrencyStatus.account as Account.CryptoPortfolio @@ -293,9 +293,8 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = null, - onReduceByAmount = any(), - onReduceToAmount = any(), - onBuyClick = any(), + actions = any(), + getFeeError = any(), ) } } @@ -341,9 +340,8 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = fee, - onReduceByAmount = any(), - onReduceToAmount = any(), - onBuyClick = any(), + actions = any(), + getFeeError = any(), ) } returns persistentListOf() @@ -355,6 +353,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = fee, isTangemPayWithdrawal = false, + feeError = null, ) assertThat(result.swapButton.isEnabled).isTrue() @@ -365,9 +364,8 @@ internal class SwapTransferStateBuilderTest { transferState = transferState, feeCryptoCurrencyStatus = null, fee = fee, - onReduceByAmount = any(), - onReduceToAmount = any(), - onBuyClick = any(), + actions = any(), + getFeeError = any(), ) } } @@ -390,6 +388,7 @@ internal class SwapTransferStateBuilderTest { uiStateHolder = baseStateHolder(), feePaidCryptoCurrencyStatus = null, fee = mockk(relaxed = true), + feeError = null, ) val sendCard = result.sendCardData as SwapCardState.SwapCardData @@ -424,6 +423,7 @@ internal class SwapTransferStateBuilderTest { uiStateHolder = baseStateHolder(), feePaidCryptoCurrencyStatus = null, fee = null, + feeError = null, ) val receiveCard = result.receiveCardData as SwapCardState.SwapCardData @@ -451,6 +451,7 @@ internal class SwapTransferStateBuilderTest { uiStateHolder = baseStateHolder(), feePaidCryptoCurrencyStatus = null, fee = null, + feeError = null, ) val receiveCard = result.receiveCardData as SwapCardState.SwapCardData @@ -481,6 +482,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = mockk(relaxed = true), isTangemPayWithdrawal = false, + feeError = null, ) val receiveCard = result.receiveCardData as SwapCardState.SwapCardData @@ -515,6 +517,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = fee, isTangemPayWithdrawal = false, + feeError = null, ) assertThat(result.transferFooter).isInstanceOf(TextReference.Combined::class.java) @@ -538,20 +541,27 @@ internal class SwapTransferStateBuilderTest { isAccountsMode = false, ) val statusWithNetwork = buildStatusWithNetwork(hasFiatFeeRate = true) - val dataState = SwapProcessDataState(fromSwapCurrencyStatus = statusWithNetwork) + // The fee's fiat value is derived from the fee-paid currency's fiat rate, not the raw crypto fee. + val feePaidStatus = buildSwapCurrencyStatus(coldWallet) + val feePaidRate = feePaidStatus.status.value.fiatRate!! + val dataState = SwapProcessDataState( + fromSwapCurrencyStatus = statusWithNetwork, + feePaidCryptoCurrency = feePaidStatus.status, + ) val feeValue = BigDecimal("0.001") val fee = Fee.Common( amount = Amount(currencySymbol = "ETH", value = feeValue, decimals = 18), ) val uiState = baseStateHolder() val appCurrency = transferState.appCurrency - val expectedFiatSending = (fromAmount * QUOTE).plus(feeValue).format { + val fiatFeeValue = feePaidRate.multiply(feeValue) + val expectedFiatSending = (fromAmount * QUOTE).plus(fiatFeeValue).format { fiat( fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, ) } - val expectedFiatFee = feeValue.format { + val expectedFiatFee = fiatFeeValue.format { fiat( fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, @@ -566,6 +576,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = fee, isTangemPayWithdrawal = false, + feeError = null, ) assertThat(result.transferFooter).isEqualTo( @@ -611,6 +622,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = fee, isTangemPayWithdrawal = false, + feeError = null, ) assertThat(result.transferFooter).isEqualTo( @@ -741,6 +753,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = null, isTangemPayWithdrawal = true, + feeError = null, ) assertThat(result.swapButton.isEnabled).isTrue() @@ -776,6 +789,7 @@ internal class SwapTransferStateBuilderTest { feePaidCryptoCurrencyStatus = null, fee = null, isTangemPayWithdrawal = false, + feeError = null, ) assertThat(result.swapButton.isEnabled).isFalse() @@ -924,6 +938,7 @@ internal class SwapTransferStateBuilderTest { isAccountsMode = isAccountsMode, isFeeCoverage = isFeeCoverage, sendingAmount = toAmount, + tronFeeNotificationShowCount = 0, isSendingAmountLoading = isSendingAmountLoading, ) }