Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-06 12:39:07 +05:00
parent 18d4a01642
commit bced1ca48d
11 changed files with 845 additions and 119 deletions

View file

@ -469,7 +469,13 @@
<string name="domain_receive_assets_onboarding_title">Send funds using only</string>
<string name="earn_best_opportunities">Best opportunities</string>
<string name="earn_filter_all_networks">All networks</string>
<string name="earn_filter_my_networks">My networks</string>
<string name="earn_filter_all_types">All types</string>
<string name="earn_filter_networks">Networks</string>
<string name="earn_clear_filter">Clear filter</string>
<string name="earn_filter_by">Filter by</string>
<string name="earn_no_results">No results</string>
<string name="earn_empty">The list is temporarily empty as its being refreshed. Check back in a moment.</string>
<string name="earn_mostly_used">Mostly used</string>
<string name="earn_title">Earn</string>
<string name="email_preface_wc_error">Hi support team, I\'ve encountered an error with code: %s</string>

View file

@ -4,13 +4,24 @@ import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.features.feed.components.earn.DefaultEarnComponent
import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM
import com.tangem.features.feed.ui.earn.state.EarnFilterByNetworkBottomSheetContentUM
import com.tangem.features.feed.ui.earn.state.EarnFilterByTypeBottomSheetContentUM
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
import com.tangem.features.feed.ui.earn.state.EarnListUM
import com.tangem.features.feed.ui.earn.state.EarnUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@Stable
@ -22,18 +33,213 @@ internal class EarnModel @Inject constructor(
private val params = paramsContainer.require<DefaultEarnComponent.Params>()
// TODO [REDACTED_TASK_KEY] remove mocks
private val mockNetworks: ImmutableList<EarnFilterNetworkUM> = persistentListOf(
EarnFilterNetworkUM.AllNetworks(
text = resourceReference(R.string.earn_filter_all_networks),
isSelected = false,
),
EarnFilterNetworkUM.MyNetworks(
text = resourceReference(R.string.earn_filter_my_networks),
isSelected = false,
),
EarnFilterNetworkUM.Network(
id = "ethereum",
text = stringReference("Ethereum"),
symbol = stringReference("ETH"),
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/ethereum.png",
isSelected = false,
),
EarnFilterNetworkUM.Network(
id = "polygon",
text = stringReference("Polygon"),
symbol = stringReference("MATIC"),
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/matic-network.png",
isSelected = false,
),
EarnFilterNetworkUM.Network(
id = "base",
text = stringReference("Base"),
symbol = stringReference("BASE"),
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/base.png",
isSelected = false,
),
EarnFilterNetworkUM.Network(
id = "avalanche",
text = stringReference("Avalance"),
symbol = stringReference("AVAX"),
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/avalanche-2.png",
isSelected = false,
),
EarnFilterNetworkUM.Network(
id = "cardano",
text = stringReference("Cardano"),
symbol = stringReference("ADA"),
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/cardano.png",
isSelected = false,
),
EarnFilterNetworkUM.Network(
id = "bsc",
text = stringReference("Bitcoin Smart Chain"),
symbol = stringReference("BSC"),
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/binancecoin.png",
isSelected = false,
),
EarnFilterNetworkUM.Network(
id = "bitcoin",
text = stringReference("Bitcoin"),
symbol = stringReference("BTC"),
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/bitcoin.png",
isSelected = false,
),
EarnFilterNetworkUM.Network(
id = "solana",
text = stringReference("Solana"),
symbol = stringReference("SOL"),
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/solana.png",
isSelected = false,
),
EarnFilterNetworkUM.Network(
id = "ton",
text = stringReference("Ton"),
symbol = stringReference("TON"),
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/the-open-network.png",
isSelected = false,
),
)
val state: StateFlow<EarnUM>
field = MutableStateFlow(createInitialState())
private fun createInitialState(): EarnUM = EarnUM(
mostlyUsed = EarnListUM.Loading,
bestOpportunities = EarnListUM.Loading,
selectedNetworkFilter = null,
selectedTypeFilter = null,
networkFilters = persistentListOf(),
typeFilters = persistentListOf(),
onBackClick = params.onBackClick,
onNetworkFilterClick = { },
onTypeFilterClick = { },
)
private fun createInitialState(): EarnUM {
val defaultNetworkFilter = mockNetworks.first()
val defaultTypeFilter = EarnFilterTypeUM.All
return EarnUM(
mostlyUsed = EarnListUM.Loading,
bestOpportunities = EarnBestOpportunitiesUM.Loading,
selectedTypeFilter = defaultTypeFilter,
selectedNetworkFilter = defaultNetworkFilter,
filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(defaultTypeFilter),
filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(defaultNetworkFilter),
onBackClick = params.onBackClick,
onNetworkFilterClick = ::onNetworkFilterClick,
onTypeFilterClick = ::onTypeFilterClick,
)
}
private fun createTypeFilterBottomSheetConfig(
selectedOption: EarnFilterTypeUM,
isShown: Boolean = false,
): TangemBottomSheetConfig {
return TangemBottomSheetConfig(
isShown = isShown,
onDismissRequest = ::hideTypeFilterBottomSheet,
content = EarnFilterByTypeBottomSheetContentUM(
selectedOption = selectedOption,
onOptionClick = ::onTypeFilterOptionClick,
),
)
}
private fun createNetworkFilterBottomSheetConfig(
selectedNetwork: EarnFilterNetworkUM,
isShown: Boolean = false,
): TangemBottomSheetConfig {
val updatedNetworks = mockNetworks.map { network ->
when {
network is EarnFilterNetworkUM.AllNetworks && selectedNetwork is EarnFilterNetworkUM.AllNetworks ->
network.copy(isSelected = true)
network is EarnFilterNetworkUM.MyNetworks && selectedNetwork is EarnFilterNetworkUM.MyNetworks ->
network.copy(isSelected = true)
network is EarnFilterNetworkUM.Network && selectedNetwork is EarnFilterNetworkUM.Network &&
network.id == selectedNetwork.id ->
network.copy(isSelected = true)
else -> network
}
}
return TangemBottomSheetConfig(
isShown = isShown,
onDismissRequest = ::hideNetworkFilterBottomSheet,
content = EarnFilterByNetworkBottomSheetContentUM(
selectedNetwork = selectedNetwork,
networks = persistentListOf(*updatedNetworks.toTypedArray()),
onOptionClick = ::onNetworkFilterOptionClick,
),
)
}
private fun onTypeFilterClick() {
state.update { currentState ->
currentState.copy(
filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(
selectedOption = currentState.selectedTypeFilter,
isShown = true,
),
)
}
}
private fun onNetworkFilterClick() {
state.update { currentState ->
currentState.copy(
filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(
selectedNetwork = currentState.selectedNetworkFilter,
isShown = true,
),
)
}
}
private fun hideTypeFilterBottomSheet() {
state.update { currentState ->
currentState.copy(
filterByTypeBottomSheet = currentState.filterByTypeBottomSheet.copy(isShown = false),
)
}
}
private fun hideNetworkFilterBottomSheet() {
state.update { currentState ->
currentState.copy(
filterByNetworkBottomSheet = currentState.filterByNetworkBottomSheet.copy(isShown = false),
)
}
}
private fun onTypeFilterOptionClick(type: EarnFilterTypeUM) {
state.update { currentState ->
currentState.copy(
selectedTypeFilter = type,
filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(selectedOption = type),
)
}
}
private fun onNetworkFilterOptionClick(network: EarnFilterNetworkUM) {
state.update { currentState ->
currentState.copy(
selectedNetworkFilter = network,
filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(selectedNetwork = network),
)
}
}
// TODO [REDACTED_TASK_KEY] remove
@Suppress("UnusedPrivateMember")
private fun onClearFiltersClick() {
val defaultNetworkFilter = mockNetworks.first()
val defaultTypeFilter = EarnFilterTypeUM.All
state.update { currentState ->
currentState.copy(
selectedTypeFilter = defaultTypeFilter,
selectedNetworkFilter = defaultNetworkFilter,
filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(selectedOption = defaultTypeFilter),
filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(
selectedNetwork = defaultNetworkFilter,
),
)
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.feed.ui.earn
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@ -9,12 +10,16 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
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.unit.dp
import com.tangem.core.ui.R
@ -37,10 +42,16 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.feed.ui.earn.components.EarnFilterByNetworkBottomSheet
import com.tangem.features.feed.ui.earn.components.EarnFilterByTypeBottomSheet
import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholder
import com.tangem.features.feed.ui.earn.components.EarnListItem
import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder
import com.tangem.features.feed.ui.earn.state.EarnFilterUM
import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
import com.tangem.features.feed.ui.earn.state.EarnListUM
import com.tangem.features.feed.ui.earn.state.EarnUM
@ -51,6 +62,10 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
val background = LocalMainBottomSheetColor.current.value
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
EarnFilterByTypeBottomSheet(config = state.filterByTypeBottomSheet)
EarnFilterByNetworkBottomSheet(config = state.filterByNetworkBottomSheet)
LazyColumn(
modifier = modifier
.fillMaxSize()
@ -79,8 +94,8 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
SpacerH(12.dp)
BestOpportunitiesFilters(
state = state.bestOpportunities,
selectedNetworkFilter = state.selectedNetworkFilter,
selectedTypeFilter = state.selectedTypeFilter,
selectedNetworkFilterText = state.selectedNetworkFilterText,
selectedTypeFilterText = state.selectedTypeFilterText,
onNetworkFilterClick = state.onNetworkFilterClick,
onTypeFilterClick = state.onTypeFilterClick,
)
@ -94,40 +109,42 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
@Composable
private fun MostlyUsedContent(state: EarnListUM) {
when (state) {
is EarnListUM.Loading -> {
MostlyUsedPlaceholder()
}
is EarnListUM.Content -> {
LazyRow(
contentPadding = PaddingValues(
horizontal = 16.dp,
vertical = 12.dp,
),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items(
items = state.items,
key = { "${it.tokenName}-${it.network}" },
) { item ->
MostlyUsedCard(
item = item,
onClick = item.onItemClick,
)
}
AnimatedContent(state) { st ->
when (st) {
is EarnListUM.Loading -> {
MostlyUsedPlaceholder()
}
}
is EarnListUM.Error -> {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(
is EarnListUM.Content -> {
LazyRow(
contentPadding = PaddingValues(
horizontal = 16.dp,
vertical = 12.dp,
),
contentAlignment = Alignment.Center,
) {
UnableToLoadData(onRetryClick = state.onRetryClicked)
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items(
items = st.items,
key = { "${it.tokenName}-${it.network}" },
) { item ->
MostlyUsedCard(
item = item,
onClick = item.onItemClick,
)
}
}
}
is EarnListUM.Error -> {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 16.dp,
vertical = 12.dp,
),
contentAlignment = Alignment.Center,
) {
UnableToLoadData(onRetryClick = st.onRetryClicked)
}
}
}
}
@ -184,40 +201,46 @@ private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier:
@Composable
private fun BestOpportunitiesFilters(
state: EarnListUM,
selectedNetworkFilter: EarnFilterUM?,
selectedTypeFilter: EarnFilterUM?,
state: EarnBestOpportunitiesUM,
selectedNetworkFilterText: TextReference,
selectedTypeFilterText: TextReference,
onNetworkFilterClick: () -> Unit,
onTypeFilterClick: () -> Unit,
) {
when (state) {
is EarnListUM.Loading -> {
FilterButtonsShimmer()
}
is EarnListUM.Content -> {
FilterButtons(
selectedNetworkFilter = selectedNetworkFilter,
selectedTypeFilter = selectedTypeFilter,
isEnabled = true,
onNetworkFilterClick = onNetworkFilterClick,
onTypeFilterClick = onTypeFilterClick,
)
}
is EarnListUM.Error -> {
FilterButtons(
selectedNetworkFilter = selectedNetworkFilter,
selectedTypeFilter = selectedTypeFilter,
isEnabled = false,
onNetworkFilterClick = onNetworkFilterClick,
onTypeFilterClick = onTypeFilterClick,
)
AnimatedContent(state) { st ->
when (st) {
is EarnBestOpportunitiesUM.Loading -> {
FilterButtonsShimmer()
}
is EarnBestOpportunitiesUM.EmptyFiltered,
is EarnBestOpportunitiesUM.Content,
-> {
FilterButtons(
selectedNetworkFilterText = selectedNetworkFilterText,
selectedTypeFilterText = selectedTypeFilterText,
isEnabled = true,
onNetworkFilterClick = onNetworkFilterClick,
onTypeFilterClick = onTypeFilterClick,
)
}
is EarnBestOpportunitiesUM.Empty,
is EarnBestOpportunitiesUM.Error,
-> {
FilterButtons(
selectedNetworkFilterText = selectedNetworkFilterText,
selectedTypeFilterText = selectedTypeFilterText,
isEnabled = false,
onNetworkFilterClick = onNetworkFilterClick,
onTypeFilterClick = onTypeFilterClick,
)
}
}
}
}
private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) {
private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM) {
when (state) {
is EarnListUM.Loading -> {
is EarnBestOpportunitiesUM.Loading -> {
val lastIndex = PLACEHOLDER_ITEMS_COUNT - 1
items(
count = PLACEHOLDER_ITEMS_COUNT,
@ -233,7 +256,19 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) {
)
}
}
is EarnListUM.Content -> {
is EarnBestOpportunitiesUM.Empty -> {
item(key = "best_opportunities_empty") {
SpacerH(12.dp)
BestOpportunitiesEmpty()
}
}
is EarnBestOpportunitiesUM.EmptyFiltered -> {
item(key = "best_opportunities_empty_filtered") {
SpacerH(12.dp)
BestOpportunitiesEmptyFiltered(onClearFilterClick = state.onClearFilterClick)
}
}
is EarnBestOpportunitiesUM.Content -> {
if (state.items.isNotEmpty()) {
val lastIndex = state.items.lastIndex
itemsIndexed(
@ -252,7 +287,7 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) {
}
}
}
is EarnListUM.Error -> {
is EarnBestOpportunitiesUM.Error -> {
item(key = "best_opportunities_error") {
SpacerH(12.dp)
Box(
@ -275,8 +310,8 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) {
@Composable
private fun FilterButtons(
selectedNetworkFilter: EarnFilterUM?,
selectedTypeFilter: EarnFilterUM?,
selectedNetworkFilterText: TextReference,
selectedTypeFilterText: TextReference,
isEnabled: Boolean,
onNetworkFilterClick: () -> Unit,
onTypeFilterClick: () -> Unit,
@ -287,8 +322,7 @@ private fun FilterButtons(
) {
SecondarySmallButton(
config = SmallButtonConfig(
text = selectedNetworkFilter?.name
?: resourceReference(R.string.earn_filter_all_networks),
text = selectedNetworkFilterText,
onClick = onNetworkFilterClick,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
isEnabled = isEnabled,
@ -299,8 +333,7 @@ private fun FilterButtons(
SecondarySmallButton(
config = SmallButtonConfig(
text = selectedTypeFilter?.name
?: resourceReference(R.string.earn_filter_all_types),
text = selectedTypeFilterText,
onClick = onTypeFilterClick,
icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24),
isEnabled = isEnabled,
@ -326,6 +359,66 @@ private fun FilterButtonsShimmer(modifier: Modifier = Modifier) {
}
}
@Composable
private fun BestOpportunitiesEmpty(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(vertical = 32.dp, horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
modifier = Modifier
.fillMaxWidth(),
painter = painterResource(R.drawable.ic_empty_64),
contentDescription = null,
tint = Color.Unspecified,
)
SpacerH(24.dp)
Text(
modifier = Modifier
.padding(horizontal = 32.dp),
text = stringResourceSafe(R.string.earn_empty),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun BestOpportunitiesEmptyFiltered(onClearFilterClick: () -> Unit, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.background(
color = TangemTheme.colors.background.action,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.padding(vertical = 32.dp, horizontal = 12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResourceSafe(R.string.earn_no_results),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
SpacerH(12.dp)
SecondarySmallButton(
config = SmallButtonConfig(
text = resourceReference(R.string.earn_clear_filter),
onClick = onClearFilterClick,
),
)
}
}
@Composable
private fun SectionHeader(title: String, modifier: Modifier = Modifier) {
Text(
@ -348,7 +441,7 @@ private fun EarnContentPreview() {
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
state = EarnUM(
state = previewEarnUM(
mostlyUsed = EarnListUM.Content(
items = persistentListOf(
previewEarnListItemUM(),
@ -359,7 +452,7 @@ private fun EarnContentPreview() {
),
),
),
bestOpportunities = EarnListUM.Content(
bestOpportunities = EarnBestOpportunitiesUM.Content(
items = persistentListOf(
previewEarnListItemUM(
tokenName = "Cosmos Hub",
@ -373,13 +466,6 @@ private fun EarnContentPreview() {
),
),
),
selectedNetworkFilter = null,
selectedTypeFilter = null,
networkFilters = persistentListOf(),
typeFilters = persistentListOf(),
onBackClick = {},
onNetworkFilterClick = {},
onTypeFilterClick = {},
),
)
}
@ -396,16 +482,9 @@ private fun EarnContentLoadingPreview() {
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
state = EarnUM(
state = previewEarnUM(
mostlyUsed = EarnListUM.Loading,
bestOpportunities = EarnListUM.Loading,
selectedNetworkFilter = null,
selectedTypeFilter = null,
networkFilters = persistentListOf(),
typeFilters = persistentListOf(),
onBackClick = {},
onNetworkFilterClick = {},
onTypeFilterClick = {},
bestOpportunities = EarnBestOpportunitiesUM.Loading,
),
)
}
@ -422,7 +501,7 @@ private fun EarnContentErrorPreview() {
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
state = EarnUM(
state = previewEarnUM(
mostlyUsed = EarnListUM.Content(
items = persistentListOf(
previewEarnListItemUM(),
@ -433,14 +512,35 @@ private fun EarnContentErrorPreview() {
),
),
),
bestOpportunities = EarnListUM.Error(onRetryClicked = {}),
selectedNetworkFilter = null,
selectedTypeFilter = null,
networkFilters = persistentListOf(),
typeFilters = persistentListOf(),
onBackClick = {},
onNetworkFilterClick = {},
onTypeFilterClick = {},
bestOpportunities = EarnBestOpportunitiesUM.Error(onRetryClicked = {}),
),
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnContentEmptyPreview() {
TangemThemePreview {
val background = TangemTheme.colors.background.tertiary
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember { mutableStateOf(background) },
) {
EarnContent(
state = previewEarnUM(
mostlyUsed = EarnListUM.Content(
items = persistentListOf(
previewEarnListItemUM(),
previewEarnListItemUM(
tokenName = "Cosmos",
symbol = "ATOM",
network = "Cosmos",
),
),
),
bestOpportunities = EarnBestOpportunitiesUM.Empty,
),
)
}
@ -469,4 +569,22 @@ private fun previewEarnListItemUM(
onItemClick = {},
)
private fun previewEarnUM(
mostlyUsed: EarnListUM = EarnListUM.Loading,
bestOpportunities: EarnBestOpportunitiesUM = EarnBestOpportunitiesUM.Loading,
): EarnUM = EarnUM(
mostlyUsed = mostlyUsed,
bestOpportunities = bestOpportunities,
selectedTypeFilter = EarnFilterTypeUM.All,
selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(
text = resourceReference(R.string.earn_filter_all_networks),
isSelected = true,
),
filterByTypeBottomSheet = TangemBottomSheetConfig.Empty,
filterByNetworkBottomSheet = TangemBottomSheetConfig.Empty,
onBackClick = {},
onNetworkFilterClick = {},
onTypeFilterClick = {},
)
private const val PLACEHOLDER_ITEMS_COUNT = 8

View file

@ -0,0 +1,245 @@
package com.tangem.features.feed.ui.earn.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
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.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.currency.icon.CoinIcon
import com.tangem.core.ui.components.inputrow.InputRowChecked
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.rows.RowContentContainer
import com.tangem.core.ui.components.rows.RowText
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.earn.state.EarnFilterByNetworkBottomSheetContentUM
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun EarnFilterByNetworkBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<EarnFilterByNetworkBottomSheetContentUM>(
config = config,
titleText = resourceReference(R.string.earn_filter_by),
containerColor = TangemTheme.colors.background.tertiary,
content = { Content(it) },
)
}
@Composable
private fun Content(content: EarnFilterByNetworkBottomSheetContentUM) {
val allMyNetworks = remember(content) {
content.networks.filterIsInstance<EarnFilterNetworkUM.AllNetworks>() +
content.networks.filterIsInstance<EarnFilterNetworkUM.MyNetworks>()
}
val specificNetworks = remember(content) { content.networks.filterIsInstance<EarnFilterNetworkUM.Network>() }
LazyColumn(
contentPadding = PaddingValues(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
allMyNetworksList(
allMyNetworks = allMyNetworks,
onOptionClicked = content.onOptionClick,
)
if (specificNetworks.isNotEmpty()) {
networksHeader()
specificNetworksList(
specificNetworks = specificNetworks,
onOptionClicked = content.onOptionClick,
)
}
}
}
private fun LazyListScope.allMyNetworksList(
allMyNetworks: List<EarnFilterNetworkUM>,
onOptionClicked: (EarnFilterNetworkUM) -> Unit,
) {
itemsIndexed(
items = allMyNetworks,
key = { _, item ->
when (item) {
is EarnFilterNetworkUM.AllNetworks -> "all_networks"
is EarnFilterNetworkUM.MyNetworks -> "my_networks"
is EarnFilterNetworkUM.Network -> item.id
}
},
) { index, item ->
DividerContainer(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = allMyNetworks.lastIndex,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { onOptionClicked(item) },
showDivider = index != allMyNetworks.lastIndex,
) {
InputRowChecked(
text = item.text,
checked = item.isSelected,
)
}
}
}
private fun LazyListScope.networksHeader() {
item(key = "networks_header") {
SpacerH(TangemTheme.dimens.spacing16)
Text(
modifier = Modifier
.fillMaxWidth()
.background(
color = TangemTheme.colors.background.action,
shape = RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
),
)
.padding(
start = 12.dp,
end = 12.dp,
top = 12.dp,
bottom = 4.dp,
),
text = stringResourceSafe(id = R.string.earn_filter_networks),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
private fun LazyListScope.specificNetworksList(
specificNetworks: List<EarnFilterNetworkUM.Network>,
onOptionClicked: (EarnFilterNetworkUM) -> Unit,
) {
itemsIndexed(
items = specificNetworks,
key = { _, item -> item.id },
) { index, item ->
DividerContainer(
modifier = Modifier
.height(52.dp)
.roundedShapeItemDecoration(
currentIndex = index + 1,
lastIndex = specificNetworks.lastIndex + 1,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { onOptionClicked(item) },
showDivider = false,
) {
RowContentContainer(
modifier = Modifier
.heightIn(52.dp)
.padding(horizontal = 12.dp),
icon = {
CoinIcon(
modifier = Modifier.size(22.dp),
url = item.iconUrl,
alpha = 1f,
colorFilter = null,
fallbackResId = R.drawable.ic_custom_token_44,
)
},
text = {
RowText(
mainText = item.text.resolveReference(),
secondText = item.symbol.resolveReference(),
accentMainText = true,
accentSecondText = false,
)
},
action = {
if (item.isSelected) {
Icon(
painter = painterResource(R.drawable.ic_check_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
)
}
},
)
}
}
}
@Preview(widthDp = 360, heightDp = 800)
@Preview(widthDp = 360, heightDp = 800, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview(
alwaysShowBottomSheets = true,
) {
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
EarnFilterByNetworkBottomSheet(
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = EarnFilterByNetworkBottomSheetContentUM(
selectedNetwork = EarnFilterNetworkUM.AllNetworks(
text = resourceReference(R.string.earn_filter_all_networks),
isSelected = true,
),
networks = persistentListOf(
EarnFilterNetworkUM.AllNetworks(
text = resourceReference(R.string.earn_filter_all_networks),
isSelected = true,
),
EarnFilterNetworkUM.MyNetworks(
text = stringReference("My network"),
isSelected = false,
),
EarnFilterNetworkUM.Network(
id = "ethereum",
text = stringReference("Ethereum"),
symbol = stringReference("ETH"),
iconUrl = null,
isSelected = false,
),
EarnFilterNetworkUM.Network(
id = "polygon",
text = stringReference("Polygon"),
symbol = stringReference("MATIC"),
iconUrl = null,
isSelected = false,
),
),
onOptionClick = {},
),
),
)
}
}
}

View file

@ -0,0 +1,85 @@
package com.tangem.features.feed.ui.earn.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.inputrow.InputRowChecked
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.ui.earn.state.EarnFilterByTypeBottomSheetContentUM
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
@Composable
internal fun EarnFilterByTypeBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet<EarnFilterByTypeBottomSheetContentUM>(
config = config,
titleText = resourceReference(R.string.earn_filter_by),
containerColor = TangemTheme.colors.background.tertiary,
content = { Content(it) },
)
}
@Composable
private fun Content(content: EarnFilterByTypeBottomSheetContentUM) {
Column(
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
EarnFilterTypeUM.entries.forEachIndexed { index, type ->
DividerContainer(
modifier = Modifier
.roundedShapeItemDecoration(
currentIndex = index,
lastIndex = EarnFilterTypeUM.entries.lastIndex,
addDefaultPadding = false,
)
.background(TangemTheme.colors.background.action)
.clickable { content.onOptionClick(type) },
showDivider = index != EarnFilterTypeUM.entries.lastIndex,
) {
InputRowChecked(
text = type.text,
checked = type == content.selectedOption,
)
}
}
}
}
@Preview(widthDp = 360, heightDp = 640)
@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview(
alwaysShowBottomSheets = true,
) {
Box(Modifier.background(TangemTheme.colors.background.secondary)) {
EarnFilterByTypeBottomSheet(
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {},
content = EarnFilterByTypeBottomSheetContentUM(
selectedOption = EarnFilterTypeUM.All,
onOptionClick = {},
),
),
)
}
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.feed.ui.earn.state
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import kotlinx.collections.immutable.ImmutableList
internal data class EarnFilterByNetworkBottomSheetContentUM(
val selectedNetwork: EarnFilterNetworkUM,
val networks: ImmutableList<EarnFilterNetworkUM>,
val onOptionClick: (EarnFilterNetworkUM) -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,8 @@
package com.tangem.features.feed.ui.earn.state
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
internal data class EarnFilterByTypeBottomSheetContentUM(
val selectedOption: EarnFilterTypeUM,
val onOptionClick: (EarnFilterTypeUM) -> Unit,
) : TangemBottomSheetConfigContent

View file

@ -0,0 +1,29 @@
package com.tangem.features.feed.ui.earn.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
@Immutable
internal sealed class EarnFilterNetworkUM {
abstract val text: TextReference
abstract val isSelected: Boolean
data class AllNetworks(
override val text: TextReference,
override val isSelected: Boolean,
) : EarnFilterNetworkUM()
data class MyNetworks(
override val text: TextReference,
override val isSelected: Boolean,
) : EarnFilterNetworkUM()
data class Network(
val id: String,
override val text: TextReference,
val symbol: TextReference,
val iconUrl: String?,
override val isSelected: Boolean,
) : EarnFilterNetworkUM()
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.feed.ui.earn.state
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
internal enum class EarnFilterTypeUM(val text: TextReference) {
All(resourceReference(R.string.earn_filter_all_types)),
Staking(resourceReference(R.string.common_staking)),
YieldMode(resourceReference(R.string.markets_sort_by_yield_mode_title)),
}

View file

@ -12,6 +12,15 @@ internal sealed interface EarnListUM {
data class Error(val onRetryClicked: () -> Unit) : EarnListUM
}
@Immutable
internal sealed interface EarnBestOpportunitiesUM {
data object Loading : EarnBestOpportunitiesUM
data object Empty : EarnBestOpportunitiesUM
data class EmptyFiltered(val onClearFilterClick: () -> Unit) : EarnBestOpportunitiesUM
data class Content(val items: ImmutableList<EarnListItemUM>) : EarnBestOpportunitiesUM
data class Error(val onRetryClicked: () -> Unit) : EarnBestOpportunitiesUM
}
@Immutable
internal data class EarnListItemUM(
val network: TextReference,

View file

@ -1,26 +1,25 @@
package com.tangem.features.feed.ui.earn.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal data class EarnUM(
val mostlyUsed: EarnListUM,
val bestOpportunities: EarnListUM,
val selectedNetworkFilter: EarnFilterUM?,
val selectedTypeFilter: EarnFilterUM?,
val networkFilters: ImmutableList<EarnFilterUM>,
val typeFilters: ImmutableList<EarnFilterUM>,
val bestOpportunities: EarnBestOpportunitiesUM,
val selectedTypeFilter: EarnFilterTypeUM,
val selectedNetworkFilter: EarnFilterNetworkUM,
val filterByTypeBottomSheet: TangemBottomSheetConfig,
val filterByNetworkBottomSheet: TangemBottomSheetConfig,
val onBackClick: () -> Unit,
val onNetworkFilterClick: () -> Unit,
val onTypeFilterClick: () -> Unit,
)
) {
@Immutable
internal data class EarnFilterUM(
val id: String,
val name: TextReference,
val isSelected: Boolean,
val onClick: () -> Unit,
)
val selectedNetworkFilterText: TextReference
get() = selectedNetworkFilter.text
val selectedTypeFilterText: TextReference
get() = selectedTypeFilter.text
}