Updated on 2026-08-14

This commit is contained in:
Tangem 2024-01-19 13:26:04 +01:00
parent 531fdf166f
commit 3d73befd95
22 changed files with 94 additions and 111 deletions

View file

@ -83,7 +83,7 @@ sealed interface TransactionCardType {
) : TransactionCardType
data class ReadOnly(
val highPriceImpact: String? = null,
val onWarningClick: (() -> Unit)? = null,
@StringRes override val headerResId: Int = R.string.swapping_to_title,
) : TransactionCardType
}
@ -104,18 +104,12 @@ sealed interface SwapWarning {
object InsufficientFunds : SwapWarning
data class NoAvailableTokensToSwap(val notificationConfig: NotificationConfig) : SwapWarning
data class GenericWarning(
val message: String? = null,
val message: TextReference? = null,
val type: GenericWarningType = GenericWarningType.OTHER,
val shouldWrapMessage: Boolean = false,
val onClick: () -> Unit,
) : SwapWarning
// data class RateExpired(val onClick: () -> Unit) : SwapWarning
/**
* High price impact warning
*
* @property priceImpact in format = 10 (means 10%)
*/
data class HighPriceImpact(val priceImpact: Int, val notificationConfig: NotificationConfig) : SwapWarning
data class GeneralError(val notificationConfig: NotificationConfig) : SwapWarning
data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning
data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning

View file

@ -23,4 +23,5 @@ data class UiActions(
val onBuyClick: () -> Unit,
val onPolicyClick: (String) -> Unit,
val onTosClick: (String) -> Unit,
val onReceiveCardWarningClick: () -> Unit,
)

View file

@ -11,7 +11,6 @@ sealed class FeeItemState {
val amountCrypto: String,
val symbolCrypto: String,
val amountFiatFormatted: String,
val explanation: TextReference?,
val isClickable: Boolean,
val onClick: () -> Unit,
) : FeeItemState()

View file

@ -109,7 +109,6 @@ private fun ChooseFeeBottomSheetContent_Preview() {
amountCrypto = "1000",
symbolCrypto = "MATIC",
amountFiatFormatted = "(10$)",
explanation = null,
isClickable = false,
onClick = {},
),
@ -119,7 +118,6 @@ private fun ChooseFeeBottomSheetContent_Preview() {
amountCrypto = "2000",
symbolCrypto = "MATIC",
amountFiatFormatted = "(10$)",
explanation = null,
isClickable = false,
onClick = {},
),

View file

@ -3,8 +3,6 @@ package com.tangem.feature.swap.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.material3.Divider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -26,13 +24,12 @@ fun FeeItemBlock(state: FeeItemState) {
@Composable
fun FeeItem(state: FeeItemState.Content) {
Column(
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(start = TangemTheme.dimens.spacing12)
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
.clickable(
onClick = state.onClick,
@ -43,32 +40,13 @@ fun FeeItem(state: FeeItemState.Content) {
val description = "${state.amountCrypto}${state.symbolCrypto} (${state.amountFiatFormatted})"
SimpleActionRow(
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
),
title = state.title.resolveReference(),
description = description,
isClickable = state.isClickable,
)
state.explanation?.let {
Divider(
color = TangemTheme.colors.stroke.primary,
thickness = TangemTheme.dimens.size0_5,
modifier = Modifier.padding(
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
end = TangemTheme.dimens.spacing2,
),
)
Text(
text = it.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(
bottom = TangemTheme.dimens.spacing10,
end = TangemTheme.dimens.spacing16,
),
)
}
}
}
@ -81,10 +59,6 @@ private fun FeeItemPreview() {
amountCrypto = "1000",
symbolCrypto = "MATIC",
amountFiatFormatted = "(1000$)",
explanation = stringReference(
"Additionally, the network fee for sending the exchanged funds back to your address is " +
"included in the rate",
),
isClickable = false,
onClick = {},
)

View file

@ -113,6 +113,12 @@ private fun ProviderContentState(
modifier = Modifier.padding(start = TangemTheme.dimens.spacing12),
) {
Row {
Text(
text = stringResource(id = R.string.express_by_provider),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(end = TangemTheme.dimens.spacing4),
)
AnimatedContent(targetState = state.name, label = "") {
Text(
text = it,

View file

@ -212,7 +212,7 @@ internal class StateBuilder(
if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder
if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder
val warnings = getWarningsForSuccessState(quoteModel, fromToken)
val feeState = createFeeState(quoteModel.txFee, selectedFeeType, swapProvider)
val feeState = createFeeState(quoteModel.txFee, selectedFeeType)
val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus
val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus
return uiStateHolder.copy(
@ -231,7 +231,7 @@ internal class StateBuilder(
isBalanceHidden = isBalanceHiddenProvider(),
),
receiveCardData = SwapCardState.SwapCardData(
type = TransactionCardType.ReadOnly(),
type = TransactionCardType.ReadOnly(actions.onReceiveCardWarningClick),
amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()),
amountEquivalent = getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat),
token = toCurrencyStatus,
@ -332,15 +332,6 @@ internal class StateBuilder(
warnings.add(SwapWarning.InsufficientFunds)
}
val priceImpact = quoteModel.priceImpact
if (priceImpact is PriceImpact.ValueWithNotify && priceImpact.value > PRICE_IMPACT_THRESHOLD) {
warnings.add(
SwapWarning.HighPriceImpact(
priceImpact = priceImpact.getIntPercentValue(),
notificationConfig = highPriceImpactNotificationConfig(),
),
)
}
if (quoteModel.permissionState is PermissionDataState.PermissionLoading) {
warnings.add(
SwapWarning.TransactionInProgressWarning(
@ -735,7 +726,7 @@ internal class StateBuilder(
)
}
private fun createFeeState(txFeeState: TxFeeState, feeType: FeeType, swapProvider: SwapProvider): FeeItemState {
private fun createFeeState(txFeeState: TxFeeState, feeType: FeeType): FeeItemState {
val isClickable: Boolean
val fee = when (txFeeState) {
TxFeeState.Empty -> return FeeItemState.Empty
@ -758,14 +749,9 @@ internal class StateBuilder(
return FeeItemState.Content(
feeType = feeType,
title = resourceReference(R.string.common_fee_label),
title = resourceReference(R.string.common_network_fee_title),
amountCrypto = fee.feeCryptoFormatted,
symbolCrypto = fee.cryptoSymbol,
explanation = if (swapProvider.type == ExchangeProviderType.CEX) {
resourceReference(R.string.express_cex_fee_explanation)
} else {
null
},
amountFiatFormatted = fee.feeFiatFormatted,
isClickable = isClickable,
onClick = actions.onClickFee,
@ -839,6 +825,25 @@ internal class StateBuilder(
)
}
fun createImpactAlert(
uiState: SwapStateHolder,
providerType: ExchangeProviderType,
onAlertClick: () -> Unit,
): SwapStateHolder {
val message = when (providerType) {
ExchangeProviderType.CEX -> resourceReference(R.string.express_cex_fee_explanation)
ExchangeProviderType.DEX -> resourceReference(R.string.swapping_high_price_impact_description)
}
return uiState.copy(
alert = SwapWarning.GenericWarning(
message = message,
onClick = onAlertClick,
type = GenericWarningType.OTHER,
),
changeCardsButtonState = ChangeCardsButtonState.ENABLED,
)
}
fun addAlert(uiState: SwapStateHolder, onClick: () -> Unit): SwapStateHolder {
return uiState.copy(
alert = SwapWarning.GenericWarning(
@ -852,7 +857,7 @@ internal class StateBuilder(
fun addWarning(
uiState: SwapStateHolder,
message: String?,
message: TextReference?,
shouldWrapMessage: Boolean = false,
onClick: () -> Unit,
): SwapStateHolder {
@ -1072,21 +1077,19 @@ internal class StateBuilder(
return listOf(
FeeItemState.Content(
feeType = this.normalFee.feeType,
title = resourceReference(R.string.common_fee_label),
title = resourceReference(R.string.common_network_fee_title),
amountCrypto = this.normalFee.feeCryptoFormatted,
symbolCrypto = this.normalFee.cryptoSymbol,
amountFiatFormatted = this.normalFee.feeFiatFormatted,
explanation = null,
isClickable = true,
onClick = {},
),
FeeItemState.Content(
feeType = this.priorityFee.feeType,
title = resourceReference(R.string.common_fee_label),
title = resourceReference(R.string.common_network_fee_title),
amountCrypto = this.priorityFee.feeCryptoFormatted,
symbolCrypto = this.priorityFee.cryptoSymbol,
amountFiatFormatted = this.priorityFee.feeFiatFormatted,
explanation = null,
isClickable = true,
onClick = {},
),
@ -1129,14 +1132,6 @@ internal class StateBuilder(
)
}
private fun highPriceImpactNotificationConfig(): NotificationConfig {
return NotificationConfig(
title = resourceReference(R.string.swapping_high_price_impact),
subtitle = resourceReference(R.string.swapping_high_price_impact_description),
iconResId = R.drawable.ic_alert_circle_24,
)
}
private fun createUnableToCoverFeeNotificationConfig(
fromToken: CryptoCurrency,
onBuyClick: () -> Unit,

View file

@ -267,7 +267,7 @@ private fun DropdownSelector(
@Composable
private fun FeeItem(fee: String) {
InformationItem(
subtitle = stringResource(id = R.string.common_fee_label),
subtitle = stringResource(id = R.string.common_network_fee_title),
value = fee,
)
}

View file

@ -109,7 +109,7 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi
val message = if (state.alert.type == GenericWarningType.NETWORK) {
stringResource(id = R.string.disclaimer_error_loading)
} else {
state.alert.message ?: stringResource(id = R.string.swapping_generic_error)
state.alert.message?.resolveReference() ?: stringResource(id = R.string.swapping_generic_error)
}
SimpleOkDialog(
message = message,
@ -326,12 +326,6 @@ private fun SwapWarnings(warnings: List<SwapWarning>) {
) {
warnings.forEach { warning ->
when (warning) {
is SwapWarning.HighPriceImpact -> {
Notification(
config = warning.notificationConfig,
iconTint = TangemTheme.colors.icon.warning,
)
}
is SwapWarning.PermissionNeeded -> {
Notification(
config = warning.notificationConfig,
@ -340,9 +334,9 @@ private fun SwapWarnings(warnings: List<SwapWarning>) {
is SwapWarning.GenericWarning -> {
val message = warning.message?.let {
if (warning.shouldWrapMessage) {
String.format(stringResource(id = R.string.swapping_error_wrapper), it)
String.format(stringResource(id = R.string.swapping_error_wrapper), it.resolveReference())
} else {
it
it.resolveReference()
}
} ?: stringResource(id = R.string.swapping_generic_error)
RefreshableWaringCard(
@ -472,7 +466,6 @@ private val state = SwapStateHolder(
amountCrypto = "100",
symbolCrypto = "1000",
amountFiatFormatted = "(100)",
explanation = null,
isClickable = true,
onClick = {},
),

View file

@ -92,7 +92,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad
)
SpacerH16()
InputRowDefault(
title = TextReference.Res(R.string.common_fee_label),
title = TextReference.Res(R.string.common_network_fee_title),
text = state.fee,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
@ -284,14 +285,19 @@ private fun Content(
.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),
)
IconButton(
onClick = {
type.onWarningClick?.invoke()
},
modifier = Modifier.size(size = TangemTheme.dimens.size20),
) {
Icon(
painter = painterResource(id = R.drawable.ic_alert_24),
contentDescription = null,
tint = TangemTheme.colors.icon.attention,
modifier = Modifier.align(Alignment.CenterVertically),
)
}
}
} else {
AnimatedContent(targetState = amountEquivalent, label = "") {
@ -468,6 +474,7 @@ private fun makePriceImpactBalanceWarning(value: String, priceImpactPercents: In
private fun Preview_SwapMainCard_InLightTheme() {
TangemTheme(isDark = false) {
TransactionCardPreview()
TransactionCardPreviewWithPriceImpact()
}
}
@ -476,6 +483,7 @@ private fun Preview_SwapMainCard_InLightTheme() {
private fun Preview_SwapMainCard_InDarkTheme() {
TangemTheme(isDark = true) {
TransactionCardPreview()
TransactionCardPreviewWithPriceImpact()
}
}
@ -494,4 +502,20 @@ private fun TransactionCardPreview() {
)
}
@Composable
@Suppress("MagicNumber")
private fun TransactionCardPreviewWithPriceImpact() {
TransactionCard(
type = TransactionCardType.ReadOnly(),
amountEquivalent = "1 000 000",
tokenIconUrl = "",
tokenCurrency = "DAI",
networkIconRes = R.drawable.img_polygon_22,
onChangeTokenClick = {},
balance = "123",
textFieldValue = TextFieldValue(),
priceImpact = PriceImpact.Value(0.15F),
)
}
// endregion preview

View file

@ -876,6 +876,15 @@ internal class SwapViewModel @Inject constructor(
onTosClick = {
swapRouter.openUrl(it)
},
onReceiveCardWarningClick = {
val selectedProvider = dataState.selectedProvider ?: return@UiActions
uiState = stateBuilder.createImpactAlert(
uiState = uiState,
providerType = selectedProvider.type,
) {
uiState = stateBuilder.clearAlert(uiState)
}
},
)
}