Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-12 09:37:22 +01:00
parent d66e528975
commit ac7d3f1607
18 changed files with 301 additions and 26 deletions

View file

@ -16,8 +16,11 @@ interface AddToPortfolioPreselectedDataComponent : ComposableBottomSheetComponen
data class Params(
val tokenToAdd: TokenToAdd,
val callback: Callback,
val analyticsParams: AnalyticsParams,
)
data class AnalyticsParams(val source: String)
interface Callback {
fun onDismiss()
fun onSuccess(addedToken: CryptoCurrency, walletId: UserWalletId)

View file

@ -77,6 +77,7 @@ internal class DefaultEarnComponent(
params = AddToPortfolioPreselectedDataComponent.Params(
tokenToAdd = config.tokenToAdd,
callback = earnModel.addToPortfolioCallback,
analyticsParams = AddToPortfolioPreselectedDataComponent.AnalyticsParams(config.source),
),
)
}

View file

@ -9,6 +9,7 @@ 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.analytics.models.AnalyticsParam
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
@ -75,6 +76,9 @@ internal class DefaultFeedComponent(
params = Params(
tokenToAdd = config.tokenToAdd,
callback = feedComponentModel.addToPortfolioCallback,
analyticsParams = AddToPortfolioPreselectedDataComponent.AnalyticsParams(
AnalyticsParam.ScreensSources.Markets.value,
),
),
)
}

View file

@ -6,7 +6,10 @@ import com.tangem.features.feed.components.market.details.portfolio.add.AddToPor
internal sealed interface FeedBottomSheetRoute {
data class AddToPortfolio(val tokenToAdd: AddToPortfolioPreselectedDataComponent.TokenToAdd) : FeedBottomSheetRoute
data class AddToPortfolio(
val tokenToAdd: AddToPortfolioPreselectedDataComponent.TokenToAdd,
val source: String,
) : FeedBottomSheetRoute
data class NetworkFilter(val params: EarnNetworkFilterComponent.Params) : FeedBottomSheetRoute

View file

@ -25,6 +25,7 @@ import com.tangem.features.feed.components.market.details.portfolio.add.*
import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent
import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
@ -125,9 +126,11 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
return@channelFlow
}
sendAddTokenOpenedAnalytics(selectedNetworkValue.cryptoCurrency)
navigation.replaceAll(AddToPortfolioRoutes.AddToken)
val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first()
messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added)))
sendSuccessAddedAnalytics(addedToken.currency)
finishSuccessFlow(addedToken.currency, selectedPortfolioValue.userWallet.walletId)
}
.catch { throwable ->
@ -267,6 +270,25 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor(
},
)
.filterNotNull()
private fun sendSuccessAddedAnalytics(cryptoCurrency: CryptoCurrency) {
analyticsEventHandler.send(
EarnAnalyticsEvent.TokenAdded(
tokenSymbol = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
),
)
}
private fun sendAddTokenOpenedAnalytics(cryptoCurrency: CryptoCurrency) {
analyticsEventHandler.send(
EarnAnalyticsEvent.AddTokenScreenOpened(
tokenSymbol = cryptoCurrency.symbol,
blockchain = cryptoCurrency.network.name,
source = params.analyticsParams.source,
),
)
}
}
@ModelScoped

View file

@ -7,9 +7,11 @@ import com.arkivanov.decompose.router.slot.activate
import com.arkivanov.decompose.router.slot.dismiss
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.domain.earn.model.EarnFilter
import com.tangem.domain.earn.model.EarnFilterNetwork
import com.tangem.domain.earn.model.EarnFilterType
@ -24,6 +26,7 @@ import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent
import com.tangem.features.feed.components.earn.EarnTypeFilterComponent
import com.tangem.features.feed.components.feed.FeedBottomSheetRoute
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent
import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkConverter
import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConverter
import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter
@ -59,6 +62,7 @@ internal class EarnModel @Inject constructor(
private val setEarnFilterUseCase: SetEarnFilterUseCase,
private val appRouter: AppRouter,
private val stateController: EarnStateController,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private val params = paramsContainer.require<DefaultEarnComponent.Params>()
@ -115,6 +119,7 @@ internal class EarnModel @Inject constructor(
) { items, error, paginationStatus ->
val hasActiveFilters = state.value.selectedTypeFilter != EarnFilterTypeUM.All ||
state.value.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks
error?.let(::handleBestOpportunitiesErrorAnalytics)
EarnListStateManager.calculateState(
items = items,
error = error,
@ -242,7 +247,14 @@ internal class EarnModel @Inject constructor(
}
}
private fun onEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency) {
private fun onEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency, source: String) {
analyticsEventHandler.send(
EarnAnalyticsEvent.OpportunitySelected(
tokenSymbol = earnTokenWithCurrency.earnToken.tokenSymbol,
blockchain = earnTokenWithCurrency.cryptoCurrency.network.name,
source = source,
),
)
bottomSheetNavigation.activate(
FeedBottomSheetRoute.AddToPortfolio(
tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd(
@ -256,6 +268,7 @@ internal class EarnModel @Inject constructor(
name = earnTokenWithCurrency.earnToken.tokenName,
symbol = earnTokenWithCurrency.earnToken.tokenSymbol,
),
source = source,
),
)
}
@ -283,14 +296,33 @@ internal class EarnModel @Inject constructor(
}
bottomSheetNavigation.dismiss()
}
private fun onMostlyUsedScrolled() {
analyticsEventHandler.send(EarnAnalyticsEvent.MostlyUsedCarouselScrolled())
}
/* end of clicks area */
private fun updateInitialState() {
analyticsEventHandler.send(EarnAnalyticsEvent.EarnOpened())
stateController.update(
UpdateEarnUMInitialStateTransformer(
onBackClick = params.onBackClick,
onNetworkFilterClick = ::onNetworkFilterClick,
onTypeFilterClick = ::onTypeFilterClick,
onScroll = ::onMostlyUsedScrolled,
),
)
}
private fun handleBestOpportunitiesErrorAnalytics(error: Throwable) {
val (code, message) = when (error) {
is ApiResponseError.HttpException -> error.code.numericCode to error.message.orEmpty()
else -> null to ""
}
analyticsEventHandler.send(
EarnAnalyticsEvent.BestOpportunitiesLoadError(
code = code,
message = message,
),
)
}

View file

@ -0,0 +1,102 @@
package com.tangem.features.feed.model.earn.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_CODE
import com.tangem.core.analytics.models.AnalyticsParam.Key.ERROR_MESSAGE
import com.tangem.core.analytics.models.IS_NOT_HTTP_ERROR
import com.tangem.core.analytics.models.OneTimePerSessionEvent
internal sealed class EarnAnalyticsEvent(
event: String,
params: Map<String, String> = emptyMap(),
) : AnalyticsEvent(category = "Earn", event = event, params = params) {
class EarnOpened : EarnAnalyticsEvent(event = "Page Opened")
class MostlyUsedCarouselScrolled : EarnAnalyticsEvent(event = "Mostly Used Carousel Scrolled"),
OneTimePerSessionEvent {
override val oneTimeEventId: String = event
}
data class BestOpportunitiesFilterNetworkApplied(
private val networkId: String?,
private val filterType: FilterNetworkAnalytic,
) : EarnAnalyticsEvent(
event = "Best Opportunities Filter Network Applied",
params = mapOf(
"Network Filter Type" to filterType.value,
"NetworkId" to networkId.orEmpty(),
),
)
data class BestOpportunitiesFilterTypeApplied(
private val filterTypeAnalytic: FilterTypeAnalytic,
) : EarnAnalyticsEvent(
event = "Best Opportunities Filter Type Applied",
params = mapOf("Type" to filterTypeAnalytic.value),
)
data class OpportunitySelected(
private val tokenSymbol: String,
private val blockchain: String,
private val source: String,
) : EarnAnalyticsEvent(
event = "Opportunity selected",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to tokenSymbol,
AnalyticsParam.BLOCKCHAIN to blockchain,
AnalyticsParam.SOURCE to source,
),
)
data class AddTokenScreenOpened(
private val tokenSymbol: String,
private val blockchain: String,
private val source: String,
) : EarnAnalyticsEvent(
event = "Add Token Screen Opened",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to tokenSymbol,
AnalyticsParam.BLOCKCHAIN to blockchain,
AnalyticsParam.SOURCE to source,
),
)
data class TokenAdded(
private val tokenSymbol: String,
private val blockchain: String,
) : EarnAnalyticsEvent(
event = "Token Added",
params = mapOf(
AnalyticsParam.TOKEN_PARAM to tokenSymbol,
AnalyticsParam.BLOCKCHAIN to blockchain,
),
)
data class BestOpportunitiesLoadError(
private val code: Int?,
private val message: String,
) : EarnAnalyticsEvent(
event = "Best Opportunities Load Error",
params = mapOf(
ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(),
ERROR_MESSAGE to message,
),
)
}
internal const val BEST_OPPORTUNITIES_SOURCE = "Best Opportunity"
internal const val MOSTLY_USED_SOURCE = "Mostly Used"
internal enum class FilterNetworkAnalytic(val value: String) {
ALL_NETWORKS("All Networks"),
MY_NETWORKS("My Networks"),
SPECIFIC("Specific"),
}
internal enum class FilterTypeAnalytic(val value: String) {
YIELD("Yield"),
STAKING("Staking"),
ALL_TYPES("All Types"),
}

View file

@ -1,13 +1,17 @@
package com.tangem.features.feed.model.earn.filters
import androidx.compose.runtime.Stable
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent
import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConverter
import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent
import com.tangem.features.feed.model.earn.analytics.FilterNetworkAnalytic
import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkConverter
import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConverter
import com.tangem.features.feed.ui.earn.state.EarnFilterByNetworkBottomSheetContentUM
import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.flow.MutableStateFlow
@ -18,6 +22,7 @@ import javax.inject.Inject
@ModelScoped
internal class EarnNetworkFilterModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
paramsContainer: ParamsContainer,
) : Model() {
@ -31,7 +36,22 @@ internal class EarnNetworkFilterModel @Inject constructor(
networks = params.allFilters
.map { EarnFilterNetworkConverter().convert(it) }
.toPersistentList(),
onOptionClick = { filterUM -> params.onFilterSelected(EarnFilterNetworkUMConverter().convert(filterUM)) },
onOptionClick = ::handleOnOptionClick,
)
}
private fun handleOnOptionClick(filter: EarnFilterNetworkUM) {
params.onFilterSelected(EarnFilterNetworkUMConverter().convert(filter))
val (networkId, filterType) = when (filter) {
is EarnFilterNetworkUM.AllNetworks -> "" to FilterNetworkAnalytic.ALL_NETWORKS
is EarnFilterNetworkUM.MyNetworks -> "" to FilterNetworkAnalytic.MY_NETWORKS
is EarnFilterNetworkUM.Network -> filter.id to FilterNetworkAnalytic.SPECIFIC
}
analyticsEventHandler.send(
EarnAnalyticsEvent.BestOpportunitiesFilterNetworkApplied(
networkId = networkId,
filterType = filterType,
),
)
}
}

View file

@ -1,13 +1,17 @@
package com.tangem.features.feed.model.earn.filters
import androidx.compose.runtime.Stable
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.feed.components.earn.EarnTypeFilterComponent
import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent
import com.tangem.features.feed.model.earn.analytics.FilterTypeAnalytic
import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter
import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeUMConverter
import com.tangem.features.feed.ui.earn.state.EarnFilterByTypeBottomSheetContentUM
import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -17,6 +21,7 @@ import javax.inject.Inject
@ModelScoped
internal class EarnTypeFilterModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
paramsContainer: ParamsContainer,
) : Model() {
@ -28,9 +33,20 @@ internal class EarnTypeFilterModel @Inject constructor(
private fun configureInitialState(): EarnFilterByTypeBottomSheetContentUM {
return EarnFilterByTypeBottomSheetContentUM(
selectedOption = EarnFilterTypeConverter().convert(params.selectedFilter),
onOptionClick = { filterTypeUM ->
params.onFilterSelected(EarnFilterTypeUMConverter().convert(filterTypeUM))
},
onOptionClick = ::handleOnOptionClick,
)
}
private fun handleOnOptionClick(filter: EarnFilterTypeUM) {
params.onFilterSelected(EarnFilterTypeUMConverter().convert(filter))
analyticsEventHandler.send(
EarnAnalyticsEvent.BestOpportunitiesFilterTypeApplied(
filterTypeAnalytic = when (filter) {
EarnFilterTypeUM.All -> FilterTypeAnalytic.ALL_TYPES
EarnFilterTypeUM.Staking -> FilterTypeAnalytic.STAKING
EarnFilterTypeUM.YieldMode -> FilterTypeAnalytic.YIELD
},
),
)
}
}

View file

@ -18,10 +18,6 @@ internal class EarnStateController @Inject constructor() {
val value: EarnUM get() = uiState.value
fun update(function: (EarnUM) -> EarnUM) {
mutableUiState.update(function = function)
}
fun update(transformer: EarnUMTransformer) {
mutableUiState.update(function = transformer::transform)
}
@ -35,6 +31,7 @@ internal class EarnStateController @Inject constructor() {
onBackClick = {},
onNetworkFilterClick = {},
onTypeFilterClick = {},
onSliderScroll = {},
)
}
}

View file

@ -6,6 +6,7 @@ internal class UpdateEarnUMInitialStateTransformer(
private val onBackClick: () -> Unit,
private val onNetworkFilterClick: () -> Unit,
private val onTypeFilterClick: () -> Unit,
private val onScroll: () -> Unit,
) : EarnUMTransformer {
override fun transform(prevState: EarnUM): EarnUM {
@ -13,6 +14,7 @@ internal class UpdateEarnUMInitialStateTransformer(
onBackClick = onBackClick,
onNetworkFilterClick = onNetworkFilterClick,
onTypeFilterClick = onTypeFilterClick,
onSliderScroll = onScroll,
)
}
}

View file

@ -3,6 +3,7 @@ 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.model.earn.analytics.MOSTLY_USED_SOURCE
import com.tangem.features.feed.ui.earn.state.EarnListUM
import com.tangem.features.feed.ui.earn.state.EarnUM
import kotlinx.collections.immutable.toPersistentList
@ -10,11 +11,13 @@ import java.math.BigDecimal
internal class UpdateMostlyUsedStateTransformer(
private val earnResult: EarnTopToken?,
private val onItemClick: (EarnTokenWithCurrency) -> Unit,
private val onItemClick: (EarnTokenWithCurrency, source: String) -> Unit,
private val onRetryClick: () -> Unit,
) : EarnUMTransformer {
private val converter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick)
private val converter = EarnTokenWithCurrencyToListItemUMConverter(
onItemClick = { token -> onItemClick(token, MOSTLY_USED_SOURCE) },
)
override fun transform(prevState: EarnUM): EarnUM {
return when (earnResult) {

View file

@ -5,6 +5,7 @@ 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.model.earn.analytics.BEST_OPPORTUNITIES_SOURCE
import com.tangem.features.feed.ui.earn.state.EarnListItemUM
import com.tangem.pagination.BatchAction
import com.tangem.pagination.PaginationStatus
@ -20,12 +21,14 @@ import kotlinx.coroutines.launch
internal class EarnListBatchFlowManager(
getEarnTokensBatchFlowUseCase: GetEarnTokensBatchFlowUseCase,
private val configProvider: Provider<EarnTokensListConfig>,
private val onItemClick: (EarnTokenWithCurrency) -> Unit,
private val onItemClick: (EarnTokenWithCurrency, source: String) -> 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 converter = EarnTokenWithCurrencyToListItemUMConverter(
onItemClick = { onItemClick(it, BEST_OPPORTUNITIES_SOURCE) },
)
private val batchFlow = getEarnTokensBatchFlowUseCase(
context = EarnTokensBatchingContext(

View file

@ -33,6 +33,7 @@ import com.tangem.features.feed.components.feed.FeedBottomSheetRoute
import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent
import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle
import com.tangem.features.feed.impl.R
import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent
import com.tangem.features.feed.model.feed.analytics.FeedAnalyticsEvent
import com.tangem.features.feed.model.feed.state.FeedMarketsBatchFlowManager
import com.tangem.features.feed.model.feed.state.FeedStateController
@ -172,6 +173,7 @@ internal class FeedComponentModel @Inject constructor(
onItemClick = ::handleEarnTokenClick,
onRetryClick = ::fetchEarnData,
earnResult = earnResult,
analyticsEventHandler = analyticsEventHandler,
),
)
}
@ -249,7 +251,7 @@ internal class FeedComponentModel @Inject constructor(
onSliderEndReached = {
analyticsEventHandler.send(FeedAnalyticsEvent.NewsCarouselEndReached())
},
onOpenEarnPageClick = params.feedClickIntents::onOpenEarnPage,
onOpenEarnPageClick = ::handleEarnPageOpenClicked,
),
news = NewsUM(
content = persistentListOf(),
@ -294,7 +296,6 @@ internal class FeedComponentModel @Inject constructor(
onMarketOpenClick = ::handleMarketOpenClicked,
onArticleClick = ::handleArticleClicked,
onOpenAllNews = ::handleOpenAllNews,
onOpenEarnPageClick = params.feedClickIntents::onOpenEarnPage,
),
)
}
@ -397,6 +398,13 @@ internal class FeedComponentModel @Inject constructor(
}
private fun handleEarnTokenClick(earnTokenWithCurrency: EarnTokenWithCurrency) {
analyticsEventHandler.send(
EarnAnalyticsEvent.OpportunitySelected(
tokenSymbol = earnTokenWithCurrency.earnToken.tokenSymbol,
blockchain = earnTokenWithCurrency.cryptoCurrency.network.name,
source = AnalyticsParam.ScreensSources.Markets.value,
),
)
bottomSheetNavigation.activate(
FeedBottomSheetRoute.AddToPortfolio(
tokenToAdd = AddToPortfolioPreselectedDataComponent.TokenToAdd(
@ -410,9 +418,17 @@ internal class FeedComponentModel @Inject constructor(
name = earnTokenWithCurrency.earnToken.tokenName,
symbol = earnTokenWithCurrency.earnToken.tokenSymbol,
),
source = AnalyticsParam.ScreensSources.Markets.value,
),
)
}
private fun handleEarnPageOpenClicked() {
if (state.value.earnListUM is EarnListUM.Content) {
params.feedClickIntents.onOpenEarnPage()
analyticsEventHandler.send(FeedAnalyticsEvent.EarnScreenOpened())
}
}
/* end of clicks area */
/* start of utils area */

View file

@ -83,4 +83,20 @@ internal sealed class FeedAnalyticsEvent(
class AllWidgetsLoadError : FeedAnalyticsEvent(event = "All Widgets Load Error")
class TokenSearchedClicked : FeedAnalyticsEvent(event = "Token Searched Clicked")
class EarnScreenOpened : FeedAnalyticsEvent(
event = "Earn Screen Opened",
params = mapOf(SOURCE to AnalyticsParam.ScreensSources.Markets.value),
)
data class EarnLoadError(
private val code: Int?,
private val message: String,
) : FeedAnalyticsEvent(
event = "Earn Load Error",
params = mapOf(
ERROR_CODE to (code ?: IS_NOT_HTTP_ERROR).toString(),
ERROR_MESSAGE to message,
),
)
}

View file

@ -1,8 +1,11 @@
package com.tangem.features.feed.model.feed.state.transformers
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.models.earn.EarnError
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.model.feed.analytics.FeedAnalyticsEvent
import com.tangem.features.feed.ui.earn.state.EarnListUM
import com.tangem.features.feed.ui.feed.state.FeedListUM
import kotlinx.collections.immutable.toPersistentList
@ -13,6 +16,7 @@ internal class UpdateEarnStateTransformer(
private val onItemClick: (EarnTokenWithCurrency) -> Unit,
private val onRetryClick: () -> Unit,
private val earnResult: EarnTopToken?,
private val analyticsEventHandler: AnalyticsEventHandler,
) : FeedListUMTransformer {
private val earnTokenWithCurrencyConverter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick)
@ -25,8 +29,11 @@ internal class UpdateEarnStateTransformer(
}
else -> {
earnResult.fold(
ifLeft = {
handleErrorState(prevState)
ifLeft = { earnError ->
handleErrorState(
currentState = prevState,
result = earnError,
)
},
ifRight = { earnTokensWithCurrency ->
handleDataState(
@ -43,7 +50,18 @@ internal class UpdateEarnStateTransformer(
return currentState.copy(earnListUM = null)
}
private fun handleErrorState(currentState: FeedListUM): FeedListUM {
private fun handleErrorState(currentState: FeedListUM, result: EarnError): FeedListUM {
if (currentState.earnListUM != EarnListUM.Loading) return currentState
val (code, message) = when (result) {
is EarnError.HttpError -> result.code to result.message
is EarnError.NotHttpError -> null to ""
}
analyticsEventHandler.send(
FeedAnalyticsEvent.EarnLoadError(
code = code,
message = message,
),
)
return currentState.copy(earnListUM = EarnListUM.Error(onRetryClick))
}

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onFirstVisible
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
@ -73,7 +74,10 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
}
item(key = "mostly_used_content") {
MostlyUsedContent(state = state.mostlyUsed)
MostlyUsedContent(
state = state.mostlyUsed,
onScroll = state.onSliderScroll,
)
}
item(key = "best_opportunities_header") {
@ -105,7 +109,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) {
}
@Composable
private fun MostlyUsedContent(state: EarnListUM) {
private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) {
AnimatedContent(
targetState = state,
contentKey = { it::class.java },
@ -122,11 +126,21 @@ private fun MostlyUsedContent(state: EarnListUM) {
),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
items(
itemsIndexed(
items = st.items,
key = { "${it.tokenName}-${it.network}" },
) { item ->
key = { _, item -> "${item.tokenName}-${item.network}" },
) { index, item ->
val cardModifier = Modifier.conditional(
condition = index == FOURTH_ITEM_INDEX,
modifier = {
onFirstVisible(
minFractionVisible = 0.5f,
callback = onScroll,
)
},
)
MostlyUsedCard(
modifier = cardModifier,
item = item,
onClick = item.onItemClick,
)
@ -579,6 +593,8 @@ private fun previewEarnUM(
onBackClick = {},
onNetworkFilterClick = {},
onTypeFilterClick = {},
onSliderScroll = {},
)
private const val PLACEHOLDER_ITEMS_COUNT = 8
private const val PLACEHOLDER_ITEMS_COUNT = 8
private const val FOURTH_ITEM_INDEX = 3

View file

@ -12,6 +12,7 @@ internal data class EarnUM(
val onBackClick: () -> Unit,
val onNetworkFilterClick: () -> Unit,
val onTypeFilterClick: () -> Unit,
val onSliderScroll: () -> Unit,
) {
val selectedTypeFilterText: TextReference