Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-30 16:06:33 +01:00
parent 50fa269746
commit 4cb147b27e
7 changed files with 345 additions and 10 deletions

View file

@ -515,6 +515,13 @@ internal class SendConfirmModel @Inject constructor(
feeCryptoCurrencyStatus = getCurrencyStatusForFeePayment(),
),
)
val balance = cryptoCurrencyStatus.value.amount.orZero()
val feeValue = confirmData.fee?.amount?.value.orZero()
val isTotalSendingMoreThanBalance = confirmData.enteredAmount.orZero() + feeValue > balance
val isFeeSubtractedFromAmount = isAmountSubtractAvailable && isTotalSendingMoreThanBalance
// Fee alone can't be covered by the balance → nothing can be sent, footer must show $0.
val isFeeExceedingBalance = isAmountSubtractAvailable && feeValue > balance
_uiState.update { state ->
state.copy(
confirmUM = SendConfirmationNotificationsTransformerV2(
@ -524,6 +531,8 @@ internal class SendConfirmModel @Inject constructor(
cryptoCurrency = cryptoCurrencyStatus.currency,
appCurrency = appCurrency,
analyticsCategoryName = params.analyticsCategoryName,
isFeeSubtractedFromAmount = isFeeSubtractedFromAmount,
isFeeExceedingBalance = isFeeExceedingBalance,
).transform(uiState.value.confirmUM),
)
}

View file

@ -21,7 +21,9 @@ import com.tangem.features.send.common.ui.state.ConfirmUM
import com.tangem.features.send.impl.R
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
@Suppress("LongParameterList")
internal class SendConfirmationNotificationsTransformerV2(
private val feeSelectorUM: FeeSelectorUM,
private val amountUM: AmountState,
@ -29,6 +31,8 @@ internal class SendConfirmationNotificationsTransformerV2(
private val cryptoCurrency: CryptoCurrency,
private val appCurrency: AppCurrency,
private val analyticsCategoryName: String,
private val isFeeSubtractedFromAmount: Boolean,
private val isFeeExceedingBalance: Boolean,
) : Transformer<ConfirmUM> {
override fun transform(prevState: ConfirmUM): ConfirmUM {
val state = prevState as? ConfirmUM.Content ?: return prevState
@ -73,10 +77,14 @@ internal class SendConfirmationNotificationsTransformerV2(
val isFeeConvertibleToFiat = feeSelectorUM.feeExtraInfo.isFeeConvertibleToFiat
val fiatSendingValue = if (isFeeConvertibleToFiat) {
fiatFeeValue?.let { fiatAmountValue?.plus(it) }
} else {
fiatAmountValue
val fiatSendingValue = when {
!isFeeConvertibleToFiat -> fiatAmountValue
// Fee alone exceeds the balance → the transaction can't go through, nothing is sent.
isFeeExceedingBalance -> BigDecimal.ZERO
// When the fee is subtracted from the amount, the entered amount is the gross that already
// includes the fee, so it must not be added again — that double-counts it.
isFeeSubtractedFromAmount -> fiatAmountValue
else -> fiatFeeValue?.let { fiatAmountValue?.plus(it) }
}
val fiatSending = fiatSendingValue.format {

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.send.confirm.model.transformers
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.google.common.truth.Truth.assertThat
import com.tangem.blockchain.common.Amount
@ -8,6 +8,11 @@ import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -18,7 +23,9 @@ import com.tangem.features.send.api.entity.FeeFiatRateUM
import com.tangem.features.send.api.entity.FeeItem
import com.tangem.features.send.api.entity.FeeNonce
import com.tangem.features.send.api.entity.FeeSelectorUM
import com.tangem.features.send.api.utils.formatFooterFiatFee
import com.tangem.features.send.common.ui.state.ConfirmUM
import com.tangem.features.send.impl.R
import com.tangem.features.send.send.confirm.model.transformers.SendConfirmationNotificationsTransformerV2
import io.mockk.mockk
import io.mockk.verify
@ -77,6 +84,8 @@ class SendConfirmationNotificationsTransformerV2Test {
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
isFeeSubtractedFromAmount = false,
isFeeExceedingBalance = false,
)
val initialState: ConfirmUM = ConfirmUM.Empty
@ -99,6 +108,8 @@ class SendConfirmationNotificationsTransformerV2Test {
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
isFeeSubtractedFromAmount = false,
isFeeExceedingBalance = false,
)
val initialState = createTestConfirmUM()
@ -121,6 +132,8 @@ class SendConfirmationNotificationsTransformerV2Test {
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
isFeeSubtractedFromAmount = false,
isFeeExceedingBalance = false,
)
val initialState = createTestConfirmUM()
@ -134,6 +147,58 @@ class SendConfirmationNotificationsTransformerV2Test {
assertThat(content.sendingFooter).isNotEqualTo(initialState.sendingFooter)
}
@Test
fun `GIVEN fee subtracted from amount WHEN transform THEN footer sending excludes the fee`() = runTest {
// GIVEN: fee is taken out of the entered amount, so the footer must show the amount alone (not amount + fee).
val feeSelectorUM = createFiatConvertibleFeeSelectorUM(feeValue = BigDecimal("0.001"))
val amountUM = createTestAmountUM()
val transformer = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
isFeeSubtractedFromAmount = true,
isFeeExceedingBalance = false,
)
// WHEN
val result = transformer.transform(createTestConfirmUM())
// THEN: sending = entered fiat amount (50.00), fee NOT added on top.
val content = result as ConfirmUM.Content
assertThat(content.sendingFooter).isEqualTo(
expectedFiatFooter(sendingValue = BigDecimal("50.00"), feeSelectorUM = feeSelectorUM),
)
}
@Test
fun `GIVEN fee exceeds balance WHEN transform THEN footer sending is zero`() = runTest {
// GIVEN: the fee alone exceeds the balance → nothing can be sent.
val feeSelectorUM = createFiatConvertibleFeeSelectorUM(feeValue = BigDecimal("0.001"))
val amountUM = createTestAmountUM()
val transformer = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
isFeeSubtractedFromAmount = true,
isFeeExceedingBalance = true,
)
// WHEN
val result = transformer.transform(createTestConfirmUM())
// THEN: sending = $0.
val content = result as ConfirmUM.Content
assertThat(content.sendingFooter).isEqualTo(
expectedFiatFooter(sendingValue = BigDecimal.ZERO, feeSelectorUM = feeSelectorUM),
)
}
@Test
fun `GIVEN fee too high WHEN transform THEN returns state with too high notification`() = runTest {
// GIVEN
@ -146,6 +211,8 @@ class SendConfirmationNotificationsTransformerV2Test {
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
isFeeSubtractedFromAmount = false,
isFeeExceedingBalance = false,
)
val initialState = createTestConfirmUM()
@ -171,6 +238,8 @@ class SendConfirmationNotificationsTransformerV2Test {
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
isFeeSubtractedFromAmount = false,
isFeeExceedingBalance = false,
)
val initialState = createTestConfirmUM()
@ -197,6 +266,8 @@ class SendConfirmationNotificationsTransformerV2Test {
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
isFeeSubtractedFromAmount = false,
isFeeExceedingBalance = false,
)
val initialState = createTestConfirmUM()
@ -264,6 +335,43 @@ class SendConfirmationNotificationsTransformerV2Test {
)
}
private fun createFiatConvertibleFeeSelectorUM(feeValue: BigDecimal): FeeSelectorUM.Content {
val fee = Fee.Common(amount = Amount(currencySymbol = "SOL", value = feeValue, decimals = 8))
return FeeSelectorUM.Content(
isPrimaryButtonEnabled = true,
fees = TransactionFee.Single(fee),
feeItems = persistentListOf(FeeItem.Market(fee)),
selectedFeeItem = FeeItem.Market(fee),
feeExtraInfo = FeeExtraInfo(
isFeeApproximate = false,
isFeeConvertibleToFiat = true,
isTronToken = false,
feeCryptoCurrencyStatus = cryptoCurrencyStatus,
),
feeFiatRateUM = FeeFiatRateUM(rate = BigDecimal("50000"), appCurrency = appCurrency),
feeNonce = FeeNonce.Nonce(nonce = BigInteger.ZERO, onNonceChange = {}),
)
}
/** Builds the expected footer reference for a fiat-convertible fee, mirroring the transformer's formatting. */
private fun expectedFiatFooter(sendingValue: BigDecimal, feeSelectorUM: FeeSelectorUM.Content): TextReference {
val fee = feeSelectorUM.selectedFeeItem.fee
val fiatFeeValue = fee.amount.value?.multiply(feeSelectorUM.feeFiatRateUM!!.rate)
val sending = sendingValue.format {
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
}
val feeText = formatFooterFiatFee(
amount = fee.amount.copy(value = fiatFeeValue),
isFeeConvertibleToFiat = true,
isFeeApproximate = feeSelectorUM.feeExtraInfo.isFeeApproximate,
appCurrency = appCurrency,
)
return resourceReference(
id = R.string.send_summary_transaction_description,
formatArgs = wrappedList(sending, feeText),
)
}
private fun createNormalFeeSelectorUM(): FeeSelectorUM.Content {
val fee = Fee.Common(
amount = Amount(