Updated on 2026-08-14
This commit is contained in:
parent
d66e528975
commit
ac7d3f1607
18 changed files with 301 additions and 26 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ internal class DefaultEarnComponent(
|
|||
params = AddToPortfolioPreselectedDataComponent.Params(
|
||||
tokenToAdd = config.tokenToAdd,
|
||||
callback = earnModel.addToPortfolioCallback,
|
||||
analyticsParams = AddToPortfolioPreselectedDataComponent.AnalyticsParams(config.source),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -12,6 +12,7 @@ internal data class EarnUM(
|
|||
val onBackClick: () -> Unit,
|
||||
val onNetworkFilterClick: () -> Unit,
|
||||
val onTypeFilterClick: () -> Unit,
|
||||
val onSliderScroll: () -> Unit,
|
||||
) {
|
||||
|
||||
val selectedTypeFilterText: TextReference
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue