Updated on 2026-08-14

This commit is contained in:
Tangem 2024-02-27 19:58:29 +01:00
parent 7575df8bf6
commit f1aa5f3a6e
5 changed files with 65 additions and 61 deletions

View file

@ -25,7 +25,7 @@ sealed class ProviderState {
val subtitle: TextReference,
val selectionType: SelectionType,
val additionalBadge: AdditionalBadge,
val percentLowerThenBest: PercentLowerThanBest = PercentLowerThanBest.Empty,
val percentLowerThenBest: PercentDifference = PercentDifference.Empty,
override val onProviderClick: (String) -> Unit,
) : ProviderState()
@ -50,9 +50,9 @@ sealed class ProviderState {
}
}
sealed class PercentLowerThanBest {
data class Value(val value: Float) : PercentLowerThanBest()
object Empty : PercentLowerThanBest()
sealed class PercentDifference {
data class Value(val value: Float) : PercentDifference()
object Empty : PercentDifference()
}
object ProviderPercentDiffComparator : Comparator<ProviderState> {
@ -66,14 +66,14 @@ object ProviderPercentDiffComparator : Comparator<ProviderState> {
if (o1 is ProviderState.Content && o2 is ProviderState.Content) {
val o1Percent = o1.percentLowerThenBest
val o2Percent = o2.percentLowerThenBest
if (o1Percent is PercentLowerThanBest.Value && o2Percent !is PercentLowerThanBest.Value) {
if (o1Percent is PercentDifference.Value && o2Percent !is PercentDifference.Value) {
return -1
}
if (o1Percent !is PercentLowerThanBest.Value && o2Percent is PercentLowerThanBest.Value) {
if (o1Percent !is PercentDifference.Value && o2Percent is PercentDifference.Value) {
return 1
}
return if (o1Percent is PercentLowerThanBest.Value && o2Percent is PercentLowerThanBest.Value) {
o1Percent.value.compareTo(o2Percent.value)
return if (o1Percent is PercentDifference.Value && o2Percent is PercentDifference.Value) {
o2Percent.value.compareTo(o1Percent.value)
} else {
0
}

View file

@ -19,7 +19,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig
import com.tangem.feature.swap.models.states.PercentLowerThanBest
import com.tangem.feature.swap.models.states.PercentDifference
import com.tangem.feature.swap.models.states.ProviderState
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.toImmutableList
@ -114,7 +114,7 @@ private fun ChooseProviderBottomSheet_Preview() {
iconUrl = "",
subtitle = stringReference("1 000 000"),
additionalBadge = ProviderState.AdditionalBadge.BestTrade,
percentLowerThenBest = PercentLowerThanBest.Value(-1.0f),
percentLowerThenBest = PercentDifference.Value(-1.0f),
selectionType = ProviderState.SelectionType.SELECT,
onProviderClick = {},
),

View file

@ -25,7 +25,7 @@ import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.states.PercentLowerThanBest
import com.tangem.feature.swap.models.states.PercentDifference
import com.tangem.feature.swap.models.states.ProviderState
/**
@ -93,11 +93,8 @@ private fun ProviderContentState(
.padding(start = TangemTheme.dimens.spacing12)
.size(size = TangemTheme.dimens.size40)
.clip(TangemTheme.shapes.roundedCorners8),
model = ImageRequest.Builder(context = LocalContext.current)
.data(state.iconUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
model = ImageRequest.Builder(context = LocalContext.current).data(state.iconUrl)
.crossfade(enable = true).allowHardware(false).build(),
loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) },
error = {
ErrorProviderIcon(
@ -135,10 +132,12 @@ private fun ProviderContentState(
)
}
when (state.additionalBadge) {
ProviderState.AdditionalBadge.BestTrade ->
BestTradeItem(Modifier.padding(start = TangemTheme.dimens.spacing4))
ProviderState.AdditionalBadge.PermissionRequired ->
PermissionBadgeItem(Modifier.padding(start = TangemTheme.dimens.spacing4))
ProviderState.AdditionalBadge.BestTrade -> BestTradeItem(
Modifier.padding(start = TangemTheme.dimens.spacing4),
)
ProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem(
Modifier.padding(start = TangemTheme.dimens.spacing4),
)
ProviderState.AdditionalBadge.Empty -> {
// no-op
}
@ -159,14 +158,19 @@ private fun ProviderContentState(
maxLines = 1,
)
}
if (state.percentLowerThenBest is PercentLowerThanBest.Value &&
state.percentLowerThenBest.value > 0
if (state.percentLowerThenBest is PercentDifference.Value &&
state.percentLowerThenBest.value != 0f
) {
val textColor = if (state.percentLowerThenBest.value > 0) {
TangemTheme.colors.icon.accent
} else {
TangemTheme.colors.text.warning
}
AnimatedContent(targetState = state.percentLowerThenBest.value, label = "") {
Text(
text = "-$it%",
text = if (it > 0) "+$it%" else "$it%",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.warning,
color = textColor,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
overflow = TextOverflow.Ellipsis,
maxLines = 1,
@ -195,11 +199,8 @@ private fun ProviderUnavailableState(
.padding(start = TangemTheme.dimens.spacing12)
.size(size = TangemTheme.dimens.size40)
.clip(TangemTheme.shapes.roundedCorners8),
model = ImageRequest.Builder(context = LocalContext.current)
.data(state.iconUrl)
.crossfade(enable = true)
.allowHardware(false)
.build(),
model = ImageRequest.Builder(context = LocalContext.current).data(state.iconUrl)
.crossfade(enable = true).allowHardware(false).build(),
loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) },
error = {
ErrorProviderIcon(
@ -295,8 +296,7 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) {
@Composable
private fun BoxScope.ProviderChevron(selectionType: ProviderState.SelectionType, isSelected: Boolean) {
when (selectionType) {
ProviderState.SelectionType.NONE -> {
/* no-op */
ProviderState.SelectionType.NONE -> { /* no-op */
}
ProviderState.SelectionType.CLICK -> {
Icon(
@ -342,11 +342,10 @@ private fun BaseContainer(modifier: Modifier = Modifier, content: @Composable Bo
@Composable
private fun ErrorProviderIcon(modifier: Modifier = Modifier) {
Box(
modifier = modifier
.background(
color = TangemTheme.colors.background.secondary,
shape = TangemTheme.shapes.roundedCorners8,
),
modifier = modifier.background(
color = TangemTheme.colors.background.secondary,
shape = TangemTheme.shapes.roundedCorners8,
),
contentAlignment = Alignment.Center,
) {
Icon(
@ -366,7 +365,7 @@ private fun BestTradeItem(modifier: Modifier = Modifier) {
),
) {
Text(
text = "Best rate",
text = stringResource(R.string.express_provider_best_rate),
style = TangemTheme.typography.caption1,
color = TangemTheme.colors.icon.accent,
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6),
@ -417,7 +416,7 @@ private fun ProviderItem_Content_Preview() {
iconUrl = "",
subtitle = stringReference("1 000 000"),
additionalBadge = ProviderState.AdditionalBadge.PermissionRequired,
percentLowerThenBest = PercentLowerThanBest.Value(-1.0f),
percentLowerThenBest = PercentDifference.Value(12.0f),
selectionType = ProviderState.SelectionType.SELECT,
onProviderClick = {},
)

View file

@ -1024,6 +1024,7 @@ internal class StateBuilder(
fun showSelectProviderBottomSheet(
uiState: SwapStateHolder,
selectedProviderId: String,
bestRatedProviderId: String,
pricesLowerBest: Map<String, Float>,
providersStates: Map<SwapProvider, SwapState>,
unavailableProviders: List<SwapProvider>,
@ -1031,7 +1032,7 @@ internal class StateBuilder(
): SwapStateHolder {
val availableProvidersStates = providersStates.entries
.mapNotNull {
it.convertToProviderBottomSheetState(pricesLowerBest, actions.onProviderSelect)
it.convertToProviderBottomSheetState(pricesLowerBest, bestRatedProviderId, actions.onProviderSelect)
}
.sortedWith(ProviderPercentDiffComparator)
val unavailableProviderStates = unavailableProviders.map {
@ -1072,8 +1073,8 @@ internal class StateBuilder(
it.copy(
subtitle = stringReference(rateString),
percentLowerThenBest = pricesLowerBest[it.id]?.let { percent ->
PercentLowerThanBest.Value(percent)
} ?: PercentLowerThanBest.Empty,
PercentDifference.Value(percent)
} ?: PercentDifference.Value(0f),
)
} else {
it
@ -1179,18 +1180,21 @@ internal class StateBuilder(
private fun Map.Entry<SwapProvider, SwapState>.convertToProviderBottomSheetState(
pricesLowerBest: Map<String, Float>,
bestRatedProviderId: String,
onProviderSelect: (String) -> Unit,
): ProviderState? {
val provider = this.key
return when (val state = this.value) {
is SwapState.EmptyAmountState -> null
is SwapState.QuotesLoadedState -> provider.convertToContentSelectableProviderState(
isBestRate = false, // not show best rate in bottom sheet
state = state,
onProviderClick = onProviderSelect,
pricesLowerBest = pricesLowerBest,
selectionType = ProviderState.SelectionType.SELECT,
)
is SwapState.QuotesLoadedState -> {
provider.convertToContentSelectableProviderState(
isBestRate = bestRatedProviderId == provider.providerId,
state = state,
onProviderClick = onProviderSelect,
pricesLowerBest = pricesLowerBest,
selectionType = ProviderState.SelectionType.SELECT,
)
}
is SwapState.SwapError -> getProviderStateForError(
swapProvider = provider,
fromToken = state.fromTokenInfo.cryptoCurrencyStatus.currency,
@ -1282,7 +1286,7 @@ internal class StateBuilder(
subtitle = stringReference(rateString),
additionalBadge = badge,
selectionType = selectionType,
percentLowerThenBest = PercentLowerThanBest.Empty,
percentLowerThenBest = PercentDifference.Empty,
onProviderClick = onProviderClick,
)
}
@ -1312,8 +1316,8 @@ internal class StateBuilder(
additionalBadge = additionalBadge,
selectionType = selectionType,
percentLowerThenBest = pricesLowerBest[this.providerId]?.let { percent ->
PercentLowerThanBest.Value(percent)
} ?: PercentLowerThanBest.Value(0f),
PercentDifference.Value(percent)
} ?: PercentDifference.Value(0f),
onProviderClick = onProviderClick,
)
}
@ -1347,7 +1351,7 @@ internal class StateBuilder(
selectionType = selectionType,
subtitle = alertText,
additionalBadge = ProviderState.AdditionalBadge.Empty,
percentLowerThenBest = PercentLowerThanBest.Empty,
percentLowerThenBest = PercentDifference.Empty,
onProviderClick = onProviderClick,
)
}

View file

@ -49,7 +49,6 @@ import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.Locale
import javax.inject.Inject
import kotlin.math.absoluteValue
import kotlin.properties.Delegates
typealias SuccessLoadedSwapData = Map<SwapProvider, SwapState.QuotesLoadedState>
@ -325,7 +324,7 @@ internal class SwapViewModel @Inject constructor(
val (provider, state) = updateLoadedQuotes(providersState)
setupLoadedState(provider, state, fromToken)
val successStates = providersState.getLastLoadedSuccessStates()
val pricesLowerBest = getPricesLowerBest(successStates)
val pricesLowerBest = getPricesLowerBest(provider.providerId, successStates)
uiState = stateBuilder.updateProvidersBottomSheetContent(
uiState = uiState,
pricesLowerBest = pricesLowerBest,
@ -908,13 +907,14 @@ internal class SwapViewModel @Inject constructor(
onProviderClick = { providerId ->
analyticsEventHandler.send(SwapEvents.ProviderClicked)
val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates()
val pricesLowerBest = getPricesLowerBest(states)
val pricesLowerBest = getPricesLowerBest(providerId, states)
val unavailableProviders = getUnavailableProvidersFor(dataState.lastLoadedSwapStates)
uiState = stateBuilder.showSelectProviderBottomSheet(
uiState = uiState,
selectedProviderId = providerId,
pricesLowerBest = pricesLowerBest,
unavailableProviders = unavailableProviders,
bestRatedProviderId = findBestQuoteProvider(states)?.providerId ?: providerId,
providersStates = dataState.lastLoadedSwapStates,
) { uiState = stateBuilder.dismissBottomSheet(uiState) }
},
@ -1023,17 +1023,18 @@ internal class SwapViewModel @Inject constructor(
}?.key
}
private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map<String, Float> {
val bestRateEntry = state.maxByOrNull { it.value.toTokenInfo.tokenAmount.value } ?: return emptyMap()
val bestRate = bestRateEntry.value.toTokenInfo.tokenAmount.value
private fun getPricesLowerBest(selectedProviderId: String, state: SuccessLoadedSwapData): Map<String, Float> {
val selectedProviderEntry = state.filter { it.key.providerId == selectedProviderId }.entries.firstOrNull()
?: return emptyMap()
val selectedProviderRate = selectedProviderEntry.value.toTokenInfo.tokenAmount.value
val hundredPercent = BigDecimal("100")
return state.entries.mapNotNull {
if (it.key != bestRateEntry.key) {
if (it.key != selectedProviderEntry.key) {
val amount = it.value.toTokenInfo.tokenAmount.value
val percentDiff = BigDecimal.ONE.minus(
amount.divide(bestRate, RoundingMode.HALF_UP),
selectedProviderRate.divide(amount, RoundingMode.HALF_UP),
).multiply(hundredPercent)
it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat().absoluteValue
it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat()
} else {
null
}