From 5b516f2c9baf01ed29faf46db23e1e3cb01c06ad Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 May 2026 18:07:31 +0200 Subject: [PATCH] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 4 + core/ui/build.gradle.kts | 1 + .../provider/ProviderTypeFilterPicker.kt | 50 +++++++++++ .../express/models/ProviderFilterType.kt | 3 + .../swap/v2/api/SwapFeatureToggles.kt | 5 ++ .../swap/v2/impl/DefaultSwapFeatureToggles.kt | 13 +++ .../SwapChooseProviderComponent.kt | 1 + .../SwapChooseProviderBottomSheetContent.kt | 3 + .../model/SwapChooseProviderModel.kt | 56 +++++++++++-- .../ui/SwapChooseProviderBottomSheet.kt | 52 +++++++++--- .../SwapChooseProviderContentPreview.kt | 3 + .../swap/v2/impl/di/SwapFeatureModules.kt | 21 +++++ .../features/swap/SwapFeatureToggles.kt | 1 + .../feature/swap/DefaultSwapFeatureToggles.kt | 4 + .../tangem/feature/swap/model/SwapModel.kt | 5 +- .../tangem/feature/swap/models/UiActions.kt | 2 + .../states/ChooseProviderBottomSheetConfig.kt | 5 ++ .../swap/ui/ChooseProviderBottomSheet.kt | 64 ++++++++++++--- .../tangem/feature/swap/ui/StateBuilder.kt | 82 +++++++++++++++---- .../swap/StateBuilderInitialStateTest.kt | 4 +- .../feature/swap/StateBuilderPairsTest.kt | 4 +- .../feature/swap/StateBuilderQuotesTest.kt | 4 +- .../feature/swap/StateBuilderSwapDataTest.kt | 4 +- 23 files changed, 339 insertions(+), 52 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderTypeFilterPicker.kt create mode 100644 domain/express/models/src/main/java/com/tangem/domain/express/models/ProviderFilterType.kt create mode 100644 features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt create mode 100644 features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/di/SwapFeatureModules.kt diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 485f0eab26..009f3f6d17 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -78,5 +78,9 @@ { "name": "AND_15310_ADD_FUNDS_STAGE1", "version": "undefined" + }, + { + "name": "AND_15009_SWAP_PROVIDER_FILTER_ENABLED", + "version": "undefined" } ] diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 3423aacdbf..cb71f6b87c 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -116,6 +116,7 @@ tasks.named("preBuild") { dependencies { /** Project - Domain */ implementation(projects.domain.appTheme.models) + implementation(projects.domain.express.models) implementation(projects.domain.models) implementation(projects.domain.tokens.models) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderTypeFilterPicker.kt b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderTypeFilterPicker.kt new file mode 100644 index 0000000000..af6a9062f1 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/provider/ProviderTypeFilterPicker.kt @@ -0,0 +1,50 @@ +package com.tangem.core.ui.components.provider + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Modifier +import com.tangem.core.ui.R +import com.tangem.domain.express.models.ProviderFilterType +import com.tangem.core.ui.ds.tabs.TangemSegmentUM +import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemThemeRedesign +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun ProviderTypeFilterPicker( + availableFilters: ImmutableList, + selectedFilter: ProviderFilterType, + 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) + }, + ) + } + } +} \ No newline at end of file diff --git a/domain/express/models/src/main/java/com/tangem/domain/express/models/ProviderFilterType.kt b/domain/express/models/src/main/java/com/tangem/domain/express/models/ProviderFilterType.kt new file mode 100644 index 0000000000..bacf182dcf --- /dev/null +++ b/domain/express/models/src/main/java/com/tangem/domain/express/models/ProviderFilterType.kt @@ -0,0 +1,3 @@ +package com.tangem.domain.express.models + +enum class ProviderFilterType { ALL, CEX, DEX } \ No newline at end of file diff --git a/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt new file mode 100644 index 0000000000..b0fb0a7b2c --- /dev/null +++ b/features/swap-v2/api/src/main/java/com/tangem/features/swap/v2/api/SwapFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.swap.v2.api + +interface SwapFeatureToggles { + val isSwapProviderFilterEnabled: Boolean +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt new file mode 100644 index 0000000000..cf73e0fed6 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/DefaultSwapFeatureToggles.kt @@ -0,0 +1,13 @@ +package com.tangem.features.swap.v2.impl + +import com.tangem.core.configtoggle.FeatureToggles +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.swap.v2.api.SwapFeatureToggles +import javax.inject.Inject + +internal class DefaultSwapFeatureToggles @Inject constructor( + private val featureToggles: FeatureTogglesManager, +) : SwapFeatureToggles { + override val isSwapProviderFilterEnabled: Boolean = + featureToggles.isFeatureEnabled(FeatureToggles.AND_15009_SWAP_PROVIDER_FILTER_ENABLED) +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt index 351e5cbad0..e27c0254ba 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/SwapChooseProviderComponent.kt @@ -45,6 +45,7 @@ internal class SwapChooseProviderComponent( SwapChooseProviderContent( contentUM = state.value, onProviderClick = model::onProviderClick, + onFilterSelect = model::onFilterSelect, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt index 7be1b3c0c7..d6d935bcf9 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/entity/SwapChooseProviderBottomSheetContent.kt @@ -1,6 +1,7 @@ package com.tangem.features.swap.v2.impl.chooseprovider.entity import com.tangem.core.ui.components.provider.entity.ProviderChooseUM +import com.tangem.domain.express.models.ProviderFilterType import com.tangem.domain.express.models.ExpressProvider import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import kotlinx.collections.immutable.ImmutableList @@ -9,6 +10,8 @@ internal data class SwapChooseProviderBottomSheetContent( val providerList: ImmutableList, val isApplyFCARestrictions: Boolean, val selectedProvider: ExpressProvider, + val selectedFilter: ProviderFilterType, + val availableFilters: ImmutableList, ) internal data class SwapProviderListItem( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt index 9d550636df..bc5c02da91 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/model/SwapChooseProviderModel.kt @@ -3,8 +3,11 @@ 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.ProviderFilterType import com.tangem.domain.express.models.ExpressError +import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions +import com.tangem.features.swap.v2.api.SwapFeatureToggles import com.tangem.features.swap.v2.impl.chooseprovider.SwapChooseProviderComponent import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProviderBottomSheetContent import com.tangem.features.swap.v2.impl.chooseprovider.model.converter.SwapProviderListItemConverter @@ -12,6 +15,7 @@ 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.persistentListOf import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -21,6 +25,7 @@ import javax.inject.Inject internal class SwapChooseProviderModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val swapFeatureToggles: SwapFeatureToggles, ) : Model() { private val params: SwapChooseProviderComponent.Params = paramsContainer.require() @@ -46,18 +51,53 @@ internal class SwapChooseProviderModel @Inject constructor( params.onDismiss() } + fun onFilterSelect(filterType: ProviderFilterType) { + val filteredProviders = getDisplayableProviders(params.providers) + .filter { matchesTypeFilter(it, filterType) } + uiState.value = uiState.value.copy( + providerList = swapProviderListItemConverter.convertList(filteredProviders) + .filterNotNull() + .toPersistentList(), + selectedFilter = filterType, + ) + } + private fun getInitialState(): SwapChooseProviderBottomSheetContent { - val filteredProviderList = params.providers.filter { swapQuoteUM -> + val displayableProviders = getDisplayableProviders(params.providers) + val hasCex = displayableProviders.any { it.provider?.type == ExpressProviderType.CEX } + val hasDex = displayableProviders.any { + it.provider?.type == ExpressProviderType.DEX || it.provider?.type == ExpressProviderType.DEX_BRIDGE + } + val availableFilters = if (swapFeatureToggles.isSwapProviderFilterEnabled && hasCex && hasDex) { + persistentListOf(ProviderFilterType.ALL, ProviderFilterType.CEX, ProviderFilterType.DEX) + } else { + persistentListOf() + } + return SwapChooseProviderBottomSheetContent( + isApplyFCARestrictions = isNeedApplyFCARestrictions && params.selectedProvider.isRestrictedByFCA(), + providerList = swapProviderListItemConverter.convertList(displayableProviders) + .filterNotNull() + .toPersistentList(), + selectedProvider = params.selectedProvider, + selectedFilter = ProviderFilterType.ALL, + availableFilters = availableFilters, + ) + } + + private fun getDisplayableProviders(allProviders: List): List { + return allProviders.filter { swapQuoteUM -> swapQuoteUM is SwapQuoteUM.Content || swapQuoteUM is SwapQuoteUM.Allowance || (swapQuoteUM as? SwapQuoteUM.Error)?.expressError is ExpressError.AmountError } - return SwapChooseProviderBottomSheetContent( - isApplyFCARestrictions = isNeedApplyFCARestrictions && params.selectedProvider.isRestrictedByFCA(), - providerList = swapProviderListItemConverter.convertList(filteredProviderList) - .filterNotNull() - .toPersistentList(), - selectedProvider = params.selectedProvider, - ) + } + + private fun matchesTypeFilter(quote: SwapQuoteUM, filterType: ProviderFilterType): Boolean { + val type = quote.provider?.type ?: return filterType == ProviderFilterType.ALL + return when (filterType) { + ProviderFilterType.ALL -> true + ProviderFilterType.CEX -> type == ExpressProviderType.CEX + ProviderFilterType.DEX -> type == ExpressProviderType.DEX || type == ExpressProviderType.DEX_BRIDGE + } } } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt index 77f8f0cbef..2fc2b41aed 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/SwapChooseProviderBottomSheet.kt @@ -4,27 +4,36 @@ 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.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview 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 androidx.compose.ui.util.fastForEach 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.ProviderTypeFilterPicker import com.tangem.core.ui.components.provider.entity.ProviderChooseUM +import com.tangem.domain.express.models.ProviderFilterType import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.selectedBorder @@ -36,6 +45,7 @@ import com.tangem.features.swap.v2.impl.chooseprovider.entity.SwapChooseProvider import com.tangem.features.swap.v2.impl.chooseprovider.ui.preview.SwapChooseProviderContentPreview import com.tangem.features.swap.v2.impl.common.entity.SwapQuoteUM import com.tangem.features.swap.v2.impl.notifications.entity.SwapNotificationUM +import kotlinx.collections.immutable.persistentListOf private const val DISABLED_COLORS_ALPHA = 0.5f @@ -55,23 +65,38 @@ internal fun SwapChooseProviderBottomSheet(config: TangemBottomSheetConfig, cont } } +@Suppress("LongMethod") @Composable internal fun SwapChooseProviderContent( contentUM: SwapChooseProviderBottomSheetContent, onProviderClick: (SwapQuoteUM) -> Unit, + onFilterSelect: (ProviderFilterType) -> Unit, modifier: Modifier = Modifier, ) { + val density = LocalDensity.current + var minHeight by remember { mutableStateOf(0.dp) } Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier.padding(horizontal = 12.dp), + modifier = modifier + .padding(horizontal = 12.dp) + .heightIn(min = minHeight) + .onSizeChanged { size -> + with(density) { + val h = size.height.toDp() + if (h > minHeight) minHeight = h + } + }, ) { - 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), - ) + if (contentUM.availableFilters.isNotEmpty()) { + ProviderTypeFilterPicker( + availableFilters = contentUM.availableFilters, + selectedFilter = contentUM.selectedFilter, + onFilterSelect = onFilterSelect, + modifier = Modifier + .padding(horizontal = 4.dp) + .padding(bottom = 12.dp), + ) + } AnimatedVisibility( modifier = Modifier.padding(top = 12.dp), visible = contentUM.isApplyFCARestrictions, @@ -83,7 +108,7 @@ internal fun SwapChooseProviderContent( ) } SpacerH12() - contentUM.providerList.fastForEachIndexed { index, provider -> + contentUM.providerList.fastForEach { provider -> SwapProviderItem( state = provider.swapProviderState, modifier = Modifier @@ -144,8 +169,15 @@ private fun SwapChooseProviderContent_Preview( providerList = params.providerList, isApplyFCARestrictions = true, selectedProvider = SwapChooseProviderContentPreview.provider1, + selectedFilter = ProviderFilterType.ALL, + availableFilters = persistentListOf( + ProviderFilterType.ALL, + ProviderFilterType.CEX, + ProviderFilterType.DEX, + ), ), onProviderClick = {}, + onFilterSelect = {}, ) } } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt index 4765401f15..f95b0f336a 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/chooseprovider/ui/preview/SwapChooseProviderContentPreview.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.audits.AuditLabelUM import com.tangem.core.ui.components.provider.entity.ProviderChooseUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.express.models.ProviderFilterType import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.express.models.ExpressRateType @@ -114,5 +115,7 @@ internal object SwapChooseProviderContentPreview { ), selectedProvider = provider1, isApplyFCARestrictions = false, + selectedFilter = ProviderFilterType.ALL, + availableFilters = persistentListOf(), ) } \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/di/SwapFeatureModules.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/di/SwapFeatureModules.kt new file mode 100644 index 0000000000..3318c6d6c3 --- /dev/null +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/di/SwapFeatureModules.kt @@ -0,0 +1,21 @@ +package com.tangem.features.swap.v2.impl.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.swap.v2.api.SwapFeatureToggles +import com.tangem.features.swap.v2.impl.DefaultSwapFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object SwapFeatureModules { + + @Provides + @Singleton + fun provideSwapFeatureToggles(featureTogglesManager: FeatureTogglesManager): SwapFeatureToggles { + return DefaultSwapFeatureToggles(featureTogglesManager) + } +} \ No newline at end of file diff --git a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt index 38e6caa619..834da0be35 100644 --- a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt +++ b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapFeatureToggles.kt @@ -4,4 +4,5 @@ interface SwapFeatureToggles { val isSwapSwitchToTransferEnabled: Boolean val isSwapIntegratedApproveEnabled: Boolean val isSwapAbEnabled: Boolean + val isSwapProviderFilterEnabled: Boolean } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt index 368ed8672f..a170fffc23 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapFeatureToggles.kt @@ -20,4 +20,8 @@ internal class DefaultSwapFeatureToggles @Inject constructor( override val isSwapAbEnabled: Boolean = featureTogglesManager.isFeatureEnabled( toggle = FeatureToggles.SWAP_AB_ENABLED, ) + + override val isSwapProviderFilterEnabled: Boolean = featureTogglesManager.isFeatureEnabled( + toggle = FeatureToggles.AND_15009_SWAP_PROVIDER_FILTER_ENABLED, + ) } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 627188e09c..444a6df6b4 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -195,7 +195,7 @@ internal class SwapModel @Inject constructor( appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), isAccountsModeProvider = Provider { isAccountsMode }, isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork, - shouldShowAbMenu = swapFeatureToggles.isSwapAbEnabled, + swapFeatureToggles = swapFeatureToggles, ) private val inputNumberFormatter = InputNumberFormatter( @@ -1596,6 +1596,9 @@ internal class SwapModel @Inject constructor( ) } }, + onProviderFilterSelect = { filterType -> + uiState = stateBuilder.updateProviderFilterType(uiState, filterType) + }, onBuyClick = { val fromSwapCurrencyStatus = dataState.fromSwapCurrencyStatus ?: return@UiActions val feePaidCryptoCurrency = dataState.feePaidCryptoCurrency ?: return@UiActions diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 31ccca8739..69307b7b51 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.models +import com.tangem.domain.express.models.ProviderFilterType import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.SwapUIMode import com.tangem.feature.swap.domain.models.ui.TxFee @@ -22,6 +23,7 @@ internal data class UiActions( val onSelectFeeType: (TxFee.Legacy) -> Unit, val onProviderClick: (String) -> Unit, val onProviderSelect: (String) -> Unit, + val onProviderFilterSelect: (ProviderFilterType) -> Unit, val onBuyClick: () -> Unit, val onSelectTokenClick: (TokenSelectionDirection) -> Unit, val onSuccess: () -> Unit, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt index abcec59888..15f11b4346 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/ChooseProviderBottomSheetConfig.kt @@ -2,10 +2,15 @@ package com.tangem.feature.swap.models.states import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.domain.express.models.ProviderFilterType import kotlinx.collections.immutable.ImmutableList data class ChooseProviderBottomSheetConfig( val selectedProviderId: String, val providers: ImmutableList, + val allProviders: ImmutableList, val notification: NotificationUM?, + val selectedFilter: ProviderFilterType, + val availableFilters: ImmutableList, + val onFilterSelect: (ProviderFilterType) -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 181996f1c4..873f90dc83 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -5,14 +5,21 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.heightIn 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.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -23,6 +30,8 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig 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.ProviderTypeFilterPicker +import com.tangem.domain.express.models.ProviderFilterType import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.selectedBorder import com.tangem.core.ui.extensions.stringReference @@ -40,9 +49,13 @@ fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { TangemModalBottomSheet( config = config, containerColor = TangemTheme.colors.background.tertiary, - title = { + title = { content -> TangemModalBottomSheetTitle( - title = resourceReference(R.string.express_choose_providers_title), + title = if (content.availableFilters.isNotEmpty()) { + resourceReference(R.string.express_provider_for_swap) + } else { + resourceReference(R.string.express_choose_providers_title) + }, endIconRes = R.drawable.ic_close_24, onEndClick = config.onDismissRequest, ) @@ -56,16 +69,39 @@ fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { @Suppress("LongMethod") @Composable private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetConfig) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text( - text = stringResourceSafe(R.string.express_choose_providers_subtitle), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.secondary, - modifier = Modifier - .padding(bottom = 14.dp) - .padding(horizontal = TangemTheme.dimens.spacing56), - textAlign = TextAlign.Center, - ) + val density = LocalDensity.current + var minHeight by remember { mutableStateOf(0.dp) } + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .heightIn(min = minHeight) + .onSizeChanged { size -> + with(density) { + val h = size.height.toDp() + if (h > minHeight) minHeight = h + } + }, + ) { + if (content.availableFilters.isNotEmpty()) { + ProviderTypeFilterPicker( + availableFilters = content.availableFilters, + selectedFilter = content.selectedFilter, + onFilterSelect = content.onFilterSelect, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp), + ) + } else { + Text( + text = stringResourceSafe(R.string.express_choose_providers_subtitle), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + modifier = Modifier + .padding(bottom = 14.dp) + .padding(horizontal = TangemTheme.dimens.spacing56), + textAlign = TextAlign.Center, + ) + } if (content.notification != null) { Notification( config = content.notification.config, @@ -160,6 +196,10 @@ private fun Preview_ChooseProviderBottomSheet() { subtitle = resourceReference(R.string.warning_express_providers_fca_warning_description), ), providers = providers, + allProviders = providers, + selectedFilter = ProviderFilterType.ALL, + availableFilters = persistentListOf(), + onFilterSelect = {}, ) TangemThemePreview { ChooseProviderBottomSheet( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index c2684c811a..fd8ab0c1ef 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -11,6 +11,7 @@ import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToI import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.domain.express.models.ProviderFilterType import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.* import com.tangem.core.ui.res.TangemTheme @@ -23,12 +24,12 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.swap.models.SwapCurrencyStatus import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork +import com.tangem.feature.swap.converters.SwapProviderStateBuilder import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount import com.tangem.feature.swap.domain.models.domain.RateType -import com.tangem.feature.swap.converters.SwapProviderStateBuilder import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.domain.SwapUIMode import com.tangem.feature.swap.domain.models.ui.* @@ -37,6 +38,7 @@ import com.tangem.feature.swap.model.SwapProcessDataState import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.SwapButton.Mode import com.tangem.feature.swap.models.states.* +import com.tangem.features.swap.SwapFeatureToggles import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.utils.Provider @@ -59,7 +61,7 @@ internal class StateBuilder( private val appCurrencyProvider: Provider, private val isAccountsModeProvider: Provider, private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork, - private val shouldShowAbMenu: Boolean, + private val swapFeatureToggles: SwapFeatureToggles, ) { private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) @@ -99,7 +101,7 @@ internal class StateBuilder( isInsufficientFunds = false, swapUIMode = swapUIMode, onSwapUIModeChange = actions.onSwapUIModeChange, - shouldShowAbMenu = shouldShowAbMenu, + shouldShowAbMenu = swapFeatureToggles.isSwapAbEnabled, ) } @@ -1029,10 +1031,28 @@ internal class StateBuilder( val isAnyFCABadge = availableProvidersStates.any { (it as? ProviderState.Content)?.additionalBadge == ProviderState.AdditionalBadge.FCAWarningList } + val hasCex = availableProvidersStates.any { state -> + (state as? ProviderState.Content)?.type == ExchangeProviderType.CEX.providerName || + (state as? ProviderState.Unavailable)?.type == ExchangeProviderType.CEX.providerName + } + val hasDex = availableProvidersStates.any { state -> + val providerType = (state as? ProviderState.Content)?.type ?: (state as? ProviderState.Unavailable)?.type + providerType == ExchangeProviderType.DEX.providerName || + providerType == ExchangeProviderType.DEX_BRIDGE.providerName + } + val availableFilters = if (swapFeatureToggles.isSwapProviderFilterEnabled && hasCex && hasDex) { + persistentListOf(ProviderFilterType.ALL, ProviderFilterType.CEX, ProviderFilterType.DEX) + } else { + persistentListOf() + } val config = ChooseProviderBottomSheetConfig( selectedProviderId = selectedProviderId, providers = availableProvidersStates, + allProviders = availableProvidersStates, notification = SwapNotificationUM.Error.FCAWarningList.takeIf { isAnyFCABadge }, + selectedFilter = ProviderFilterType.ALL, + availableFilters = availableFilters, + onFilterSelect = actions.onProviderFilterSelect, ) return uiState.copy( bottomSheetConfig = TangemBottomSheetConfig( @@ -1050,23 +1070,24 @@ internal class StateBuilder( ): SwapStateHolder { val config = uiState.bottomSheetConfig?.content as? ChooseProviderBottomSheetConfig return if (config != null) { - val providers = config.providers + fun updateState(providerState: ProviderState): ProviderState { + val tokenInfo = tokenSwapInfoForProviders[providerState.id] + return if (providerState is ProviderState.Content && tokenInfo != null) { + providerState.copy( + subtitle = SwapProviderStateBuilder.buildSelectableSubtitle(tokenInfo), + percentLowerThenBest = pricesLowerBest[providerState.id]?.let { percent -> + PercentDifference.Value(percent) + } ?: PercentDifference.Value(0f), + ) + } else { + providerState + } + } uiState.copy( bottomSheetConfig = uiState.bottomSheetConfig.copy( content = config.copy( - providers = providers.map { providerState -> - val tokenInfo = tokenSwapInfoForProviders[providerState.id] - if (providerState is ProviderState.Content && tokenInfo != null) { - providerState.copy( - subtitle = SwapProviderStateBuilder.buildSelectableSubtitle(tokenInfo), - percentLowerThenBest = pricesLowerBest[providerState.id]?.let { percent -> - PercentDifference.Value(percent) - } ?: PercentDifference.Value(0f), - ) - } else { - providerState - } - }.toImmutableList(), + providers = config.providers.map(::updateState).toImmutableList(), + allProviders = config.allProviders.map(::updateState).toImmutableList(), ), ), ) @@ -1075,6 +1096,19 @@ internal class StateBuilder( } } + fun updateProviderFilterType(uiState: SwapStateHolder, filterType: ProviderFilterType): SwapStateHolder { + val config = uiState.bottomSheetConfig?.content as? ChooseProviderBottomSheetConfig ?: return uiState + val filtered = config.allProviders.filter { matchesTypeFilter(it, filterType) }.toImmutableList() + return uiState.copy( + bottomSheetConfig = uiState.bottomSheetConfig.copy( + content = config.copy( + providers = filtered, + selectedFilter = filterType, + ), + ), + ) + } + fun showSelectFeeBottomSheet( uiState: SwapStateHolder, selectedFee: FeeType, @@ -1208,4 +1242,18 @@ internal class StateBuilder( is Account.Payment -> AccountIconUM.Payment } } + + private fun matchesTypeFilter(state: ProviderState, filterType: ProviderFilterType): Boolean { + val typeStr = when (state) { + is ProviderState.Content -> state.type + is ProviderState.Unavailable -> state.type + else -> null + } ?: return filterType == ProviderFilterType.ALL + return when (filterType) { + ProviderFilterType.ALL -> true + ProviderFilterType.CEX -> typeStr == ExchangeProviderType.CEX.providerName + ProviderFilterType.DEX -> typeStr == ExchangeProviderType.DEX.providerName || + typeStr == ExchangeProviderType.DEX_BRIDGE.providerName + } + } } \ No newline at end of file diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderInitialStateTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderInitialStateTest.kt index cd31b0db91..adb20d2e84 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderInitialStateTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderInitialStateTest.kt @@ -17,6 +17,7 @@ import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.ui.StateBuilder +import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.Provider import io.mockk.every import io.mockk.mockk @@ -31,6 +32,7 @@ internal class StateBuilderInitialStateTest { private val appCurrencyProvider: Provider = mockk() private val isAccountsModeProvider: Provider = mockk() private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk() + private val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true) private lateinit var sut: StateBuilder @@ -48,7 +50,7 @@ internal class StateBuilderInitialStateTest { appCurrencyProvider = appCurrencyProvider, isAccountsModeProvider = isAccountsModeProvider, isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork, - shouldShowAbMenu = false, + swapFeatureToggles = swapFeatureToggles, ) } diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderPairsTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderPairsTest.kt index 3045de86cd..cc41bb298f 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderPairsTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderPairsTest.kt @@ -12,6 +12,7 @@ import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.ui.StateBuilder +import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.Provider import io.mockk.every import io.mockk.mockk @@ -26,6 +27,7 @@ internal class StateBuilderPairsTest { private val appCurrencyProvider: Provider = mockk() private val isAccountsModeProvider: Provider = mockk() private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk() + private val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true) private lateinit var sut: StateBuilder @@ -53,7 +55,7 @@ internal class StateBuilderPairsTest { appCurrencyProvider = appCurrencyProvider, isAccountsModeProvider = isAccountsModeProvider, isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork, - shouldShowAbMenu = false, + swapFeatureToggles = swapFeatureToggles, ) } diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderQuotesTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderQuotesTest.kt index e03feca1b0..fef9a581bb 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderQuotesTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderQuotesTest.kt @@ -14,6 +14,7 @@ import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.ui.StateBuilder +import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.Provider import io.mockk.every import io.mockk.mockk @@ -29,6 +30,7 @@ internal class StateBuilderQuotesTest { private val appCurrencyProvider: Provider = mockk() private val isAccountsModeProvider: Provider = mockk() private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk() + private val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true) private lateinit var sut: StateBuilder @@ -57,7 +59,7 @@ internal class StateBuilderQuotesTest { appCurrencyProvider = appCurrencyProvider, isAccountsModeProvider = isAccountsModeProvider, isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork, - shouldShowAbMenu = false, + swapFeatureToggles = swapFeatureToggles, ) } diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt index 7d74bddb02..d83dfd97c9 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/StateBuilderSwapDataTest.kt @@ -12,6 +12,7 @@ import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.models.states.SwapNotificationUM import com.tangem.feature.swap.ui.StateBuilder +import com.tangem.features.swap.SwapFeatureToggles import com.tangem.utils.Provider import io.mockk.every import io.mockk.mockk @@ -31,6 +32,7 @@ internal class StateBuilderSwapDataTest { private val appCurrencyProvider: Provider = mockk() private val isAccountsModeProvider: Provider = mockk() private val isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork = mockk() + private val swapFeatureToggles: SwapFeatureToggles = mockk(relaxed = true) private lateinit var sut: StateBuilder @@ -59,7 +61,7 @@ internal class StateBuilderSwapDataTest { appCurrencyProvider = appCurrencyProvider, isAccountsModeProvider = isAccountsModeProvider, isGaslessFeeSupportedForNetwork = isGaslessFeeSupportedForNetwork, - shouldShowAbMenu = false, + swapFeatureToggles = swapFeatureToggles, ) }