Updated on 2026-08-14
This commit is contained in:
parent
8adc179449
commit
d5f9410905
35 changed files with 892 additions and 324 deletions
|
|
@ -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<String>? = null,
|
||||
): ApiResponse<EarnListResponse>
|
||||
|
||||
@GET("v1/earn/networks")
|
||||
suspend fun getEarnNetworks(@Query("type") type: String): ApiResponse<EarnNetworkListResponse>
|
||||
suspend fun getEarnNetworks(@Query("type") type: String? = null): ApiResponse<EarnNetworkListResponse>
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<String>?,
|
||||
val isForEarn: Boolean,
|
||||
)
|
||||
|
|
@ -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<EarnNetworks?>
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ class FetchEarnNetworksUseCase(
|
|||
private val repository: EarnRepository,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(type: String): Either<Throwable, Unit> {
|
||||
suspend operator fun invoke(): Either<Throwable, Unit> {
|
||||
return Either.catch {
|
||||
repository.fetchEarnNetworks(type = type)
|
||||
repository.fetchEarnNetworks()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ internal class FeedEntryChildFactory @Inject constructor(
|
|||
DefaultEarnComponent(
|
||||
appComponentContext = appComponentContext,
|
||||
params = child.params,
|
||||
addToPortfolioComponentFactory = addToPortfolioPreselectedDataComponent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<EarnModel, Params>(params = params)
|
||||
|
||||
private val bottomSheetSlot = childSlot(
|
||||
source = earnModel.bottomSheetNavigation,
|
||||
serializer = FeedPortfolioRoute.serializer(),
|
||||
handleBackButton = false,
|
||||
childFactory = ::bottomSheetChild,
|
||||
)
|
||||
|
||||
@Composable
|
||||
override fun Title(bottomSheetState: State<BottomSheetState>) {
|
||||
val background = LocalMainBottomSheetColor.current.value
|
||||
|
|
@ -42,11 +57,29 @@ internal class DefaultEarnComponent(
|
|||
|
||||
@Composable
|
||||
override fun Content(bottomSheetState: State<BottomSheetState>, 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
|
||||
|
|
|
|||
|
|
@ -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<EarnTokenWithCurrency, EarnListItemUM> {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DefaultEarnComponent.Params>()
|
||||
private val earnNetworks = MutableStateFlow<EarnNetworks>(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<EarnFilterNetworkUM> = 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<FeedPortfolioRoute> = 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<EarnUM>
|
||||
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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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<EarnUM> = MutableStateFlow(value = getInitialState())
|
||||
|
||||
val uiState: StateFlow<EarnUM> 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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<EarnNetwork, EarnFilterNetworkUM.Network> {
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<EarnTokenWithCurrency> {
|
||||
it.earnToken.apy.toBigDecimalOrNull() ?: BigDecimal.ZERO
|
||||
}.thenBy { it.earnToken.tokenName },
|
||||
)
|
||||
.map(converter::convert)
|
||||
.toPersistentList()
|
||||
prevState.copy(mostlyUsed = EarnListUM.Content(items = newItems))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<EarnTokensListConfig>,
|
||||
private val onItemClick: (EarnTokenWithCurrency) -> Unit,
|
||||
private val modelScope: CoroutineScope,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
private val actionsFlow = MutableSharedFlow<BatchAction<Int, EarnTokensListConfig, Nothing>>()
|
||||
private val converter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick)
|
||||
|
||||
private val batchFlow = getEarnTokensBatchFlowUseCase(
|
||||
context = EarnTokensBatchingContext(
|
||||
actionsFlow = actionsFlow,
|
||||
coroutineScope = modelScope,
|
||||
),
|
||||
batchSize = DEFAULT_BATCH_SIZE,
|
||||
)
|
||||
|
||||
val uiItems: StateFlow<ImmutableList<EarnListItemUM>> =
|
||||
batchFlow.state
|
||||
.scan(persistentListOf<EarnListItemUM>() 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<Throwable?> = 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<PaginationStatus<List<EarnTokenWithCurrency>>> = 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<EarnListItemUM>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<EarnTokenWithCurrency>,
|
||||
): FeedListUM {
|
||||
val newItems = earnTokensWithCurrency
|
||||
.sortedWith(
|
||||
compareByDescending<EarnTokenWithCurrency> {
|
||||
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<EarnTokenWithCurrency> {
|
||||
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<EarnTokenWithCurrency, EarnListItemUM> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<ImmutableList<ArticleConfigUM>>
|
||||
get() = batchFlow.state
|
||||
.map { batchListState ->
|
||||
batchListState.data
|
||||
.flatMap { batch -> batch.data }
|
||||
.let { articles -> converter.convert(articles) }
|
||||
val uiItems: StateFlow<ImmutableList<ArticleConfigUM>> =
|
||||
batchFlow.state
|
||||
.scan(persistentListOf<ArticleConfigUM>() 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,
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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<EarnListItemUM>) : EarnBestOpportunitiesUM
|
||||
data class Content(
|
||||
val items: ImmutableList<EarnListItemUM>,
|
||||
val onLoadMore: () -> Unit,
|
||||
) : EarnBestOpportunitiesUM
|
||||
data class Error(val onRetryClicked: () -> Unit) : EarnBestOpportunitiesUM
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue