diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml
index 72e7efe111..5132421c98 100644
--- a/core/res/src/main/res/values-de/strings.xml
+++ b/core/res/src/main/res/values-de/strings.xml
@@ -694,7 +694,7 @@
Lege die Karte oder Ring an
Deine Wallet ist eingerichtet und einsatzbereit!
Wallet erfolgreich importiert
- Wiederherstellung %d%
+ Wiederherstellung %d%%
Du hast deine biometrischen Daten aktualisiert, scanne deine Karte oder Ring, um einzutreten
Dein Guthaben sollte höher sein als der Gebührenwert, um eine Überweisung zu tätigen
Unzureichende Mittel
diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml
index e4a52817fd..6dadf3d9d5 100644
--- a/core/res/src/main/res/values-ja/strings.xml
+++ b/core/res/src/main/res/values-ja/strings.xml
@@ -689,7 +689,7 @@
カードまたはリングをタップ
ウォレットは同期され、準備ができています。\n一部のトークンが見つかりませんか?
ウォレットのインポートが完了しました
- %d%を復元中
+ %d%%を復元中
生体認証を更新しました。カードまたはリングをスキャンして入ってください。
送金するには、残高が手数料額より高くなければなりません。
残高不足
diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml
index de83ee0e8f..c6642f4bfa 100644
--- a/core/res/src/main/res/values-pt-rBR/strings.xml
+++ b/core/res/src/main/res/values-pt-rBR/strings.xml
@@ -306,6 +306,7 @@
hora
Importar
Em andamento
+ Saldo insuficiente
Mais tarde
Saber mais
%1$s falta
@@ -692,6 +693,9 @@
Toque para digitalizar
Toque para assinar
Toque no Cartão ou no Anel
+ Sua carteira está sincronizada e pronta. \n Faltam alguns tokens?
+ Carteira importada com sucesso.
+ Restaurando %d%%
Você atualizou seus dados biométricos. Escaneie seu cartão ou toque a campainha para entrar.
Seu saldo deve ser superior ao valor da taxa para efetuar uma transferência.
Saldo insuficiente
@@ -752,6 +756,7 @@
Resultado
Veja tokens com capitalização de mercado inferior a 100 mil dólares.
Mostrar tokens
+ Criptomoedas, notícias e muito mais
Nenhum resultado
Selecione a rede
Selecione a carteira
@@ -824,9 +829,11 @@
Posição no ranking de criptomoedas entre todas as moedas com base na capitalização de mercado.
Posicionamento de mercado
Fornecimento máximo
+ Circulação e Fornecimento Máximo
O número máximo de moedas ou tokens que podem existir para uma determinada criptomoeda.
Fornecimento máximo
Métricas
+ Sem limite
Links oficiais
Desempenho de preço
Repositório
@@ -1101,6 +1108,12 @@
Configurações
Você não concedeu acesso à sua câmera.
Acesso à câmera negado
+ O token solicitado não foi adicionado à sua carteira. Adicione-o e tente novamente.
+ Token não adicionado
+ Desculpe, este código QR não pôde ser reconhecido.
+ Código QR não reconhecido
+ Esta rede não é compatível com nenhum dos tokens adicionados. Adicione um token compatível para enviar criptomoedas.
+ Nenhum token compatível encontrado
Não é necessário memorando
%1$s (%2$s) sobre %3$s rede
%1$s sobre %2$s rede
@@ -1246,6 +1259,11 @@
Limitação de transação
Opcional
Alinhe o seu código QR com o quadrado para escaneá-lo. Certifique-se de escanear corretamente. %s endereço de rede.
+ Ao utilizar uma taxa fixa, o valor que você recebe fica garantido no momento da troca. Isso protege você de variações de preço durante a transação.
+ Taxa fixa
+ Uma taxa flutuante significa que o valor final que você recebe pode variar ligeiramente com base nas condições de mercado entre o momento em que você inicia e conclui a operação de swap.
+ Taxa flutuante
+ A taxa é fixa.
Recente
Destinatário
Endereço inválido
@@ -1289,6 +1307,7 @@
O destinatário recebe %s
Tem certeza de que deseja cancelar a conversão? Seus dados anteriores serão apagados.
Remover conversão
+ Algo deu errado. Tente novamente.
Enviar com troca
Transação enviada
Prepare-se para escanear o Cartão ou Anel que deseja configurar.
@@ -1482,6 +1501,7 @@
Trocar essa quantidade de tokens selecionados causará um impacto significativo no preço e reduzirá seu resultado.
Alto impacto nos preços
Fundos insuficientes
+ Não há fundos suficientes para concluir esta transação. Reduza o valor a receber ou adicione mais fundos.
Conceder permissão
Trocar
Trocar...
@@ -1648,6 +1668,7 @@
Token em %%imagem%% %1$s rede
O %1$s (%2$sO token ) é a principal moeda da plataforma. %3$s rede e não pode ser ocultada enquanto você tiver outros tokens dessa rede na lista
Não foi possível ocultar %s
+ N/A
Mostrar código QR
Troque este token por outro em %1$s taxas de serviço a partir de fevereiro %2$s-%3$s.
Trocar com Changelly, %s tarifas
diff --git a/core/res/src/main/res/values-zh-rCN/strings.xml b/core/res/src/main/res/values-zh-rCN/strings.xml
index be6b9c60c8..9a07214403 100644
--- a/core/res/src/main/res/values-zh-rCN/strings.xml
+++ b/core/res/src/main/res/values-zh-rCN/strings.xml
@@ -6,7 +6,7 @@
解鎖
您的錢包已同步並準備就緒。\n缺少某些代幣?
錢包導入成功
- 恢復%d%
+ 恢復%d%%
天
月
週
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index 1823294875..d4a0a1480f 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -696,7 +696,7 @@
Tap the card or ring
Your wallet is synced and ready.\nSome tokens missing?
Wallet successfully imported
- Restoring %d%
+ Restoring %d%%
You have updated biometrics, scan your card or ring to enter
Your balance should be higher than the fee value to make a transfer
Not enough balance
@@ -1500,6 +1500,7 @@
Fee estimation error. Please send feedback to support.
You swap
Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.
+ You may receive significantly less due to low liquidity. Try a smaller amount or another provider.
High price impact
Insufficient funds
Not enough funds to complete this transaction. Reduce the amount to receive or add more funds.
@@ -1509,6 +1510,8 @@
You receive
Choose token
not available
+ Not enough liquidity for this trade.\nReduce the amount or choose another provider.
+ Trade too large
We would be happy to receive your feedback
Tangem Pay is now in beta
Card frozen
diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt
index 726604b2b1..2a200563fa 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt
@@ -16,7 +16,6 @@ object SwapTokenScreenTestTags {
const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON"
const val SELECT_TOKEN_ICON = "SWAP_TOKEN_SCREEN_SELECT_TOKEN_ICON"
const val RECEIVE_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT"
- const val RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT"
const val RECEIVE_FIAT_AMOUNT_INFORMATION_ICON = "SWAP_TOKEN_SCREEN_PRICE_IMPACT_INFORMATION_ICON"
const val SWAP_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_SWAP_FIAT_AMOUNT"
}
\ No newline at end of file
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 14b10c2aad..2a22cca812 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
@@ -17,6 +17,7 @@ import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldS
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.blockchainsdk.utils.toBlockchain
import com.tangem.blockchainsdk.utils.toNetworkId
+import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer
@@ -67,11 +68,12 @@ 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
import dagger.assisted.AssistedInject
import kotlinx.coroutines.coroutineScope
-import com.tangem.utils.logging.TangemLogger
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
@@ -1289,12 +1291,14 @@ internal class SwapInteractorImpl @AssistedInject constructor(
private suspend fun createEmptyAmountState(): SwapState {
val appCurrency = getSelectedAppCurrencyUseCase.unwrap()
return SwapState.EmptyAmountState(
- zeroAmountEquivalent = BigDecimal.ZERO.format {
- fiat(
- fiatCurrencyCode = appCurrency.code,
- fiatCurrencySymbol = appCurrency.symbol,
- )
- },
+ zeroAmountEquivalent = stringReference(
+ BigDecimal.ZERO.format {
+ fiat(
+ fiatCurrencyCode = appCurrency.code,
+ fiatCurrencySymbol = appCurrency.symbol,
+ )
+ },
+ ),
)
}
@@ -1523,7 +1527,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
val rates = getQuotes(fromToken.currency.id)
val fromTokenSwapInfo = TokenSwapInfo(
tokenAmount = amount,
- amountFiat = rates[fromToken.currency.id]?.multiply(amount.value)
+ amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value)
?: BigDecimal.ZERO,
cryptoCurrencyStatus = fromToken,
account = fromAccount,
@@ -1687,7 +1691,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
val rates = getQuotes(feeCurrencyId)
return rates[feeCurrencyId]?.let { rate ->
fees.map { fee ->
- rate.multiply(fee).format {
+ rate.fiatRate.multiply(fee).format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
@@ -1855,7 +1859,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
val rates = getQuotes(fromToken.currency.id)
val fromTokenSwapInfo = TokenSwapInfo(
tokenAmount = amount,
- amountFiat = rates[fromToken.currency.id]?.multiply(amount.value)
+ amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value)
?: BigDecimal.ZERO,
cryptoCurrencyStatus = fromToken,
account = fromAccount,
@@ -1963,21 +1967,21 @@ internal class SwapInteractorImpl @AssistedInject constructor(
tokenAmount = fromTokenAmount,
account = fromAccount,
cryptoCurrencyStatus = fromTokenStatus,
- amountFiat = rates[fromToken.id]?.multiply(fromTokenAmount.value)
+ amountFiat = rates[fromToken.id]?.fiatRate?.multiply(fromTokenAmount.value)
?: BigDecimal.ZERO,
),
toTokenInfo = TokenSwapInfo(
tokenAmount = toTokenAmount,
cryptoCurrencyStatus = toTokenStatus,
account = toAccount,
- amountFiat = rates[toToken.id]?.multiply(toTokenAmount.value)
+ amountFiat = rates[toToken.id]?.fiatRate?.multiply(toTokenAmount.value)
?: BigDecimal.ZERO,
),
priceImpact = calculatePriceImpact(
fromTokenAmount = fromTokenAmount.value,
- fromRate = rates[fromToken.id]?.toDouble() ?: 0.0,
+ fromQuoteStatus = rates[fromToken.id],
toTokenAmount = toTokenAmount.value,
- toRate = rates[toToken.id]?.toDouble() ?: 0.0,
+ toRate = rates[toToken.id]?.fiatRate,
),
swapDataModel = swapData,
swapProvider = provider,
@@ -2446,17 +2450,42 @@ internal class SwapInteractorImpl @AssistedInject constructor(
private fun calculatePriceImpact(
fromTokenAmount: BigDecimal,
- fromRate: Double,
+ fromQuoteStatus: QuoteStatus.Data?,
toTokenAmount: BigDecimal,
- toRate: Double,
+ toRate: BigDecimal?,
): PriceImpact {
- val fromTokenFiatValue = fromTokenAmount.multiply(fromRate.toBigDecimal())
- val toTokenFiatValue = toTokenAmount.multiply(toRate.toBigDecimal())
- val value = (BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP)).toFloat()
- return PriceImpact.Value(value)
+ if (fromQuoteStatus == null) return PriceImpact.Empty
+
+ val fromTokenFiatValue = fromTokenAmount.multiply(fromQuoteStatus.fiatRate)
+ val toTokenFiatValue = toTokenAmount.multiply(toRate.orZero())
+ val value = BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP)
+
+ val fromAmountUSD = if (fromQuoteStatus.fiatRateUSD != BigDecimal.ZERO) {
+ fromTokenAmount.multiply(fromQuoteStatus.fiatRateUSD)
+ } else {
+ PRICE_IMPACT_AMOUNT_MIN_THRESHOLD
+ }
+
+ 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
+ }
+
+ val type = when {
+ 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
+ }
+
+ return PriceImpact(
+ value = value,
+ amountSignificance = amountSignificance,
+ type = type,
+ )
}
- private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map {
+ private suspend fun getQuotes(vararg ids: CryptoCurrency.ID): Map {
val set = ids.mapNotNullTo(destination = hashSetOf(), transform = CryptoCurrency.ID::rawCurrencyId)
.getQuotesOrEmpty()
@@ -2465,7 +2494,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
val found = set.find { it.rawCurrencyId == id.rawCurrencyId && it.value is QuoteStatus.Data }
?: return@mapNotNull null
- id to (found.value as QuoteStatus.Data).fiatRate
+ id to found.value as QuoteStatus.Data
}
.toMap()
}
@@ -2523,6 +2552,10 @@ 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_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%
private const val INFINITY_SYMBOL = "∞"
}
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 c735c821d4..fa1c9bdf73 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
@@ -1,6 +1,8 @@
package com.tangem.feature.swap.domain.models.ui
+import androidx.compose.runtime.Immutable
import com.tangem.blockchain.common.transaction.Fee
+import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck
@@ -36,7 +38,7 @@ sealed interface SwapState {
val swapProvider: SwapProvider,
) : SwapState
- data class EmptyAmountState(val zeroAmountEquivalent: String) : SwapState
+ data class EmptyAmountState(val zeroAmountEquivalent: TextReference) : SwapState
data class SwapError(
val fromTokenInfo: TokenSwapInfo,
@@ -45,18 +47,31 @@ sealed interface SwapState {
) : SwapState
}
-sealed class PriceImpact {
+@Immutable
+data class PriceImpact(
+ val value: BigDecimal,
+ val amountSignificance: AmountSignificance,
+ val type: Type,
+) {
- abstract val value: Float
+ enum class Type {
+ NONE, LOW, MEDIUM, HIGH
+ }
- data class Empty(override val value: Float = 0f) : PriceImpact()
+ enum class AmountSignificance {
+ LOW, MEDIUM, HIGH
+ }
- data class Value(override val value: Float) : PriceImpact()
-
- fun getIntPercentValue() = (value * HUNDRED_PERCENTS).toInt()
+ fun shouldDisableButton(): Boolean {
+ return type == Type.HIGH && amountSignificance == AmountSignificance.HIGH
+ }
companion object {
- private const val HUNDRED_PERCENTS = 100
+ val Empty = PriceImpact(
+ value = BigDecimal.ZERO,
+ amountSignificance = AmountSignificance.LOW,
+ type = Type.NONE,
+ )
}
}
diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt
index 789816698c..53cf08c449 100644
--- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt
+++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt
@@ -1700,7 +1700,7 @@ internal class SwapModel @Inject constructor(
onReceiveCardWarningClick = {
val selectedProvider = dataState.selectedProvider ?: return@UiActions
val currencySymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return@UiActions
- val isPriceImpact = uiState.priceImpact is PriceImpact.Value
+ val isPriceImpact = uiState.priceImpact.type != PriceImpact.Type.NONE
showSwapInfoAlert(isPriceImpact, currencySymbol, selectedProvider)
},
onLinkClick = urlOpener::openUrl,
diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt
index 5bf13c6e4d..6586b11550 100644
--- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt
+++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt
@@ -118,6 +118,7 @@ internal class SwapNotificationsFactory(
maybeAddNetworkFeeCoverageWarning(quoteModel, selectedFeeType)
maybeAddUnableCoverFeeWarning(quoteModel, fromToken, hideFee)
maybeAddTransactionInProgressWarning(quoteModel)
+ maybeAddPriceImpactNotification(quoteModel.priceImpact)
}
return warnings.toPersistentList()
}
@@ -143,6 +144,18 @@ internal class SwapNotificationsFactory(
}
}
+ private fun MutableList.maybeAddPriceImpactNotification(priceImpact: PriceImpact) {
+ if (priceImpact.amountSignificance == PriceImpact.AmountSignificance.LOW) return
+
+ val notification = when (priceImpact.type) {
+ PriceImpact.Type.HIGH -> SwapNotificationUM.Warning.TradeTooHigh
+ PriceImpact.Type.MEDIUM -> SwapNotificationUM.Warning.HighPriceImpact
+ else -> return
+ }
+
+ add(notification)
+ }
+
@Suppress("LongMethod")
private fun MutableList.maybeAddDomainWarnings(
quoteModel: SwapState.QuotesLoadedState,
diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt
index f3d9a7bc7d..0afbd2da4b 100644
--- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt
+++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt
@@ -1,6 +1,7 @@
package com.tangem.feature.swap.models
import androidx.annotation.DrawableRes
+import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState
@@ -47,7 +48,7 @@ sealed class SwapCardState {
data class SwapCardData(
@DrawableRes val networkIconRes: Int?,
val type: TransactionCardType,
- val amountEquivalent: String?,
+ val amountEquivalent: TextReference?,
val token: CryptoCurrencyStatus?,
val coinId: String?,
val amountTextFieldValue: TextFieldValue?,
@@ -61,7 +62,7 @@ sealed class SwapCardState {
data class Empty(
val type: TransactionCardType,
- val amountEquivalent: String?,
+ val amountEquivalent: TextReference?,
val amountTextFieldValue: TextFieldValue?,
val canSelectAnotherToken: Boolean = false,
) : SwapCardState()
@@ -75,6 +76,7 @@ data class SwapButton(
val onClick: () -> Unit,
)
+@Immutable
sealed interface TransactionCardType {
val accountTitleUM: AccountTitleUM?
diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt
index e158fd5f3b..96a0293a82 100644
--- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt
+++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt
@@ -194,6 +194,17 @@ 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,
+ )
}
sealed class Info(
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 e65fefdf6c..6ba8b6a6e4 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
@@ -1,5 +1,6 @@
package com.tangem.feature.swap.ui
+import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.ui.account.AccountTitleUM
@@ -12,10 +13,8 @@ import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.extensions.*
-import com.tangem.core.ui.format.bigdecimal.anyDecimals
-import com.tangem.core.ui.format.bigdecimal.crypto
-import com.tangem.core.ui.format.bigdecimal.fiat
-import com.tangem.core.ui.format.bigdecimal.format
+import com.tangem.core.ui.format.bigdecimal.*
+import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
@@ -37,6 +36,7 @@ import com.tangem.feature.swap.models.states.*
import com.tangem.feature.swap.presentation.R
import com.tangem.feature.swap.utils.formatToUIRepresentation
import com.tangem.utils.Provider
+import com.tangem.utils.StringsSigns
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.StringsSigns.TILDE_SIGN
import kotlinx.collections.immutable.ImmutableList
@@ -127,7 +127,7 @@ internal class StateBuilder(
onSuccess = actions.onSuccess,
providerState = ProviderState.Empty(),
shouldShowMaxAmount = false,
- priceImpact = PriceImpact.Empty(),
+ priceImpact = PriceImpact.Empty,
isInsufficientFunds = false,
)
}
@@ -141,7 +141,7 @@ internal class StateBuilder(
sendCardData = SwapCardState.SwapCardData(
type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable),
amountTextFieldValue = null,
- amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
+ amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
token = fromToken,
tokenIconUrl = fromToken.currency.iconUrl,
coinId = fromToken.currency.network.backendId,
@@ -154,7 +154,7 @@ internal class StateBuilder(
),
receiveCardData = SwapCardState.Empty(
type = TransactionCardType.ReadOnly(),
- amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
+ amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
amountTextFieldValue = TextFieldValue(
text = "0",
),
@@ -169,7 +169,7 @@ internal class StateBuilder(
onClick = { },
),
changeCardsButtonState = ChangeCardsButtonState.DISABLED,
- priceImpact = PriceImpact.Empty(),
+ priceImpact = PriceImpact.Empty,
)
}
@@ -187,7 +187,7 @@ internal class StateBuilder(
accountTitleUM = getFromCardAccountTitle(fromAccount),
),
amountTextFieldValue = null,
- amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
+ amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
token = fromToken,
tokenIconUrl = fromToken.currency.iconUrl,
coinId = fromToken.currency.network.backendId,
@@ -205,7 +205,7 @@ internal class StateBuilder(
amountTextFieldValue = TextFieldValue(
text = "0",
),
- amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
+ amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
token = toToken,
tokenIconUrl = toToken.currency.iconUrl,
coinId = toToken.currency.network.backendId,
@@ -226,7 +226,7 @@ internal class StateBuilder(
),
changeCardsButtonState = ChangeCardsButtonState.DISABLED,
providerState = ProviderState.Empty(),
- priceImpact = PriceImpact.Empty(),
+ priceImpact = PriceImpact.Empty,
)
}
@@ -294,7 +294,7 @@ internal class StateBuilder(
providerState = ProviderState.Loading(),
permissionState = uiStateHolder.permissionState,
changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS,
- priceImpact = PriceImpact.Empty(),
+ priceImpact = PriceImpact.Empty,
shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toToken),
)
}
@@ -360,6 +360,7 @@ internal class StateBuilder(
)
}
}
+ val priceImpact = quoteModel.priceImpact
return uiStateHolder.copy(
sendCardData = SwapCardState.SwapCardData(
type = sendInput,
@@ -386,7 +387,26 @@ internal class StateBuilder(
.formatToUIRepresentation()
.appendApproximateSign(),
),
- amountEquivalent = getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat),
+ 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
+ },
+ )
+ },
+ ),
+ ),
token = toCurrencyStatus,
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
coinId = toCurrencyStatus.currency.network.backendId,
@@ -410,7 +430,7 @@ internal class StateBuilder(
fee = feeState,
swapButton = SwapButton(
walletInteractionIcon = walletInterationIcon(userWalletProvider()),
- isEnabled = getSwapButtonEnabled(notifications),
+ isEnabled = getSwapButtonEnabled(notifications, priceImpact),
isHoldToConfirm = isHoldToConfirmEnabled,
onClick = actions.onSwapClick,
),
@@ -424,11 +444,7 @@ internal class StateBuilder(
onProviderClick = actions.onProviderClick,
needApplyFCARestrictions = needApplyFCARestrictions,
),
- priceImpact = if (quoteModel.priceImpact.value > PRICE_IMPACT_THRESHOLD) {
- quoteModel.priceImpact
- } else {
- PriceImpact.Empty()
- },
+ priceImpact = quoteModel.priceImpact,
tosState = createTosState(swapProvider),
shouldShowMaxAmount = shouldShowMaxAmount(fromToken, toCurrencyStatus.currency),
)
@@ -462,7 +478,7 @@ internal class StateBuilder(
quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included
}
- private fun getSwapButtonEnabled(notifications: ImmutableList): Boolean {
+ private fun getSwapButtonEnabled(notifications: ImmutableList, priceImpact: PriceImpact): Boolean {
return notifications.none { notification ->
notification is SwapNotificationUM.Error || notification is NotificationUM.Error ||
notification is SwapNotificationUM.Warning.ExpressError ||
@@ -471,7 +487,7 @@ internal class StateBuilder(
notification is SwapNotificationUM.Warning.SwapNotSupported ||
notification is SwapNotificationUM.Warning.NeedReserveToCreateAccount ||
notification is SwapNotificationUM.Info.PermissionNeeded
- }
+ } && !priceImpact.shouldDisableButton()
}
@Suppress("LongParameterList")
@@ -510,7 +526,7 @@ internal class StateBuilder(
amountTextFieldValue = TextFieldValue(
text = "0",
),
- amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
+ amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
token = toToken,
tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl,
coinId = toToken.currency.network.backendId,
@@ -523,7 +539,7 @@ internal class StateBuilder(
)
} ?: SwapCardState.Empty(
type = type,
- amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}",
+ amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO),
amountTextFieldValue = TextFieldValue(
text = "0",
),
@@ -546,7 +562,7 @@ internal class StateBuilder(
),
changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible),
providerState = providerState,
- priceImpact = PriceImpact.Empty(),
+ priceImpact = PriceImpact.Empty,
tosState = createTosState(swapProvider),
)
}
@@ -644,7 +660,7 @@ internal class StateBuilder(
),
changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible),
providerState = ProviderState.Empty(),
- priceImpact = PriceImpact.Empty(),
+ priceImpact = PriceImpact.Empty,
)
}
@@ -852,8 +868,8 @@ internal class StateBuilder(
toTitle = getToCardAccountTitle(toAccount = dataState.toAccount),
fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()),
toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()),
- fromTokenFiatAmount = stringReference(fromFiatAmount),
- toTokenFiatAmount = stringReference(toFiatAmount),
+ fromTokenFiatAmount = fromFiatAmount,
+ toTokenFiatAmount = toFiatAmount,
fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency),
toTokenIconState = iconStateConverter.convert(toCryptoCurrency),
onExploreButtonClick = onExploreClick,
@@ -892,8 +908,8 @@ internal class StateBuilder(
toTitle = getToCardAccountTitle(toAccount = dataState.toAccount),
fromTokenAmount = stringReference(swapTransactionState.fromAmount.orEmpty()),
toTokenAmount = stringReference(swapTransactionState.toAmount.orEmpty()),
- fromTokenFiatAmount = stringReference(fromFiatAmount),
- toTokenFiatAmount = stringReference(toFiatAmount),
+ fromTokenFiatAmount = fromFiatAmount,
+ toTokenFiatAmount = toFiatAmount,
fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency),
toTokenIconState = iconStateConverter.convert(toCryptoCurrency),
onExploreButtonClick = onExploreClick,
@@ -1267,15 +1283,17 @@ internal class StateBuilder(
return amount.format { crypto(symbol, currency.decimals) }
}
- private fun getFormattedFiatAmount(amount: BigDecimal?): String {
+ private fun getFormattedFiatAmount(amount: BigDecimal?): TextReference {
val appCurrency = appCurrencyProvider()
- return amount.format {
- fiat(
- fiatCurrencyCode = appCurrency.code,
- fiatCurrencySymbol = appCurrency.symbol,
- )
- }
+ return stringReference(
+ amount.format {
+ fiat(
+ fiatCurrencyCode = appCurrency.code,
+ fiatCurrencySymbol = appCurrency.symbol,
+ )
+ },
+ )
}
private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String {
@@ -1339,7 +1357,6 @@ internal class StateBuilder(
const val ADDRESS_MIN_LENGTH = 11
const val ADDRESS_FIRST_PART_LENGTH = 7
const val ADDRESS_SECOND_PART_LENGTH = 4
- private const val PRICE_IMPACT_THRESHOLD = 0.1
private const val MAX_DECIMALS_TO_SHOW = 8
private const val IF_ZERO_DECIMALS_TO_SHOW = 2
private const val FEE_READ_MORE_URL_FIRST_PART = "https://tangem.com/"
diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
index 2fdde2b04a..10d12201f0 100644
--- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
+++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
@@ -346,6 +346,7 @@ private fun SwapNotifications(notifications: List) {
Notification(
config = notification.config,
iconTint = when (notification) {
+ is SwapNotificationUM.Warning.TradeTooHigh -> TangemTheme.colors.icon.warning
is SwapNotificationUM.Error.UnableToCoverFeeWarning,
is NotificationUM.Error.TokenExceedsBalance,
is NotificationUM.Error.ExceedsBalance,
@@ -412,7 +413,7 @@ private val sendCard = SwapCardState.SwapCardData(
accountTitleUM = null,
),
amountTextFieldValue = TextFieldValue(),
- amountEquivalent = "1 000 000",
+ amountEquivalent = stringReference("1 000 000"),
tokenIconUrl = "",
tokenCurrency = "DAI",
isNotNativeToken = true,
@@ -427,7 +428,7 @@ private val sendCard = SwapCardState.SwapCardData(
private val receiveCard = SwapCardState.SwapCardData(
type = TransactionCardType.ReadOnly(),
amountTextFieldValue = TextFieldValue(),
- amountEquivalent = "1 000 000",
+ amountEquivalent = stringReference("1 000 000"),
tokenIconUrl = "",
tokenCurrency = "DAI",
isNotNativeToken = true,
@@ -466,7 +467,7 @@ private val state = SwapStateHolder(
permissionState = GiveTxPermissionState.InProgress,
blockchainId = "POLYGON",
providerState = ProviderState.Loading(),
- priceImpact = PriceImpact.Empty(),
+ priceImpact = PriceImpact.Empty,
shouldShowMaxAmount = true,
isInsufficientFunds = false,
onSuccess = {},
diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt
index b29e4b8257..1886f3effd 100644
--- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt
+++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt
@@ -26,9 +26,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
-import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@@ -41,8 +39,7 @@ import com.tangem.common.ui.account.AccountTitleUM
import com.tangem.common.ui.account.CryptoPortfolioIconConverter
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
-import com.tangem.core.ui.extensions.resourceReference
-import com.tangem.core.ui.extensions.stringResourceSafe
+import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SwapTokenScreenTestTags
@@ -59,7 +56,7 @@ fun TransactionCard(
balance: String,
tokenIconUrl: String,
tokenCurrency: String,
- amountEquivalent: String?,
+ amountEquivalent: TextReference?,
priceImpact: PriceImpact,
textFieldValue: TextFieldValue?,
modifier: Modifier = Modifier,
@@ -129,7 +126,7 @@ fun TransactionCard(
@Composable
fun TransactionCardEmpty(
type: TransactionCardType,
- amountEquivalent: String?,
+ amountEquivalent: TextReference?,
textFieldValue: TextFieldValue?,
modifier: Modifier = Modifier,
onChangeTokenClick: (() -> Unit)? = null,
@@ -157,7 +154,7 @@ fun TransactionCardEmpty(
type = type,
amountEquivalent = amountEquivalent,
textFieldValue = textFieldValue,
- priceImpact = PriceImpact.Empty(),
+ priceImpact = PriceImpact.Empty,
)
}
@@ -250,7 +247,7 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie
@Composable
private fun Content(
type: TransactionCardType,
- amountEquivalent: String?,
+ amountEquivalent: TextReference?,
priceImpact: PriceImpact,
textFieldValue: TextFieldValue?,
) {
@@ -320,27 +317,13 @@ private fun Content(
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20),
verticalAlignment = Alignment.CenterVertically,
) {
- if (priceImpact is PriceImpact.Value) {
+ AnimatedContent(targetState = amountEquivalent, label = "") { amount ->
Text(
- text = makePriceImpactBalanceWarning(
- amountEquivalent,
- priceImpact.getIntPercentValue(),
- ),
+ text = amount.resolveAnnotatedReference(),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
- modifier = Modifier.testTag(
- SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING,
- ),
+ modifier = Modifier.testTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT),
)
- } else {
- AnimatedContent(targetState = amountEquivalent, label = "") { amount ->
- Text(
- text = amount,
- color = TangemTheme.colors.text.tertiary,
- style = TangemTheme.typography.body2,
- modifier = Modifier.testTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT),
- )
- }
}
if (type.shouldShowWarning) {
SpacerW4()
@@ -353,10 +336,10 @@ private fun Content(
Icon(
painter = painterResource(id = R.drawable.ic_information_24),
contentDescription = null,
- tint = if (priceImpact is PriceImpact.Value) {
- TangemTheme.colors.text.attention
- } else {
- TangemTheme.colors.text.tertiary
+ tint = when (priceImpact.type) {
+ PriceImpact.Type.HIGH -> TangemTheme.colors.text.warning
+ PriceImpact.Type.MEDIUM -> TangemTheme.colors.text.attention
+ else -> TangemTheme.colors.text.tertiary
},
modifier = Modifier
.align(Alignment.CenterVertically)
@@ -368,7 +351,7 @@ private fun Content(
} else {
AnimatedContent(targetState = amountEquivalent, label = "") { amount ->
Text(
- text = amount,
+ text = amount.resolveAnnotatedReference(),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body2,
modifier = Modifier
@@ -530,19 +513,6 @@ fun ChangeTokenSelector() {
}
}
-@Composable
-private fun makePriceImpactBalanceWarning(value: String, priceImpactPercents: Int): AnnotatedString {
- val fullValue = "$value (-$priceImpactPercents%)"
- return buildAnnotatedString {
- append(fullValue)
- addStyle(
- style = SpanStyle(color = TangemTheme.colors.text.attention),
- start = value.length,
- end = fullValue.length,
- )
- }
-}
-
// region preview
@Preview(widthDp = 328, heightDp = 116, showBackground = true)
@@ -602,14 +572,14 @@ private fun TransactionCardPreview() {
inputError = TransactionCardType.InputError.Empty,
accountTitleUM = null,
),
- amountEquivalent = "1 000 000",
+ amountEquivalent = stringReference("1 000 000"),
tokenIconUrl = "",
tokenCurrency = "DAI",
networkIconRes = R.drawable.img_polygon_22,
onChangeTokenClick = {},
balance = "123",
textFieldValue = TextFieldValue(),
- priceImpact = PriceImpact.Empty(),
+ priceImpact = PriceImpact.Empty,
)
}
@@ -625,14 +595,24 @@ private fun TransactionCardPreviewWithPriceImpact() {
icon = CryptoPortfolioIconConverter.convert(CryptoPortfolioIcon.ofDefaultCustomAccount()),
),
),
- amountEquivalent = "1 000 000",
+ amountEquivalent = combinedReference(
+ stringReference("1 000 000 $"),
+ styledStringReference(
+ " (-15%)",
+ { SpanStyle(color = TangemTheme.colors.text.attention) },
+ ),
+ ),
tokenIconUrl = "",
tokenCurrency = "DAI",
networkIconRes = R.drawable.img_polygon_22,
onChangeTokenClick = {},
balance = "123",
textFieldValue = TextFieldValue("1000000.0000000000000000000000000"),
- priceImpact = PriceImpact.Value(0.15F),
+ priceImpact = PriceImpact(
+ value = 0.15F.toBigDecimal(),
+ type = PriceImpact.Type.MEDIUM,
+ amountSignificance = PriceImpact.AmountSignificance.HIGH,
+ ),
)
}
@@ -647,14 +627,14 @@ private fun TransactionCardPreviewWithoutPriceImpact() {
icon = CryptoPortfolioIconConverter.convert(CryptoPortfolioIcon.ofDefaultCustomAccount()),
),
),
- amountEquivalent = "1 000 000",
+ amountEquivalent = stringReference("1 000 000"),
tokenIconUrl = "",
tokenCurrency = "DAI",
networkIconRes = R.drawable.img_polygon_22,
onChangeTokenClick = {},
balance = "123",
textFieldValue = TextFieldValue(),
- priceImpact = PriceImpact.Empty(),
+ priceImpact = PriceImpact.Empty,
)
}