From ac7d3f1607ad920425c5b3b4fa0ee2677c147928 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 09:37:22 +0100 Subject: [PATCH] Updated on 2026-08-14 --- .../AddToPortfolioPreselectedDataComponent.kt | 3 + .../components/earn/DefaultEarnComponent.kt | 1 + .../components/feed/DefaultFeedComponent.kt | 4 + .../components/feed/FeedBottomSheetRoute.kt | 5 +- .../AddToPortfolioPreselectedDataModel.kt | 22 ++++ .../features/feed/model/earn/EarnModel.kt | 34 +++++- .../earn/analytics/EarnAnalyticsEvent.kt | 102 ++++++++++++++++++ .../earn/filters/EarnNetworkFilterModel.kt | 24 ++++- .../model/earn/filters/EarnTypeFilterModel.kt | 22 +++- .../model/earn/state/EarnStateController.kt | 5 +- .../UpdateEarnUMInitialStateTransformer.kt | 2 + .../UpdateMostlyUsedStateTransformer.kt | 7 +- .../statemanager/EarnListBatchFlowManager.kt | 7 +- .../feed/model/feed/FeedComponentModel.kt | 20 +++- .../feed/analytics/FeedAnalyticsEvent.kt | 16 +++ .../UpdateEarnStateTransformer.kt | 24 ++++- .../features/feed/ui/earn/EarnContent.kt | 28 +++-- .../features/feed/ui/earn/state/EarnUM.kt | 1 + 18 files changed, 301 insertions(+), 26 deletions(-) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt index f901b35f54..c7feac3a8c 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt @@ -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) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt index 679bc440fa..a0b7dcb1e8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt @@ -77,6 +77,7 @@ internal class DefaultEarnComponent( params = AddToPortfolioPreselectedDataComponent.Params( tokenToAdd = config.tokenToAdd, callback = earnModel.addToPortfolioCallback, + analyticsParams = AddToPortfolioPreselectedDataComponent.AnalyticsParams(config.source), ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index fba9eedfbe..7fbaa5c4fe 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -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, + ), ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt index 6a2ad72b81..fea9189d03 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt @@ -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 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt index 8beb2880d3..79ca66cf2b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt @@ -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 diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index a26dd26493..bb91b6a704 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -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() @@ -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, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt new file mode 100644 index 0000000000..c608bc31c7 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt @@ -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 = 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"), +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt index 47f92e70d1..793c286f54 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnNetworkFilterModel.kt @@ -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, + ), ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt index 01e2454dad..11414b134f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/filters/EarnTypeFilterModel.kt @@ -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 + }, + ), ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt index 4af99089f3..6cec5cd393 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt @@ -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 = {}, ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt index f60b57a8e0..d46ac02956 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateEarnUMInitialStateTransformer.kt @@ -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, ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt index 9f280044d9..a3a8317268 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateTransformer.kt @@ -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) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt index 685771c34d..c241d58af4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/statemanager/EarnListBatchFlowManager.kt @@ -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, - private val onItemClick: (EarnTokenWithCurrency) -> Unit, + private val onItemClick: (EarnTokenWithCurrency, source: String) -> Unit, private val modelScope: CoroutineScope, private val dispatchers: CoroutineDispatcherProvider, ) { private val actionsFlow = MutableSharedFlow>() - private val converter = EarnTokenWithCurrencyToListItemUMConverter(onItemClick = onItemClick) + private val converter = EarnTokenWithCurrencyToListItemUMConverter( + onItemClick = { onItemClick(it, BEST_OPPORTUNITIES_SOURCE) }, + ) private val batchFlow = getEarnTokensBatchFlowUseCase( context = EarnTokensBatchingContext( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index be776002e3..fde61b8825 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -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 */ diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt index e40a8503c2..b6279ab0cb 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/analytics/FeedAnalyticsEvent.kt @@ -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, + ), + ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt index 3d4594e1eb..fd81cc6428 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt @@ -1,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)) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 86139c45f7..0b7845919d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -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 \ No newline at end of file +private const val PLACEHOLDER_ITEMS_COUNT = 8 +private const val FOURTH_ITEM_INDEX = 3 \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt index b0686ee0e8..a11f66f48e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt @@ -12,6 +12,7 @@ internal data class EarnUM( val onBackClick: () -> Unit, val onNetworkFilterClick: () -> Unit, val onTypeFilterClick: () -> Unit, + val onSliderScroll: () -> Unit, ) { val selectedTypeFilterText: TextReference