Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-30 13:29:03 +05:00
commit 56a3a67eb7
12 changed files with 443 additions and 20 deletions

View file

@ -23,6 +23,7 @@ import androidx.constraintlayout.compose.Visibility
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.audits.AuditLabel
import com.tangem.core.ui.components.audits.AuditLabelUM
import com.tangem.core.ui.components.badge.Badge
@ -50,7 +51,6 @@ private const val DISABLED_ICON_ALPHA = 0.4f
fun ProviderChooseCrypto(providerChooseUM: ProviderChooseUM, onClick: () -> Unit, modifier: Modifier = Modifier) {
ConstraintLayout(
modifier = modifier
.background(TangemTheme.colors.background.action)
.clip(RoundedCornerShape(14.dp))
.selectedBorder(isSelected = providerChooseUM.isSelected)
.clickable(
@ -133,13 +133,23 @@ private fun IconContent(iconUrl: String, modifier: Modifier = Modifier) {
SubcomposeAsyncImage(
modifier = modifier
.size(40.dp)
.clip(RoundedCornerShape(8.dp))
.background(TangemColorPalette.Light1),
.clip(RoundedCornerShape(8.dp)),
model = ImageRequest.Builder(context = LocalContext.current)
.data(iconUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
loading = {
RectangleShimmer(radius = 8.dp)
},
error = {
Box(
modifier = Modifier.background(
color = TangemColorPalette.Light1,
shape = RoundedCornerShape(8.dp),
),
)
},
contentDescription = null,
)
}

View file

@ -58,6 +58,7 @@ internal class SwapAmountSetQuotesTransformer(
val percent = quote.quoteAmount / bestQuote.quoteAmount - BigDecimal.ONE
quote.copy(
diffPercent = DifferencePercent.Diff(
isPositive = percent.isPositive(),
percent = stringReference(
if (percent.isPositive()) {
"${StringsSigns.PLUS}${percent.format { percent() }}"

View file

@ -13,5 +13,6 @@ internal data class SwapChooseProviderBottomSheetContent(
internal data class SwapProviderListItem(
val providerUM: ProviderChooseUM,
val swapProviderState: SwapProviderState,
val quote: SwapQuoteUM,
)

View file

@ -0,0 +1,34 @@
package com.tangem.features.swap.v2.impl.chooseprovider.entity
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.DifferencePercent
@Deprecated("Use ProviderChooseUM with new design")
@Immutable
internal sealed class SwapProviderState {
abstract val isSelected: Boolean
data object Empty : SwapProviderState() {
override val isSelected = false
}
data class Content(
override val isSelected: Boolean,
val name: String,
val type: String,
val iconUrl: String,
val subtitle: TextReference,
val additionalBadge: AdditionalBadge,
val diffPercent: DifferencePercent,
) : SwapProviderState()
@Immutable
sealed class AdditionalBadge {
data object FCAWarningList : AdditionalBadge()
data object BestTrade : AdditionalBadge()
data object Empty : AdditionalBadge()
data object PermissionRequired : AdditionalBadge()
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.swap.v2.impl.chooseprovider.model
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions
import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent
import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent
@ -10,6 +11,7 @@ import com.tangem.features.swap.v2.impl.chooseprovider.model.converter.SwapProvi
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.isSingleItem
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -30,6 +32,7 @@ internal class SwapChooseProviderModel @Inject constructor(
cryptoCurrency = params.cryptoCurrency,
selectedProvider = params.selectedProvider,
needApplyFCARestrictions = needApplyFCARestrictions,
needBestRateBadge = params.providers.filterIsInstance<SwapQuoteUM.Content>().isSingleItem().not(),
)
}
@ -42,9 +45,14 @@ internal class SwapChooseProviderModel @Inject constructor(
}
private fun getInitialState(): SwapChooseProviderBottomSheetContent {
val filteredProviderList = params.providers.filter {
it is SwapQuoteUM.Content ||
it is SwapQuoteUM.Allowance ||
(it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError
}
return SwapChooseProviderBottomSheetContent(
isApplyFCARestrictions = needApplyFCARestrictions && params.selectedProvider.isRestrictedByFCA(),
providerList = swapProviderListItemConverter.convertList(params.providers)
providerList = swapProviderListItemConverter.convertList(filteredProviderList)
.filterNotNull()
.toPersistentList(),
selectedProvider = params.selectedProvider,

View file

@ -22,11 +22,21 @@ internal class SwapProviderListItemConverter(
private val cryptoCurrency: CryptoCurrency,
private val selectedProvider: ExpressProvider,
private val needApplyFCARestrictions: Boolean,
needBestRateBadge: Boolean,
) : Converter<SwapQuoteUM, SwapProviderListItem?> {
private val providerStateConverter = SwapProviderStateConverter(
cryptoCurrency = cryptoCurrency,
selectedProvider = selectedProvider,
needApplyFCARestrictions = needApplyFCARestrictions,
isNeedBestRateBadge = needBestRateBadge,
)
override fun convert(value: SwapQuoteUM): SwapProviderListItem? {
val provider = value.provider ?: return null
return SwapProviderListItem(
swapProviderState = providerStateConverter.convert(value),
providerUM = ProviderChooseUM(
title = stringReference(provider.name),
subtitle = stringReference(provider.type.typeName),

View file

@ -0,0 +1,92 @@
package com.tangem.features.swap.v2.impl.chooseprovider.model.converter
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ExpressProvider
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderState
import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderState.AdditionalBadge
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA
import com.tangem.utils.converter.Converter
@Deprecated("Remove with new design")
internal class SwapProviderStateConverter(
private val cryptoCurrency: CryptoCurrency,
private val selectedProvider: ExpressProvider,
private val isNeedBestRateBadge: Boolean,
private val needApplyFCARestrictions: Boolean,
) : Converter<SwapQuoteUM, SwapProviderState> {
override fun convert(value: SwapQuoteUM): SwapProviderState {
return when (value) {
is SwapQuoteUM.Content -> value.convertToContent()
is SwapQuoteUM.Error -> value.convertToErrorContent()
is SwapQuoteUM.Allowance,
SwapQuoteUM.Empty,
SwapQuoteUM.Loading,
-> SwapProviderState.Empty
}
}
private fun SwapQuoteUM.Content.convertToContent(): SwapProviderState {
val isBestRate = when (diffPercent) {
SwapQuoteUM.Content.DifferencePercent.Best -> true
else -> false
}
val additionalBadge = when {
needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList
isNeedBestRateBadge && isBestRate && !needApplyFCARestrictions -> AdditionalBadge.BestTrade
else -> AdditionalBadge.Empty
}
return SwapProviderState.Content(
name = provider.name,
iconUrl = provider.imageLarge,
type = provider.type.typeName,
subtitle = quoteAmountValue,
additionalBadge = additionalBadge,
diffPercent = diffPercent,
isSelected = provider == selectedProvider,
)
}
private fun SwapQuoteUM.Error.convertToErrorContent(): SwapProviderState {
val additionalBadge = when {
needApplyFCARestrictions && provider.isRestrictedByFCA() -> AdditionalBadge.FCAWarningList
else -> AdditionalBadge.Empty
}
return SwapProviderState.Content(
name = provider.name,
iconUrl = provider.imageLarge,
type = provider.type.typeName,
subtitle = when (val error = expressError) {
is ExpressError.AmountError.TooSmallError -> resourceReference(
id = R.string.express_provider_min_amount,
formatArgs = wrappedList(
error.amount.format { crypto(cryptoCurrency) },
),
)
is ExpressError.AmountError.NotEnoughAllowanceError,
is ExpressError.AmountError.TooBigError,
-> resourceReference(
id = R.string.express_provider_max_amount,
formatArgs = wrappedList(
error.amount.format { crypto(cryptoCurrency) },
),
)
else -> TextReference.EMPTY
},
additionalBadge = additionalBadge,
diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty,
isSelected = false,
)
}
}

View file

@ -2,13 +2,16 @@ package com.tangem.features.swap.v2.impl.chooseprovider.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -16,14 +19,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet
import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.provider.ProviderChooseCrypto
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.selectedBorder
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -57,13 +60,14 @@ internal fun SwapChooseProviderContent(
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 13.dp),
modifier = modifier.padding(horizontal = 13.dp),
) {
Text(
text = stringResourceSafe(id = R.string.onramp_choose_provider_title_hint),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.padding(bottom = 4.dp),
)
AnimatedVisibility(
modifier = Modifier.padding(top = 12.dp),
@ -75,12 +79,18 @@ internal fun SwapChooseProviderContent(
iconTint = TangemTheme.colors.icon.warning,
)
}
SpacerH12()
contentUM.providerList.fastForEachIndexed { index, provider ->
ProviderChooseCrypto(
providerChooseUM = provider.providerUM,
onClick = { onProviderClick(provider.quote) },
modifier = modifier
.conditional(index == 0) { padding(top = 24.dp) },
SwapProviderItem(
state = provider.swapProviderState,
modifier = Modifier
.clip(RoundedCornerShape(14.dp))
.selectedBorder(isSelected = provider.swapProviderState.isSelected)
.clickable(
enabled = provider.quote !is SwapQuoteUM.Error,
onClick = { onProviderClick(provider.quote) },
)
.padding(12.dp),
)
}
Icon(

View file

@ -4,11 +4,7 @@ import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@ -27,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette
@ -57,7 +54,7 @@ fun SwapChooseProviderContent(expressProvider: ExpressProvider?, onClick: () ->
) {
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_exchange_horizontal_24),
ImageVector.vectorResource(R.drawable.ic_stack_new_24),
),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
@ -72,13 +69,21 @@ fun SwapChooseProviderContent(expressProvider: ExpressProvider?, onClick: () ->
SubcomposeAsyncImage(
modifier = modifier
.size(20.dp)
.clip(RoundedCornerShape(4.dp))
.background(TangemColorPalette.Light1),
.clip(RoundedCornerShape(4.dp)),
model = ImageRequest.Builder(context = LocalContext.current)
.data(expressProvider?.imageLarge)
.crossfade(enable = true)
.allowHardware(false)
.build(),
loading = { RectangleShimmer(radius = 4.dp) },
error = {
Box(
modifier = Modifier.background(
color = TangemColorPalette.Light1,
shape = RoundedCornerShape(4.dp),
),
)
},
contentDescription = null,
)
Text(

View file

@ -0,0 +1,232 @@
package com.tangem.features.swap.v2.impl.chooseprovider.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
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.chooseprovider.entity.SwapProviderState
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
@Deprecated("Use ProviderChooseCrypto with new design")
@Composable
internal fun SwapProviderItem(state: SwapProviderState, modifier: Modifier = Modifier) {
when (state) {
is SwapProviderState.Content -> ProviderContentState(
state = state,
modifier = modifier,
)
is SwapProviderState.Empty -> { /* no-op */
}
}
}
@Suppress("LongMethod")
@Composable
private fun ProviderContentState(state: SwapProviderState.Content, modifier: Modifier = Modifier) {
Row(
modifier = modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
SubcomposeAsyncImage(
modifier = Modifier
.size(size = 40.dp)
.clip(TangemTheme.shapes.roundedCorners8),
model = ImageRequest.Builder(context = LocalContext.current).data(state.iconUrl)
.crossfade(enable = true).allowHardware(false).build(),
loading = { RectangleShimmer(radius = 8.dp) },
error = {
ErrorProviderIcon(Modifier.size(size = 40.dp))
},
contentDescription = null,
)
Column(modifier = Modifier.padding(start = 12.dp)) {
Row {
Text(
text = state.name,
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.primary1,
)
Text(
text = state.type,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(start = 4.dp),
)
val badgeModifier = Modifier.padding(start = 4.dp)
when (state.additionalBadge) {
SwapProviderState.AdditionalBadge.FCAWarningList -> FCABadgeItem(badgeModifier)
SwapProviderState.AdditionalBadge.BestTrade -> BestTradeItem(badgeModifier)
SwapProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem(badgeModifier)
SwapProviderState.AdditionalBadge.Empty -> Unit
}
}
Row(
modifier = Modifier.padding(top = 2.dp),
) {
Text(
text = state.subtitle.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
if (state.diffPercent is SwapQuoteUM.Content.DifferencePercent.Diff) {
val textColor = if (state.diffPercent.isPositive) {
TangemTheme.colors.icon.accent
} else {
TangemTheme.colors.text.warning
}
Text(
text = state.diffPercent.percent.resolveReference(),
style = TangemTheme.typography.body2,
color = textColor,
modifier = Modifier.padding(start = 4.dp),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
}
}
}
}
@Composable
private fun ErrorProviderIcon(modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(
color = TangemTheme.colors.background.secondary,
shape = TangemTheme.shapes.roundedCorners8,
),
contentAlignment = Alignment.Center,
) {
Icon(
modifier = Modifier.matchParentSize(),
painter = painterResource(id = R.drawable.ic_custom_token_44),
contentDescription = null,
)
}
}
@Composable
private fun BestTradeItem(modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
shape = TangemTheme.shapes.roundedCornersLarge,
),
) {
Text(
text = stringResourceSafe(R.string.express_provider_best_rate),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.accent,
modifier = Modifier.padding(horizontal = 6.dp),
maxLines = 1,
)
}
}
@Composable
private fun PermissionBadgeItem(modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(
color = TangemTheme.colors.background.secondary,
shape = TangemTheme.shapes.roundedCornersLarge,
),
) {
Text(
text = stringResourceSafe(id = R.string.express_provider_permission_needed),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(horizontal = 6.dp),
maxLines = 1,
)
}
}
@Composable
private fun FCABadgeItem(modifier: Modifier = Modifier) {
Box(
modifier = modifier.background(
color = TangemTheme.colors.background.secondary,
shape = TangemTheme.shapes.roundedCornersLarge,
),
) {
Text(
text = stringResourceSafe(id = R.string.express_provider_fca_warning_list),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(horizontal = 6.dp),
maxLines = 1,
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ProviderItemPreview(
@PreviewParameter(ProviderItemParameterProvider::class) state: Pair<SwapProviderState, Boolean>,
) {
TangemThemePreview {
SwapProviderItem(
modifier = Modifier.background(TangemTheme.colors.background.action),
state = state.first,
)
}
}
private class ProviderItemParameterProvider : CollectionPreviewParameterProvider<Pair<SwapProviderState, Boolean>>(
collection = buildList {
val contentState = SwapProviderState.Content(
name = "1inch",
type = "DEX",
iconUrl = "",
subtitle = stringReference(value = "0,64554846 DAI ≈ 1 MATIC"),
additionalBadge = SwapProviderState.AdditionalBadge.Empty,
diffPercent = SwapQuoteUM.Content.DifferencePercent.Diff(
isPositive = false,
percent = stringReference("-10%"),
),
isSelected = true,
)
val contentState2 = contentState.copy(
subtitle = stringReference(value = "1 132,46 MATIC"),
additionalBadge = SwapProviderState.AdditionalBadge.PermissionRequired,
diffPercent = SwapQuoteUM.Content.DifferencePercent.Diff(
isPositive = true,
percent = stringReference("+10%"),
),
)
add(contentState to true)
add(contentState to false)
add(contentState2 to true)
add(contentState2 to false)
},
)
// endregion Preview

View file

@ -10,6 +10,7 @@ import com.tangem.domain.express.models.ExpressRateType
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent
import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderListItem
import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapProviderState
import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
@ -65,6 +66,15 @@ internal object SwapChooseProviderContentPreview {
),
),
),
swapProviderState = SwapProviderState.Content(
name = provider1.name,
type = provider1.type.typeName,
iconUrl = "",
subtitle = stringReference("1800 POL"),
additionalBadge = SwapProviderState.AdditionalBadge.BestTrade,
diffPercent = SwapQuoteUM.Content.DifferencePercent.Best,
isSelected = true,
),
quote = quote1,
),
SwapProviderListItem(
@ -85,6 +95,15 @@ internal object SwapChooseProviderContentPreview {
),
),
quote = quote2,
swapProviderState = SwapProviderState.Content(
name = provider1.name,
type = provider1.type.typeName,
iconUrl = "",
subtitle = stringReference("1800 POL"),
additionalBadge = SwapProviderState.AdditionalBadge.BestTrade,
diffPercent = SwapQuoteUM.Content.DifferencePercent.Best,
isSelected = true,
),
),
),
selectedProvider = provider1,

View file

@ -40,6 +40,7 @@ internal sealed class SwapQuoteUM {
data object Empty : DifferencePercent()
data object Best : DifferencePercent()
data class Diff(
val isPositive: Boolean,
val percent: TextReference,
) : DifferencePercent()
}