diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 6dfe9b86ab..ab179ea317 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -335,6 +335,8 @@ Error: %s There was an error. Please try again. Give Permission + High price impact! + Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first. Transaction in progress... diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 58fc19b1e4..76d8e32bc5 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -335,6 +335,8 @@ Error: %s There was an error. Please try again. Give Permission + High price impact! + Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first. Transaction in progress... diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 225e2544a9..751cf170b2 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -335,6 +335,8 @@ Error: %s There was an error. Please try again. Give Permission + High price impact! + Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first. Transaction in progress... diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 47116aa82e..4772adc1cc 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -335,6 +335,8 @@ Ошибка: %s Произошла ошибка. Пожалуйста, попробуйте еще раз. Дать разрешение + Сильные колебания цены! + Обмен этой суммы выбранных токенов может вызвать значительные колебания цены и уменьшить получаемую сумму. Недостаточно средств Недостаточно средств для оплаты на вашем %1$s кошельке для создания транзакции. Сначала пополните свой %2$s кошелек. Транзакция в процессе… diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 6699c46a3c..6b98188598 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -335,6 +335,8 @@ 錯誤: %s 有錯誤。請再試一遍 賦予權限 + High price impact! + Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. 餘額不足 您的 %1$s 錢包中沒有足夠的資金來創建交易。首先為您的 %2$s 錢包充值 交易進行中... diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index fcaa91827d..c3a6b51606 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -335,6 +335,8 @@ Error: %s There was an error. Please try again. Give Permission + High price impact! + Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome. Insufficient funds Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first. Transaction in progress... diff --git a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt index ce10a3e537..5017af8c99 100644 --- a/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt +++ b/features/referral/presentation/src/main/java/com/tangem/feature/referral/ui/ReferralScreen.kt @@ -67,8 +67,6 @@ import com.tangem.feature.referral.presentation.R import com.valentinilk.shimmer.shimmer import kotlinx.coroutines.launch -private const val GOOGLE_DOC_LINK_WRAPPER = "https://docs.google.com/viewer?url=%s&embedded=false" - /** * Referral program screen for participant and non-participant * @@ -87,8 +85,8 @@ internal fun ReferralScreen(stateHolder: ReferralStateHolder) { sheetContent = { AgreementBottomSheetContent( url = when (val state = stateHolder.referralInfoState) { - is ReferralInfoState.NonParticipantContent -> String.format(GOOGLE_DOC_LINK_WRAPPER, state.url) - is ReferralInfoState.ParticipantContent -> String.format(GOOGLE_DOC_LINK_WRAPPER, state.url) + is ReferralInfoState.NonParticipantContent -> state.url + is ReferralInfoState.ParticipantContent -> state.url is ReferralInfoState.Loading -> "" }, ) 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 381ba9784e..9f042170b7 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 @@ -26,6 +26,7 @@ import com.tangem.lib.crypto.models.ProxyFiatCurrency import com.tangem.lib.crypto.models.transactions.SendTxResult import com.tangem.utils.toFiatString import java.math.BigDecimal +import java.math.RoundingMode import javax.inject.Inject @Suppress("LargeClass") @@ -496,6 +497,12 @@ internal class SwapInteractorImpl @Inject constructor( ), ), fee = formattedFee, + priceImpact = calculatePriceImpact( + fromTokenAmount = fromTokenAmount.value, + fromRate = rates[fromToken.id] ?: 0.0, + toTokenAmount = toTokenAmount.value, + toRate = rates[toToken.id] ?: 0.0, + ), networkCurrency = userWalletManager.getNetworkCurrency(networkId), preparedSwapConfigState = preparedSwapConfigState, swapDataModel = swapDataModel, @@ -605,6 +612,17 @@ internal class SwapInteractorImpl @Inject constructor( return amountToSwap.replace(",", ".").toBigDecimalOrNull() } + private fun calculatePriceImpact( + fromTokenAmount: BigDecimal, + fromRate: Double, + toTokenAmount: BigDecimal, + toRate: Double, + ): Float { + val toTokenFiatValue = toTokenAmount.multiply(toRate.toBigDecimal()).setScale(2, RoundingMode.HALF_UP) + val fromTokenFiatValue = fromTokenAmount.multiply(fromRate.toBigDecimal()).setScale(2, RoundingMode.HALF_UP) + return (BigDecimal.ONE - toTokenFiatValue / fromTokenFiatValue).toFloat() + } + companion object { private const val DEFAULT_SLIPPAGE = 2 private const val ZERO_BALANCE = "0" 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 879dc1f124..1515683e56 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 @@ -12,6 +12,7 @@ sealed interface SwapState { val fromTokenInfo: TokenSwapInfo, val toTokenInfo: TokenSwapInfo, val fee: String, + val priceImpact: Float, val networkCurrency: String, val preparedSwapConfigState: PreparedSwapConfigState, val permissionState: PermissionDataState = PermissionDataState.Empty, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 81d124d753..43a1953668 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -84,7 +84,12 @@ sealed interface SwapWarning { val onClick: () -> Unit, ) : SwapWarning // data class RateExpired(val onClick: () -> Unit) : SwapWarning - // object HighPriceImpact : SwapWarning + /** + * High price impact warning + * + * @property priceImpact in format = 10 (means 10%) + */ + data class HighPriceImpact(val priceImpact: Int) : SwapWarning } enum class GenericWarningType { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 08ae435304..4692699435 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -124,6 +124,10 @@ internal class StateBuilder(val actions: UiActions) { if (!quoteModel.preparedSwapConfigState.isBalanceEnough) { warnings.add(SwapWarning.InsufficientFunds) } + + if (quoteModel.priceImpact > PRICE_IMPACT_THRESHOLD) { + warnings.add(SwapWarning.HighPriceImpact((quoteModel.priceImpact * HUNDRED_PERCENTS).toInt())) + } val feeState = if (quoteModel.preparedSwapConfigState.isFeeEnough) { FeeState.Loaded(tangemFee = quoteModel.tangemFee, fee = quoteModel.fee) } else { @@ -358,5 +362,7 @@ internal class StateBuilder(val actions: UiActions) { 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 HUNDRED_PERCENTS = 100 } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index afce1309c0..6a14863ede 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -161,6 +161,7 @@ private fun MainInfo(state: SwapStateHolder) { getActiveIconRes(state.blockchainId) } val (topCard, bottomCard, button) = createRefs() + val priceImpactWarning = state.warnings.filterIsInstance().firstOrNull() TransactionCard( type = state.sendCardData.type, balance = state.sendCardData.balance, @@ -168,6 +169,7 @@ private fun MainInfo(state: SwapStateHolder) { amountEquivalent = state.sendCardData.amountEquivalent, tokenIconUrl = state.sendCardData.tokenIconUrl, tokenCurrency = state.sendCardData.tokenCurrency, + priceImpact = priceImpactWarning, networkIconRes = if (state.sendCardData.isNotNativeToken) networkIconRes else null, iconPlaceholder = state.sendCardData.coinId?.let { getActiveIconResByCoinId(it, state.networkId) @@ -185,6 +187,7 @@ private fun MainInfo(state: SwapStateHolder) { amountEquivalent = state.receiveCardData.amountEquivalent, tokenIconUrl = state.receiveCardData.tokenIconUrl, tokenCurrency = state.receiveCardData.tokenCurrency, + priceImpact = priceImpactWarning, networkIconRes = if (state.receiveCardData.isNotNativeToken) networkIconRes else null, iconPlaceholder = state.receiveCardData.coinId?.let { getActiveIconResByCoinId(it, state.networkId) @@ -289,9 +292,12 @@ private fun SwapWarnings( ) { warnings.forEach { warning -> when (warning) { - // SwapWarning.HighPriceImpact -> { - // WarningCard(title = stringResource(id = R.string.), description = stringResource(id = R.string.)) - // } + is SwapWarning.HighPriceImpact -> { + WarningCard( + title = stringResource(id = R.string.swapping_high_price_impact), + description = stringResource(id = R.string.swapping_high_price_impact_description), + ) + } is SwapWarning.PermissionNeeded -> { WarningCard( title = stringResource(id = R.string.swapping_permission_header), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 7bcea09007..1e886b5303 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -32,6 +32,9 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +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 @@ -45,7 +48,9 @@ import com.tangem.core.ui.components.ResizableText import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.components.SpacerH8 import com.tangem.core.ui.components.SpacerW16 +import com.tangem.core.ui.components.SpacerW4 import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.models.SwapWarning import com.tangem.feature.swap.models.TransactionCardType import com.valentinilk.shimmer.shimmer @@ -57,6 +62,7 @@ fun TransactionCard( tokenIconUrl: String, tokenCurrency: String, amountEquivalent: String?, + priceImpact: SwapWarning.HighPriceImpact?, textFieldValue: TextFieldValue?, modifier: Modifier = Modifier, @DrawableRes iconPlaceholder: Int? = null, @@ -82,6 +88,7 @@ fun TransactionCard( type = type, amountEquivalent = amountEquivalent, textFieldValue = textFieldValue, + priceImpact = priceImpact, ) } @@ -171,6 +178,7 @@ private fun Header( private fun Content( type: TransactionCardType, amountEquivalent: String?, + priceImpact: SwapWarning.HighPriceImpact?, textFieldValue: TextFieldValue?, ) { Row( @@ -227,12 +235,34 @@ private fun Content( SpacerH8() if (amountEquivalent != null) { - Text( - text = amountEquivalent, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), - ) + if (type is TransactionCardType.ReceiveCard && priceImpact != null) { + Row { + Text( + text = makePriceImpactBalanceWarning(amountEquivalent, priceImpact.priceImpact), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + modifier = Modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size20) + .align(Alignment.CenterVertically), + ) + SpacerW4() + Icon( + painter = painterResource(id = R.drawable.ic_alert_24), + contentDescription = null, + tint = TangemTheme.colors.icon.attention, + modifier = Modifier + .size(size = TangemTheme.dimens.size20) + .align(Alignment.CenterVertically), + ) + } + } else { + Text( + text = amountEquivalent, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size20), + ) + } } else { Box( modifier = Modifier @@ -339,6 +369,19 @@ 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, + ) + } +} + @Composable private fun TokenImageShimmer( modifier: Modifier = Modifier, @@ -384,6 +427,7 @@ private fun TransactionCardPreview() { onChangeTokenClick = {}, balance = "123", textFieldValue = TextFieldValue(), + priceImpact = null, ) }