Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-14 16:27:45 +05:00
parent dbbbdd2d33
commit d6f8549bd2
7 changed files with 118 additions and 48 deletions

View file

@ -57,6 +57,7 @@ internal class SwapQuoteUMConverter(
quote.toTokenAmount.toQuoteValue(),
),
rate = annotatedReference(rateString),
isSingleProvider = false,
)
}
} else {
@ -68,6 +69,7 @@ internal class SwapQuoteUMConverter(
quote.toTokenAmount.toQuoteValue(),
),
rate = annotatedReference(rateString),
isSingleProvider = false,
)
}
}

View file

@ -11,6 +11,7 @@ import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM.Content.Differ
import com.tangem.features.swap.v2.impl.common.isRestrictedByFCA
import com.tangem.utils.StringsSigns
import com.tangem.utils.extensions.isPositive
import com.tangem.utils.extensions.isSingleItem
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toPersistentList
@ -26,12 +27,20 @@ internal class SwapAmountSetQuotesTransformer(
override fun transform(prevState: SwapAmountUM): SwapAmountUM {
if (prevState !is SwapAmountUM.Content) return prevState
val isSingleProvider = quotes.filter {
it is SwapQuoteUM.Content || it is SwapQuoteUM.Allowance ||
(it as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError
}.isSingleItem()
val sortedQuotes = quotes.sortedWith(SwapQuotesComparator)
val bestQuote = findBestQuote(quotes) ?: SwapQuoteUM.Empty
val selectedQuote = if (isSilentReload && prevState.selectedQuote !is SwapQuoteUM.Loading) {
prevState.selectedQuote
} else {
(bestQuote as? SwapQuoteUM.Content)?.copy(diffPercent = DifferencePercent.Best) ?: bestQuote
(bestQuote as? SwapQuoteUM.Content)?.copy(
diffPercent = DifferencePercent.Best,
isSingleProvider = isSingleProvider,
) ?: bestQuote
}
val selectQuoteTransformer = SwapAmountSelectQuoteTransformer(
@ -47,16 +56,23 @@ internal class SwapAmountSetQuotesTransformer(
return updatedState.copy(
isPrimaryButtonEnabled = updatedState.isPrimaryButtonEnabled && quotes.isNotEmpty(),
swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote),
swapQuotes = getQuotesWithDiff(sortedQuotes, bestQuote, isSingleProvider),
)
}
private fun getQuotesWithDiff(sortedQuotes: List<SwapQuoteUM>, bestQuote: SwapQuoteUM): ImmutableList<SwapQuoteUM> {
private fun getQuotesWithDiff(
sortedQuotes: List<SwapQuoteUM>,
bestQuote: SwapQuoteUM,
isSingleProvider: Boolean,
): ImmutableList<SwapQuoteUM> {
return sortedQuotes.sortedWith(SwapQuotesComparator)
.map { quote ->
if (quote is SwapQuoteUM.Content && bestQuote is SwapQuoteUM.Content) {
if (quote.provider.providerId == bestQuote.provider.providerId) {
quote.copy(diffPercent = DifferencePercent.Best)
quote.copy(
diffPercent = DifferencePercent.Best,
isSingleProvider = isSingleProvider,
)
} else {
// current / selected - 1
val percent = quote.quoteAmount / bestQuote.quoteAmount - BigDecimal.ONE

View file

@ -23,7 +23,9 @@ import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstrainedLayoutReference
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.ConstraintLayoutScope
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.amountScreen.ui.AmountBlockV2
import com.tangem.core.ui.extensions.TextReference
@ -34,6 +36,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.swap.v2.impl.R
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountFieldUM
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountType
import com.tangem.features.swap.v2.impl.amount.entity.SwapAmountUM
import com.tangem.features.swap.v2.impl.amount.ui.preview.SwapAmountContentPreview
import com.tangem.features.swap.v2.impl.chooseprovider.ui.SwapChooseProviderContent
@ -63,33 +66,11 @@ internal fun SwapAmountBlockContent(
),
) {
val (from, to, separator, provider) = createRefs()
AmountBlockV2(
amountState = amountUM.primaryAmount.amountField,
isClickDisabled = true,
isEditingDisabled = false,
modifier = Modifier.constrainAs(from) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
extraContent = { SwapPriceImpact(amountFieldUM = amountUM.primaryAmount, onInfoClick = onInfoClick) },
)
AmountBlockV2(
amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy(
title = resourceReference(R.string.send_with_swap_recipient_amount_title),
availableBalance = TextReference.EMPTY,
availableBalanceShort = TextReference.EMPTY,
) ?: amountUM.secondaryAmount.amountField,
isClickDisabled = true,
isEditingDisabled = false,
modifier = Modifier.constrainAs(to) {
top.linkTo(from.bottom, 8.dp)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
extraContent = {
SwapPriceImpact(amountFieldUM = amountUM.secondaryAmount, onInfoClick = onInfoClick)
},
SwapAmountBlock(
amountUM = amountUM,
fromAmountRef = from,
toAmountRef = to,
onInfoClick = onInfoClick,
)
SwapAmountDivider(
modifier = Modifier.constrainAs(separator) {
@ -103,6 +84,7 @@ internal fun SwapAmountBlockContent(
val isBestRate = quoteContent?.diffPercent is SwapQuoteUM.Content.DifferencePercent.Best
SwapChooseProviderContent(
isBestRate = isBestRate,
isSingleProvider = quoteContent?.isSingleProvider == true,
showBestRateAnimation = amountUM.showBestRateAnimation,
expressProvider = amountUM.selectedQuote.provider,
onClick = onProviderSelectClick,
@ -119,29 +101,88 @@ internal fun SwapAmountBlockContent(
}
@Composable
private fun SwapPriceImpact(amountFieldUM: SwapAmountFieldUM, onInfoClick: () -> Unit) {
private fun ConstraintLayoutScope.SwapAmountBlock(
amountUM: SwapAmountUM.Content,
fromAmountRef: ConstrainedLayoutReference,
toAmountRef: ConstrainedLayoutReference,
onInfoClick: () -> Unit,
) {
AmountBlockV2(
amountState = amountUM.primaryAmount.amountField,
isClickDisabled = true,
isEditingDisabled = false,
modifier = Modifier.constrainAs(fromAmountRef) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
extraContent = {
SwapPriceImpact(
amountFieldUM = amountUM.primaryAmount,
selectedAmountType = amountUM.selectedAmountType,
onInfoClick = onInfoClick,
)
},
)
AmountBlockV2(
amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy(
title = resourceReference(R.string.send_with_swap_recipient_amount_title),
availableBalance = TextReference.EMPTY,
availableBalanceShort = TextReference.EMPTY,
) ?: amountUM.secondaryAmount.amountField,
isClickDisabled = true,
isEditingDisabled = false,
modifier = Modifier.constrainAs(toAmountRef) {
top.linkTo(fromAmountRef.bottom, 8.dp)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
extraContent = {
SwapPriceImpact(
amountFieldUM = amountUM.secondaryAmount,
selectedAmountType = amountUM.selectedAmountType,
onInfoClick = onInfoClick,
)
},
)
}
@Composable
private fun SwapPriceImpact(
amountFieldUM: SwapAmountFieldUM,
selectedAmountType: SwapAmountType,
onInfoClick: () -> Unit,
) {
if (amountFieldUM.amountType == selectedAmountType) return
val priceImpact = (amountFieldUM as? SwapAmountFieldUM.Content)?.priceImpact
val iconColor = if (priceImpact != null) {
TangemTheme.colors.icon.attention
} else {
TangemTheme.colors.icon.informative
}
if (priceImpact != null) {
Text(
text = priceImpact.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.attention,
)
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_information_24),
),
tint = TangemTheme.colors.icon.attention,
contentDescription = null,
modifier = Modifier
.size(20.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onClick = onInfoClick,
),
)
}
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(R.drawable.ic_information_24),
),
tint = iconColor,
contentDescription = null,
modifier = Modifier
.size(20.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onClick = onInfoClick,
),
)
}
@Composable

View file

@ -69,6 +69,7 @@ internal data object SwapAmountContentPreview {
quoteAmountValue = stringReference("123"),
rate = stringReference("1 USD ≈ 123.123 POL"),
diffPercent = SwapQuoteUM.Content.DifferencePercent.Best,
isSingleProvider = false,
)
val emptyState = SwapAmountUM.Content(

View file

@ -50,6 +50,7 @@ import kotlinx.coroutines.delay
@Composable
fun SwapChooseProviderContent(
expressProvider: ExpressProvider?,
isSingleProvider: Boolean,
isBestRate: Boolean,
showBestRateAnimation: Boolean,
onClick: () -> Unit,
@ -62,6 +63,7 @@ fun SwapChooseProviderContent(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
onClick = onClick,
enabled = !isSingleProvider,
),
) {
HorizontalDivider(
@ -88,6 +90,7 @@ fun SwapChooseProviderContent(
ProviderInfo(
expressProvider = expressProvider,
isBestRate = isBestRate,
isSingleProvider = isSingleProvider,
showBestRateAnimation = showBestRateAnimation,
onFinishAnimation = onFinishAnimation,
)
@ -125,6 +128,7 @@ private fun FcaProviderWarning(modifier: Modifier = Modifier) {
private fun ProviderInfo(
expressProvider: ExpressProvider?,
isBestRate: Boolean,
isSingleProvider: Boolean,
showBestRateAnimation: Boolean,
onFinishAnimation: () -> Unit,
modifier: Modifier = Modifier,
@ -166,6 +170,7 @@ private fun ProviderInfo(
start.linkTo(imageRef.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
end.linkTo(iconRef.start, goneMargin = 12.dp)
},
)
Icon(
@ -180,12 +185,13 @@ private fun ProviderInfo(
start.linkTo(nameRef.end)
top.linkTo(parent.top)
bottom.linkTo(parent.bottom)
end.linkTo(parent.end, 12.dp)
end.linkTo(parent.end, margin = 12.dp)
visibility = if (isSingleProvider) Visibility.Gone else Visibility.Visible
},
)
BestRateBadge(
showBestRateAnimation = showBestRateAnimation,
isBestRate = isBestRate,
isBestRate = isBestRate && !isSingleProvider,
ref = imageRef,
onFinishAnimation = onFinishAnimation,
)
@ -325,6 +331,7 @@ private fun SwapChooseProviderContent_Preview() {
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
) {
SwapChooseProviderContent(
isSingleProvider = false,
isBestRate = true,
showBestRateAnimation = true,
expressProvider = ExpressProvider(

View file

@ -39,6 +39,7 @@ internal object SwapChooseProviderContentPreview {
quoteAmountValue = stringReference("123"),
rate = stringReference("1 USD ≈ 123.123 POL"),
diffPercent = SwapQuoteUM.Content.DifferencePercent.Best,
isSingleProvider = false,
)
private val quote2 = SwapQuoteUM.Content(
@ -47,6 +48,7 @@ internal object SwapChooseProviderContentPreview {
quoteAmountValue = stringReference("13.12"),
rate = stringReference("1 USD ≈ 12.123 POL"),
diffPercent = SwapQuoteUM.Content.DifferencePercent.Empty,
isSingleProvider = false,
)
val state = SwapChooseProviderBottomSheetContent(

View file

@ -34,6 +34,7 @@ internal sealed class SwapQuoteUM {
val quoteAmount: BigDecimal,
val quoteAmountValue: TextReference,
val diffPercent: DifferencePercent,
val isSingleProvider: Boolean,
val rate: TextReference,
) : SwapQuoteUM() {
sealed class DifferencePercent {