diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index b5cdb3898e..c0f9222dd3 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -46,6 +46,7 @@ internal sealed class SwapAmountUM { // selected swap route val swapRateType: ExpressRateType, val swapRateMode: SwapRateMode, + val priceImpact: PriceImpact?, // swap models val swapCurrencies: SwapCurrencies, @@ -79,7 +80,6 @@ sealed class SwapAmountFieldUM { data class Content( override val amountType: SwapAmountType, override val amountField: AmountState, - val priceImpact: TextReference?, val title: TextReference, val subtitleLeft: TextReference, val subtitleRight: TextReference, @@ -90,9 +90,33 @@ sealed class SwapAmountFieldUM { } @Immutable -sealed class PriceImpactUM { +data class PriceImpact( + val value: TextReference, + val amountSignificance: AmountSignificance, + val type: Type, +) { - data object Empty : PriceImpactUM() + enum class Type { + NONE, LOW, MEDIUM, HIGH + } - data class Value(val value: Float) : PriceImpactUM() + enum class AmountSignificance { + LOW, MEDIUM, HIGH + } + + fun shouldDisableButton(): Boolean { + return type == Type.HIGH && amountSignificance == AmountSignificance.HIGH + } + + fun shouldShowWarning(): Boolean { + return type.ordinal > Type.LOW.ordinal || amountSignificance.ordinal > AmountSignificance.LOW.ordinal + } + + companion object { + val Empty = PriceImpact( + value = TextReference.EMPTY, + amountSignificance = AmountSignificance.LOW, + type = Type.NONE, + ) + } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index 98c238edb1..90eea6d0fa 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -21,13 +21,14 @@ import com.tangem.datasource.local.swap.SwapBestRateAnimationStore import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.notifications.ShouldShowNotificationUseCase +import com.tangem.domain.quotes.GetCurrencyUSDQuoteUseCase import com.tangem.domain.settings.usercountry.GetUserCountryUseCase import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions -import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.swap.models.* import com.tangem.domain.swap.models.SwapDirection.Companion.withSwapDirection import com.tangem.domain.swap.usecase.GetSwapQuoteUseCase @@ -58,13 +59,13 @@ import com.tangem.utils.coroutines.PeriodicTask import com.tangem.utils.coroutines.SingleTaskScheduler import com.tangem.utils.extensions.orZero import com.tangem.utils.isNullOrZero +import com.tangem.utils.logging.TangemLogger import com.tangem.utils.transformer.update import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.util.Locale import javax.inject.Inject @@ -93,6 +94,7 @@ internal class SwapAmountModel @Inject constructor( private val shouldShowNotificationUseCase: ShouldShowNotificationUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val getWalletsUseCase: GetWalletsUseCase, + private val getCurrencyUSDQuoteUseCase: GetCurrencyUSDQuoteUseCase, ) : Model(), SwapAmountClickIntents, SwapChooseProviderComponent.ModelCallback { private val params: SwapAmountComponentParams = paramsContainer.require() @@ -107,6 +109,9 @@ internal class SwapAmountModel @Inject constructor( private var secondaryMaximumAmountBoundary: EnterAmountBoundary? = null private var secondaryMinimumAmountBoundary: EnterAmountBoundary? = null + private var primaryFiatRateUSD: BigDecimal? = null + private var secondaryFiatRateUSD: BigDecimal? = null + private var userCountry: UserCountry = UserCountry.Other(Locale.getDefault().country) val bottomSheetNavigation: SlotNavigation = SlotNavigation() val rateInfoNavigation: SlotNavigation = SlotNavigation() @@ -177,6 +182,8 @@ internal class SwapAmountModel @Inject constructor( isBalanceHidden = params.isBalanceHidingFlow.value, primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, + primaryFiatRateUSD = primaryFiatRateUSD, + secondaryFiatRateUSD = secondaryFiatRateUSD, ), ) } @@ -218,7 +225,7 @@ internal class SwapAmountModel @Inject constructor( val selectedProvider = amountUM.selectedQuote.provider ?: return swapAmountAlertFactory.priceImpactAlert( - hasPriceImpact = (amountUM.secondaryAmount as? SwapAmountFieldUM.Content)?.priceImpact != null, + hasPriceImpact = amountUM.priceImpact != null, currencySymbol = amountUM.primaryCryptoCurrencyStatus.currency.symbol, provider = selectedProvider, ) @@ -560,6 +567,8 @@ internal class SwapAmountModel @Inject constructor( val secondaryStatus = secondaryCurrency?.currencyStatus val primaryStatus = (uiState.value as? SwapAmountUM.Content)?.primaryCryptoCurrencyStatus if (secondaryStatus != null && primaryStatus != null) { + val rawId = secondaryStatus.currency.id.rawCurrencyId + secondaryFiatRateUSD = rawId?.let { id -> getCurrencyUSDQuoteUseCase(id) } initCurrencies(primaryStatus, secondaryStatus) val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 uiState.transformerUpdate( @@ -611,7 +620,13 @@ internal class SwapAmountModel @Inject constructor( ) primaryMaximumAmountBoundary = MaxEnterAmountConverter().convert(primaryStatus) + val primaryRawId = primaryStatus.currency.id.rawCurrencyId + primaryFiatRateUSD = primaryRawId?.let { id -> getCurrencyUSDQuoteUseCase(id) } + if (secondaryStatus != null) { + val secondaryRawId = secondaryStatus.currency.id.rawCurrencyId + secondaryFiatRateUSD = secondaryRawId?.let { id -> getCurrencyUSDQuoteUseCase(id) } + secondaryMinimumAmountBoundary = EnterAmountBoundary( amount = getMinimumTransactionAmountSyncUseCase .invoke( @@ -727,6 +742,8 @@ internal class SwapAmountModel @Inject constructor( isBalanceHidden = params.isBalanceHidingFlow.value, primaryMaximumAmountBoundary = primaryMaximumAmountBoundary, primaryMinimumAmountBoundary = primaryMinimumAmountBoundary, + primaryFiatRateUSD = primaryFiatRateUSD, + secondaryFiatRateUSD = secondaryFiatRateUSD, ), ) feeSelectorReloadTrigger.triggerUpdate() diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt index f0fee9face..0c883b505c 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountQuoteUtils.kt @@ -1,16 +1,17 @@ package com.tangem.features.swap.v2.impl.amount.model -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapDirection +import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM -import com.tangem.utils.extensions.isZero -import com.tangem.utils.isNullOrZero +import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM +import com.tangem.utils.StringsSigns +import com.tangem.utils.extensions.orZero import java.math.BigDecimal import java.math.RoundingMode import kotlin.math.min @@ -20,31 +21,74 @@ internal object SwapAmountQuoteUtils { private const val MAX_DECIMALS = 8 private const val MIN_DECIMALS = 2 - fun calculatePriceImpact( - fromTokenAmount: BigDecimal, - toTokenAmount: BigDecimal, + private val PRICE_IMPACT_AMOUNT_MIN_THRESHOLD = 25.toBigDecimal() // in USD + private val PRICE_IMPACT_AMOUNT_MAX_THRESHOLD = 5000.toBigDecimal() // in USD + private val PRICE_IMPACT_LOW_THRESHOLD = 0.1.toBigDecimal() // 10% + private val PRICE_IMPACT_HIGH_THRESHOLD = 0.5.toBigDecimal() // 50% + + @Suppress("ComplexMethod", "LongParameterList") + internal fun calculatePriceImpact( + quoteContent: SwapQuoteUM.Content?, swapDirection: SwapDirection, + primaryFiatRateUSD: BigDecimal?, + secondaryFiatRateUSD: BigDecimal?, primaryCryptoCurrencyStatus: CryptoCurrencyStatus, - secondaryCryptoCurrencyStatus: CryptoCurrencyStatus, - ): TextReference? { + secondaryCryptoCurrencyStatus: CryptoCurrencyStatus?, + ): PriceImpact? { + if (quoteContent == null) return null + + val fromAmount = quoteContent.fromAmount ?: return null + val toAmount = quoteContent.toAmount + val (fromRate, toRate) = if (swapDirection == SwapDirection.Direct) { - primaryCryptoCurrencyStatus.value.fiatRate to secondaryCryptoCurrencyStatus.value.fiatRate + primaryCryptoCurrencyStatus.value.fiatRate to secondaryCryptoCurrencyStatus?.value?.fiatRate } else { - secondaryCryptoCurrencyStatus.value.fiatRate to primaryCryptoCurrencyStatus.value.fiatRate + secondaryCryptoCurrencyStatus?.value?.fiatRate to primaryCryptoCurrencyStatus.value.fiatRate } - val isRatesNull = fromRate.isNullOrZero() || toRate.isNullOrZero() - val isAmountNull = fromTokenAmount.isZero() || toTokenAmount.isZero() - if (isRatesNull || isAmountNull) return null - - val fromTokenFiatValue = fromTokenAmount.multiply(fromRate) - val toTokenFiatValue = toTokenAmount.multiply(toRate) - - val value = BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP) - - return stringReference("$(-${value.format { percent(withoutSign = false) }})").takeIf { - value > 0.1.toBigDecimal() + val fromRateUsd = if (swapDirection == SwapDirection.Direct) { + primaryFiatRateUSD + } else { + secondaryFiatRateUSD } + + val fromTokenFiatValue = fromRate?.let { fromAmount.multiply(fromRate).orZero() } + val toTokenFiatValue = toRate?.let { toAmount.multiply(toRate) } + + val isFromNotZero = fromTokenFiatValue != null && fromTokenFiatValue != BigDecimal.ZERO + val isToNotZero = toTokenFiatValue != null && toTokenFiatValue != BigDecimal.ZERO + + val value = if (isFromNotZero && isToNotZero) { + BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP) + } else { + null + } + + val fromAmountUSD = fromAmount.multiply(fromRateUsd.orZero()) + + val type = when { + value == null -> PriceImpact.Type.NONE + value < PRICE_IMPACT_LOW_THRESHOLD -> PriceImpact.Type.LOW + value in PRICE_IMPACT_LOW_THRESHOLD..PRICE_IMPACT_HIGH_THRESHOLD -> PriceImpact.Type.MEDIUM + else -> PriceImpact.Type.HIGH + } + + val amountSignificance = when { + fromAmountUSD <= PRICE_IMPACT_AMOUNT_MIN_THRESHOLD -> PriceImpact.AmountSignificance.LOW + fromAmountUSD > PRICE_IMPACT_AMOUNT_MAX_THRESHOLD -> PriceImpact.AmountSignificance.HIGH + else -> PriceImpact.AmountSignificance.MEDIUM + } + + return PriceImpact( + value = stringReference("(${StringsSigns.MINUS}${value.format { percent() }})"), + amountSignificance = amountSignificance, + type = type, + ) + } + + fun isHighPriceImpact(amountUM: SwapAmountUM): Boolean { + val priceImpact = (amountUM as? SwapAmountUM.Content)?.priceImpact ?: return false + return priceImpact.shouldDisableButton() } fun calculateRate(fromAmount: BigDecimal, toAmount: BigDecimal, toAmountDecimals: Int): BigDecimal { diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt index 6bfb4de5bd..810a528c2d 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt @@ -58,7 +58,6 @@ internal class SwapAmountFieldConverter( subtitleEllipsisLeft = subtitles.subtitleEllipsisLeft, subtitleRight = subtitles.subtitleRight, subtitleEllipsisRight = subtitles.subtitleEllipsisRight, - priceImpact = null, isClickEnabled = true, amountField = AmountStateConverter( clickIntents = clickIntents, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt index e2d648a6c9..51d33f01b5 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountPrimaryReadyStateTransformer.kt @@ -64,6 +64,7 @@ internal class SwapAmountPrimaryReadyStateTransformer( appCurrency = appCurrency, isShowBestRateAnimation = isShowBestRateAnimation, isShowFCAWarning = false, + priceImpact = null, swapRateMode = (prevState as? SwapAmountUM.Content)?.swapRateMode ?: SwapRateMode.FLOAT_ONLY, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt index cc49a10c46..2fb41d9e71 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSecondaryReadyStateTransformer.kt @@ -80,6 +80,7 @@ internal class SwapAmountSecondaryReadyStateTransformer( appCurrency = appCurrency, isShowBestRateAnimation = isShowBestRateAnimation, isShowFCAWarning = false, + priceImpact = null, swapRateMode = swapRateMode, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index 627c8e5881..8caee7dee2 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -7,17 +7,18 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils.calculatePriceImpact import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountErrorConverter import com.tangem.features.swap.v2.impl.amount.model.converter.SwapAmountUpdateSubtitleConverter -import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA -import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer +import java.math.BigDecimal +@Suppress("LongParameterList") internal class SwapAmountSelectQuoteTransformer( private val quoteUM: SwapQuoteUM, private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, @@ -26,35 +27,64 @@ internal class SwapAmountSelectQuoteTransformer( private val isBalanceHidden: Boolean, private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null, private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null, + private val primaryFiatRateUSD: BigDecimal?, + private val secondaryFiatRateUSD: BigDecimal?, ) : Transformer { - @Suppress("CyclomaticComplexMethod", "LongMethod") override fun transform(prevState: SwapAmountUM): SwapAmountUM { if (prevState !is SwapAmountUM.Content) return prevState - val isPrimarySelected = prevState.selectedAmountType == SwapAmountType.From - val isSecondarySelected = prevState.selectedAmountType == SwapAmountType.To - - val primaryProviderErrorConverter = SwapAmountErrorConverter( - cryptoCurrency = prevState.primaryCryptoCurrencyStatus.currency, - ) - val secondaryProviderErrorConverter = prevState.secondaryCryptoCurrencyStatus?.let { - SwapAmountErrorConverter(cryptoCurrency = it.currency) - } - val quoteContent = quoteUM as? SwapQuoteUM.Content - val fromAmount = quoteContent?.fromAmount - val toAmount = quoteContent?.toAmount - - val primarySwapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content - val secondarySwapAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content val subtitleConverter = SwapAmountUpdateSubtitleConverter( selectedAmountType = prevState.selectedAmountType, isBalanceHidden = isBalanceHidden, ) - val newPrimaryAmount = when { + val newPrimaryAmount = getPrimaryAmount( + prevState = prevState, + quoteContent = quoteContent, + subtitleConverter = subtitleConverter, + ) + + val newSecondaryAmount = getSecondaryAmount( + prevState = prevState, + quoteContent = quoteContent, + subtitleConverter = subtitleConverter, + ) + + val priceImpact = calculatePriceImpact( + quoteContent = quoteContent, + swapDirection = prevState.swapDirection, + primaryFiatRateUSD = primaryFiatRateUSD, + secondaryFiatRateUSD = secondaryFiatRateUSD, + primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, + secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, + ) + + return prevState.copy( + isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, + selectedQuote = quoteUM, + isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, + primaryAmount = newPrimaryAmount, + priceImpact = priceImpact, + secondaryAmount = newSecondaryAmount, + ) + } + + private fun getPrimaryAmount( + prevState: SwapAmountUM.Content, + quoteContent: SwapQuoteUM.Content?, + subtitleConverter: SwapAmountUpdateSubtitleConverter, + ): SwapAmountFieldUM { + val isPrimarySelected = prevState.selectedAmountType == SwapAmountType.From + val primarySwapAmountField = prevState.primaryAmount as? SwapAmountFieldUM.Content + + val primaryProviderErrorConverter = SwapAmountErrorConverter( + cryptoCurrency = prevState.primaryCryptoCurrencyStatus.currency, + ) + val fromAmount = quoteContent?.fromAmount + return when { fromAmount != null && primaryMaximumAmountBoundary != null -> { primarySwapAmountField?.let { fromField -> subtitleConverter.updateSubtitles( @@ -103,22 +133,26 @@ internal class SwapAmountSelectQuoteTransformer( } else -> prevState.primaryAmount } + } - val newSecondaryAmount = if ( + @Suppress("CyclomaticComplexMethod") + private fun getSecondaryAmount( + prevState: SwapAmountUM.Content, + quoteContent: SwapQuoteUM.Content?, + subtitleConverter: SwapAmountUpdateSubtitleConverter, + ): SwapAmountFieldUM { + val isSecondarySelected = prevState.selectedAmountType == SwapAmountType.To + val secondaryProviderErrorConverter = prevState.secondaryCryptoCurrencyStatus?.let { + SwapAmountErrorConverter(cryptoCurrency = it.currency) + } + val fromAmount = quoteContent?.fromAmount + val toAmount = quoteContent?.toAmount + val secondarySwapAmountField = prevState.secondaryAmount as? SwapAmountFieldUM.Content + return if ( prevState.secondaryCryptoCurrencyStatus != null && secondaryMaximumAmountBoundary != null && secondarySwapAmountField != null ) { - val fromAmountForPriceImpact = fromAmount - ?: (prevState.primaryAmount.amountField as? AmountState.Data) - ?.amountTextField?.cryptoAmount?.value.orZero() - val priceImpact = calculatePriceImpact( - swapDirection = prevState.swapDirection, - fromTokenAmount = fromAmountForPriceImpact, - toTokenAmount = toAmount.orZero(), - primaryCryptoCurrencyStatus = prevState.primaryCryptoCurrencyStatus, - secondaryCryptoCurrencyStatus = prevState.secondaryCryptoCurrencyStatus, - ) val isAmountEmpty = toAmount == null val secondaryAmountError = if (isSecondarySelected) { (quoteUM as? SwapQuoteUM.Error)?.expressError @@ -166,19 +200,10 @@ internal class SwapAmountSelectQuoteTransformer( isAmountEmpty = isAmountEmpty, displayAmount = toAmount, ).copy( - priceImpact = priceImpact, amountField = secondaryAmountFieldWithError, ) } else { prevState.secondaryAmount } - - return prevState.copy( - isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, - selectedQuote = quoteUM, - isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, - primaryAmount = newPrimaryAmount, - secondaryAmount = newSecondaryAmount, - ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt index 9b84a536bf..fb4abed9ee 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSetQuotesTransformer.kt @@ -18,6 +18,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal +@Suppress("LongParameterList") internal class SwapAmountSetQuotesTransformer( private val quotes: List, private val secondaryMaximumAmountBoundary: EnterAmountBoundary?, @@ -27,6 +28,8 @@ internal class SwapAmountSetQuotesTransformer( private val isBalanceHidden: Boolean, private val primaryMaximumAmountBoundary: EnterAmountBoundary? = null, private val primaryMinimumAmountBoundary: EnterAmountBoundary? = null, + private val primaryFiatRateUSD: BigDecimal?, + private val secondaryFiatRateUSD: BigDecimal?, ) : Transformer { override fun transform(prevState: SwapAmountUM): SwapAmountUM { @@ -53,6 +56,8 @@ internal class SwapAmountSetQuotesTransformer( val updatedState = SwapAmountSelectQuoteTransformer( quoteUM = selectedQuote, + primaryFiatRateUSD = primaryFiatRateUSD, + secondaryFiatRateUSD = secondaryFiatRateUSD, secondaryMaximumAmountBoundary = secondaryMaximumAmountBoundary, secondaryMinimumAmountBoundary = secondaryMinimumAmountBoundary, isNeedApplyFCARestrictions = isNeedApplyFcaRestrictions && diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index d75eb733a2..6db639d48a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -37,9 +37,9 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.swap.v2.impl.R -import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM import com.tangem.domain.swap.models.SwapAmountType +import com.tangem.features.swap.v2.impl.R +import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent @@ -86,7 +86,7 @@ internal fun SwapAmountBlockContent( val quoteContent = amountUM.selectedQuote as? SwapQuoteUM.Content val isBestRate = quoteContent?.diffPercent is SwapQuoteUM.Content.DifferencePercent.Best SwapChooseProviderContent( - isBestRate = isBestRate, + isBestRate = isBestRate && amountUM.priceImpact?.shouldShowWarning() != true, isSingleProvider = quoteContent?.isSingleProvider == true, showBestRateAnimation = amountUM.isShowBestRateAnimation, expressProvider = amountUM.selectedQuote.provider, @@ -119,13 +119,6 @@ private fun ConstraintLayoutScope.SwapAmountBlock( start.linkTo(parent.start) end.linkTo(parent.end) }, - extraContent = { - SwapPriceImpact( - amountFieldUM = amountUM.primaryAmount, - selectedAmountType = amountUM.selectedAmountType, - onInfoClick = onInfoClick, - ) - }, ) AmountBlockV2( amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( @@ -140,35 +133,33 @@ private fun ConstraintLayoutScope.SwapAmountBlock( end.linkTo(parent.end) }, extraContent = { - SwapPriceImpact( - amountFieldUM = amountUM.secondaryAmount, - selectedAmountType = amountUM.selectedAmountType, - onInfoClick = onInfoClick, - ) + if (amountUM.secondaryAmount.amountType == SwapAmountType.To) { + SwapPriceImpact( + priceImpact = amountUM.priceImpact, + onInfoClick = onInfoClick, + ) + } }, ) } @Composable -private fun SwapPriceImpact( - amountFieldUM: SwapAmountFieldUM, - selectedAmountType: SwapAmountType, - onInfoClick: () -> Unit, -) { - if (amountFieldUM.amountType == selectedAmountType) return - - val priceImpact = (amountFieldUM as? SwapAmountFieldUM.Content)?.priceImpact - val iconColor = if (priceImpact != null) { - TangemTheme.colors.icon.attention - } else { - TangemTheme.colors.icon.informative +private fun SwapPriceImpact(priceImpact: PriceImpact?, onInfoClick: () -> Unit) { + val iconColor = when (priceImpact?.type) { + PriceImpact.Type.MEDIUM -> TangemTheme.colors.icon.attention + PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning + else -> TangemTheme.colors.icon.informative } - if (priceImpact != null) { + if (priceImpact != null && priceImpact.type.ordinal > PriceImpact.Type.LOW.ordinal) { Text( - text = priceImpact.resolveReference(), + text = priceImpact.value.resolveReference(), style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.attention, + color = when (priceImpact.type) { + PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning + PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention + else -> TangemTheme.colors.text.tertiary + }, ) } Icon( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index 6f354c847a..f15aca16d8 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -12,11 +12,11 @@ import com.tangem.domain.express.models.ExpressRateType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network +import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.swap.models.SwapCurrencies import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.swap.models.SwapRateMode import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM -import com.tangem.domain.swap.models.SwapAmountType import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.utils.StringsSigns @@ -95,6 +95,7 @@ internal data object SwapAmountContentPreview { appCurrency = AppCurrency.Default, isShowBestRateAnimation = false, isShowFCAWarning = false, + priceImpact = null, swapRateMode = SwapRateMode.FLOAT_ONLY, ) @@ -105,7 +106,6 @@ internal data object SwapAmountContentPreview { title = stringReference("Tether"), subtitleLeft = stringReference("11 101,123123456 BTC"), subtitleRight = stringReference(" ${StringsSigns.DOT} 1 212,12 $"), - priceImpact = null, isClickEnabled = false, subtitleEllipsisLeft = TextEllipsis.OffsetEnd(3), subtitleEllipsisRight = TextEllipsis.OffsetEnd(1), @@ -116,7 +116,6 @@ internal data object SwapAmountContentPreview { accountTitleUM = AccountTitleUM.Text(stringReference("Amount to receive")), ), title = stringReference("Shiba Inu"), - priceImpact = stringReference("(-10%)"), subtitleLeft = TextReference.EMPTY, subtitleRight = TextReference.EMPTY, isClickEnabled = false, @@ -135,6 +134,7 @@ internal data object SwapAmountContentPreview { isPrimaryButtonEnabled = true, isShowBestRateAnimation = false, isShowFCAWarning = true, + priceImpact = null, swapRateMode = SwapRateMode.FLOAT_AND_FIXED, ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt index f42349e6c2..98d185084b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/ConfirmData.kt @@ -6,6 +6,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.swap.models.SwapAmountType import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import java.math.BigDecimal @@ -24,4 +25,5 @@ internal data class ConfirmData( val quote: SwapQuoteUM?, val rateType: ExpressRateType?, val amountType: SwapAmountType, + val priceImpact: PriceImpact?, ) \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt index db6c8d8dbe..ecd9813de0 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/SwapNotificationsComponent.kt @@ -9,6 +9,7 @@ import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact import java.math.BigDecimal import com.tangem.features.swap.v2.impl.notifications.model.SwapNotificationsModel import com.tangem.features.swap.v2.impl.notifications.ui.swapNotifications @@ -49,6 +50,7 @@ internal class SwapNotificationsComponent( val userWalletId: UserWalletId? = null, val enteredFromAmount: BigDecimal? = null, val fromCryptoCurrencyStatus: CryptoCurrencyStatus? = null, + val priceImpact: PriceImpact? = null, ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt index fc14191b9b..de64062b75 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/entity/SwapNotificationUM.kt @@ -82,5 +82,16 @@ internal object SwapNotificationUM { onClick = onConfirmClick, ), ) + + data object HighPriceImpact : Warning( + title = resourceReference(R.string.swapping_high_price_impact_title), + subtitle = resourceReference(R.string.swapping_high_price_impact_text), + ) + + data object TradeTooHigh : Warning( + title = resourceReference(R.string.swapping_trade_too_large_title), + subtitle = resourceReference(R.string.swapping_trade_too_large_text), + iconResId = R.drawable.ic_alert_circle_24, + ) } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt index 73dc591264..7a265478bb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/model/SwapNotificationsModel.kt @@ -11,13 +11,13 @@ import com.tangem.domain.express.models.ExpressError import com.tangem.domain.transaction.usecase.ValidateTransactionUseCase import com.tangem.domain.utils.convertToSdkAmount import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger +import com.tangem.features.swap.v2.impl.amount.entity.PriceImpact import com.tangem.features.swap.v2.impl.notifications.DefaultSwapNotificationsUpdateTrigger import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsComponent.Params.SwapNotificationData import com.tangem.features.swap.v2.impl.notifications.SwapNotificationsUpdateListener import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import java.math.BigDecimal import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -26,6 +26,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import java.math.BigDecimal import javax.inject.Inject @ModelScoped @@ -68,9 +69,16 @@ internal class SwapNotificationsModel @Inject constructor( addInsufficientFundsNotification() addExpressErrorNotification() addDestinationTagRequiredNotification() + maybeAddPriceImpactNotification() } - swapNotificationsUpdateTrigger.callbackHasError(notifications.isNotEmpty()) + val hasErrorNotification = notifications + .filterNot { notification -> + notification == SwapNotificationUM.Warning.TradeTooHigh || + notification == SwapNotificationUM.Warning.HighPriceImpact + } + .isNotEmpty() + swapNotificationsUpdateTrigger.callbackHasError(hasErrorNotification) uiState.value = notifications.toImmutableList() } @@ -135,4 +143,17 @@ internal class SwapNotificationsModel @Inject constructor( add(errorNotification) } + + private fun MutableList.maybeAddPriceImpactNotification() { + val priceImpact = notificationData.priceImpact ?: return + if (!priceImpact.shouldShowWarning()) return + + val notification = when (priceImpact.type) { + PriceImpact.Type.HIGH -> SwapNotificationUM.Warning.TradeTooHigh + PriceImpact.Type.MEDIUM -> SwapNotificationUM.Warning.HighPriceImpact + else -> return + } + + add(notification) + } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/ui/SwapNotificationsContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/ui/SwapNotificationsContent.kt index 9f1ec2d283..ae06a1fc78 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/ui/SwapNotificationsContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/notifications/ui/SwapNotificationsContent.kt @@ -38,6 +38,7 @@ internal fun LazyListScope.swapNotifications( else -> TangemTheme.colors.button.disabled }, iconTint = when (item) { + is SwapNotificationUM.Warning.TradeTooHigh -> TangemTheme.colors.icon.warning is SwapNotificationUM.Error, is NotificationUM.Error.TokenExceedsBalance, is NotificationUM.Warning, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt index 98b57000b7..1b837de7c9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/SendWithSwapConfirmComponent.kt @@ -139,6 +139,7 @@ internal class SendWithSwapConfirmComponent @AssistedInject constructor( userWalletId = params.userWallet.walletId, enteredFromAmount = model.confirmData.enteredFromAmount, fromCryptoCurrencyStatus = model.confirmData.fromCryptoCurrencyStatus, + priceImpact = model.confirmData.priceImpact, ), ), ) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 3c685ed2c6..34b87c65bb 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -52,6 +52,7 @@ import com.tangem.features.swap.v2.api.subcomponents.SwapAmountUpdateTrigger import com.tangem.features.swap.v2.impl.R import com.tangem.features.swap.v2.impl.amount.SwapAmountReduceTrigger import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM +import com.tangem.features.swap.v2.impl.amount.model.SwapAmountQuoteUtils import com.tangem.features.swap.v2.impl.common.ConfirmData import com.tangem.features.swap.v2.impl.common.SwapAlertFactory import com.tangem.features.swap.v2.impl.common.SwapUtils.INCREASE_GAS_LIMIT_FOR_CEX @@ -126,7 +127,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( private val feeUMV2 get() = uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content - val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus + private val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus val secondaryCurrency: CryptoCurrency = requireNotNull(amountUM?.secondaryCryptoCurrencyStatus?.currency) { "Crypto currency must not be null" } @@ -166,6 +167,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( quote = amountUM?.selectedQuote, rateType = amountUM?.swapRateType, amountType = amountUM?.selectedAmountType ?: SwapAmountType.From, + priceImpact = amountUM?.priceImpact, ) } @@ -444,6 +446,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( userWalletId = params.userWallet.walletId, enteredFromAmount = confirmData.enteredFromAmount, fromCryptoCurrencyStatus = confirmData.fromCryptoCurrencyStatus, + priceImpact = confirmData.priceImpact, ), ) uiState.transformerUpdate( @@ -461,9 +464,11 @@ internal class SendWithSwapConfirmModel @Inject constructor( uiState.update { state -> val feeUM = state.feeSelectorUM as? FeeSelectorUM.Content val isTransactionInProcess = (state.confirmUM as? ConfirmUM.Content)?.isTransactionInProcess == true + val isHighPriceImpact = SwapAmountQuoteUtils.isHighPriceImpact(state.amountUM) + val isPrimaryButtonEnabled = !hasError && feeUM != null && !isTransactionInProcess && !isHighPriceImpact state.copy( confirmUM = (state.confirmUM as? ConfirmUM.Content)?.copy( - isPrimaryButtonEnabled = !hasError && feeUM != null && !isTransactionInProcess, + isPrimaryButtonEnabled = isPrimaryButtonEnabled, ) ?: state.confirmUM, ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 2a22cca812..405c0aa43b 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -68,7 +68,6 @@ import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* import com.tangem.lib.crypto.BlockchainUtils.SOLANA_TRANSACTION_SIZE_THRESHOLD_BYTES import com.tangem.utils.coroutines.runSuspendCatching -import com.tangem.utils.extensions.orZero import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -2457,7 +2456,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( if (fromQuoteStatus == null) return PriceImpact.Empty val fromTokenFiatValue = fromTokenAmount.multiply(fromQuoteStatus.fiatRate) - val toTokenFiatValue = toTokenAmount.multiply(toRate.orZero()) + val toTokenFiatValue = toRate?.let { toTokenAmount.multiply(toRate) } ?: return PriceImpact.Empty + val value = BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP) val fromAmountUSD = if (fromQuoteStatus.fiatRateUSD != BigDecimal.ZERO) { @@ -2467,7 +2467,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( } val amountSignificance = when { - fromAmountUSD < PRICE_IMPACT_AMOUNT_MIN_THRESHOLD -> PriceImpact.AmountSignificance.LOW + fromAmountUSD <= PRICE_IMPACT_AMOUNT_MIN_THRESHOLD -> PriceImpact.AmountSignificance.LOW fromAmountUSD > PRICE_IMPACT_AMOUNT_MAX_THRESHOLD -> PriceImpact.AmountSignificance.HIGH else -> PriceImpact.AmountSignificance.MEDIUM } @@ -2552,7 +2552,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( companion object { private const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% - private val PRICE_IMPACT_AMOUNT_MIN_THRESHOLD = 50.toBigDecimal() // in USD + private val PRICE_IMPACT_AMOUNT_MIN_THRESHOLD = 25.toBigDecimal() // in USD private val PRICE_IMPACT_AMOUNT_MAX_THRESHOLD = 5000.toBigDecimal() // in USD private val PRICE_IMPACT_LOW_THRESHOLD = 0.1.toBigDecimal() // 10% private val PRICE_IMPACT_HIGH_THRESHOLD = 0.5.toBigDecimal() // 50% diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index fa1c9bdf73..a630100e2e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -66,6 +66,10 @@ data class PriceImpact( return type == Type.HIGH && amountSignificance == AmountSignificance.HIGH } + fun shouldShowWarning(): Boolean { + return type.ordinal > Type.LOW.ordinal || amountSignificance.ordinal > AmountSignificance.LOW.ordinal + } + companion object { val Empty = PriceImpact( value = BigDecimal.ZERO, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6ba8b6a6e4..b6613eba32 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -387,26 +387,26 @@ internal class StateBuilder( .formatToUIRepresentation() .appendApproximateSign(), ), - amountEquivalent = combinedReference( - getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat), - stringReference(StringsSigns.WHITE_SPACE), - styledStringReference( - priceImpact.value.format { - percent(withoutSign = false) - }, - spanStyleReference = { - SpanStyle( - color = when (priceImpact.type) { - PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning - PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention - PriceImpact.Type.LOW, - PriceImpact.Type.NONE, - -> TangemTheme.colors.text.tertiary - }, - ) - }, - ), - ), + amountEquivalent = if (priceImpact.type.ordinal > PriceImpact.Type.LOW.ordinal) { + combinedReference( + getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat), + stringReference(StringsSigns.WHITE_SPACE), + styledStringReference( + value = "(${StringsSigns.MINUS}${priceImpact.value.format { percent() }})", + spanStyleReference = { + SpanStyle( + color = when (priceImpact.type) { + PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning + PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention + else -> TangemTheme.colors.text.tertiary + }, + ) + }, + ), + ) + } else { + getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat) + }, token = toCurrencyStatus, tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, coinId = toCurrencyStatus.currency.network.backendId, @@ -436,7 +436,7 @@ internal class StateBuilder( ), changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), providerState = swapProvider.convertToContentClickableProviderState( - isBestRate = bestRatedProviderId == swapProvider.providerId, + isBestRate = bestRatedProviderId == swapProvider.providerId && !priceImpact.shouldShowWarning(), fromTokenInfo = quoteModel.fromTokenInfo, toTokenInfo = quoteModel.toTokenInfo, isNeedBestRateBadge = isNeedBestRateBadge, @@ -444,7 +444,7 @@ internal class StateBuilder( onProviderClick = actions.onProviderClick, needApplyFCARestrictions = needApplyFCARestrictions, ), - priceImpact = quoteModel.priceImpact, + priceImpact = priceImpact, tosState = createTosState(swapProvider), shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toCurrencyStatus.currency), )