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

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