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..3301fe3f9d 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 PriceImpact.Empty + + val fromAmount = quoteContent.fromAmount ?: return PriceImpact.Empty + 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..8ffffab5a9 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/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, ) }