Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-25 11:54:27 +01:00
parent 01b7433542
commit e338fb8fdc
18 changed files with 351 additions and 68 deletions

View file

@ -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)

View file

@ -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,

View file

@ -53,4 +53,6 @@ interface SwapTransferInteractor {
cryptoAmount: BigDecimal,
toSwapCurrencyStatus: SwapCurrencyStatus,
): Either<SendTransactionError, WithdrawalResult>
suspend fun incrementTronTokenFeeShowCount(cryptoCurrencyStatus: CryptoCurrencyStatus?)
}

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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?,

View file

@ -49,6 +49,7 @@ internal data class SwapStateHolder(
val onShowPermissionBottomSheet: () -> Unit = {},
val onSwapUIModeChange: (SwapUIMode) -> Unit = {},
val onSwapTypeMenuOpened: () -> Unit = {},
val onTronBannerShown: () -> Unit = {},
)
@Immutable

View file

@ -34,4 +34,5 @@ internal data class UiActions(
val onReceiveCardWarningClick: () -> Unit,
val onSwapUIModeChange: (SwapUIMode) -> Unit,
val onSwapTypeMenuOpened: () -> Unit,
val onTronBannerShown: () -> Unit,
)

View file

@ -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),
)
}
}

View file

@ -122,6 +122,7 @@ internal class StateBuilder(
swapUIMode = swapUIMode,
onSwapUIModeChange = actions.onSwapUIModeChange,
onSwapTypeMenuOpened = actions.onSwapTypeMenuOpened,
onTronBannerShown = actions.onTronBannerShown,
shouldShowAbMenu = swapFeatureToggles.isSwapAbEnabled,
)
}

View file

@ -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<NotificationUM>) {
private fun SwapNotifications(notifications: List<NotificationUM>, 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)

View file

@ -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<NotificationUM> {
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<NotificationUM>.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<NotificationUM>.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
}
}

View file

@ -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,

View file

@ -37,6 +37,7 @@ internal class SwapAmountScreenClickIntentsTest {
onReceiveCardWarningClick = {},
onSwapUIModeChange = {},
onSwapTypeMenuOpened = {},
onTronBannerShown = {},
)
private val sut = SwapAmountScreenClickIntents(actions)

View file

@ -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<NotificationUM.Solana.RentInfo>()).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<NotificationUM.Error.ExistentialDeposit>()).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<NotificationUM.Error.MinimumAmountError>()).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<NotificationUM.Cardano.MinAdaValueCharged>()).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<NotificationUM.Warning.FeeCoverageNotification>()).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<SwapNotificationUM.Warning.NeedReserveToCreateAccount>()
@ -204,9 +201,8 @@ internal class SwapTransferNotificationsFactoryTest {
transferState = transferState,
feeCryptoCurrencyStatus = null,
fee = null,
onReduceByAmount = { _, _ -> },
onReduceToAmount = {},
onBuyClick = {},
actions = actions,
getFeeError = null,
)
assertThat(result.filterIsInstance<SwapNotificationUM.Warning.ReduceAmount>()).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<NotificationUM.Error.TokenExceedsBalance>()).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<SwapNotificationUM.Info.TronTokenFee>()).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<SwapNotificationUM.Info.TronTokenFee>()).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<SwapNotificationUM.Info.TronTokenFee>()).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<NotificationUM.Warning.NetworkFeeUnreachable>()).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<NotificationUM.Warning.TronAccountNotActivated>()
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<NotificationUM.Warning.NetworkFeeUnreachable>()).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<NotificationUM.Warning.NetworkFeeUnreachable>()).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<CryptoCurrency.Token>(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
}
}

View file

@ -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,
)
}