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