From d5f9410905449a3e8c5e9b6f0ceed20568d541fb Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 10 Feb 2026 11:23:37 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../api/tangemTech/TangemTechApi.kt | 4 +- .../api/tangemTech/models/EarnResponse.kt | 2 +- .../earn/repository/DefaultEarnRepository.kt | 12 +- .../batch/EarnTokensBatchFetcher.kt | 2 +- .../domain/earn/model/EarnTokensListConfig.kt | 6 +- .../domain/earn/repository/EarnRepository.kt | 2 +- .../earn/usecase/FetchEarnNetworksUseCase.kt | 4 +- .../tangem/domain/models/earn/EarnNetwork.kt | 4 + .../tangem/domain/models/earn/EarnToken.kt | 2 +- .../feed/components/FeedEntryChildFactory.kt | 1 + .../components/earn/DefaultEarnComponent.kt | 33 ++ ...nTokenWithCurrencyToListItemUMConverter.kt | 50 +++ .../features/feed/model/earn/EarnModel.kt | 421 +++++++++++------- .../model/earn/EarnTokensListConfigFactory.kt | 37 ++ .../model/earn/state/EarnStateController.kt | 48 ++ .../state/converter/EarnNetworksConverter.kt | 22 + .../ClearEarnUMFilterStateTransformer.kt | 28 ++ ...rnNetworkFilterSelectedStateTransformer.kt | 18 + .../EarnNetworkFilterStateTransformer.kt | 21 + .../EarnTypeFilterSelectedStateTransformer.kt | 18 + .../EarnTypeFilterStateTransformer.kt | 21 + .../state/transformers/EarnUMTransformer.kt | 7 + ...UpdateBestOpportunitiesStateTransformer.kt | 13 + .../UpdateEarnUMInitialStateTransformer.kt | 23 + .../UpdateMostlyUsedStateTransformer.kt | 38 ++ .../statemanager/EarnListBatchFlowManager.kt | 105 +++++ .../earn/statemanager/EarnListStateManager.kt | 28 ++ .../UpdateEarnStateTransformer.kt | 73 +-- .../statemanager/NewsListBatchFlowManager.kt | 22 +- .../features/feed/ui/earn/EarnContent.kt | 113 +++-- .../EarnFilterByNetworkBottomSheet.kt | 16 +- .../feed/ui/earn/state/EarnFilterNetworkUM.kt | 2 +- .../features/feed/ui/earn/state/EarnListUM.kt | 5 +- .../feed/ui/feed/components/EarnBlock.kt | 11 +- .../component/DefaultTokenReceiveComponent.kt | 4 +- 35 files changed, 892 insertions(+), 324 deletions(-) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/EarnTokenWithCurrencyToListItemUMConverter.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/converter/EarnNetworksConverter.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/ClearEarnUMFilterStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterSelectedStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnUMTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateBestOpportunitiesStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListStateManager.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 0b0731ea4d..cf0d66f054 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 @@ -213,10 +213,10 @@ interface TangemTechApi { @Query("page") page: String? = null, @Query("limit") limit: Int? = null, @Query("type") type: String? = null, - @Query("network") network: String? = null, + @Query("networkIds") networks: List? = null, ): ApiResponse @GET("v1/earn/networks") - suspend fun getEarnNetworks(@Query("type") type: String): ApiResponse + suspend fun getEarnNetworks(@Query("type") type: String? = null): ApiResponse // endregion } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt index 47ec19a2ff..b123c7d912 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/EarnResponse.kt @@ -23,7 +23,7 @@ data class EarnTokenResponse( @Json(name = "id") val id: String, @Json(name = "symbol") val symbol: String, @Json(name = "name") val name: String, - @Json(name = "address") val address: String, + @Json(name = "address") val address: String? = null, @Json(name = "decimalCount") val decimalCount: Int? = null, ) diff --git a/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt index 7a78b887bb..53c36696d4 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/DefaultEarnRepository.kt @@ -62,13 +62,16 @@ internal class DefaultEarnRepository( ).toBatchFlow() } - override suspend fun fetchEarnNetworks(type: String) { + override suspend fun fetchEarnNetworks() { runCatching(dispatchers.io) { - val response = tangemTechApi.getEarnNetworks(type = type).getOrThrow() + val response = tangemTechApi.getEarnNetworks().getOrThrow() val items = response.items.map { dto -> + val blockchain = Blockchain.fromNetworkId(dto.networkId) EarnNetwork( networkId = dto.networkId, isAdded = false, + fullName = blockchain?.fullName.orEmpty(), + symbol = blockchain?.currency.orEmpty(), ) } earnNetworksStore.store(Either.Right(items)) @@ -136,7 +139,8 @@ internal fun createCryptoCurrencyForEarnToken( cryptoCurrencyFactory: CryptoCurrencyFactory, ): CryptoCurrency? { val blockchain = Blockchain.fromNetworkId(earnToken.networkId) ?: Blockchain.Unknown - return if (earnToken.token.address.isEmpty()) { + val address = earnToken.token.address + return if (address == null) { cryptoCurrencyFactory.createCoin( blockchain = blockchain, extraDerivationPath = null, @@ -157,7 +161,7 @@ internal fun createCryptoCurrencyForEarnToken( name = earnToken.token.name, symbol = earnToken.token.symbol, decimals = earnToken.token.decimalCount ?: blockchain.decimals(), - contractAddress = earnToken.token.address, + contractAddress = address, ) } } \ No newline at end of file diff --git a/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt b/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt index 85d104f727..aeead13270 100644 --- a/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt +++ b/data/earn/src/main/java/com/tangem/data/earn/repository/batch/EarnTokensBatchFetcher.kt @@ -100,7 +100,7 @@ internal class EarnTokensBatchFetcher( page = page.toString(), limit = limit, type = params.type, - network = params.network, + networks = params.networks, ).getOrThrow() val userWallet = userWalletsListRepository.selectedUserWallet.value diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt index ab150727ee..20320ace05 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/model/EarnTokensListConfig.kt @@ -6,12 +6,12 @@ import kotlinx.serialization.Serializable * Config of request earn tokens. * * @param type of Earn (ex: yield, staking). - * @param network networkId. + * @param networks list of networkId. * @param isForEarn flag to load mostly used tokens. */ @Serializable data class EarnTokensListConfig( - val type: String, - val network: String, + val type: String?, + val networks: List?, val isForEarn: Boolean, ) \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt index 5727b0cd34..e20bab51ec 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/repository/EarnRepository.kt @@ -13,7 +13,7 @@ interface EarnRepository { /** * Load all networks for Earn and store it in data store. */ - suspend fun fetchEarnNetworks(type: String) + suspend fun fetchEarnNetworks() fun observeEarnNetworks(): Flow diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt index fda4a392d4..aa426437e7 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/FetchEarnNetworksUseCase.kt @@ -7,9 +7,9 @@ class FetchEarnNetworksUseCase( private val repository: EarnRepository, ) { - suspend operator fun invoke(type: String): Either { + suspend operator fun invoke(): Either { return Either.catch { - repository.fetchEarnNetworks(type = type) + repository.fetchEarnNetworks() } } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt index 04d3edbc79..114bbad106 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/earn/EarnNetwork.kt @@ -5,10 +5,14 @@ import kotlinx.serialization.Serializable /** * Model for networks in Earn. * @param networkId - id of network + * @param fullName - full name of network + * @param symbol - symbol of network * @param isAdded - is exists in any user's wallet */ @Serializable data class EarnNetwork( val networkId: String, + val fullName: String, + val symbol: String, val isAdded: Boolean, ) \ 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 523ba3ac45..e3fc112731 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 @@ -22,7 +22,7 @@ data class EarnToken( val tokenId: String, val tokenSymbol: String, val tokenName: String, - val tokenAddress: String, + val tokenAddress: String?, val decimalCount: Int?, ) 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 9deae9964b..02b1c82ab7 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 @@ -109,6 +109,7 @@ internal class FeedEntryChildFactory @Inject constructor( DefaultEarnComponent( appComponentContext = appComponentContext, params = child.params, + addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent, ) } } 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 index ad41d329a1..3da82e53fe 100644 --- 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 @@ -5,15 +5,22 @@ import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier 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.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.ComposableBottomSheetComponent 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.components.feed.FeedPortfolioRoute +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.model.earn.EarnModel import com.tangem.features.feed.ui.earn.EarnContent import kotlinx.serialization.Serializable @@ -21,10 +28,18 @@ import kotlinx.serialization.Serializable internal class DefaultEarnComponent( appComponentContext: AppComponentContext, private val params: Params, + private val addToPortfolioComponentFactory: AddToPortfolioPreselectedDataComponent.Factory, ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { private val earnModel = getOrCreateModel(params = params) + private val bottomSheetSlot = childSlot( + source = earnModel.bottomSheetNavigation, + serializer = FeedPortfolioRoute.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + @Composable override fun Title(bottomSheetState: State) { val background = LocalMainBottomSheetColor.current.value @@ -42,11 +57,29 @@ internal class DefaultEarnComponent( @Composable override fun Content(bottomSheetState: State, modifier: Modifier) { + val bottomSheet by bottomSheetSlot.subscribeAsState() val state by earnModel.state.collectAsStateWithLifecycle() + EarnContent( state = state, modifier = modifier, ) + 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 = earnModel.addToPortfolioCallback, + ), + ) + } } @Serializable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/EarnTokenWithCurrencyToListItemUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/EarnTokenWithCurrencyToListItemUMConverter.kt new file mode 100644 index 0000000000..59cadd9281 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/EarnTokenWithCurrencyToListItemUMConverter.kt @@ -0,0 +1,50 @@ +package com.tangem.features.feed.model.converter + +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.EarnType +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class EarnTokenWithCurrencyToListItemUMConverter( + private val onItemClick: (EarnTokenWithCurrency) -> Unit, +) : Converter { + + private val cryptoCurrencyToIconStateConverter = CryptoCurrencyToIconStateConverter() + + 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/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index 2717a8708d..17eb499815 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -1,131 +1,157 @@ package com.tangem.features.feed.model.earn import androidx.compose.runtime.Stable +import arrow.core.Either +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.R +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.earn.usecase.* +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.earn.EarnNetworks +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.feed.components.earn.DefaultEarnComponent -import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM -import com.tangem.features.feed.ui.earn.state.EarnFilterByNetworkBottomSheetContentUM -import com.tangem.features.feed.ui.earn.state.EarnFilterByTypeBottomSheetContentUM -import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM -import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM -import com.tangem.features.feed.ui.earn.state.EarnListUM -import com.tangem.features.feed.ui.earn.state.EarnUM +import com.tangem.features.feed.components.feed.FeedPortfolioRoute +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import com.tangem.features.feed.model.earn.state.EarnStateController +import com.tangem.features.feed.model.earn.state.converter.EarnNetworksConverter +import com.tangem.features.feed.model.earn.state.transformers.* +import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager +import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager +import com.tangem.features.feed.ui.earn.state.* +import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import javax.inject.Inject @Stable @ModelScoped +@Suppress("LongParameterList") internal class EarnModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val fetchEarnNetworksUseCase: FetchEarnNetworksUseCase, + private val getEarnNetworksUseCase: GetEarnNetworksUseCase, + private val getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase, + private val getTopEarnTokensUseCase: GetTopEarnTokensUseCase, + private val fetchTopEarnTokensUseCase: FetchTopEarnTokensUseCase, + private val appRouter: AppRouter, + private val stateController: EarnStateController, ) : Model() { private val params = paramsContainer.require() + private val earnNetworks = MutableStateFlow(Either.Right(emptyList())) + private val earnNetworksConverter: EarnNetworksConverter by lazy { EarnNetworksConverter() } + private val earnListConfigProvider = Provider { + createEarnTokensListConfig( + selectedTypeFilter = stateController.value.selectedTypeFilter, + selectedNetworkFilter = stateController.value.selectedNetworkFilter, + earnNetworks = earnNetworks.value, + ) + } - // TODO [REDACTED_TASK_KEY] remove mocks - private val mockNetworks: ImmutableList = persistentListOf( - EarnFilterNetworkUM.AllNetworks( - text = resourceReference(R.string.earn_filter_all_networks), - isSelected = false, - ), - EarnFilterNetworkUM.MyNetworks( - text = resourceReference(R.string.earn_filter_my_networks), - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "ethereum", - text = stringReference("Ethereum"), - symbol = stringReference("ETH"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/ethereum.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "polygon", - text = stringReference("Polygon"), - symbol = stringReference("MATIC"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/matic-network.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "base", - text = stringReference("Base"), - symbol = stringReference("BASE"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/base.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "avalanche", - text = stringReference("Avalance"), - symbol = stringReference("AVAX"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/avalanche-2.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "cardano", - text = stringReference("Cardano"), - symbol = stringReference("ADA"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/cardano.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "bsc", - text = stringReference("Bitcoin Smart Chain"), - symbol = stringReference("BSC"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/binancecoin.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "bitcoin", - text = stringReference("Bitcoin"), - symbol = stringReference("BTC"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/bitcoin.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "solana", - text = stringReference("Solana"), - symbol = stringReference("SOL"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/solana.png", - isSelected = false, - ), - EarnFilterNetworkUM.Network( - id = "ton", - text = stringReference("Ton"), - symbol = stringReference("TON"), - iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/the-open-network.png", - isSelected = false, - ), - ) + private val batchFlowManager by lazy { + EarnListBatchFlowManager( + getEarnTokensBatchFlowUseCase = getEarnTokensBatchFlowUseCase, + configProvider = earnListConfigProvider, + onItemClick = ::onEarnTokenClick, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + + 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 - field = MutableStateFlow(createInitialState()) + get() = stateController.uiState - private fun createInitialState(): EarnUM { - val defaultNetworkFilter = mockNetworks.first() - val defaultTypeFilter = EarnFilterTypeUM.All + init { + updateInitialState() + fetchEarnNetworks() + fetchTopEarnTokens() + subscribeOnNetworks() + subscribeOnBatchFlow() + subscribeToMostlyUsed() + } - return EarnUM( - mostlyUsed = EarnListUM.Loading, - bestOpportunities = EarnBestOpportunitiesUM.Loading, - selectedTypeFilter = defaultTypeFilter, - selectedNetworkFilter = defaultNetworkFilter, - filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(defaultTypeFilter), - filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(defaultNetworkFilter), - onBackClick = params.onBackClick, - onNetworkFilterClick = ::onNetworkFilterClick, - onTypeFilterClick = ::onTypeFilterClick, - ) + private fun subscribeOnBatchFlow() { + combine( + batchFlowManager.uiItems, + batchFlowManager.initialLoadingError, + batchFlowManager.paginationStatus, + ) { items, error, paginationStatus -> + val hasActiveFilters = state.value.selectedTypeFilter != EarnFilterTypeUM.All || + state.value.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks + EarnListStateManager.calculateState( + items = items, + error = error, + paginationStatus = paginationStatus, + hasActiveFilters = hasActiveFilters, + onRetryClick = { batchFlowManager.reload() }, + onLoadMore = { batchFlowManager.loadMore() }, + onClearFiltersClick = ::onClearFiltersClick, + ) + }.onEach { bestOpportunitiesState -> + stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState)) + } + .onStart { batchFlowManager.reload() } + .launchIn(modelScope) + } + + private fun subscribeToMostlyUsed() { + modelScope.launch(dispatchers.default) { + getTopEarnTokensUseCase().collect { earnResult -> + stateController.update( + UpdateMostlyUsedStateTransformer( + earnResult = earnResult, + onItemClick = ::onEarnTokenClick, + onRetryClick = ::fetchTopEarnTokens, + ), + ) + } + } + } + + private fun subscribeOnNetworks() { + modelScope.launch(dispatchers.default) { + getEarnNetworksUseCase().collect(earnNetworks) + } + } + + private fun fetchTopEarnTokens() { + modelScope.launch(dispatchers.default) { + fetchTopEarnTokensUseCase() + } + } + + private fun fetchEarnNetworks() { + modelScope.launch(dispatchers.default) { + fetchEarnNetworksUseCase() + } } private fun createTypeFilterBottomSheetConfig( @@ -137,7 +163,7 @@ internal class EarnModel @Inject constructor( onDismissRequest = ::hideTypeFilterBottomSheet, content = EarnFilterByTypeBottomSheetContentUM( selectedOption = selectedOption, - onOptionClick = ::onTypeFilterOptionClick, + onOptionClick = ::onTypeFilterOptionSelected, ), ) } @@ -146,16 +172,40 @@ internal class EarnModel @Inject constructor( selectedNetwork: EarnFilterNetworkUM, isShown: Boolean = false, ): TangemBottomSheetConfig { - val updatedNetworks = mockNetworks.map { network -> - when { - network is EarnFilterNetworkUM.AllNetworks && selectedNetwork is EarnFilterNetworkUM.AllNetworks -> - network.copy(isSelected = true) - network is EarnFilterNetworkUM.MyNetworks && selectedNetwork is EarnFilterNetworkUM.MyNetworks -> - network.copy(isSelected = true) - network is EarnFilterNetworkUM.Network && selectedNetwork is EarnFilterNetworkUM.Network && - network.id == selectedNetwork.id -> - network.copy(isSelected = true) - else -> network + val filters = buildList { + add( + EarnFilterNetworkUM.AllNetworks( + text = TextReference.Res(R.string.earn_filter_all_networks), + isSelected = false, + ), + ) + add( + EarnFilterNetworkUM.MyNetworks( + text = TextReference.Res(R.string.earn_filter_my_networks), + isSelected = false, + ), + ) + earnNetworks.value.onRight { networks -> + addAll(networks.map(earnNetworksConverter::convert)) + } + } + + val updatedNetworks = filters.map { filter -> + val shouldBeSelected = when (filter) { + is EarnFilterNetworkUM.AllNetworks -> selectedNetwork is EarnFilterNetworkUM.AllNetworks + is EarnFilterNetworkUM.MyNetworks -> selectedNetwork is EarnFilterNetworkUM.MyNetworks + is EarnFilterNetworkUM.Network -> + selectedNetwork is EarnFilterNetworkUM.Network && filter.id == selectedNetwork.id + } + + if (filter.isSelected != shouldBeSelected) { + when (filter) { + is EarnFilterNetworkUM.AllNetworks -> filter.copy(isSelected = shouldBeSelected) + is EarnFilterNetworkUM.MyNetworks -> filter.copy(isSelected = shouldBeSelected) + is EarnFilterNetworkUM.Network -> filter.copy(isSelected = shouldBeSelected) + } + } else { + filter } } @@ -165,81 +215,108 @@ internal class EarnModel @Inject constructor( content = EarnFilterByNetworkBottomSheetContentUM( selectedNetwork = selectedNetwork, networks = persistentListOf(*updatedNetworks.toTypedArray()), - onOptionClick = ::onNetworkFilterOptionClick, + onOptionClick = ::onNetworkFilterOptionSelected, ), ) } + /* start of clicks area */ private fun onTypeFilterClick() { - state.update { currentState -> - currentState.copy( - filterByTypeBottomSheet = createTypeFilterBottomSheetConfig( - selectedOption = currentState.selectedTypeFilter, - isShown = true, - ), - ) - } - } - - private fun onNetworkFilterClick() { - state.update { currentState -> - currentState.copy( - filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig( - selectedNetwork = currentState.selectedNetworkFilter, - isShown = true, - ), - ) - } + stateController.update( + EarnTypeFilterStateTransformer( + shouldShow = true, + filterByTypeBottomSheetConfig = ::createTypeFilterBottomSheetConfig, + ), + ) } private fun hideTypeFilterBottomSheet() { - state.update { currentState -> - currentState.copy( - filterByTypeBottomSheet = currentState.filterByTypeBottomSheet.copy(isShown = false), - ) - } + stateController.update( + EarnTypeFilterStateTransformer( + shouldShow = false, + filterByTypeBottomSheetConfig = ::createTypeFilterBottomSheetConfig, + ), + ) + } + + private fun onNetworkFilterClick() { + stateController.update( + EarnNetworkFilterStateTransformer( + shouldShow = true, + filterByNetworkBottomSheetConfig = ::createNetworkFilterBottomSheetConfig, + ), + ) } private fun hideNetworkFilterBottomSheet() { - state.update { currentState -> - currentState.copy( - filterByNetworkBottomSheet = currentState.filterByNetworkBottomSheet.copy(isShown = false), - ) - } + stateController.update( + EarnNetworkFilterStateTransformer( + shouldShow = false, + filterByNetworkBottomSheetConfig = ::createNetworkFilterBottomSheetConfig, + ), + ) } - private fun onTypeFilterOptionClick(type: EarnFilterTypeUM) { - state.update { currentState -> - currentState.copy( - selectedTypeFilter = type, - filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(selectedOption = type), - ) - } + private fun onTypeFilterOptionSelected(type: EarnFilterTypeUM) { + stateController.update( + EarnTypeFilterSelectedStateTransformer( + filter = type, + filterByTypeBottomSheetConfig = ::createTypeFilterBottomSheetConfig, + ), + ) + batchFlowManager.reload() } - private fun onNetworkFilterOptionClick(network: EarnFilterNetworkUM) { - state.update { currentState -> - currentState.copy( - selectedNetworkFilter = network, - filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig(selectedNetwork = network), - ) - } + private fun onNetworkFilterOptionSelected(network: EarnFilterNetworkUM) { + stateController.update( + EarnNetworkFilterSelectedStateTransformer( + filter = network, + filterByNetworkBottomSheetConfig = ::createNetworkFilterBottomSheetConfig, + ), + ) + batchFlowManager.reload() } - // TODO [REDACTED_TASK_KEY] remove - @Suppress("UnusedPrivateMember") private fun onClearFiltersClick() { - val defaultNetworkFilter = mockNetworks.first() - val defaultTypeFilter = EarnFilterTypeUM.All - state.update { currentState -> - currentState.copy( - selectedTypeFilter = defaultTypeFilter, - selectedNetworkFilter = defaultNetworkFilter, - filterByTypeBottomSheet = createTypeFilterBottomSheetConfig(selectedOption = defaultTypeFilter), - filterByNetworkBottomSheet = createNetworkFilterBottomSheetConfig( - selectedNetwork = defaultNetworkFilter, + stateController.update( + ClearEarnUMFilterStateTransformer( + filterByTypeBottomSheetConfig = ::createTypeFilterBottomSheetConfig, + filterByNetworkBottomSheetConfig = ::createNetworkFilterBottomSheetConfig, + ), + ) + batchFlowManager.reload() + } + + private fun onEarnTokenClick(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 */ + + private fun updateInitialState() { + stateController.update( + UpdateEarnUMInitialStateTransformer( + onBackClick = params.onBackClick, + onNetworkFilterClick = ::onNetworkFilterClick, + filterByTypeBottomSheetConfig = createTypeFilterBottomSheetConfig(state.value.selectedTypeFilter), + filterByNetworkBottomSheetConfig = createNetworkFilterBottomSheetConfig( + selectedNetwork = state.value.selectedNetworkFilter, + ), + onTypeFilterClick = ::onTypeFilterClick, + ), + ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt new file mode 100644 index 0000000000..08845ff277 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt @@ -0,0 +1,37 @@ +package com.tangem.features.feed.model.earn + +import com.tangem.domain.earn.model.EarnTokensListConfig +import com.tangem.domain.models.earn.EarnNetwork +import com.tangem.domain.models.earn.EarnNetworks +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM + +internal fun createEarnTokensListConfig( + selectedTypeFilter: EarnFilterTypeUM, + selectedNetworkFilter: EarnFilterNetworkUM, + earnNetworks: EarnNetworks, + isForEarn: Boolean = false, +): EarnTokensListConfig { + val type = when (selectedTypeFilter) { + EarnFilterTypeUM.All -> null + EarnFilterTypeUM.Staking -> "staking" + EarnFilterTypeUM.YieldMode -> "yield" + } + val networks = when (selectedNetworkFilter) { + is EarnFilterNetworkUM.AllNetworks -> null + is EarnFilterNetworkUM.MyNetworks -> { + earnNetworks.fold( + ifLeft = { null }, + ifRight = { networks -> + networks.filter(EarnNetwork::isAdded).map(EarnNetwork::networkId) + }, + ) + } + is EarnFilterNetworkUM.Network -> listOf(selectedNetworkFilter.id) + } + return EarnTokensListConfig( + type = type, + networks = networks, + isForEarn = isForEarn, + ) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt new file mode 100644 index 0000000000..6709151fb5 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt @@ -0,0 +1,48 @@ +package com.tangem.features.feed.model.earn.state + +import com.tangem.common.R +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.feed.model.earn.state.transformers.EarnUMTransformer +import com.tangem.features.feed.ui.earn.state.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@ModelScoped +internal class EarnStateController @Inject constructor() { + + private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) + + val uiState: StateFlow get() = mutableUiState.asStateFlow() + + val value: EarnUM get() = uiState.value + + fun update(function: (EarnUM) -> EarnUM) { + mutableUiState.update(function = function) + } + + fun update(transformer: EarnUMTransformer) { + mutableUiState.update(function = transformer::transform) + } + + private fun getInitialState(): EarnUM { + return EarnUM( + mostlyUsed = EarnListUM.Loading, + bestOpportunities = EarnBestOpportunitiesUM.Loading, + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks( + text = TextReference.Res(R.string.earn_filter_all_networks), + isSelected = true, + ), + filterByTypeBottomSheet = TangemBottomSheetConfig.Empty, + filterByNetworkBottomSheet = TangemBottomSheetConfig.Empty, + onBackClick = {}, + onNetworkFilterClick = {}, + onTypeFilterClick = {}, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/converter/EarnNetworksConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/converter/EarnNetworksConverter.kt new file mode 100644 index 0000000000..3f50e4aef8 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/converter/EarnNetworksConverter.kt @@ -0,0 +1,22 @@ +package com.tangem.features.feed.model.earn.state.converter + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.domain.models.earn.EarnNetwork +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.utils.converter.Converter + +internal class EarnNetworksConverter : Converter { + + override fun convert(value: EarnNetwork): EarnFilterNetworkUM.Network { + return EarnFilterNetworkUM.Network( + id = value.networkId, + text = TextReference.Str(value.fullName), + symbol = TextReference.Str(value.symbol), + iconRes = getActiveIconRes(Blockchain.fromNetworkId(value.networkId)?.id.orEmpty()), + isSelected = false, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/ClearEarnUMFilterStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/ClearEarnUMFilterStateTransformer.kt new file mode 100644 index 0000000000..b747c62f70 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/ClearEarnUMFilterStateTransformer.kt @@ -0,0 +1,28 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.common.R +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class ClearEarnUMFilterStateTransformer( + private val filterByTypeBottomSheetConfig: (EarnFilterTypeUM, Boolean) -> TangemBottomSheetConfig, + private val filterByNetworkBottomSheetConfig: (EarnFilterNetworkUM, Boolean) -> TangemBottomSheetConfig, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + val defaultNetworksFilter = EarnFilterNetworkUM.AllNetworks( + text = TextReference.Res(R.string.earn_filter_all_networks), + isSelected = true, + ) + + return prevState.copy( + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = defaultNetworksFilter, + filterByTypeBottomSheet = filterByTypeBottomSheetConfig(EarnFilterTypeUM.All, false), + filterByNetworkBottomSheet = filterByNetworkBottomSheetConfig(defaultNetworksFilter, false), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt new file mode 100644 index 0000000000..a9ee578d30 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class EarnNetworkFilterSelectedStateTransformer( + private val filter: EarnFilterNetworkUM, + private val filterByNetworkBottomSheetConfig: (EarnFilterNetworkUM, Boolean) -> TangemBottomSheetConfig, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy( + selectedNetworkFilter = filter, + filterByNetworkBottomSheet = filterByNetworkBottomSheetConfig(filter, false), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterStateTransformer.kt new file mode 100644 index 0000000000..5594b46a70 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterStateTransformer.kt @@ -0,0 +1,21 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class EarnNetworkFilterStateTransformer( + private val shouldShow: Boolean, + private val filterByNetworkBottomSheetConfig: (EarnFilterNetworkUM, Boolean) -> TangemBottomSheetConfig, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return if (shouldShow) { + prevState.copy( + filterByNetworkBottomSheet = filterByNetworkBottomSheetConfig(prevState.selectedNetworkFilter, true), + ) + } else { + prevState.copy(filterByNetworkBottomSheet = prevState.filterByNetworkBottomSheet.copy(isShown = false)) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterSelectedStateTransformer.kt new file mode 100644 index 0000000000..3110ddfbfc --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterSelectedStateTransformer.kt @@ -0,0 +1,18 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class EarnTypeFilterSelectedStateTransformer( + private val filter: EarnFilterTypeUM, + private val filterByTypeBottomSheetConfig: (EarnFilterTypeUM, Boolean) -> TangemBottomSheetConfig, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy( + selectedTypeFilter = filter, + filterByTypeBottomSheet = filterByTypeBottomSheetConfig(filter, false), + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterStateTransformer.kt new file mode 100644 index 0000000000..dacdb66034 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnTypeFilterStateTransformer.kt @@ -0,0 +1,21 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class EarnTypeFilterStateTransformer( + private val shouldShow: Boolean, + private val filterByTypeBottomSheetConfig: (EarnFilterTypeUM, Boolean) -> TangemBottomSheetConfig, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return if (shouldShow) { + prevState.copy( + filterByTypeBottomSheet = filterByTypeBottomSheetConfig(prevState.selectedTypeFilter, true), + ) + } else { + prevState.copy(filterByTypeBottomSheet = prevState.filterByTypeBottomSheet.copy(isShown = false)) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnUMTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnUMTransformer.kt new file mode 100644 index 0000000000..0cc4c5b3c3 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnUMTransformer.kt @@ -0,0 +1,7 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal interface EarnUMTransformer { + fun transform(prevState: EarnUM): EarnUM +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateBestOpportunitiesStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateBestOpportunitiesStateTransformer.kt new file mode 100644 index 0000000000..d460f89513 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateBestOpportunitiesStateTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class UpdateBestOpportunitiesStateTransformer( + private val newState: EarnBestOpportunitiesUM, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy(bestOpportunities = newState) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt new file mode 100644 index 0000000000..c726dfd6af --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class UpdateEarnUMInitialStateTransformer( + private val onBackClick: () -> Unit, + private val filterByTypeBottomSheetConfig: TangemBottomSheetConfig, + private val filterByNetworkBottomSheetConfig: TangemBottomSheetConfig, + private val onNetworkFilterClick: () -> Unit, + private val onTypeFilterClick: () -> Unit, +) : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy( + onBackClick = onBackClick, + filterByNetworkBottomSheet = filterByNetworkBottomSheetConfig, + filterByTypeBottomSheet = filterByTypeBottomSheetConfig, + onNetworkFilterClick = onNetworkFilterClick, + onTypeFilterClick = onTypeFilterClick, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt new file mode 100644 index 0000000000..9f280044d9 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt @@ -0,0 +1,38 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.domain.models.earn.EarnTopToken +import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter +import com.tangem.features.feed.ui.earn.state.EarnListUM +import com.tangem.features.feed.ui.earn.state.EarnUM +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +internal class UpdateMostlyUsedStateTransformer( + private val earnResult: EarnTopToken?, + private val onItemClick: (EarnTokenWithCurrency) -> Unit, + private val onRetryClick: () -> Unit, +) : EarnUMTransformer { + + private val converter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick) + + override fun transform(prevState: EarnUM): EarnUM { + return when (earnResult) { + null -> prevState.copy(mostlyUsed = EarnListUM.Loading) + else -> earnResult.fold( + ifLeft = { prevState.copy(mostlyUsed = EarnListUM.Error(onRetryClicked = onRetryClick)) }, + ifRight = { list -> + val newItems = list + .sortedWith( + compareByDescending { + it.earnToken.apy.toBigDecimalOrNull() ?: BigDecimal.ZERO + }.thenBy { it.earnToken.tokenName }, + ) + .map(converter::convert) + .toPersistentList() + prevState.copy(mostlyUsed = EarnListUM.Content(items = newItems)) + }, + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt new file mode 100644 index 0000000000..685771c34d --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt @@ -0,0 +1,105 @@ +package com.tangem.features.feed.model.earn.statemanager + +import com.tangem.domain.earn.model.EarnTokensBatchingContext +import com.tangem.domain.earn.model.EarnTokensListConfig +import com.tangem.domain.earn.usecase.GetEarnTokensBatchFlowUseCase +import com.tangem.domain.models.earn.EarnTokenWithCurrency +import com.tangem.features.feed.model.converter.EarnTokenWithCurrencyToListItemUMConverter +import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import com.tangem.pagination.BatchAction +import com.tangem.pagination.PaginationStatus +import com.tangem.utils.Provider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch + +internal class EarnListBatchFlowManager( + getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase, + private val configProvider: Provider, + private val onItemClick: (EarnTokenWithCurrency) -> Unit, + private val modelScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, +) { + private val actionsFlow = MutableSharedFlow>() + private val converter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick) + + private val batchFlow = getEarnTokensBatchFlowUseCase( + context = EarnTokensBatchingContext( + actionsFlow = actionsFlow, + coroutineScope = modelScope, + ), + batchSize = DEFAULT_BATCH_SIZE, + ) + + val uiItems: StateFlow> = + batchFlow.state + .scan(persistentListOf() to -1) { (accItems, lastProcessedBatchIndex), newState -> + if (newState.data.size <= lastProcessedBatchIndex) { + val newItems = newState.data + .flatMap { it.data } + .map(converter::convert) + .toPersistentList() + newItems to newState.data.lastIndex + } else { + val newBatches = newState.data.subList(lastProcessedBatchIndex + 1, newState.data.size) + val newItems = newBatches + .flatMap { it.data } + .map(converter::convert) + accItems.addAll(newItems) to newState.data.lastIndex + } + } + .map { (items, _) -> items } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = persistentListOf(), + ) + + val initialLoadingError: StateFlow = batchFlow.state + .map { state -> + val status = state.status + if (status is PaginationStatus.InitialLoadingError) { + status.throwable + } else { + null + } + } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = null, + ) + + val paginationStatus: StateFlow>> = batchFlow.state + .map { it.status } + .distinctUntilChanged() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = PaginationStatus.InitialLoading, + ) + + fun reload() { + modelScope.launch(dispatchers.default) { + actionsFlow.emit( + BatchAction.Reload(requestParams = configProvider()), + ) + } + } + + fun loadMore() { + modelScope.launch(dispatchers.default) { + actionsFlow.emit(BatchAction.LoadMore()) + } + } + + private companion object { + private const val DEFAULT_BATCH_SIZE = 20 + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListStateManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListStateManager.kt new file mode 100644 index 0000000000..4b53e07f06 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListStateManager.kt @@ -0,0 +1,28 @@ +package com.tangem.features.feed.model.earn.statemanager + +import com.tangem.pagination.PaginationStatus +import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM +import com.tangem.features.feed.ui.earn.state.EarnListItemUM +import kotlinx.collections.immutable.ImmutableList + +@Suppress("LongParameterList") +internal object EarnListStateManager { + + fun calculateState( + items: ImmutableList, + error: Throwable?, + paginationStatus: PaginationStatus<*>, + hasActiveFilters: Boolean, + onRetryClick: () -> Unit, + onLoadMore: () -> Unit, + onClearFiltersClick: () -> Unit, + ): EarnBestOpportunitiesUM = when { + error != null -> EarnBestOpportunitiesUM.Error(onRetryClicked = onRetryClick) + paginationStatus is PaginationStatus.InitialLoading && items.isEmpty() -> + EarnBestOpportunitiesUM.Loading + items.isEmpty() && hasActiveFilters -> + EarnBestOpportunitiesUM.EmptyFiltered(onClearFilterClick = onClearFiltersClick) + items.isEmpty() -> EarnBestOpportunitiesUM.Empty + else -> EarnBestOpportunitiesUM.Content(items = items, onLoadMore = onLoadMore) + } +} \ 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 index bf3daee21c..3d4594e1eb 100644 --- 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 @@ -1,19 +1,10 @@ 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.model.converter.EarnTokenWithCurrencyToListItemUMConverter 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 @@ -24,7 +15,7 @@ internal class UpdateEarnStateTransformer( private val earnResult: EarnTopToken?, ) : FeedListUMTransformer { - private val earnTokenWithCurrencyConverter = EarnTokenWithCurrencyConverter(onItemClick = onItemClick) + private val earnTokenWithCurrencyConverter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick) override fun transform(prevState: FeedListUM): FeedListUM { if (!isEarnEnabled) return handleEmptyState(prevState) @@ -60,52 +51,22 @@ internal class UpdateEarnStateTransformer( currentState: FeedListUM, earnTokensWithCurrency: List, ): FeedListUM { + val newItems = earnTokensWithCurrency + .sortedWith( + compareByDescending { + it.earnToken.apy.toBigDecimalOrNull() ?: BigDecimal.ZERO + }.thenBy { it.earnToken.tokenName }, + ) + .map(earnTokenWithCurrencyConverter::convert) + .toPersistentList() + + val newEarnListUM = when (val earnState = currentState.earnListUM) { + is EarnListUM.Content -> earnState.copy(items = newItems) + else -> EarnListUM.Content(items = newItems) + } + 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(), - ), + earnListUM = newEarnListUM, ) } - - 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/news/list/statemanager/NewsListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt index fa5cff5b5e..6b9886c03f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt @@ -15,6 +15,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -55,13 +56,22 @@ internal open class NewsListBatchFlowManager( initialValue = persistentListOf(), ) - val uiItems: StateFlow> - get() = batchFlow.state - .map { batchListState -> - batchListState.data - .flatMap { batch -> batch.data } - .let { articles -> converter.convert(articles) } + val uiItems: StateFlow> = + batchFlow.state + .scan(persistentListOf() to -1) { (accItems, lastProcessedBatchIndex), newState -> + if (newState.data.size <= lastProcessedBatchIndex) { + val newItems = converter.convert(newState.data.flatMap { it.data }) + .toPersistentList() + + newItems to newState.data.lastIndex + } else { + val newBatches = newState.data.subList(lastProcessedBatchIndex + 1, newState.data.size) + val newShortArticles = newBatches.flatMap { it.data } + val newItems = converter.convert(newShortArticles) + accItems.addAll(newItems) to newState.data.lastIndex + } } + .map { (items, _) -> items } .distinctUntilChanged() .stateIn( scope = modelScope, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 63753d95a1..32f85c07b6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -5,14 +5,13 @@ import androidx.compose.animation.AnimatedContent 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.foundation.lazy.* import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -20,53 +19,49 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.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.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.* +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig 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.components.list.InfiniteListHandler import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.features.feed.ui.earn.components.EarnFilterByNetworkBottomSheet -import com.tangem.features.feed.ui.earn.components.EarnFilterByTypeBottomSheet -import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholder -import com.tangem.features.feed.ui.earn.components.EarnListItem -import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder -import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM -import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM -import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM -import com.tangem.features.feed.ui.earn.state.EarnListItemUM -import com.tangem.features.feed.ui.earn.state.EarnListUM -import com.tangem.features.feed.ui.earn.state.EarnUM +import com.tangem.features.feed.ui.earn.components.* +import com.tangem.features.feed.ui.earn.state.* import kotlinx.collections.immutable.persistentListOf +private const val EARN_LOAD_MORE_BUFFER = 3 + @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() } + val listState = rememberLazyListState() EarnFilterByTypeBottomSheet(config = state.filterByTypeBottomSheet) EarnFilterByNetworkBottomSheet(config = state.filterByNetworkBottomSheet) + if (state.bestOpportunities is EarnBestOpportunitiesUM.Content) { + PaginationHandler( + listState = listState, + state = state.bestOpportunities, + ) + } + LazyColumn( + state = listState, modifier = modifier .fillMaxSize() .background(background), @@ -109,7 +104,10 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { @Composable private fun MostlyUsedContent(state: EarnListUM) { - AnimatedContent(state) { st -> + AnimatedContent( + targetState = state, + contentKey = { it::class.java }, + ) { st -> when (st) { is EarnListUM.Loading -> { MostlyUsedPlaceholder() @@ -174,9 +172,11 @@ private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: verticalAlignment = Alignment.CenterVertically, ) { Text( + modifier = Modifier.weight(weight = 1f, fill = false), text = item.tokenName.resolveReference(), color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.subtitle2, + overflow = TextOverflow.Ellipsis, maxLines = 1, ) SpacerW(4.dp) @@ -207,34 +207,15 @@ private fun BestOpportunitiesFilters( onNetworkFilterClick: () -> Unit, onTypeFilterClick: () -> Unit, ) { - AnimatedContent(state) { st -> - when (st) { - is EarnBestOpportunitiesUM.Loading -> { - FilterButtonsShimmer() - } - is EarnBestOpportunitiesUM.EmptyFiltered, - is EarnBestOpportunitiesUM.Content, - -> { - FilterButtons( - selectedNetworkFilterText = selectedNetworkFilterText, - selectedTypeFilterText = selectedTypeFilterText, - isEnabled = true, - onNetworkFilterClick = onNetworkFilterClick, - onTypeFilterClick = onTypeFilterClick, - ) - } - is EarnBestOpportunitiesUM.Empty, - is EarnBestOpportunitiesUM.Error, - -> { - FilterButtons( - selectedNetworkFilterText = selectedNetworkFilterText, - selectedTypeFilterText = selectedTypeFilterText, - isEnabled = false, - onNetworkFilterClick = onNetworkFilterClick, - onTypeFilterClick = onTypeFilterClick, - ) - } - } + when (state) { + is EarnBestOpportunitiesUM.Loading -> FilterButtonsShimmer() + else -> FilterButtons( + selectedNetworkFilterText = selectedNetworkFilterText, + selectedTypeFilterText = selectedTypeFilterText, + isEnabled = state is EarnBestOpportunitiesUM.Content || state is EarnBestOpportunitiesUM.EmptyFiltered, + onNetworkFilterClick = onNetworkFilterClick, + onTypeFilterClick = onTypeFilterClick, + ) } } @@ -424,13 +405,28 @@ private fun SectionHeader(title: String, modifier: Modifier = Modifier) { Text( modifier = modifier .fillMaxWidth() - .padding(horizontal = 16.dp), + .padding(start = 20.dp, end = 16.dp), text = title, style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, ) } +@Composable +private fun PaginationHandler(listState: LazyListState, state: EarnBestOpportunitiesUM.Content) { + InfiniteListHandler( + listState = listState, + buffer = EARN_LOAD_MORE_BUFFER, + triggerLoadMoreCheckOnItemsCountChange = true, + onLoadMore = remember(state) { + { + state.onLoadMore() + true + } + }, + ) +} + @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -465,6 +461,7 @@ private fun EarnContentPreview() { network = "Ethereum Network", ), ), + onLoadMore = {}, ), ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt index 6b31102b92..a04546676b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/EarnFilterByNetworkBottomSheet.kt @@ -1,6 +1,7 @@ package com.tangem.features.feed.ui.earn.components import android.content.res.Configuration +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -13,14 +14,15 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.currency.icon.CoinIcon import com.tangem.core.ui.components.inputrow.InputRowChecked import com.tangem.core.ui.components.inputrow.inner.DividerContainer import com.tangem.core.ui.components.rows.RowContentContainer @@ -165,12 +167,10 @@ private fun LazyListScope.specificNetworksList( .heightIn(52.dp) .padding(horizontal = 12.dp), icon = { - CoinIcon( + Image( modifier = Modifier.size(22.dp), - url = item.iconUrl, - alpha = 1f, - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, + imageVector = ImageVector.vectorResource(item.iconRes), + contentDescription = item.symbol.resolveReference(), ) }, text = { @@ -225,14 +225,14 @@ private fun Preview() { id = "ethereum", text = stringReference("Ethereum"), symbol = stringReference("ETH"), - iconUrl = null, + iconRes = R.drawable.img_btc_22, isSelected = false, ), EarnFilterNetworkUM.Network( id = "polygon", text = stringReference("Polygon"), symbol = stringReference("MATIC"), - iconUrl = null, + iconRes = R.drawable.img_btc_22, isSelected = false, ), ), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt index 1fd414edf4..bb22ced386 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterNetworkUM.kt @@ -23,7 +23,7 @@ internal sealed class EarnFilterNetworkUM { val id: String, override val text: TextReference, val symbol: TextReference, - val iconUrl: String?, + val iconRes: Int, override val isSelected: Boolean, ) : EarnFilterNetworkUM() } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt index 358cab5ee9..c4506586bc 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt @@ -17,7 +17,10 @@ internal sealed interface EarnBestOpportunitiesUM { data object Loading : EarnBestOpportunitiesUM data object Empty : EarnBestOpportunitiesUM data class EmptyFiltered(val onClearFilterClick: () -> Unit) : EarnBestOpportunitiesUM - data class Content(val items: ImmutableList) : EarnBestOpportunitiesUM + data class Content( + val items: ImmutableList, + val onLoadMore: () -> Unit, + ) : EarnBestOpportunitiesUM data class Error(val onRetryClicked: () -> Unit) : EarnBestOpportunitiesUM } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt index 129ef7438d..fb70686941 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt @@ -45,10 +45,13 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modif modifier = Modifier.padding(horizontal = 16.dp), colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), ) { - AnimatedContent(targetState = earnListUM) { earnListState -> - when (earnListState) { - is EarnListUM.Content -> EarnContentBlock(items = earnListState.items) - is EarnListUM.Error -> EarnErrorBlock(onRetryClick = earnListState.onRetryClicked) + AnimatedContent( + targetState = earnListUM, + contentKey = { it::class.java }, + ) { earnListUM -> + when (earnListUM) { + is EarnListUM.Content -> EarnContentBlock(items = earnListUM.items) + is EarnListUM.Error -> EarnErrorBlock(onRetryClick = earnListUM.onRetryClicked) EarnListUM.Loading -> EarnListPlaceholder(placeholderCount = PLACEHOLDER_ITEM_COUNT) } } diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt index 3dd7bc3947..70f0fe63d0 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/component/DefaultTokenReceiveComponent.kt @@ -52,9 +52,7 @@ internal class DefaultTokenReceiveComponent @AssistedInject constructor( ) } - override fun dismiss() { - model.params.onDismiss() - } + override fun dismiss() = model.params.onDismiss() private fun onChildBack() { when (contentStack.value.active.configuration) {