Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-20 18:07:31 +02:00
parent d8844fda91
commit 5b516f2c9b
23 changed files with 339 additions and 52 deletions

View file

@ -78,5 +78,9 @@
{
"name": "AND_15310_ADD_FUNDS_STAGE1",
"version": "undefined"
},
{
"name": "AND_15009_SWAP_PROVIDER_FILTER_ENABLED",
"version": "undefined"
}
]

View file

@ -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)

View file

@ -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<ProviderFilterType>,
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)
},
)
}
}
}

View file

@ -0,0 +1,3 @@
package com.tangem.domain.express.models
enum class ProviderFilterType { ALL, CEX, DEX }

View file

@ -0,0 +1,5 @@
package com.tangem.features.swap.v2.api
interface SwapFeatureToggles {
val isSwapProviderFilterEnabled: Boolean
}

View file

@ -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)
}

View file

@ -45,6 +45,7 @@ internal class SwapChooseProviderComponent(
SwapChooseProviderContent(
contentUM = state.value,
onProviderClick = model::onProviderClick,
onFilterSelect = model::onFilterSelect,
)
}
}

View file

@ -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<SwapProviderListItem>,
val isApplyFCARestrictions: Boolean,
val selectedProvider: ExpressProvider,
val selectedFilter: ProviderFilterType,
val availableFilters: ImmutableList<ProviderFilterType>,
)
internal data class SwapProviderListItem(

View file

@ -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<SwapQuoteUM>): List<SwapQuoteUM> {
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
}
}
}

View file

@ -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 = {},
)
}
}

View file

@ -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(),
)
}

View file

@ -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)
}
}

View file

@ -4,4 +4,5 @@ interface SwapFeatureToggles {
val isSwapSwitchToTransferEnabled: Boolean
val isSwapIntegratedApproveEnabled: Boolean
val isSwapAbEnabled: Boolean
val isSwapProviderFilterEnabled: Boolean
}

View file

@ -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,
)
}

View file

@ -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

View file

@ -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,

View file

@ -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<ProviderState>,
val allProviders: ImmutableList<ProviderState>,
val notification: NotificationUM?,
val selectedFilter: ProviderFilterType,
val availableFilters: ImmutableList<ProviderFilterType>,
val onFilterSelect: (ProviderFilterType) -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -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<ChooseProviderBottomSheetConfig>(
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(

View file

@ -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<AppCurrency>,
private val isAccountsModeProvider: Provider<Boolean>,
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
}
}
}

View file

@ -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<AppCurrency> = mockk()
private val isAccountsModeProvider: Provider<Boolean> = 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,
)
}

View file

@ -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<AppCurrency> = mockk()
private val isAccountsModeProvider: Provider<Boolean> = 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,
)
}

View file

@ -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<AppCurrency> = mockk()
private val isAccountsModeProvider: Provider<Boolean> = 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,
)
}

View file

@ -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<AppCurrency> = mockk()
private val isAccountsModeProvider: Provider<Boolean> = 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,
)
}