From 8ba59403f4eddd62642b3db69ca1ba6150451104 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 6 Feb 2026 07:35:28 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechApi.kt | 2 +- .../data/earn/DefaultEarnErrorResolver.kt | 2 +- .../data/earn/converter/EarnTokenConverter.kt | 17 +- .../tangem/domain/models/earn/EarnError.kt | 7 +- .../tangem/domain/models/earn/EarnToken.kt | 15 +- features/feed/impl/build.gradle.kts | 1 + .../components/DefaultFeedEntryComponent.kt | 9 + .../feed/components/FeedEntryChildFactory.kt | 3 + .../components/feed/DefaultFeedComponent.kt | 31 +++ .../components/feed/FeedPortfolioRoute.kt | 11 ++ .../impl/di/AddToPortfolioComponentModule.kt | 7 + .../feed/model/feed/FeedComponentModel.kt | 180 +++++++++++++----- .../feed/model/feed/FeedModelClickIntents.kt | 2 + .../model/feed/state/FeedStateController.kt | 19 +- .../UpdateEarnLoadingStateTransformer.kt | 11 ++ .../UpdateEarnStateTransformer.kt | 111 +++++++++++ .../UpdateGlobalFeedStateTransformer.kt | 93 ++++++--- .../tangem/features/feed/ui/feed/FeedList.kt | 6 +- .../preview/FeedListPreviewDataProvider.kt | 30 +++ .../features/feed/ui/feed/state/FeedListUM.kt | 3 + 20 files changed, 463 insertions(+), 97 deletions(-) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedPortfolioRoute.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnLoadingStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 7dad440994..0b0731ea4d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -207,7 +207,7 @@ interface TangemTechApi { suspend fun transactionEvents(@Body name: TransactionEventBody): ApiResponse // region Earn - @GET("v1/earn/yield/markets") + @GET("v1/earn/markets") suspend fun getEarnTokens( @Query("isForEarn") isForEarn: Boolean?, @Query("page") page: String? = null, diff --git a/data/earn/src/main/java/com/tangem/data/earn/DefaultEarnErrorResolver.kt b/data/earn/src/main/java/com/tangem/data/earn/DefaultEarnErrorResolver.kt index 1b1d416a64..52b9499bb1 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/DefaultEarnErrorResolver.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/DefaultEarnErrorResolver.kt @@ -14,7 +14,7 @@ internal class DefaultEarnErrorResolver : EarnErrorResolver { message = throwable.message.orEmpty(), ) } - else -> EarnError.NotHttpError + else -> EarnError.NotHttpError() } } } \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt b/data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt index eb33d52cb2..6dd7dc4c6e 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/converter/EarnTokenConverter.kt @@ -1,7 +1,9 @@ package com.tangem.data.earn.converter import com.tangem.datasource.api.tangemTech.models.EarnResponse +import com.tangem.domain.models.earn.EarnRewardType import com.tangem.domain.models.earn.EarnToken +import com.tangem.domain.models.earn.EarnType import com.tangem.utils.converter.Converter internal object EarnTokenConverter : Converter { @@ -10,12 +12,23 @@ internal object EarnTokenConverter : Converter { return EarnToken( apy = value.apy, networkId = value.networkId, - rewardType = value.rewardType, - type = value.type, + rewardType = convertEarnRewardType(value.rewardType), + type = convertEarnType(value.type), tokenId = value.token.id, tokenSymbol = value.token.symbol, tokenName = value.token.name, tokenAddress = value.token.address, + decimalCount = value.token.decimalCount, ) } + + private fun convertEarnRewardType(type: String): EarnRewardType { + return enumValues().find { it.name.equals(type, ignoreCase = true) } + ?: EarnRewardType.APY + } + + private fun convertEarnType(type: String): EarnType { + return enumValues().find { it.name.equals(type, ignoreCase = true) } + ?: EarnType.STAKING + } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt index d5106276e8..880febc265 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnError.kt @@ -1,11 +1,16 @@ package com.tangem.domain.models.earn +import java.util.UUID + sealed interface EarnError { data class HttpError( + val id: String = UUID.randomUUID().toString(), val code: Int, val message: String, ) : EarnError - data object NotHttpError : EarnError + data class NotHttpError( + val id: String = UUID.randomUUID().toString(), + ) : EarnError } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt index a91581e8eb..523ba3ac45 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnToken.kt @@ -17,10 +17,19 @@ import kotlinx.serialization.Serializable data class EarnToken( val apy: String, val networkId: String, - val rewardType: String, - val type: String, + val rewardType: EarnRewardType, + val type: EarnType, val tokenId: String, val tokenSymbol: String, val tokenName: String, val tokenAddress: String, -) \ No newline at end of file + val decimalCount: Int?, +) + +enum class EarnRewardType { + APR, APY +} + +enum class EarnType { + STAKING, YIELD +} \ No newline at end of file diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index e16f11c113..beb8db8937 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { implementation(projects.domain.news) implementation(projects.domain.yieldSupply.models) implementation(projects.domain.yieldSupply) + implementation(projects.domain.earn) // FIXME [REDACTED_TASK_KEY] // Remove the "Buy" and "Sell" actions from the redux middleware. diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt index edbc89d7d4..9b8a539048 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt @@ -22,6 +22,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.news.model.NewsListConfig +import com.tangem.features.feed.components.earn.DefaultEarnComponent import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent @@ -131,6 +132,14 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( override fun onOpenAllNews() { innerRouter.push(FeedEntryChildFactory.Child.NewsList) } + + override fun onOpenEarnPage() { + innerRouter.push( + FeedEntryChildFactory.Child.Earn( + params = DefaultEarnComponent.Params(onBackClick = { onChildBack() }), + ), + ) + } } private val stack: Value> = 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 3ecec193e2..9deae9964b 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 @@ -10,6 +10,7 @@ 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.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent import com.tangem.features.feed.components.news.details.DefaultNewsDetailsComponent @@ -21,6 +22,7 @@ internal class FeedEntryChildFactory @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val accountsFeatureToggles: AccountsFeatureToggles, private val portfolioComponentFactory: MarketsPortfolioComponent.Factory, + private val addToPortfolioPreselectedDataComponent: AddToPortfolioPreselectedDataComponent.Factory, ) { @Serializable @@ -100,6 +102,7 @@ internal class FeedEntryChildFactory @Inject constructor( DefaultFeedComponent( appComponentContext = appComponentContext, params = DefaultFeedComponent.FeedParams(feedClickIntents = feedEntryClickIntents), + addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent, ) } is Child.Earn -> { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index f7073e2ca2..d7f9b477fa 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -6,10 +6,16 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.LifecycleStartEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.model.feed.FeedComponentModel import com.tangem.features.feed.model.feed.FeedModelClickIntents import com.tangem.features.feed.ui.feed.FeedList @@ -18,10 +24,18 @@ import com.tangem.features.feed.ui.feed.FeedListHeader internal class DefaultFeedComponent( appComponentContext: AppComponentContext, private val params: FeedParams, + private val addToPortfolioComponentFactory: AddToPortfolioPreselectedDataComponent.Factory, ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { private val feedComponentModel = getOrCreateModel(params = params) + private val bottomSheetSlot = childSlot( + source = feedComponentModel.bottomSheetNavigation, + serializer = FeedPortfolioRoute.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Title(bottomSheetState: State) { val state by feedComponentModel.state.collectAsStateWithLifecycle() @@ -40,11 +54,28 @@ internal class DefaultFeedComponent( } } + val bottomSheet by bottomSheetSlot.subscribeAsState() val state by feedComponentModel.state.collectAsStateWithLifecycle() FeedList( modifier = modifier, state = state, ) + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: FeedPortfolioRoute, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent = when (config) { + is FeedPortfolioRoute.AddToPortfolio -> { + addToPortfolioComponentFactory.create( + context = childByContext(componentContext), + params = AddToPortfolioPreselectedDataComponent.Params( + tokenToAdd = config.tokenToAdd, + callback = feedComponentModel.addToPortfolioCallback, + ), + ) + } } data class FeedParams(val feedClickIntents: FeedModelClickIntents) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedPortfolioRoute.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedPortfolioRoute.kt new file mode 100644 index 0000000000..afaf4193dd --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedPortfolioRoute.kt @@ -0,0 +1,11 @@ +package com.tangem.features.feed.components.feed + +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import kotlinx.serialization.Serializable + +@Serializable +sealed interface FeedPortfolioRoute { + + @Serializable + data class AddToPortfolio(val tokenToAdd: AddToPortfolioPreselectedDataComponent.TokenToAdd) : FeedPortfolioRoute +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioComponentModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioComponentModule.kt index b522e1b6e3..1233c2be6a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioComponentModule.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioComponentModule.kt @@ -2,7 +2,9 @@ package com.tangem.features.feed.components.market.details.portfolio.add.impl.di import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.components.market.details.portfolio.add.impl.DefaultAddToPortfolioComponent +import com.tangem.features.feed.components.market.details.portfolio.add.impl.DefaultAddToPortfolioPreselectedDataComponent import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.DefaultAddToPortfolioManager import dagger.Binds import dagger.Module @@ -18,4 +20,9 @@ internal interface AddToPortfolioComponentModule { @Binds fun bindAddToPortfolioManagerFactory(factory: DefaultAddToPortfolioManager.Factory): AddToPortfolioManager.Factory + + @Binds + fun bindAddToPortfolioPreselectedDataComponent( + factory: DefaultAddToPortfolioPreselectedDataComponent.Factory, + ): AddToPortfolioPreselectedDataComponent.Factory } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index 8c1774fcf8..c5cbabead3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -2,6 +2,11 @@ package com.tangem.features.feed.model.feed import androidx.compose.runtime.Stable import arrow.core.getOrElse +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam @@ -12,20 +17,28 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.earn.usecase.FetchTopEarnTokensUseCase +import com.tangem.domain.earn.usecase.GetTopEarnTokensUseCase import com.tangem.domain.markets.GetTopFiveMarketTokenUseCase +import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketListConfig import com.tangem.domain.markets.toSerializableParam +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase import com.tangem.features.feed.components.feed.DefaultFeedComponent +import com.tangem.features.feed.components.feed.FeedPortfolioRoute +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.feed.impl.R import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent import com.tangem.features.feed.model.feed.state.FeedMarketsBatchFlowManager import com.tangem.features.feed.model.feed.state.FeedStateController -import com.tangem.features.feed.model.feed.state.transformers.UpdateGlobalFeedStateTransformer -import com.tangem.features.feed.model.feed.state.transformers.UpdateMarketChartsTransformer -import com.tangem.features.feed.model.feed.state.transformers.UpdateTrendingNewsStateTransformer +import com.tangem.features.feed.model.feed.state.transformers.* import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.feed.state.* import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -41,22 +54,26 @@ import javax.inject.Inject @Stable @ModelScoped -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class FeedComponentModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val fetchTrendingNewsUseCase: FetchTrendingNewsUseCase, private val manageTrendingNewsUseCase: ManageTrendingNewsUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val stateController: FeedStateController, + private val feedFeatureToggle: FeedFeatureToggle, + private val fetchTopEarnTokensUseCase: FetchTopEarnTokensUseCase, + private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase, + private val appRouter: AppRouter, getTopFiveMarketTokenUseCase: GetTopFiveMarketTokenUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, paramsContainer: ParamsContainer, ) : Model() { - private val params = paramsContainer.require() - private var quotesUpdateJob: Job? = null + private val params = paramsContainer.require() + private val currentAppCurrency = getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } }.stateIn( @@ -72,7 +89,22 @@ internal class FeedComponentModel @Inject constructor( dispatchers = dispatchers, ) - internal val state: StateFlow + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + val addToPortfolioCallback = object : AddToPortfolioPreselectedDataComponent.Callback { + override fun onDismiss() = bottomSheetNavigation.dismiss() + override fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId) { + bottomSheetNavigation.dismiss() + appRouter.push( + AppRoute.CurrencyDetails( + userWalletId = walletId, + currency = addedToken, + ), + ) + } + } + + val state: StateFlow get() = stateController.uiState val isVisibleOnScreen = MutableStateFlow(false) @@ -81,29 +113,38 @@ internal class FeedComponentModel @Inject constructor( initializeState() updateCallbacks() fetchTrendingNews() + fetchEarnData() + fetchCharts() subscribeOnCurrencyUpdate() - loadCharts() + subscribeOnDataState() + } + + private fun subscribeOnDataState() { modelScope.launch(dispatchers.default) { combine( flow = marketsBatchFlowManager.itemsByOrder, flow2 = marketsBatchFlowManager.loadingStatesByOrder, flow3 = marketsBatchFlowManager.errorStatesByOrder, flow4 = manageTrendingNewsUseCase.observeTrendingNews(), - ) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder, trendingNewsResult -> + flow5 = getTopEarnTokensUseCase(), + ) { itemsByOrder, loadingStatesByOrder, errorStatesByOrder, trendingNewsResult, earnResult -> val globalStateTransformer = UpdateGlobalFeedStateTransformer( loadingStatesByOrder = loadingStatesByOrder, errorStatesByOrder = errorStatesByOrder, trendingNewsResult = trendingNewsResult, + earnResult = earnResult, onRetryClicked = { modelScope.launch(dispatchers.default) { stateController.update { currentState -> currentState.copy(globalState = GlobalFeedState.Loading) } - fetchTrendingNewsUseCase.invoke() + fetchTrendingNews() + fetchEarnData() marketsBatchFlowManager.reloadAll() } }, analyticsEventHandler = analyticsEventHandler, + feedFeatureToggle = feedFeatureToggle, ) val currentState = stateController.value @@ -123,13 +164,15 @@ internal class FeedComponentModel @Inject constructor( ), UpdateTrendingNewsStateTransformer( result = trendingNewsResult, - onRetryClicked = { - modelScope.launch(dispatchers.default) { - fetchTrendingNewsUseCase.invoke() - } - }, + onRetryClicked = ::fetchTrendingNews, analyticsEventHandler = analyticsEventHandler, ), + UpdateEarnStateTransformer( + isEarnEnabled = feedFeatureToggle.isEarnBlockEnabled, + onItemClick = ::handleEarnTokenClick, + onRetryClick = ::fetchEarnData, + earnResult = earnResult, + ), ) } @@ -144,12 +187,6 @@ internal class FeedComponentModel @Inject constructor( } } - private fun fetchTrendingNews() { - modelScope.launch(dispatchers.default) { - fetchTrendingNewsUseCase() - } - } - private fun subscribeOnCurrencyUpdate() { modelScope.launch(dispatchers.default) { currentAppCurrency.drop(1).collect { @@ -158,7 +195,21 @@ internal class FeedComponentModel @Inject constructor( } } - private fun loadCharts() { + private fun fetchTrendingNews() { + modelScope.launch(dispatchers.default) { + fetchTrendingNewsUseCase() + } + } + + private fun fetchEarnData() { + if (!feedFeatureToggle.isEarnBlockEnabled) return + modelScope.launch(dispatchers.default) { + stateController.update(UpdateEarnLoadingStateTransformer()) + fetchTopEarnTokensUseCase() + } + } + + private fun fetchCharts() { modelScope.launch(dispatchers.default) { TokenMarketListConfig.Order.entries.forEach { order -> marketsBatchFlowManager.getOnLastBatchLoadedSuccessFlow(order)?.collect { batchKey -> @@ -198,6 +249,7 @@ internal class FeedComponentModel @Inject constructor( onSliderEndReached = { analyticsEventHandler.send(FeedAnalyticsEvent.NewsCarouselEndReached()) }, + onOpenEarnPageClick = params.feedClickIntents::onOpenEarnPage, ), news = NewsUM( content = persistentListOf(), @@ -214,14 +266,41 @@ internal class FeedComponentModel @Inject constructor( currentSortByType = SortByTypeUM.TopGainers, ), globalState = GlobalFeedState.Loading, + earnListUM = if (feedFeatureToggle.isEarnBlockEnabled) { + EarnListUM.Loading + } else { + null + }, ) } - private fun getCurrentDate(): String { - val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault()) - return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate) + private fun startQuotesUpdateTimer() { + quotesUpdateJob?.cancel() + quotesUpdateJob = modelScope.launch { + while (true) { + delay(DELAY_TO_FETCH_QUOTES) + isVisibleOnScreen.first { it } + marketsBatchFlowManager.updateQuotes() + } + } } + private fun updateCallbacks() { + stateController.update { feedListUM -> + feedListUM.copy( + feedListCallbacks = feedListUM.feedListCallbacks.copy( + onSortTypeClick = ::handleSortTypeClicked, + onMarketItemClick = ::handleMarketItemClicked, + onMarketOpenClick = ::handleMarketOpenClicked, + onArticleClick = ::handleArticleClicked, + onOpenAllNews = ::handleOpenAllNews, + onOpenEarnPageClick = params.feedClickIntents::onOpenEarnPage, + ), + ) + } + } + + /* start of clicks area */ private fun handleSortTypeClicked(sortByType: SortByTypeUM) { stateController.update { currentState -> val updatedCharts = currentState.marketChartConfig.marketCharts.mapValues { (chartSortType, chart) -> @@ -249,31 +328,6 @@ internal class FeedComponentModel @Inject constructor( } } - private fun startQuotesUpdateTimer() { - quotesUpdateJob?.cancel() - quotesUpdateJob = modelScope.launch { - while (true) { - delay(DELAY_TO_FETCH_QUOTES) - isVisibleOnScreen.first { it } - marketsBatchFlowManager.updateQuotes() - } - } - } - - private fun updateCallbacks() { - stateController.update { feedListUM -> - feedListUM.copy( - feedListCallbacks = feedListUM.feedListCallbacks.copy( - onSortTypeClick = ::handleSortTypeClicked, - onMarketItemClick = ::handleMarketItemClicked, - onMarketOpenClick = ::handleMarketOpenClicked, - onArticleClick = ::handleArticleClicked, - onOpenAllNews = ::handleOpenAllNews, - ), - ) - } - } - private fun handleMarketItemClicked(item: MarketsListItemUM) { val tokenMarket = marketsBatchFlowManager.getTokenMarketById(item.id) if (tokenMarket != null) { @@ -342,6 +396,26 @@ internal class FeedComponentModel @Inject constructor( params.feedClickIntents.onOpenAllNews() } + private fun handleEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency) { + bottomSheetNavigation.activate( + FeedPortfolioRoute.AddToPortfolio( + tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd( + network = TokenMarketInfo.Network( + networkId = earnTokenWithCurrency.earnToken.networkId, + isExchangeable = false, + contractAddress = earnTokenWithCurrency.earnToken.tokenAddress, + decimalCount = earnTokenWithCurrency.earnToken.decimalCount, + ), + id = CryptoCurrency.RawID(earnTokenWithCurrency.earnToken.tokenId), + name = earnTokenWithCurrency.earnToken.tokenName, + symbol = earnTokenWithCurrency.earnToken.tokenSymbol, + ), + ), + ) + } + /* end of clicks area */ + + /* start of utils area */ private fun SortByTypeUM.toOrder(): TokenMarketListConfig.Order { return when (this) { SortByTypeUM.Rating -> TokenMarketListConfig.Order.ByRating @@ -354,6 +428,12 @@ internal class FeedComponentModel @Inject constructor( } } + private fun getCurrentDate(): String { + val localDate = DateTime(DateTime.now(), DateTimeZone.getDefault()) + return DateTimeFormatters.formatDate(formatter = DateTimeFormatters.dateDMMM, date = localDate) + } + /* end of utils area */ + companion object { private const val DELAY_TO_FETCH_QUOTES = 60_000L } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt index d3ade33796..b9778a2ed6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedModelClickIntents.kt @@ -27,4 +27,6 @@ internal interface FeedModelClickIntents { paginationConfig: NewsListConfig? = null, ) fun onOpenAllNews() + + fun onOpenEarnPage() } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt index 161b83222b..99440c5949 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt @@ -1,8 +1,11 @@ package com.tangem.features.feed.model.feed.state import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.feed.model.feed.state.transformers.FeedListUMTransformer import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.feed.state.* import kotlinx.collections.immutable.persistentHashMapOf import kotlinx.collections.immutable.persistentListOf @@ -13,7 +16,9 @@ import kotlinx.coroutines.flow.update import javax.inject.Inject @ModelScoped -internal class FeedStateController @Inject constructor() { +internal class FeedStateController @Inject constructor( + private val feedFeatureToggle: FeedFeatureToggle, +) { private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) @@ -33,15 +38,11 @@ internal class FeedStateController @Inject constructor() { transformers.forEach { mutableUiState.update(function = it::transform) } } - fun clear() { - mutableUiState.update { getInitialState() } - } - private fun getInitialState(): FeedListUM { return FeedListUM( currentDate = "", feedListSearchBar = FeedListSearchBar( - placeholderText = com.tangem.core.ui.extensions.TextReference.EMPTY, + placeholderText = TextReference.EMPTY, onBarClick = {}, ), feedListCallbacks = FeedListCallbacks( @@ -53,6 +54,7 @@ internal class FeedStateController @Inject constructor() { onSortTypeClick = {}, onSliderScroll = {}, onSliderEndReached = {}, + onOpenEarnPageClick = {}, ), news = NewsUM( content = persistentListOf(), @@ -65,6 +67,11 @@ internal class FeedStateController @Inject constructor() { currentSortByType = SortByTypeUM.Trending, ), globalState = GlobalFeedState.Loading, + earnListUM = if (feedFeatureToggle.isEarnBlockEnabled) { + EarnListUM.Loading + } else { + null + }, ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnLoadingStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnLoadingStateTransformer.kt new file mode 100644 index 0000000000..22a796a576 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnLoadingStateTransformer.kt @@ -0,0 +1,11 @@ +package com.tangem.features.feed.model.feed.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnListUM +import com.tangem.features.feed.ui.feed.state.FeedListUM + +internal class UpdateEarnLoadingStateTransformer : FeedListUMTransformer { + + override fun transform(prevState: FeedListUM): FeedListUM { + return prevState.copy(earnListUM = EarnListUM.Loading) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt new file mode 100644 index 0000000000..bf3daee21c --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt @@ -0,0 +1,111 @@ +package com.tangem.features.feed.model.feed.state.transformers + +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.domain.models.earn.EarnRewardType +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.earn.EarnTopToken +import com.tangem.domain.models.earn.EarnType +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import com.tangem.features.feed.ui.earn.state.EarnListUM +import com.tangem.features.feed.ui.feed.state.FeedListUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class UpdateEarnStateTransformer( + private val isEarnEnabled: Boolean, + private val onItemClick: (EarnTokenWithCurrency) -> Unit, + private val onRetryClick: () -> Unit, + private val earnResult: EarnTopToken?, +) : FeedListUMTransformer { + + private val earnTokenWithCurrencyConverter = EarnTokenWithCurrencyConverter(onItemClick = onItemClick) + + override fun transform(prevState: FeedListUM): FeedListUM { + if (!isEarnEnabled) return handleEmptyState(prevState) + return when (earnResult) { + null -> { + handleEmptyState(prevState) + } + else -> { + earnResult.fold( + ifLeft = { + handleErrorState(prevState) + }, + ifRight = { earnTokensWithCurrency -> + handleDataState( + currentState = prevState, + earnTokensWithCurrency = earnTokensWithCurrency, + ) + }, + ) + } + } + } + + private fun handleEmptyState(currentState: FeedListUM): FeedListUM { + return currentState.copy(earnListUM = null) + } + + private fun handleErrorState(currentState: FeedListUM): FeedListUM { + return currentState.copy(earnListUM = EarnListUM.Error(onRetryClick)) + } + + private fun handleDataState( + currentState: FeedListUM, + earnTokensWithCurrency: List, + ): FeedListUM { + return currentState.copy( + earnListUM = EarnListUM.Content( + items = earnTokensWithCurrency + .sortedWith( + compareByDescending { + it.earnToken.apy.toBigDecimalOrNull() ?: BigDecimal.ZERO + }.thenBy { it.earnToken.tokenName }, + ) + .map { earnToken -> + earnTokenWithCurrencyConverter.convert(earnToken) + } + .toPersistentList(), + ), + ) + } + + private class EarnTokenWithCurrencyConverter( + private val onItemClick: (EarnTokenWithCurrency) -> Unit, + ) : Converter { + override fun convert(value: EarnTokenWithCurrency): EarnListItemUM { + return EarnListItemUM( + network = TextReference.Str(value.networkName), + symbol = TextReference.Str(value.earnToken.tokenSymbol), + tokenName = TextReference.Str(value.earnToken.tokenName), + currencyIconState = CryptoCurrencyToIconStateConverter().convert(value.cryptoCurrency), + earnValue = when (value.earnToken.rewardType) { + EarnRewardType.APR -> TextReference.Res( + id = R.string.staking_apr_earn_badge, + formatArgs = wrappedList(convertPercent(value.earnToken.apy)), + ) + EarnRewardType.APY -> TextReference.Res( + id = R.string.yield_module_earn_badge, + formatArgs = wrappedList(convertPercent(value.earnToken.apy)), + ) + }, + earnType = when (value.earnToken.type) { + EarnType.STAKING -> TextReference.Res(R.string.common_staking) + EarnType.YIELD -> TextReference.Res(R.string.common_yield_mode) + }, + onItemClick = { onItemClick(value) }, + ) + } + + private fun convertPercent(value: String): TextReference { + val percent = BigDecimal(value).format { percent(withPercentSign = false) } + return TextReference.Str(percent) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt index dc78f2b7a0..56d9d140a4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateGlobalFeedStateTransformer.kt @@ -1,57 +1,58 @@ package com.tangem.features.feed.model.feed.state.transformers import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.models.earn.EarnTopToken import com.tangem.domain.models.news.TrendingNews +import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.feed.state.FeedListUM import com.tangem.features.feed.ui.feed.state.GlobalFeedState import com.tangem.features.feed.ui.feed.state.NewsUMState +@Suppress("LongParameterList") internal class UpdateGlobalFeedStateTransformer( private val loadingStatesByOrder: Map, private val errorStatesByOrder: Map, private val trendingNewsResult: TrendingNews, + private val earnResult: EarnTopToken?, private val onRetryClicked: () -> Unit, private val analyticsEventHandler: AnalyticsEventHandler, + private val feedFeatureToggle: FeedFeatureToggle, ) : FeedListUMTransformer { - @Suppress("CyclomaticComplexMethod") override fun transform(prevState: FeedListUM): FeedListUM { - val previousGlobalState = prevState.globalState + val blockStates = buildList { + add(getNewsState(prevState)) + add(getChartsState()) + if (feedFeatureToggle.isEarnBlockEnabled) { + add(getEarnState(prevState)) + } + } - val isNewsLoading = prevState.news.newsUMState == NewsUMState.LOADING - val isNewsError = trendingNewsResult is TrendingNews.Error || - prevState.news.newsUMState == NewsUMState.ERROR + val newGlobalState = when { + blockStates.all { it == BlockState.ERROR } -> { + if (prevState.globalState !is GlobalFeedState.Error) { + sendErrorAnalytics() + } + GlobalFeedState.Error(onRetryClicked = onRetryClicked) + } + blockStates.any { it == BlockState.LOADING } -> GlobalFeedState.Loading + else -> GlobalFeedState.Content + } + return prevState.copy(globalState = newGlobalState) + } + + private fun areAllChartsError(): Boolean { val hasLoadingInCharts = loadingStatesByOrder.values.any { it } val hasErrorInMarketChart = errorStatesByOrder[SortByTypeUM.Rating] != null val hasErrorInMarketPulseChart = errorStatesByOrder .any { it.key != SortByTypeUM.Rating && it.value != null } - - val areAllChartsLoading = loadingStatesByOrder.values.isNotEmpty() && loadingStatesByOrder.values.all { it } - val areAllChartsError = when { - hasErrorInMarketChart && hasErrorInMarketPulseChart -> true - hasErrorInMarketPulseChart && hasLoadingInCharts -> true - hasErrorInMarketChart && hasLoadingInCharts -> true - else -> false - } - - val newGlobalState = when { - isNewsError && areAllChartsLoading -> GlobalFeedState.Loading - isNewsError && areAllChartsError -> { - if (previousGlobalState !is GlobalFeedState.Error) { - sendErrorAnalytics() - } - GlobalFeedState.Error( - onRetryClicked = onRetryClicked, - ) - } - isNewsLoading && areAllChartsError -> GlobalFeedState.Loading - isNewsLoading && areAllChartsLoading -> GlobalFeedState.Loading - else -> GlobalFeedState.Content - } - return prevState.copy(globalState = newGlobalState) + return hasErrorInMarketChart && hasErrorInMarketPulseChart || + hasErrorInMarketPulseChart && hasLoadingInCharts || + hasErrorInMarketChart && hasLoadingInCharts } private fun sendErrorAnalytics() { @@ -59,4 +60,38 @@ internal class UpdateGlobalFeedStateTransformer( FeedAnalyticsEvent.AllWidgetsLoadError(), ) } + + private fun getNewsState(prevState: FeedListUM): BlockState { + val isError = trendingNewsResult is TrendingNews.Error || prevState.news.newsUMState == NewsUMState.ERROR + if (isError) return BlockState.ERROR + + val isLoading = prevState.news.newsUMState == NewsUMState.LOADING && trendingNewsResult !is TrendingNews.Data + if (isLoading) return BlockState.LOADING + + return BlockState.CONTENT + } + + private fun getChartsState(): BlockState { + if (areAllChartsError()) return BlockState.ERROR + + val isLoading = loadingStatesByOrder.values.isNotEmpty() && loadingStatesByOrder.values.all { it } + if (isLoading) return BlockState.LOADING + + return BlockState.CONTENT + } + + private fun getEarnState(prevState: FeedListUM): BlockState { + val isError = when (earnResult) { + null -> false + else -> earnResult.isLeft() + } + if (isError) return BlockState.ERROR + + val isLoading = prevState.earnListUM is EarnListUM.Loading && earnResult == null + if (isLoading) return BlockState.LOADING + + return BlockState.CONTENT + } + + private enum class BlockState { LOADING, ERROR, CONTENT } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt index 27a5de8ffe..943dd2fce3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/FeedList.kt @@ -147,10 +147,8 @@ private fun FeedListContent(state: FeedListUM, modifier: Modifier = Modifier) { ) EarnBlock( - onSeeAllClick = { - // TODO handle in [REDACTED_TASK_KEY] - }, - earnListUM = null, // TODO in [REDACTED_TASK_KEY] + onSeeAllClick = state.feedListCallbacks.onOpenEarnPageClick, + earnListUM = state.earnListUM, ) MarketPulseBlock( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt index 62d38e2004..ceaf16a400 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt @@ -3,13 +3,18 @@ package com.tangem.features.feed.ui.feed.preview import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import com.tangem.features.feed.ui.earn.state.EarnListUM import com.tangem.features.feed.ui.feed.state.* import kotlinx.collections.immutable.* @@ -34,6 +39,7 @@ internal object FeedListPreviewDataProvider { onSortTypeClick = {}, onSliderScroll = {}, onSliderEndReached = {}, + onOpenEarnPageClick = {}, ), news = NewsUM( content = articles.filter { it.isTrending.not() }.toImmutableList(), @@ -45,6 +51,9 @@ internal object FeedListPreviewDataProvider { marketCharts = createMarketCharts(marketItems, includeErrorState = false), currentSortByType = SortByTypeUM.TopGainers, ), + earnListUM = EarnListUM.Content( + items = createEarnListItemsUM(), + ), ) } @@ -250,4 +259,25 @@ internal object FeedListPreviewDataProvider { updateTimestamp = 0, ) } + + private fun createEarnListItemsUM(): ImmutableList { + return List(5) { + 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 6.54%"), + earnType = stringReference("Yield"), + onItemClick = {}, + ) + }.toPersistentList() + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt index d9c50773a0..8d8a014175 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt @@ -5,6 +5,7 @@ import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.common.ui.news.ArticleConfigUM import com.tangem.core.ui.extensions.TextReference import com.tangem.features.feed.model.market.list.state.SortByTypeUM +import com.tangem.features.feed.ui.earn.state.EarnListUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap import kotlinx.collections.immutable.persistentListOf @@ -17,6 +18,7 @@ internal data class FeedListUM( val trendingArticle: ArticleConfigUM?, val marketChartConfig: MarketChartConfig, val globalState: GlobalFeedState = GlobalFeedState.Content, + val earnListUM: EarnListUM?, ) internal data class FeedListCallbacks( @@ -28,6 +30,7 @@ internal data class FeedListCallbacks( val onSortTypeClick: (sortBy: SortByTypeUM) -> Unit, val onSliderScroll: () -> Unit, val onSliderEndReached: () -> Unit, + val onOpenEarnPageClick: () -> Unit, ) internal data class FeedListSearchBar(