Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-28 11:21:24 +05:00
parent 0bfeea6bc6
commit 3a84102e1d
13 changed files with 996 additions and 2 deletions

View file

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

View file

@ -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<EarnModel, Params>(params = params)
@Composable
override fun Title(bottomSheetState: State<BottomSheetState>) {
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<BottomSheetState>, modifier: Modifier) {
val state by earnModel.state.collectAsStateWithLifecycle()
EarnContent(
state = state,
modifier = modifier,
)
}
@Serializable
data class Params(
val onBackClick: () -> Unit,
)
}

View file

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

View file

@ -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<DefaultEarnComponent.Params>()
val state: StateFlow<EarnUM>
field = MutableStateFlow(createInitialState())
private fun createInitialState(): EarnUM = EarnUM(
mostlyUsed = EarnListUM.Loading,
bestOpportunities = EarnListUM.Loading,
selectedNetworkFilter = null,
selectedTypeFilter = null,
networkFilters = persistentListOf(),
typeFilters = persistentListOf(),
onBackClick = params.onBackClick,
onNetworkFilterClick = { },
onTypeFilterClick = { },
)
}

View file

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

View file

@ -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 = {},
),
)
}
}
}

View file

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

View file

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

View file

@ -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<EarnListItemUM>) : 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,
)

View file

@ -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<EarnFilterUM>,
val typeFilters: ImmutableList<EarnFilterUM>,
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,
)