Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-01 17:26:08 +02:00
parent 8c2e30f9bf
commit 438f502a4c
5 changed files with 83 additions and 26 deletions

View file

@ -1,7 +1,7 @@
package com.tangem.core.ui.components.provider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.tangem.core.ui.R
import com.tangem.domain.express.models.ProviderFilterType
@ -20,31 +20,31 @@ fun ProviderTypeFilterPicker(
onFilterSelect: (ProviderFilterType) -> Unit,
modifier: Modifier = Modifier,
) {
val segments = availableFilters.map { filter ->
TangemSegmentUM(
id = filter.name,
title = when (filter) {
ProviderFilterType.ALL -> resourceReference(R.string.common_all)
ProviderFilterType.CEX -> TextReference.Str("CEX")
ProviderFilterType.DEX -> TextReference.Str("DEX")
},
)
}.toImmutableList()
val selectedSegment = segments.firstOrNull { it.id == selectedFilter.name }
TangemThemeRedesign {
// key() forces recomposition when selectedFilter changes to re-seed initialSelectedItem,
// because TangemSegmentedPicker owns its selection state internally via remember.
key(selectedFilter) {
TangemSegmentedPicker(
items = segments,
initialSelectedItem = selectedSegment,
isFixed = true,
modifier = modifier,
onClick = { segment ->
val filterType = availableFilters.firstOrNull { it.name == segment.id }
if (filterType != null) onFilterSelect(filterType)
val segments = remember(availableFilters) {
availableFilters.map { filter ->
TangemSegmentUM(
id = filter.name,
title = when (filter) {
ProviderFilterType.ALL -> resourceReference(R.string.common_all)
ProviderFilterType.CEX -> TextReference.Str("CEX")
ProviderFilterType.DEX -> TextReference.Str("DEX")
},
)
}
}.toImmutableList()
}
val selectedSegment = remember(segments, selectedFilter) {
segments.firstOrNull { it.id == selectedFilter.name }
}
TangemThemeRedesign {
TangemSegmentedPicker(
items = segments,
initialSelectedItem = selectedSegment,
isFixed = true,
modifier = modifier,
onClick = { segment ->
val filterType = availableFilters.firstOrNull { it.name == segment.id }
if (filterType != null) onFilterSelect(filterType)
},
)
}
}

View file

@ -74,6 +74,8 @@ internal object SwapProviderStateBuilder {
permissionState: PermissionDataState,
pricesLowerBest: Map<String, Float>,
selectionType: ProviderState.SelectionType,
isBestRate: Boolean = false,
isNeedBestRateBadge: Boolean = false,
needApplyFCARestrictions: Boolean,
onProviderClick: (String) -> Unit,
): ProviderState.Content {
@ -83,6 +85,8 @@ internal object SwapProviderStateBuilder {
provider = provider,
needApplyFCARestrictions = needApplyFCARestrictions,
permissionState = permissionState,
isBestRate = isBestRate,
isNeedBestRateBadge = isNeedBestRateBadge,
),
selectionType = selectionType,
percentLowerThenBest = pricesLowerBest[provider.providerId]

View file

@ -1698,12 +1698,15 @@ internal class SwapModel @Inject constructor(
analyticsEventHandler.send(SwapEvents.ProviderClicked())
val states = dataState.lastLoadedSwapStates.getLastLoadedSuccessStates()
val pricesLowerBest = getPricesLowerBest(providerId, states)
val bestRatedProviderId = findBestQuoteProvider(states)?.providerId ?: providerId
uiState = stateBuilder.showSelectProviderBottomSheet(
uiState = uiState,
selectedProviderId = providerId,
pricesLowerBest = pricesLowerBest,
providersStates = dataState.lastLoadedSwapStates,
needApplyFCARestrictions = userCountry.needApplyFCARestrictions(),
bestRatedProviderId = bestRatedProviderId,
isNeedBestRateBadge = dataState.lastLoadedSwapStates.consideredProvidersStates().size > 1,
) { uiState = stateBuilder.dismissBottomSheet(uiState) }
},
onProviderSelect = { providerId ->

View file

@ -1068,6 +1068,8 @@ internal class StateBuilder(
pricesLowerBest: Map<String, Float>,
providersStates: Map<SwapProvider, SwapState>,
needApplyFCARestrictions: Boolean,
bestRatedProviderId: String,
isNeedBestRateBadge: Boolean,
onDismiss: () -> Unit,
): SwapStateHolder {
val availableProvidersStates = providersStates.entries
@ -1076,6 +1078,8 @@ internal class StateBuilder(
pricesLowerBest = pricesLowerBest,
onProviderSelect = actions.onProviderSelect,
needApplyFCARestrictions = needApplyFCARestrictions,
bestRatedProviderId = bestRatedProviderId,
isNeedBestRateBadge = isNeedBestRateBadge,
)
}
.sortedWith(ProviderPercentDiffComparator)
@ -1180,6 +1184,8 @@ internal class StateBuilder(
pricesLowerBest: Map<String, Float>,
onProviderSelect: (String) -> Unit,
needApplyFCARestrictions: Boolean,
bestRatedProviderId: String,
isNeedBestRateBadge: Boolean,
): ProviderState? {
val provider = this.key
return when (val state = this.value) {
@ -1192,6 +1198,8 @@ internal class StateBuilder(
pricesLowerBest = pricesLowerBest,
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = needApplyFCARestrictions,
isBestRate = bestRatedProviderId == provider.providerId && !state.priceImpact.shouldShowWarning(),
isNeedBestRateBadge = isNeedBestRateBadge,
onProviderClick = onProviderSelect,
)
}

View file

@ -220,7 +220,7 @@ internal class SwapProviderStateBuilderTest {
}
@Test
fun `GIVEN best rate badge inputs WHEN buildContentSelectable THEN BestTrade badge is never set`() {
fun `GIVEN best rate AND no FCA AND no permission WHEN buildContentSelectable THEN BestTrade badge`() {
val provider = provider(id = "any", isRecommended = false)
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
@ -231,6 +231,48 @@ internal class SwapProviderStateBuilderTest {
pricesLowerBest = emptyMap(),
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = false,
isBestRate = true,
isNeedBestRateBadge = true,
onProviderClick = onProviderClick,
)
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.BestTrade)
}
@Test
fun `GIVEN isNeedBestRateBadge false WHEN buildContentSelectable THEN no BestTrade badge`() {
val provider = provider(id = "any", isRecommended = false)
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
val result = SwapProviderStateBuilder.buildContentSelectable(
provider = provider,
toTokenInfo = info,
permissionState = PermissionDataState.Empty,
pricesLowerBest = emptyMap(),
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = false,
isBestRate = true,
isNeedBestRateBadge = false,
onProviderClick = onProviderClick,
)
assertThat(result.additionalBadge).isEqualTo(ProviderState.AdditionalBadge.Empty)
}
@Test
fun `GIVEN isBestRate false AND badge enabled WHEN buildContentSelectable THEN no BestTrade badge`() {
val provider = provider(id = "any", isRecommended = false)
val info = tokenInfo(symbol = "USDT", decimals = 6, amount = BigDecimal("100"))
val result = SwapProviderStateBuilder.buildContentSelectable(
provider = provider,
toTokenInfo = info,
permissionState = PermissionDataState.Empty,
pricesLowerBest = emptyMap(),
selectionType = ProviderState.SelectionType.SELECT,
needApplyFCARestrictions = false,
isBestRate = false,
isNeedBestRateBadge = true,
onProviderClick = onProviderClick,
)