diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index ed8c132108..ae10eb765b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -469,7 +469,13 @@ Send funds using only Best opportunities All networks + My networks All types + Networks + Clear filter + Filter by + No results + The list is temporarily empty as it’s being refreshed. Check back in a moment. Mostly used Earn Hi support team, I\'ve encountered an error with code: %s diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index f1471cf802..2717a8708d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -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() + // TODO [REDACTED_TASK_KEY] remove mocks + private val mockNetworks: ImmutableList = 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 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, + ), + ) + } + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 657d7c312d..63753d95a1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -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 \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt new file mode 100644 index 0000000000..6b31102b92 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt @@ -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( + 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() + + content.networks.filterIsInstance() + } + val specificNetworks = remember(content) { content.networks.filterIsInstance() } + + 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, + 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, + 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 = {}, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByTypeBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByTypeBottomSheet.kt new file mode 100644 index 0000000000..7790ff977e --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByTypeBottomSheet.kt @@ -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( + 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 = {}, + ), + ), + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByNetworkBottomSheetContentUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByNetworkBottomSheetContentUM.kt new file mode 100644 index 0000000000..101b80fbce --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByNetworkBottomSheetContentUM.kt @@ -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, + val onOptionClick: (EarnFilterNetworkUM) -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByTypeBottomSheetContentUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByTypeBottomSheetContentUM.kt new file mode 100644 index 0000000000..25de36afa0 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterByTypeBottomSheetContentUM.kt @@ -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 \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt new file mode 100644 index 0000000000..1fd414edf4 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt @@ -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() +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterTypeUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterTypeUM.kt new file mode 100644 index 0000000000..0738da510c --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterTypeUM.kt @@ -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)), +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt index ff28635572..358cab5ee9 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt @@ -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) : EarnBestOpportunitiesUM + data class Error(val onRetryClicked: () -> Unit) : EarnBestOpportunitiesUM +} + @Immutable internal data class EarnListItemUM( val network: TextReference, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt index 8425666307..a0b6870d0a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt @@ -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, - val typeFilters: ImmutableList, + 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, -) \ No newline at end of file + val selectedNetworkFilterText: TextReference + get() = selectedNetworkFilter.text + + val selectedTypeFilterText: TextReference + get() = selectedTypeFilter.text +} \ No newline at end of file