Updated on 2026-08-14
This commit is contained in:
parent
531fdf166f
commit
3d73befd95
22 changed files with 94 additions and 111 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -23,4 +23,5 @@ data class UiActions(
|
|||
val onBuyClick: () -> Unit,
|
||||
val onPolicyClick: (String) -> Unit,
|
||||
val onTosClick: (String) -> Unit,
|
||||
val onReceiveCardWarningClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue