From 3a84102e1d6d803a5fc45334885b4b0bed5dd6a0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 28 Jan 2026 11:21:24 +0500 Subject: [PATCH] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 5 + .../components/currency/icon/CurrencyIcon.kt | 9 + .../currency/icon/CurrencyIconTopBadge.kt | 7 +- .../feed/components/FeedEntryChildFactory.kt | 11 + .../components/earn/DefaultEarnComponent.kt | 56 +++ .../tangem/features/feed/di/ModelModule.kt | 6 + .../features/feed/model/earn/EarnModel.kt | 39 ++ .../features/feed/ui/earn/EarnContent.kt | 472 ++++++++++++++++++ .../feed/ui/earn/components/EarnListItem.kt | 154 ++++++ .../ui/earn/components/EarnListPlaceholder.kt | 108 ++++ .../earn/components/MostlyUsedPlaceholder.kt | 81 +++ .../features/feed/ui/earn/state/EarnListUM.kt | 24 + .../features/feed/ui/earn/state/EarnUM.kt | 26 + 13 files changed, 996 insertions(+), 2 deletions(-) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListItem.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListPlaceholder.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedPlaceholder.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 234bdd266d..8f7fdf9929 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -463,6 +463,11 @@ Sending assets in other networks will result in permanent loss. %s network Send funds using only + Best opportunities + All networks + All types + Mostly used + Earn Hi support team, I\'ve encountered an error with code: %s WalletConnect error You have used a card or ring from another wallet. Tap the card or ring associated with this wallet diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index 21e71e7535..8439ed711d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -12,6 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -34,6 +35,8 @@ fun CurrencyIcon( state: CurrencyIconState, modifier: Modifier = Modifier, shouldDisplayNetwork: Boolean = true, + networkBadgeSize: Dp = 14.dp, + networkBadgeBackground: Color = TangemTheme.colors.background.primary, withFixedSize: Boolean = true, iconSize: Dp = 36.dp, ) { @@ -64,6 +67,8 @@ fun CurrencyIcon( icon = state, modifier = iconModifier, shouldShowTopBadge = shouldDisplayNetwork, + networkBadgeSize = networkBadgeSize, + networkBadgeBackground = networkBadgeBackground, ) } } @@ -111,6 +116,8 @@ private fun EmptyIcon(@DrawableRes resId: Int, modifier: Modifier = Modifier) { private fun BoxScope.ContentIconContainer( icon: CurrencyIconState, shouldShowTopBadge: Boolean, + networkBadgeSize: Dp, + networkBadgeBackground: Color, modifier: Modifier = Modifier, ) { val networkBadgeOffset = TangemTheme.dimens.spacing4 @@ -131,8 +138,10 @@ private fun BoxScope.ContentIconContainer( .offset(x = networkBadgeOffset, y = -networkBadgeOffset) .align(Alignment.TopEnd), iconResId = requireNotNull(icon.topBadgeIconResId), + size = networkBadgeSize, alpha = alpha, colorFilter = colorFilter, + background = networkBadgeBackground, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt index f7020ce9e4..334c7dedc2 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconTopBadge.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp import com.tangem.core.ui.res.TangemTheme @Composable @@ -21,10 +22,12 @@ fun CurrencyIconTopBadge( colorFilter: ColorFilter?, modifier: Modifier = Modifier, background: Color = TangemTheme.colors.background.primary, + size: Dp = TangemTheme.dimens.size14, ) { + val innerSpacing = TangemTheme.dimens.spacing2 Box( modifier = modifier - .size(TangemTheme.dimens.size18) + .size(size + innerSpacing * 2) .background( color = background, shape = CircleShape, @@ -32,7 +35,7 @@ fun CurrencyIconTopBadge( ) { Image( modifier = Modifier - .padding(all = TangemTheme.dimens.spacing2) + .padding(all = innerSpacing) .matchParentSize(), painter = painterResource(id = iconResId), colorFilter = colorFilter, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index 0168a3c88a..3ecec193e2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -7,6 +7,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.Route import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.features.feed.components.earn.DefaultEarnComponent import com.tangem.features.feed.components.feed.DefaultFeedComponent import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent @@ -45,6 +46,10 @@ internal class FeedEntryChildFactory @Inject constructor( @Serializable @Immutable data class NewsDetails(val params: DefaultNewsDetailsComponent.Params) : Child + + @Serializable + @Immutable + data class Earn(val params: DefaultEarnComponent.Params) : Child } fun createChild( @@ -97,6 +102,12 @@ internal class FeedEntryChildFactory @Inject constructor( params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents), ) } + is Child.Earn -> { + DefaultEarnComponent( + appComponentContext = appComponentContext, + params = child.params, + ) + } } } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt new file mode 100644 index 0000000000..ad41d329a1 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt @@ -0,0 +1,56 @@ +package com.tangem.features.feed.components.earn + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.R +import com.tangem.core.ui.components.appbar.TangemTopAppBar +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.features.feed.model.earn.EarnModel +import com.tangem.features.feed.ui.earn.EarnContent +import kotlinx.serialization.Serializable + +internal class DefaultEarnComponent( + appComponentContext: AppComponentContext, + private val params: Params, +) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { + + private val earnModel = getOrCreateModel(params = params) + + @Composable + override fun Title(bottomSheetState: State) { + val background = LocalMainBottomSheetColor.current.value + val state by earnModel.state.collectAsStateWithLifecycle() + TangemTopAppBar( + containerColor = background, + title = stringResourceSafe(R.string.earn_title), + startButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_back_24, + onClicked = state.onBackClick, + isEnabled = bottomSheetState.value == BottomSheetState.EXPANDED, + ), + ) + } + + @Composable + override fun Content(bottomSheetState: State, modifier: Modifier) { + val state by earnModel.state.collectAsStateWithLifecycle() + EarnContent( + state = state, + modifier = modifier, + ) + } + + @Serializable + data class Params( + val onBackClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt index 6feb4b7bfc..f81263e79f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/di/ModelModule.kt @@ -3,6 +3,7 @@ package com.tangem.features.feed.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.feed.model.FeedEntryModel +import com.tangem.features.feed.model.earn.EarnModel import com.tangem.features.feed.model.feed.FeedComponentModel import com.tangem.features.feed.model.market.details.MarketsTokenDetailsModel import com.tangem.features.feed.model.market.list.MarketsListModel @@ -18,6 +19,11 @@ import dagger.multibindings.IntoMap @InstallIn(ModelComponent::class) internal interface ModelModule { + @Binds + @IntoMap + @ClassKey(EarnModel::class) + fun bindsEarnModel(model: EarnModel): Model + @Binds @IntoMap @ClassKey(FeedComponentModel::class) 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 new file mode 100644 index 0000000000..f1471cf802 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -0,0 +1,39 @@ +package com.tangem.features.feed.model.earn + +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.features.feed.components.earn.DefaultEarnComponent +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.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class EarnModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params = paramsContainer.require() + + 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 = { }, + ) +} \ 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 new file mode 100644 index 0000000000..657d7c312d --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -0,0 +1,472 @@ +package com.tangem.features.feed.ui.earn + +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.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed +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.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SmallButtonShimmer +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.UnableToLoadData +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +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.decorations.roundedShapeItemDecoration +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.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.EarnListItemUM +import com.tangem.features.feed.ui.earn.state.EarnListUM +import com.tangem.features.feed.ui.earn.state.EarnUM +import kotlinx.collections.immutable.persistentListOf + +@Composable +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() } + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(background), + contentPadding = PaddingValues(bottom = bottomBarHeight), + ) { + item(key = "mostly_used_header") { + SectionHeader( + title = stringResourceSafe(R.string.earn_mostly_used), + modifier = Modifier.padding(top = 16.dp), + ) + } + + item(key = "mostly_used_content") { + MostlyUsedContent(state = state.mostlyUsed) + } + + item(key = "best_opportunities_header") { + SectionHeader( + title = stringResourceSafe(R.string.earn_best_opportunities), + modifier = Modifier.padding(top = 20.dp), + ) + } + + item(key = "best_opportunities_filters") { + SpacerH(12.dp) + BestOpportunitiesFilters( + state = state.bestOpportunities, + selectedNetworkFilter = state.selectedNetworkFilter, + selectedTypeFilter = state.selectedTypeFilter, + onNetworkFilterClick = state.onNetworkFilterClick, + onTypeFilterClick = state.onTypeFilterClick, + ) + } + + bestOpportunitiesItems( + state = state.bestOpportunities, + ) + } +} + +@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, + ) + } + } + } + is EarnListUM.Error -> { + Box( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 12.dp, + ), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData(onRetryClick = state.onRetryClicked) + } + } + } +} + +@Composable +private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .width(148.dp) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(onClick = onClick) + .padding(12.dp), + ) { + CurrencyIcon( + modifier = Modifier.size(32.dp), + state = item.currencyIconState, + shouldDisplayNetwork = true, + networkBadgeSize = 12.dp, + networkBadgeBackground = TangemTheme.colors.background.action, + ) + + SpacerH(8.dp) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = item.tokenName.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + maxLines = 1, + ) + SpacerW(4.dp) + Text( + text = item.symbol.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + maxLines = 1, + ) + } + + SpacerH(2.dp) + + Text( + text = item.earnValue.resolveReference(), + color = TangemTheme.colors.text.accent, + style = TangemTheme.typography.caption1, + maxLines = 1, + ) + } +} + +@Composable +private fun BestOpportunitiesFilters( + state: EarnListUM, + selectedNetworkFilter: EarnFilterUM?, + selectedTypeFilter: EarnFilterUM?, + 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, + ) + } + } +} + +private fun LazyListScope.bestOpportunitiesItems(state: EarnListUM) { + when (state) { + is EarnListUM.Loading -> { + val lastIndex = PLACEHOLDER_ITEMS_COUNT - 1 + items( + count = PLACEHOLDER_ITEMS_COUNT, + key = { "placeholder_$it" }, + ) { index -> + EarnItemPlaceholder( + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = lastIndex, + backgroundColor = TangemTheme.colors.background.action, + ), + ) + } + } + is EarnListUM.Content -> { + if (state.items.isNotEmpty()) { + val lastIndex = state.items.lastIndex + itemsIndexed( + items = state.items, + key = { _, item -> "${item.tokenName}-${item.network}" }, + ) { index, item -> + EarnListItem( + item = item, + modifier = Modifier + .roundedShapeItemDecoration( + currentIndex = index, + lastIndex = lastIndex, + backgroundColor = TangemTheme.colors.background.action, + ), + ) + } + } + } + is EarnListUM.Error -> { + item(key = "best_opportunities_error") { + SpacerH(12.dp) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(vertical = 32.dp, horizontal = 12.dp), + contentAlignment = Alignment.Center, + ) { + UnableToLoadData(onRetryClick = state.onRetryClicked) + } + } + } + } +} + +@Composable +private fun FilterButtons( + selectedNetworkFilter: EarnFilterUM?, + selectedTypeFilter: EarnFilterUM?, + isEnabled: Boolean, + onNetworkFilterClick: () -> Unit, + onTypeFilterClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.padding(horizontal = 16.dp), + ) { + SecondarySmallButton( + config = SmallButtonConfig( + text = selectedNetworkFilter?.name + ?: resourceReference(R.string.earn_filter_all_networks), + onClick = onNetworkFilterClick, + icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), + isEnabled = isEnabled, + ), + ) + + SpacerWMax() + + SecondarySmallButton( + config = SmallButtonConfig( + text = selectedTypeFilter?.name + ?: resourceReference(R.string.earn_filter_all_types), + onClick = onTypeFilterClick, + icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), + isEnabled = isEnabled, + ), + ) + } +} + +@Composable +private fun FilterButtonsShimmer(modifier: Modifier = Modifier) { + Row( + modifier = modifier.padding(horizontal = 16.dp), + ) { + SmallButtonShimmer( + modifier = Modifier.width(110.dp), + ) + + SpacerWMax() + + SmallButtonShimmer( + modifier = Modifier.width(90.dp), + ) + } +} + +@Composable +private fun SectionHeader(title: String, modifier: Modifier = Modifier) { + Text( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + text = title, + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + ) +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun EarnContentPreview() { + TangemThemePreview { + val background = TangemTheme.colors.background.tertiary + CompositionLocalProvider( + LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, + ) { + EarnContent( + state = EarnUM( + mostlyUsed = EarnListUM.Content( + items = persistentListOf( + previewEarnListItemUM(), + previewEarnListItemUM( + tokenName = "Cosmos", + symbol = "ATOM", + network = "Cosmos", + ), + ), + ), + bestOpportunities = EarnListUM.Content( + items = persistentListOf( + previewEarnListItemUM( + tokenName = "Cosmos Hub", + symbol = "ATOM", + network = "Cosmos network", + ), + previewEarnListItemUM( + tokenName = "Tether", + symbol = "USDT", + network = "Ethereum Network", + ), + ), + ), + selectedNetworkFilter = null, + selectedTypeFilter = null, + networkFilters = persistentListOf(), + typeFilters = persistentListOf(), + onBackClick = {}, + onNetworkFilterClick = {}, + onTypeFilterClick = {}, + ), + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun EarnContentLoadingPreview() { + TangemThemePreview { + val background = TangemTheme.colors.background.tertiary + CompositionLocalProvider( + LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, + ) { + EarnContent( + state = EarnUM( + mostlyUsed = EarnListUM.Loading, + bestOpportunities = EarnListUM.Loading, + selectedNetworkFilter = null, + selectedTypeFilter = null, + networkFilters = persistentListOf(), + typeFilters = persistentListOf(), + onBackClick = {}, + onNetworkFilterClick = {}, + onTypeFilterClick = {}, + ), + ) + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun EarnContentErrorPreview() { + TangemThemePreview { + val background = TangemTheme.colors.background.tertiary + CompositionLocalProvider( + LocalMainBottomSheetColor provides remember { mutableStateOf(background) }, + ) { + EarnContent( + state = EarnUM( + mostlyUsed = EarnListUM.Content( + items = persistentListOf( + previewEarnListItemUM(), + previewEarnListItemUM( + tokenName = "Cosmos", + symbol = "ATOM", + network = "Cosmos", + ), + ), + ), + bestOpportunities = EarnListUM.Error(onRetryClicked = {}), + selectedNetworkFilter = null, + selectedTypeFilter = null, + networkFilters = persistentListOf(), + typeFilters = persistentListOf(), + onBackClick = {}, + onNetworkFilterClick = {}, + onTypeFilterClick = {}, + ), + ) + } + } +} + +@Composable +private fun previewEarnListItemUM( + tokenName: String = "USDC", + symbol: String = "USDC", + network: String = "Ethereum", +): EarnListItemUM = EarnListItemUM( + network = stringReference(network), + symbol = stringReference(symbol), + tokenName = stringReference(tokenName), + currencyIconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_eth_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + earnValue = stringReference("APY 6.54%"), + earnType = stringReference("Yield"), + onItemClick = {}, +) + +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/EarnListItem.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListItem.kt new file mode 100644 index 0000000000..5f9a56de50 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListItem.kt @@ -0,0 +1,154 @@ +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.RectangleShape +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.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.feed.ui.earn.state.EarnListItemUM + +@Composable +internal fun EarnListItem(item: EarnListItemUM, modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .clip(RectangleShape) + .clickable { item.onItemClick() } + .padding( + horizontal = 12.dp, + vertical = 15.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + CurrencyIcon( + modifier = Modifier.size(36.dp), + state = item.currencyIconState, + shouldDisplayNetwork = true, + networkBadgeBackground = TangemTheme.colors.background.action, + ) + + SpacerW(12.dp) + + Column(modifier = Modifier.weight(1f)) { + TokenTitle( + name = item.tokenName.resolveReference(), + symbol = item.symbol.resolveReference(), + ) + SpacerH(2.dp) + Text( + text = item.network.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + SpacerW(8.dp) + + Column(horizontalAlignment = Alignment.End) { + Text( + text = item.earnValue.resolveReference(), + color = TangemTheme.colors.text.accent, + style = TangemTheme.typography.body2, + maxLines = 1, + ) + SpacerH(2.dp) + Text( + text = item.earnType.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.caption2, + maxLines = 1, + ) + } + } +} + +@Composable +private fun TokenTitle(name: String, symbol: String, modifier: Modifier = Modifier) { + Row(modifier = modifier) { + Text( + modifier = Modifier + .weight(1f, fill = false) + .alignByBaseline(), + text = name, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + SpacerW(4.dp) + Text( + modifier = Modifier.alignByBaseline(), + text = symbol, + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body2, + maxLines = 1, + overflow = TextOverflow.Visible, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun EarnListItemPreview() { + TangemThemePreview { + Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) { + EarnListItem( + item = EarnListItemUM( + network = stringReference("Ethereum"), + symbol = stringReference("ETH"), + tokenName = stringReference("Ethereum"), + currencyIconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_eth_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + earnValue = stringReference("APY 8.50%"), + earnType = stringReference("Yield"), + onItemClick = {}, + ), + ) + EarnListItem( + item = EarnListItemUM( + network = stringReference("Ethereum"), + symbol = stringReference("USDT"), + tokenName = stringReference("Tether"), + currencyIconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_eth_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + earnValue = stringReference("APY 8.50%"), + earnType = stringReference("Yield"), + onItemClick = {}, + ), + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListPlaceholder.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListPlaceholder.kt new file mode 100644 index 0000000000..87f135e933 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnListPlaceholder.kt @@ -0,0 +1,108 @@ +package com.tangem.features.feed.ui.earn.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +internal fun EarnListPlaceholder(modifier: Modifier = Modifier) { + Column( + modifier = modifier.fillMaxSize(), + ) { + repeat(PLACEHOLDER_ITEMS_COUNT) { + EarnItemPlaceholder() + } + } +} + +@Composable +internal fun EarnItemPlaceholder(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .fillMaxWidth() + .padding( + horizontal = 16.dp, + vertical = 12.dp, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + CircleShimmer( + modifier = Modifier.size(36.dp), + ) + + SpacerW(12.dp) + + Column( + modifier = Modifier + .weight(1f) + .padding(top = 4.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + ) { + RectangleShimmer( + modifier = Modifier + .width(70.dp) + .height(12.dp), + radius = 4.dp, + ) + SpacerWMax() + RectangleShimmer( + modifier = Modifier + .width(40.dp) + .height(12.dp), + radius = 4.dp, + ) + } + + SpacerH(8.dp) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + RectangleShimmer( + modifier = Modifier + .width(52.dp) + .height(12.dp), + radius = 4.dp, + ) + SpacerWMax() + RectangleShimmer( + modifier = Modifier + .width(40.dp) + .height(12.dp), + radius = 4.dp, + ) + } + } + } +} + +private const val PLACEHOLDER_ITEMS_COUNT = 8 + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun EarnListPlaceholderPreview() { + TangemThemePreview { + Box( + modifier = Modifier + .background(TangemTheme.colors.background.tertiary), + ) { + EarnListPlaceholder() + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedPlaceholder.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedPlaceholder.kt new file mode 100644 index 0000000000..ef300fa0b0 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedPlaceholder.kt @@ -0,0 +1,81 @@ +package com.tangem.features.feed.ui.earn.components + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.CircleShimmer +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +@Composable +internal fun MostlyUsedPlaceholder(modifier: Modifier = Modifier) { + LazyRow( + modifier = modifier, + contentPadding = PaddingValues( + horizontal = 16.dp, + vertical = 12.dp, + ), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(PLACEHOLDER_ITEMS_COUNT) { + MostlyUsedItemPlaceholder() + } + } +} + +@Composable +fun MostlyUsedItemPlaceholder(modifier: Modifier = Modifier) { + Column( + modifier = modifier + .width(148.dp) + .height(102.dp) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(12.dp), + ) { + CircleShimmer( + modifier = Modifier + .size(32.dp), + ) + SpacerH(8.dp) + RectangleShimmer( + modifier = Modifier + .width(74.dp) + .height(20.dp), + radius = 4.dp, + ) + SpacerH(2.dp) + RectangleShimmer( + modifier = Modifier + .width(84.dp) + .height(16.dp), + radius = 4.dp, + ) + } +} + +private const val PLACEHOLDER_ITEMS_COUNT = 3 + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun MostlyUsedPlaceholderPreview() { + TangemThemePreview { + MostlyUsedPlaceholder() + } +} \ 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 new file mode 100644 index 0000000000..ff28635572 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt @@ -0,0 +1,24 @@ +package com.tangem.features.feed.ui.earn.state + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal sealed interface EarnListUM { + data object Loading : EarnListUM + data class Content(val items: ImmutableList) : EarnListUM + data class Error(val onRetryClicked: () -> Unit) : EarnListUM +} + +@Immutable +internal data class EarnListItemUM( + val network: TextReference, + val symbol: TextReference, + val tokenName: TextReference, + val currencyIconState: CurrencyIconState, + val earnValue: TextReference, + val earnType: TextReference, + val onItemClick: () -> Unit, +) \ No newline at end of file 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 new file mode 100644 index 0000000000..8425666307 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt @@ -0,0 +1,26 @@ +package com.tangem.features.feed.ui.earn.state + +import androidx.compose.runtime.Immutable +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 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