Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-14 14:27:34 +05:00
parent c7792de43b
commit ebc4350919
9 changed files with 2218 additions and 8 deletions

View file

@ -8,6 +8,10 @@ android {
namespace = "com.tangem.features.send.v2.api"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Core */
implementation(projects.core.decompose)
@ -34,4 +38,14 @@ dependencies {
/** Other */
implementation(deps.arrow.core)
implementation(deps.kotlin.immutable.collections)
// region Tests
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
testImplementation(projects.common.test)
testImplementation(projects.domain.staking.models)
// endregion
}

View file

@ -0,0 +1,99 @@
package com.tangem.features.send.v2.api.subcomponents.feeSelector.utils
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.send.v2.api.entity.FeeItem
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.utils.extensions.isZero
import java.math.BigDecimal
import java.math.RoundingMode
object FeeCalculationUtils {
private val FEE_MAX_DIFF = BigDecimal("5")
/**
* Check and calculates subtracted amount
*/
fun checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable: Boolean,
cryptoCurrencyStatus: CryptoCurrencyStatus,
amountValue: BigDecimal,
feeValue: BigDecimal,
reduceAmountBy: BigDecimal,
): BigDecimal {
val balance = cryptoCurrencyStatus.value.amount ?: return amountValue
val isFeeCoverage = checkFeeCoverage(
isSubtractAvailable = isAmountSubtractAvailable,
balance = balance,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
)
return if (isFeeCoverage) {
balance.minus(reduceAmountBy).minus(feeValue)
} else {
amountValue
}
}
/**
* Check if custom fee is too high
*/
fun checkIfCustomFeeTooHigh(feeSelectorUM: FeeSelectorUM.Content): Pair<Boolean, String> {
val defaultResult = false to ""
if (feeSelectorUM.selectedFeeItem !is FeeItem.Custom) return defaultResult
val customAmount = feeSelectorUM.selectedFeeItem.customValues.firstOrNull() ?: return defaultResult
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return defaultResult
val highValue = multipleFees.priority.amount.value ?: return defaultResult
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
val diff = if (highValue > BigDecimal.ZERO) {
customValue / highValue
} else {
BigDecimal.ZERO
}
val isFeeTooHigh = diff > FEE_MAX_DIFF
return isFeeTooHigh to diff.parseBigDecimal(0, RoundingMode.HALF_UP)
}
/**
* Check if custom fee is too low
*/
fun checkIfCustomFeeTooLow(feeSelectorUM: FeeSelectorUM.Content): Boolean {
if (feeSelectorUM.selectedFeeItem !is FeeItem.Custom) return false
val multipleFees = feeSelectorUM.fees as? TransactionFee.Choosable ?: return false
val minimumValue = multipleFees.minimum.amount.value ?: return false
val customAmount = feeSelectorUM.selectedFeeItem.customValues.firstOrNull() ?: return false
val customValue = customAmount.value.parseToBigDecimal(customAmount.decimals)
return minimumValue > customValue
}
/**
* Checks if sending amount with fee is greater than balance
*/
fun checkFeeCoverage(
isSubtractAvailable: Boolean,
balance: BigDecimal,
amountValue: BigDecimal,
feeValue: BigDecimal,
reduceAmountBy: BigDecimal?,
): Boolean {
if (!isSubtractAvailable) return false
val reducedBy = balance - (reduceAmountBy ?: BigDecimal.ZERO)
return reducedBy < amountValue + feeValue && reducedBy > feeValue && reducedBy >= amountValue
}
/**
* Checks if fee exceeds fee paid currency balance
*/
fun checkExceedBalance(feeBalance: BigDecimal?, feeAmount: BigDecimal?): Boolean {
return feeAmount == null || feeBalance == null || feeAmount.isZero() || feeAmount > feeBalance
}
}

View file

@ -0,0 +1,188 @@
package com.tangem.features.send.v2.api.subcomponents.feeSelector.utils
import com.google.common.truth.Truth.assertThat
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import io.mockk.mockk
import org.junit.jupiter.api.Test
import java.math.BigDecimal
class FeeCalculationUtilsTest {
@Test
fun `GIVEN amount subtract available and fee coverage needed WHEN checkAndCalculateSubtractedAmount THEN returns subtracted amount`() {
// GIVEN
val isAmountSubtractAvailable = true
val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("6"))
val amountValue = BigDecimal("5")
val feeValue = BigDecimal("2")
val reduceAmountBy = BigDecimal("1")
// WHEN
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
)
// THEN
assertThat(result).isEqualTo(BigDecimal("3"))
}
@Test
fun `GIVEN amount subtract not available WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() {
// GIVEN
val isAmountSubtractAvailable = false
val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("10"))
val amountValue = BigDecimal("5")
val feeValue = BigDecimal("2")
val reduceAmountBy = BigDecimal("1")
// WHEN
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
)
// THEN
assertThat(result).isEqualTo(amountValue)
}
@Test
fun `GIVEN sufficient balance for amount and fee WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() {
// GIVEN
val isAmountSubtractAvailable = true
val cryptoCurrencyStatus = createCryptoCurrencyStatus(BigDecimal("10"))
val amountValue = BigDecimal("5")
val feeValue = BigDecimal("2")
val reduceAmountBy = BigDecimal("1")
// WHEN
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
)
// THEN
assertThat(result).isEqualTo(amountValue)
}
@Test
fun `GIVEN no balance WHEN checkAndCalculateSubtractedAmount THEN returns original amount`() {
// GIVEN
val isAmountSubtractAvailable = true
val cryptoCurrencyStatus = createCryptoCurrencyStatus(null)
val amountValue = BigDecimal("5")
val feeValue = BigDecimal("2")
val reduceAmountBy = BigDecimal("1")
// WHEN
val result = FeeCalculationUtils.checkAndCalculateSubtractedAmount(
isAmountSubtractAvailable = isAmountSubtractAvailable,
cryptoCurrencyStatus = cryptoCurrencyStatus,
amountValue = amountValue,
feeValue = feeValue,
reduceAmountBy = reduceAmountBy,
)
// THEN
assertThat(result).isEqualTo(amountValue)
}
@Test
fun `GIVEN fee exceeds balance WHEN checkExceedBalance THEN returns true`() {
// GIVEN
val feeBalance = BigDecimal("5")
val feeAmount = BigDecimal("10")
// WHEN
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
// THEN
assertThat(result).isTrue()
}
@Test
fun `GIVEN fee within balance WHEN checkExceedBalance THEN returns false`() {
// GIVEN
val feeBalance = BigDecimal("10")
val feeAmount = BigDecimal("5")
// WHEN
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
// THEN
assertThat(result).isFalse()
}
@Test
fun `GIVEN null fee amount WHEN checkExceedBalance THEN returns true`() {
// GIVEN
val feeBalance = BigDecimal("10")
val feeAmount: BigDecimal? = null
// WHEN
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
// THEN
assertThat(result).isTrue()
}
@Test
fun `GIVEN null fee balance WHEN checkExceedBalance THEN returns true`() {
// GIVEN
val feeBalance: BigDecimal? = null
val feeAmount = BigDecimal("5")
// WHEN
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
// THEN
assertThat(result).isTrue()
}
@Test
fun `GIVEN zero fee amount WHEN checkExceedBalance THEN returns true`() {
// GIVEN
val feeBalance = BigDecimal("10")
val feeAmount = BigDecimal.ZERO
// WHEN
val result = FeeCalculationUtils.checkExceedBalance(feeBalance, feeAmount)
// THEN
assertThat(result).isTrue()
}
private fun createCryptoCurrencyStatus(amount: BigDecimal?): CryptoCurrencyStatus {
val value = if (amount != null) {
CryptoCurrencyStatus.Loaded(
amount = amount,
fiatAmount = BigDecimal.ZERO,
fiatRate = BigDecimal.ZERO,
priceChange = BigDecimal.ZERO,
yieldBalance = null,
hasCurrentNetworkTransactions = false,
pendingTransactions = emptySet(),
networkAddress = mockk(relaxed = true),
sources = CryptoCurrencyStatus.Sources(),
)
} else {
CryptoCurrencyStatus.NoAmount(
priceChange = BigDecimal.ZERO,
fiatRate = BigDecimal.ZERO,
)
}
return CryptoCurrencyStatus(
currency = mockk(relaxed = true),
value = value,
)
}
}

View file

@ -11,6 +11,10 @@ android {
namespace = "com.tangem.features.send.v2.impl"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
/** Api */
implementation(projects.features.sendV2.api)
@ -79,4 +83,13 @@ dependencies {
/** DI */
implementation(deps.hilt.android)
kapt(deps.hilt.kapt)
// region Tests
testImplementation(deps.test.coroutine)
testImplementation(deps.test.junit5)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.mockk)
testImplementation(deps.test.truth)
testImplementation(projects.common.test)
// endregion
}

View file

@ -54,6 +54,7 @@ import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmIn
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSendingStateTransformer
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmSentStateTransformer
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmationNotificationsTransformer
import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmationNotificationsTransformerV2
import com.tangem.features.send.v2.send.ui.state.SendUM
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadListener
import com.tangem.features.send.v2.subcomponents.fee.SendFeeCheckReloadTrigger
@ -474,14 +475,25 @@ internal class SendConfirmModel @Inject constructor(
)
_uiState.update {
it.copy(
confirmUM = SendConfirmationNotificationsTransformer(
feeUM = uiState.value.feeUM,
amountUM = uiState.value.amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
appCurrency = appCurrency,
analyticsCategoryName = params.analyticsCategoryName,
).transform(uiState.value.confirmUM),
confirmUM = if (uiState.value.isRedesignEnabled) {
SendConfirmationNotificationsTransformerV2(
feeSelectorUM = uiState.value.feeSelectorUM,
amountUM = uiState.value.amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
appCurrency = appCurrency,
analyticsCategoryName = params.analyticsCategoryName,
).transform(uiState.value.confirmUM)
} else {
SendConfirmationNotificationsTransformer(
feeUM = uiState.value.feeUM,
amountUM = uiState.value.amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrencyStatus.currency,
appCurrency = appCurrency,
analyticsCategoryName = params.analyticsCategoryName,
).transform(uiState.value.confirmUM)
},
)
}
}

View file

@ -0,0 +1,110 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.tangem.blockchain.common.transaction.Fee
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.stringReference
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.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.subcomponents.feeSelector.utils.FeeCalculationUtils
import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.common.utils.formatFooterFiatFee
import com.tangem.features.send.v2.common.utils.getTronTokenFeeSendingText
import com.tangem.features.send.v2.impl.R
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal class SendConfirmationNotificationsTransformerV2(
private val feeSelectorUM: FeeSelectorUM,
private val amountUM: AmountState,
private val analyticsEventHandler: AnalyticsEventHandler,
private val cryptoCurrency: CryptoCurrency,
private val appCurrency: AppCurrency,
private val analyticsCategoryName: String,
) : Transformer<ConfirmUM> {
override fun transform(prevState: ConfirmUM): ConfirmUM {
val state = prevState as? ConfirmUM.Content ?: return prevState
val feeSelectorUM = feeSelectorUM as? FeeSelectorUM.Content ?: return prevState
return state.copy(
sendingFooter = getSendingFooterText(),
notifications = buildList {
addTooHighNotification(feeSelectorUM)
addTooLowNotification(feeSelectorUM)
}.toPersistentList(),
)
}
private fun MutableList<NotificationUM>.addTooLowNotification(feeSelectorUM: FeeSelectorUM.Content) {
if (FeeCalculationUtils.checkIfCustomFeeTooLow(feeSelectorUM)) {
add(NotificationUM.Warning.FeeTooLow)
analyticsEventHandler.send(
CommonSendAnalyticEvents.NoticeTransactionDelays(
categoryName = analyticsCategoryName,
token = cryptoCurrency.symbol,
),
)
}
}
private fun MutableList<NotificationUM>.addTooHighNotification(feeSelectorUM: FeeSelectorUM.Content) {
val (isFeeTooHigh, diff) = FeeCalculationUtils.checkIfCustomFeeTooHigh(feeSelectorUM)
if (isFeeTooHigh) {
add(NotificationUM.Warning.TooHigh(diff))
}
}
private fun getSendingFooterText(): TextReference {
val feeSelectorUM = feeSelectorUM as? FeeSelectorUM.Content
val amountUM = amountUM as? AmountState.Data
val fee = feeSelectorUM?.selectedFeeItem?.fee
if (fee == null || amountUM == null) return TextReference.EMPTY
val fiatAmountValue = amountUM.amountTextField.fiatAmount.value
val fiatFeeValue = feeSelectorUM.feeFiatRateUM?.rate?.let { fee.amount.value?.multiply(it) }
val fiatSendingValue = if (feeSelectorUM.feeFiatRateUM != null) {
fiatFeeValue?.let { fiatAmountValue?.plus(it) }
} else {
fiatAmountValue
}
val fiatSending = fiatSendingValue.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
val fiatFee = formatFooterFiatFee(
amount = fee.amount.copy(value = fiatFeeValue),
isFeeConvertibleToFiat = feeSelectorUM.feeFiatRateUM != null,
isFeeApproximate = feeSelectorUM.isFeeApproximate,
appCurrency = appCurrency,
)
return if (fee is Fee.Tron) {
getTronTokenFeeSendingText(
fee = fee,
fiatFee = fiatFee,
fiatSending = stringReference(fiatSending),
)
} else {
resourceReference(
id = if (feeSelectorUM.feeFiatRateUM != null) {
R.string.send_summary_transaction_description
} else {
R.string.send_summary_transaction_description_no_fiat_fee
},
formatArgs = wrappedList(fiatSending, fiatFee),
)
}
}
}

View file

@ -0,0 +1,439 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
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.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import java.math.BigInteger
import java.util.Locale
import org.junit.jupiter.api.BeforeAll
class SendConfirmationNotificationsTransformerTest {
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true)
private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$")
private val analyticsCategoryName = "test_category"
@Test
fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest {
// GIVEN
val feeUM: FeeUM = mockk(relaxed = true)
val amountUM: AmountState = mockk(relaxed = true)
val transformer = SendConfirmationNotificationsTransformer(
feeUM = feeUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val initialState: ConfirmUM = ConfirmUM.Empty
// WHEN
val result = transformer.transform(initialState)
// THEN
assertThat(result).isEqualTo(initialState)
}
@Test
fun `GIVEN fee UM not content WHEN transform THEN returns original state`() = runTest {
// GIVEN
val feeUM: FeeUM = FeeUM.Empty()
val amountUM: AmountState = mockk(relaxed = true)
val transformer = SendConfirmationNotificationsTransformer(
feeUM = feeUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val initialState: ConfirmUM.Content = createTestConfirmUM()
// WHEN
val result = transformer.transform(initialState)
// THEN
assertThat(result).isEqualTo(initialState)
}
@Test
fun `GIVEN normal fee WHEN transform THEN returns state with footer and no notifications`() = runTest {
// GIVEN
val feeUM = createNormalFeeUM()
val amountUM = createTestAmountUM()
val transformer = SendConfirmationNotificationsTransformer(
feeUM = feeUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val initialState = createTestConfirmUM()
// WHEN
val result = transformer.transform(initialState)
// THEN
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
val content = result as ConfirmUM.Content
assertThat(content.notifications).isEmpty()
assertThat(content.sendingFooter).isNotEqualTo(initialState.sendingFooter)
}
@Test
fun `GIVEN fee too high WHEN transform THEN returns state with too high notification`() = runTest {
// GIVEN
val feeUM = createFeeTooHighUM()
val amountUM = createTestAmountUM()
val transformer = SendConfirmationNotificationsTransformer(
feeUM = feeUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val initialState = createTestConfirmUM()
// WHEN
val result = transformer.transform(initialState)
// THEN
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
val content = result as ConfirmUM.Content
assertThat(content.notifications).hasSize(1)
assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java)
}
@Test
fun `GIVEN fee too low WHEN transform THEN returns state with too low notification`() = runTest {
// GIVEN
val feeUM = createFeeTooLowUM()
val amountUM = createTestAmountUM()
val transformer = SendConfirmationNotificationsTransformer(
feeUM = feeUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val initialState = createTestConfirmUM()
// WHEN
val result = transformer.transform(initialState)
// THEN
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
val content = result as ConfirmUM.Content
assertThat(content.notifications).hasSize(1)
assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java)
verify { analyticsEventHandler.send(any()) }
}
@Test
fun `GIVEN both fee too high and too low WHEN transform THEN returns state with both notifications`() = runTest {
// GIVEN
val feeUM = createFeeTooHighAndTooLowUM()
val amountUM = createTestAmountUM()
val transformer = SendConfirmationNotificationsTransformer(
feeUM = feeUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val initialState = createTestConfirmUM()
// WHEN
val result = transformer.transform(initialState)
// THEN
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
val content = result as ConfirmUM.Content
assertThat(content.notifications).hasSize(2)
assertThat(content.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue()
assertThat(content.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue()
}
private fun createTestConfirmUM(): ConfirmUM.Content {
return ConfirmUM.Content(
isPrimaryButtonEnabled = true,
walletName = mockk(relaxed = true),
isSending = false,
showTapHelp = false,
sendingFooter = mockk(relaxed = true),
notifications = persistentListOf(),
)
}
private fun createTestAmountUM(): AmountState.Data {
val cryptoAmount = com.tangem.domain.tokens.model.Amount(
currencySymbol = "SOL",
value = BigDecimal("1.5"),
decimals = 8,
)
val fiatAmount = com.tangem.domain.tokens.model.Amount(
currencySymbol = "USD",
value = BigDecimal("50.00"),
decimals = 2,
)
return AmountState.Data(
isPrimaryButtonEnabled = true,
isRedesignEnabled = false,
title = mockk(relaxed = true),
availableBalance = mockk(relaxed = true),
tokenName = mockk(relaxed = true),
tokenIconState = mockk(relaxed = true),
segmentedButtonConfig = persistentListOf(),
selectedButton = 0,
isSegmentedButtonsEnabled = false,
amountTextField = AmountFieldModel(
value = "1.5",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
cryptoAmount = cryptoAmount,
fiatAmount = fiatAmount,
isFiatValue = false,
fiatValue = "50.00",
isFiatUnavailable = false,
isValuePasted = false,
onValuePastedTriggerDismiss = {},
isError = false,
isWarning = false,
error = mockk(relaxed = true),
),
appCurrency = appCurrency,
isEditingDisabled = false,
reduceAmountBy = BigDecimal.ZERO,
isIgnoreReduce = false,
)
}
private fun createNormalFeeUM(): FeeUM.Content {
val fee = Fee.Common(
amount = com.tangem.blockchain.common.Amount(
currencySymbol = "SOL",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Single(fee)
return FeeUM.Content(
feeSelectorUM = FeeSelectorUM.Content(
fees = transactionFee,
selectedType = FeeType.Market,
selectedFee = fee,
customValues = persistentListOf(),
nonce = BigInteger.ZERO,
),
rate = BigDecimal("50000"),
isFeeConvertibleToFiat = true,
isFeeApproximate = false,
isTronToken = false,
isEditingDisabled = false,
isPrimaryButtonEnabled = true,
appCurrency = AppCurrency.Default,
isCustomSelected = false,
notifications = persistentListOf(),
)
}
private fun createFeeTooHighUM(): FeeUM.Content {
val priorityFee = Fee.Common(
amount = com.tangem.blockchain.common.Amount(
currencySymbol = "SOL",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val minimumFee = Fee.Common(
amount = com.tangem.blockchain.common.Amount(
currencySymbol = "SOL",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val customFee = Fee.Common(
amount = com.tangem.blockchain.common.Amount(
currencySymbol = "SOL",
value = BigDecimal("0.01"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Choosable(
minimum = minimumFee,
normal = minimumFee,
priority = priorityFee,
)
return FeeUM.Content(
feeSelectorUM = FeeSelectorUM.Content(
fees = transactionFee,
selectedType = FeeType.Custom,
selectedFee = customFee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.01",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "SOL",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
nonce = BigInteger.ZERO,
),
rate = BigDecimal("50000"),
isFeeConvertibleToFiat = true,
isFeeApproximate = false,
isTronToken = false,
isEditingDisabled = false,
isPrimaryButtonEnabled = true,
appCurrency = AppCurrency.Default,
isCustomSelected = false,
notifications = persistentListOf(),
)
}
private fun createFeeTooLowUM(): FeeUM.Content {
val fee = Fee.Common(
amount = com.tangem.blockchain.common.Amount(
currencySymbol = "SOL",
value = BigDecimal("0.0001"),
decimals = 8,
),
)
val minimumFee = Fee.Common(
amount = com.tangem.blockchain.common.Amount(
currencySymbol = "SOL",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Choosable(
minimum = minimumFee,
normal = fee,
priority = fee,
)
return FeeUM.Content(
feeSelectorUM = FeeSelectorUM.Content(
fees = transactionFee,
selectedType = FeeType.Custom,
selectedFee = fee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.0001",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "SOL",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
nonce = BigInteger.ZERO,
),
rate = BigDecimal("50000"),
isFeeConvertibleToFiat = true,
isFeeApproximate = false,
isTronToken = false,
isEditingDisabled = false,
isPrimaryButtonEnabled = true,
appCurrency = AppCurrency.Default,
isCustomSelected = false,
notifications = persistentListOf(),
)
}
private fun createFeeTooHighAndTooLowUM(): FeeUM.Content {
val priorityFee = Fee.Common(
amount = com.tangem.blockchain.common.Amount(
currencySymbol = "SOL",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val minimumFee = Fee.Common(
amount = com.tangem.blockchain.common.Amount(
currencySymbol = "SOL",
value = BigDecimal("0.01"),
decimals = 8,
),
)
val customFee = Fee.Common(
amount = com.tangem.blockchain.common.Amount(
currencySymbol = "SOL",
value = BigDecimal("0.008"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Choosable(
minimum = minimumFee,
normal = minimumFee,
priority = priorityFee,
)
return FeeUM.Content(
feeSelectorUM = FeeSelectorUM.Content(
fees = transactionFee,
selectedType = FeeType.Custom,
selectedFee = customFee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.008",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "SOL",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
nonce = BigInteger.ZERO,
),
rate = BigDecimal("50000"),
isFeeConvertibleToFiat = true,
isFeeApproximate = false,
isTronToken = false,
isEditingDisabled = false,
isPrimaryButtonEnabled = true,
appCurrency = AppCurrency.Default,
isCustomSelected = false,
notifications = persistentListOf(),
)
}
companion object {
@JvmStatic
@BeforeAll
fun setUpLocale() {
Locale.setDefault(Locale.US)
}
}
}

View file

@ -0,0 +1,488 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.google.common.truth.Truth.assertThat
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.Amount as DomainAmount
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
import com.tangem.features.send.v2.api.entity.FeeItem
import com.tangem.features.send.v2.api.entity.FeeFiatRateUM
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import java.math.BigInteger
import java.util.Locale
import org.junit.jupiter.api.BeforeAll
class SendConfirmationNotificationsTransformerV2Test {
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true)
private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$")
private val analyticsCategoryName = "test_category"
@Test
fun `GIVEN non content state WHEN transform THEN returns original state`() = runTest {
// GIVEN
val feeSelectorUM: FeeSelectorUM = mockk(relaxed = true)
val amountUM: AmountState = mockk(relaxed = true)
val transformer = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val initialState: ConfirmUM = ConfirmUM.Empty
// WHEN
val result = transformer.transform(initialState)
// THEN
assertThat(result).isEqualTo(initialState)
}
@Test
fun `GIVEN fee selector not content WHEN transform THEN returns original state`() = runTest {
// GIVEN
val feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading
val amountUM: AmountState = mockk(relaxed = true)
val transformer = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val initialState = createTestConfirmUM()
// WHEN
val result = transformer.transform(initialState)
// THEN
assertThat(result).isEqualTo(initialState)
}
@Test
fun `GIVEN normal fee WHEN transform THEN returns state with footer and no notifications`() = runTest {
// GIVEN
val feeSelectorUM = createNormalFeeSelectorUM()
val amountUM = createTestAmountUM()
val transformer = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val initialState = createTestConfirmUM()
// WHEN
val result = transformer.transform(initialState)
// THEN
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
val content = result as ConfirmUM.Content
assertThat(content.notifications).isEmpty()
assertThat(content.sendingFooter).isNotEqualTo(initialState.sendingFooter)
}
@Test
fun `GIVEN fee too high WHEN transform THEN returns state with too high notification`() = runTest {
// GIVEN
val feeSelectorUM = createFeeTooHighUM()
val amountUM = createTestAmountUM()
val transformer = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val initialState = createTestConfirmUM()
// WHEN
val result = transformer.transform(initialState)
// THEN
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
val content = result as ConfirmUM.Content
assertThat(content.notifications).hasSize(1)
assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java)
}
@Test
fun `GIVEN fee too low WHEN transform THEN returns state with too low notification`() = runTest {
// GIVEN
val feeSelectorUM = createFeeTooLowUM()
val amountUM = createTestAmountUM()
val transformer = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val initialState = createTestConfirmUM()
// WHEN
val result = transformer.transform(initialState)
// THEN
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
val content = result as ConfirmUM.Content
assertThat(content.notifications).hasSize(1)
assertThat(content.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java)
verify { analyticsEventHandler.send(any()) }
}
@Test
fun `GIVEN both fee too high and too low WHEN transform THEN returns state with both notifications`() = runTest {
// GIVEN
val feeSelectorUM = createFeeTooHighAndTooLowUM()
val amountUM = createTestAmountUM()
val transformer = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val initialState = createTestConfirmUM()
// WHEN
val result = transformer.transform(initialState)
// THEN
assertThat(result).isInstanceOf(ConfirmUM.Content::class.java)
val content = result as ConfirmUM.Content
assertThat(content.notifications).hasSize(2)
assertThat(content.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue()
assertThat(content.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue()
}
private fun createTestConfirmUM(): ConfirmUM.Content {
return ConfirmUM.Content(
isPrimaryButtonEnabled = true,
walletName = mockk(relaxed = true),
isSending = false,
showTapHelp = false,
sendingFooter = mockk(relaxed = true),
notifications = persistentListOf(),
)
}
private fun createTestAmountUM(): AmountState.Data {
val cryptoAmount = DomainAmount(
currencySymbol = "SOL",
value = BigDecimal("1.5"),
decimals = 8,
)
val fiatAmount = DomainAmount(
currencySymbol = "USD",
value = BigDecimal("50.00"),
decimals = 2,
)
return AmountState.Data(
isPrimaryButtonEnabled = true,
isRedesignEnabled = false,
title = mockk(relaxed = true),
availableBalance = mockk(relaxed = true),
tokenName = mockk(relaxed = true),
tokenIconState = mockk(relaxed = true),
segmentedButtonConfig = persistentListOf(),
selectedButton = 0,
isSegmentedButtonsEnabled = false,
amountTextField = AmountFieldModel(
value = "1.5",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
cryptoAmount = cryptoAmount,
fiatAmount = fiatAmount,
isFiatValue = false,
fiatValue = "50.00",
isFiatUnavailable = false,
isValuePasted = false,
onValuePastedTriggerDismiss = {},
isError = false,
isWarning = false,
error = mockk(relaxed = true),
),
appCurrency = appCurrency,
isEditingDisabled = false,
reduceAmountBy = BigDecimal.ZERO,
isIgnoreReduce = false,
)
}
private fun createNormalFeeSelectorUM(): FeeSelectorUM.Content {
val fee = Fee.Common(
amount = Amount(
currencySymbol = "SOL",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Single(fee)
return FeeSelectorUM.Content(
fees = transactionFee,
feeItems = persistentListOf(FeeItem.Market(fee)),
selectedFeeItem = FeeItem.Market(fee),
isFeeApproximate = false,
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
appCurrency = appCurrency,
),
displayNonceInput = false,
nonce = BigInteger.ZERO,
onNonceChange = {},
)
}
private fun createFeeTooHighUM(): FeeSelectorUM.Content {
val priorityFee = Fee.Common(
amount = Amount(
currencySymbol = "SOL",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val minimumFee = Fee.Common(
amount = Amount(
currencySymbol = "SOL",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Choosable(
minimum = minimumFee,
normal = minimumFee,
priority = priorityFee,
)
return FeeSelectorUM.Content(
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Custom(
fee = Fee.Common(
amount = Amount(
currencySymbol = "SOL",
value = BigDecimal("0.01"),
decimals = 8,
),
),
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.01",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "SOL",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
),
),
selectedFeeItem = FeeItem.Custom(
fee = Fee.Common(
amount = Amount(
currencySymbol = "SOL",
value = BigDecimal("0.01"),
decimals = 8,
),
),
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.01",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "SOL",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
),
isFeeApproximate = false,
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
appCurrency = appCurrency,
),
displayNonceInput = false,
nonce = BigInteger.ZERO,
onNonceChange = {},
)
}
private fun createFeeTooLowUM(): FeeSelectorUM.Content {
val fee = Fee.Common(
amount = Amount(
currencySymbol = "SOL",
value = BigDecimal("0.0001"),
decimals = 8,
),
)
val minimumFee = Fee.Common(
amount = Amount(
currencySymbol = "SOL",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Choosable(
minimum = minimumFee,
normal = fee,
priority = fee,
)
return FeeSelectorUM.Content(
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Custom(
fee = fee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.0001",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "SOL",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
),
),
selectedFeeItem = FeeItem.Custom(
fee = fee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.0001",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "SOL",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
),
isFeeApproximate = false,
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
appCurrency = appCurrency,
),
displayNonceInput = false,
nonce = BigInteger.ZERO,
onNonceChange = {},
)
}
private fun createFeeTooHighAndTooLowUM(): FeeSelectorUM.Content {
val priorityFee = Fee.Common(
amount = Amount(
currencySymbol = "SOL",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val minimumFee = Fee.Common(
amount = Amount(
currencySymbol = "SOL",
value = BigDecimal("0.01"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Choosable(
minimum = minimumFee,
normal = minimumFee,
priority = priorityFee,
)
return FeeSelectorUM.Content(
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Custom(
fee = Fee.Common(
amount = Amount(
currencySymbol = "SOL",
value = BigDecimal("0.008"),
decimals = 8,
),
),
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.008",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "SOL",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
),
),
selectedFeeItem = FeeItem.Custom(
fee = Fee.Common(
amount = Amount(
currencySymbol = "SOL",
value = BigDecimal("0.008"),
decimals = 8,
),
),
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.008",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "SOL",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
),
isFeeApproximate = false,
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
appCurrency = appCurrency,
),
displayNonceInput = false,
nonce = BigInteger.ZERO,
onNonceChange = {},
)
}
companion object {
@JvmStatic
@BeforeAll
fun setUpLocale() {
Locale.setDefault(Locale.US)
}
}
}

View file

@ -0,0 +1,847 @@
package com.tangem.features.send.v2.send.confirm.model.transformers
import com.google.common.truth.Truth.assertThat
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.models.AmountFieldModel
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMV2
import com.tangem.features.send.v2.common.ui.state.ConfirmUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM
import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeType
import com.tangem.features.send.v2.api.entity.CustomFeeFieldUM
import com.tangem.features.send.v2.api.entity.FeeItem
import com.tangem.features.send.v2.api.entity.FeeFiatRateUM
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import kotlinx.collections.immutable.persistentListOf
import org.junit.jupiter.api.BeforeAll
import java.math.BigDecimal
import java.math.BigInteger
import java.util.Locale
import com.tangem.domain.tokens.model.Amount as DomainAmount
class TransformersComparisonTest {
private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true)
private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true)
private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$")
private val analyticsCategoryName = "test_category"
@Test
fun `GIVEN equivalent input data WHEN both transformers transform THEN they produce equal ConfirmUM`() = runTest {
// GIVEN
val initialConfirmUM = createTestConfirmUM()
val amountUM = createTestAmountUM()
val feeUM = createTestFeeUM()
val feeSelectorUMV2 = createTestFeeSelectorUMV2()
// WHEN
val transformerV1 = SendConfirmationNotificationsTransformer(
feeUM = feeUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val transformerV2 = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUMV2,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val resultV1 = transformerV1.transform(initialConfirmUM)
val resultV2 = transformerV2.transform(initialConfirmUM)
// THEN
assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java)
assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java)
val contentV1 = resultV1 as ConfirmUM.Content
val contentV2 = resultV2 as ConfirmUM.Content
assertThat(contentV1.notifications.size).isEqualTo(contentV2.notifications.size)
assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter)
assertThat(contentV1.isPrimaryButtonEnabled).isEqualTo(contentV2.isPrimaryButtonEnabled)
assertThat(contentV1.isSending).isEqualTo(contentV2.isSending)
assertThat(contentV1.showTapHelp).isEqualTo(contentV2.showTapHelp)
}
@Test
fun `GIVEN fee too low WHEN both transformers transform THEN they produce equal notifications`() = runTest {
// GIVEN
val initialConfirmUM = createTestConfirmUM()
val amountUM = createTestAmountUM()
val feeUM = createFeeTooLowUM()
val feeSelectorUMV2 = createFeeTooLowUMV2()
// WHEN
val transformerV1 = SendConfirmationNotificationsTransformer(
feeUM = feeUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val transformerV2 = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUMV2,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val resultV1 = transformerV1.transform(initialConfirmUM)
val resultV2 = transformerV2.transform(initialConfirmUM)
// THEN
assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java)
assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java)
val contentV1 = resultV1 as ConfirmUM.Content
val contentV2 = resultV2 as ConfirmUM.Content
assertThat(contentV1.notifications).hasSize(1)
assertThat(contentV2.notifications).hasSize(1)
assertThat(contentV1.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java)
assertThat(contentV2.notifications.first()).isInstanceOf(NotificationUM.Warning.FeeTooLow::class.java)
assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter)
// Verify analytics event was sent for both transformers
verify(exactly = 2) { analyticsEventHandler.send(any()) }
}
@Test
fun `GIVEN fee too high WHEN both transformers transform THEN they produce equal notifications`() = runTest {
// GIVEN
val initialConfirmUM = createTestConfirmUM()
val amountUM = createTestAmountUM()
val feeUM = createFeeTooHighUM()
val feeSelectorUMV2 = createFeeTooHighUMV2()
// WHEN
val transformerV1 = SendConfirmationNotificationsTransformer(
feeUM = feeUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val transformerV2 = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUMV2,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val resultV1 = transformerV1.transform(initialConfirmUM)
val resultV2 = transformerV2.transform(initialConfirmUM)
// THEN
assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java)
assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java)
val contentV1 = resultV1 as ConfirmUM.Content
val contentV2 = resultV2 as ConfirmUM.Content
assertThat(contentV1.notifications).hasSize(1)
assertThat(contentV2.notifications).hasSize(1)
assertThat(contentV1.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java)
assertThat(contentV2.notifications.first()).isInstanceOf(NotificationUM.Warning.TooHigh::class.java)
val tooHighV1 = contentV1.notifications.first() as NotificationUM.Warning.TooHigh
val tooHighV2 = contentV2.notifications.first() as NotificationUM.Warning.TooHigh
assertThat(tooHighV1.value).isEqualTo(tooHighV2.value)
assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter)
}
@Test
fun `GIVEN fee both too high and too low WHEN both transformers transform THEN they produce equal notifications`() =
runTest {
// GIVEN
val initialConfirmUM = createTestConfirmUM()
val amountUM = createTestAmountUM()
val feeUM = createFeeTooHighAndTooLowUM()
val feeSelectorUMV2 = createFeeTooHighAndTooLowUMV2()
// WHEN
val transformerV1 = SendConfirmationNotificationsTransformer(
feeUM = feeUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val transformerV2 = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUMV2,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val resultV1 = transformerV1.transform(initialConfirmUM)
val resultV2 = transformerV2.transform(initialConfirmUM)
// THEN
assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java)
assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java)
val contentV1 = resultV1 as ConfirmUM.Content
val contentV2 = resultV2 as ConfirmUM.Content
assertThat(contentV1.notifications).hasSize(2)
assertThat(contentV2.notifications).hasSize(2)
assertThat(contentV1.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue()
assertThat(contentV1.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue()
assertThat(contentV2.notifications.any { it is NotificationUM.Warning.FeeTooLow }).isTrue()
assertThat(contentV2.notifications.any { it is NotificationUM.Warning.TooHigh }).isTrue()
assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter)
verify(exactly = 2) { analyticsEventHandler.send(any()) }
}
@Test
fun `GIVEN normal fee WHEN both transformers transform THEN they produce equal notifications`() = runTest {
// GIVEN
val initialConfirmUM = createTestConfirmUM()
val amountUM = createTestAmountUM()
val feeUM = createNormalFeeUM()
val feeSelectorUMV2 = createNormalFeeSelectorUMV2()
// WHEN
val transformerV1 = SendConfirmationNotificationsTransformer(
feeUM = feeUM,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val transformerV2 = SendConfirmationNotificationsTransformerV2(
feeSelectorUM = feeSelectorUMV2,
amountUM = amountUM,
analyticsEventHandler = analyticsEventHandler,
cryptoCurrency = cryptoCurrency,
appCurrency = appCurrency,
analyticsCategoryName = analyticsCategoryName,
)
val resultV1 = transformerV1.transform(initialConfirmUM)
val resultV2 = transformerV2.transform(initialConfirmUM)
// THEN
assertThat(resultV1).isInstanceOf(ConfirmUM.Content::class.java)
assertThat(resultV2).isInstanceOf(ConfirmUM.Content::class.java)
val contentV1 = resultV1 as ConfirmUM.Content
val contentV2 = resultV2 as ConfirmUM.Content
assertThat(contentV1.notifications).isEmpty()
assertThat(contentV2.notifications).isEmpty()
assertThat(contentV1.sendingFooter).isEqualTo(contentV2.sendingFooter)
}
private fun createTestConfirmUM(): ConfirmUM.Content {
return ConfirmUM.Content(
isPrimaryButtonEnabled = true,
walletName = mockk(relaxed = true),
isSending = false,
showTapHelp = false,
sendingFooter = mockk(relaxed = true),
notifications = persistentListOf(),
)
}
private fun createTestAmountUM(): AmountState.Data {
val cryptoAmount = DomainAmount(
currencySymbol = "TST",
value = BigDecimal("1.5"),
decimals = 8,
)
val fiatAmount = DomainAmount(
currencySymbol = "USD",
value = BigDecimal("50.00"),
decimals = 2,
)
return AmountState.Data(
isPrimaryButtonEnabled = true,
isRedesignEnabled = false,
title = mockk(relaxed = true),
availableBalance = mockk(relaxed = true),
tokenName = mockk(relaxed = true),
tokenIconState = mockk(relaxed = true),
segmentedButtonConfig = persistentListOf(),
selectedButton = 0,
isSegmentedButtonsEnabled = false,
amountTextField = AmountFieldModel(
value = "1.5",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
cryptoAmount = cryptoAmount,
fiatAmount = fiatAmount,
isFiatValue = false,
fiatValue = "50.00",
isFiatUnavailable = false,
isValuePasted = false,
onValuePastedTriggerDismiss = {},
isError = false,
isWarning = false,
error = mockk(relaxed = true),
),
appCurrency = appCurrency,
isEditingDisabled = false,
reduceAmountBy = BigDecimal.ZERO,
isIgnoreReduce = false,
)
}
private fun createTestFeeUM(): FeeUM.Content {
val fee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Single(fee)
return FeeUM.Content(
feeSelectorUM = FeeSelectorUM.Content(
fees = transactionFee,
selectedType = FeeType.Market,
selectedFee = fee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.001",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "TST",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
nonce = BigInteger.ZERO,
),
rate = BigDecimal("50000"),
isFeeConvertibleToFiat = true,
isFeeApproximate = false,
isTronToken = false,
isEditingDisabled = false,
isPrimaryButtonEnabled = true,
appCurrency = AppCurrency.Default,
isCustomSelected = false,
notifications = persistentListOf(),
)
}
private fun createTestFeeSelectorUMV2(): FeeSelectorUMV2.Content {
val fee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Single(fee)
return FeeSelectorUMV2.Content(
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Market(fee),
),
selectedFeeItem = FeeItem.Market(fee),
isFeeApproximate = false,
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
appCurrency = appCurrency,
),
displayNonceInput = false,
nonce = BigInteger.ZERO,
onNonceChange = {},
)
}
private fun createFeeTooLowUM(): FeeUM.Content {
val fee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.0001"),
decimals = 8,
),
)
val minimumFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Choosable(
minimum = minimumFee,
normal = fee,
priority = fee,
)
return FeeUM.Content(
feeSelectorUM = FeeSelectorUM.Content(
fees = transactionFee,
selectedType = FeeType.Custom,
selectedFee = fee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.0001",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "TST",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
nonce = BigInteger.ZERO,
),
rate = BigDecimal("50000"),
isFeeConvertibleToFiat = true,
isFeeApproximate = false,
isTronToken = false,
isEditingDisabled = false,
isPrimaryButtonEnabled = true,
appCurrency = AppCurrency.Default,
isCustomSelected = true,
notifications = persistentListOf(),
)
}
private fun createFeeTooLowUMV2(): FeeSelectorUMV2.Content {
val fee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.0001"),
decimals = 8,
),
)
val minimumFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Choosable(
minimum = minimumFee,
normal = fee,
priority = fee,
)
return FeeSelectorUMV2.Content(
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Custom(
fee = fee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.0001",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "TST",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
),
),
selectedFeeItem = FeeItem.Custom(
fee = fee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.0001",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "TST",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
),
isFeeApproximate = false,
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
appCurrency = appCurrency,
),
displayNonceInput = false,
nonce = BigInteger.ZERO,
onNonceChange = {},
)
}
private fun createFeeTooHighUM(): FeeUM.Content {
val priorityFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val minimumFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val customFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.01"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Choosable(
minimum = minimumFee,
normal = minimumFee,
priority = priorityFee,
)
return FeeUM.Content(
feeSelectorUM = FeeSelectorUM.Content(
fees = transactionFee,
selectedType = FeeType.Custom,
selectedFee = customFee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.01",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "TST",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
nonce = BigInteger.ZERO,
),
rate = BigDecimal("50000"),
isFeeConvertibleToFiat = true,
isFeeApproximate = false,
isTronToken = false,
isEditingDisabled = false,
isPrimaryButtonEnabled = true,
appCurrency = AppCurrency.Default,
isCustomSelected = true,
notifications = persistentListOf(),
)
}
private fun createFeeTooHighUMV2(): FeeSelectorUMV2.Content {
val priorityFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val minimumFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val customFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.01"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Choosable(
minimum = minimumFee,
normal = minimumFee,
priority = priorityFee,
)
return FeeSelectorUMV2.Content(
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Custom(
fee = customFee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.01",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "TST",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
),
),
selectedFeeItem = FeeItem.Custom(
fee = customFee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.01",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "TST",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
),
isFeeApproximate = false,
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
appCurrency = appCurrency,
),
displayNonceInput = false,
nonce = BigInteger.ZERO,
onNonceChange = {},
)
}
private fun createFeeTooHighAndTooLowUM(): FeeUM.Content {
val priorityFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val minimumFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.01"),
decimals = 8,
),
)
val customFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.008"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Choosable(
minimum = minimumFee,
normal = minimumFee,
priority = priorityFee,
)
return FeeUM.Content(
feeSelectorUM = FeeSelectorUM.Content(
fees = transactionFee,
selectedType = FeeType.Custom,
selectedFee = customFee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.008",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "TST",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
nonce = BigInteger.ZERO,
),
rate = BigDecimal("50000"),
isFeeConvertibleToFiat = true,
isFeeApproximate = false,
isTronToken = false,
isEditingDisabled = false,
isPrimaryButtonEnabled = true,
appCurrency = AppCurrency.Default,
isCustomSelected = true,
notifications = persistentListOf(),
)
}
private fun createFeeTooHighAndTooLowUMV2(): FeeSelectorUMV2.Content {
val priorityFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val minimumFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.01"),
decimals = 8,
),
)
val customFee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.008"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Choosable(
minimum = minimumFee,
normal = minimumFee,
priority = priorityFee,
)
return FeeSelectorUMV2.Content(
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Custom(
fee = customFee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.008",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "TST",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
),
),
selectedFeeItem = FeeItem.Custom(
fee = customFee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.008",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "TST",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
),
isFeeApproximate = false,
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
appCurrency = appCurrency,
),
displayNonceInput = false,
nonce = BigInteger.ZERO,
onNonceChange = {},
)
}
private fun createNormalFeeUM(): FeeUM.Content {
val fee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Single(fee)
return FeeUM.Content(
feeSelectorUM = FeeSelectorUM.Content(
fees = transactionFee,
selectedType = FeeType.Market,
selectedFee = fee,
customValues = persistentListOf(
CustomFeeFieldUM(
value = "0.001",
onValueChange = {},
keyboardOptions = mockk(relaxed = true),
keyboardActions = mockk(relaxed = true),
symbol = "TST",
decimals = 8,
title = mockk(relaxed = true),
footer = mockk(relaxed = true),
),
),
nonce = BigInteger.ZERO,
),
rate = BigDecimal("50000"),
isFeeConvertibleToFiat = true,
isFeeApproximate = false,
isTronToken = false,
isEditingDisabled = false,
isPrimaryButtonEnabled = true,
appCurrency = AppCurrency.Default,
isCustomSelected = false,
notifications = persistentListOf(),
)
}
private fun createNormalFeeSelectorUMV2(): FeeSelectorUMV2.Content {
val fee = Fee.Common(
amount = Amount(
currencySymbol = "TST",
value = BigDecimal("0.001"),
decimals = 8,
),
)
val transactionFee = TransactionFee.Single(fee)
return FeeSelectorUMV2.Content(
fees = transactionFee,
feeItems = persistentListOf(
FeeItem.Market(fee),
),
selectedFeeItem = FeeItem.Market(fee),
isFeeApproximate = false,
feeFiatRateUM = FeeFiatRateUM(
rate = BigDecimal("50000"),
appCurrency = appCurrency,
),
displayNonceInput = false,
nonce = BigInteger.ZERO,
onNonceChange = {},
)
}
companion object {
@JvmStatic
@BeforeAll
fun setUpLocale() {
Locale.setDefault(Locale.US)
}
}
}