diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt index 265631000e..0e396ca622 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt @@ -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 { @@ -66,14 +66,14 @@ object ProviderPercentDiffComparator : Comparator { 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 } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 51757b8c16..301815f572 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -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 = {}, ), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt index eaddec7e0c..34e7da0b20 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt @@ -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 = {}, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 0da7c077d1..84aba17a86 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -1024,6 +1024,7 @@ internal class StateBuilder( fun showSelectProviderBottomSheet( uiState: SwapStateHolder, selectedProviderId: String, + bestRatedProviderId: String, pricesLowerBest: Map, providersStates: Map, unavailableProviders: List, @@ -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.convertToProviderBottomSheetState( pricesLowerBest: Map, + 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, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index be2e616805..75561ff004 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -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 @@ -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 { - 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 { + 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 }