Updated on 2026-08-14

This commit is contained in:
Tangem 2024-09-16 18:24:55 +03:00
parent b32cc4a928
commit 2ba0f3d4a1
42 changed files with 581 additions and 74 deletions

View file

@ -194,6 +194,12 @@ internal class ChildFactory @Inject constructor(
token = route.token,
appCurrency = route.appCurrency,
showPortfolio = route.showPortfolio,
analyticsParams = route.analyticsParams?.let {
MarketsTokenDetailsComponent.AnalyticsParams(
blockchain = it.blockchain,
source = it.source,
)
},
),
componentFactory = marketsTokenDetailsComponentFactory,
)

View file

@ -272,5 +272,13 @@ sealed class AppRoute(val path: String) : Route {
val token: TokenMarketParams,
val appCurrency: AppCurrency,
val showPortfolio: Boolean,
) : AppRoute(path = "/markets_token_details/${token.id}/$showPortfolio")
val analyticsParams: AnalyticsParams? = null,
) : AppRoute(path = "/markets_token_details/${token.id}/$showPortfolio") {
@Serializable
data class AnalyticsParams(
val blockchain: String?,
val source: String,
)
}
}

View file

@ -1,8 +1,10 @@
package com.tangem.datasource.api.markets.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class TokenMarketChartResponse(
@Json(name = "prices")
val prices: Map<Long, BigDecimal>,

View file

@ -1,8 +1,10 @@
package com.tangem.datasource.api.markets.models.response
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class TokenMarketInfoResponse(
@Json(name = "id")
val id: String,
@ -30,6 +32,7 @@ data class TokenMarketInfoResponse(
val pricePerformance: PricePerformance?,
) {
@JsonClass(generateAdapter = true)
data class PriceChangePercentage(
@Json(name = "24h")
val day: BigDecimal?,
@ -47,6 +50,7 @@ data class TokenMarketInfoResponse(
val allTime: BigDecimal?,
)
@JsonClass(generateAdapter = true)
data class Network(
@Json(name = "network_id")
val networkId: String,
@ -58,6 +62,7 @@ data class TokenMarketInfoResponse(
val decimalCount: Int?,
)
@JsonClass(generateAdapter = true)
data class Insights(
@Json(name = "holders_change")
val holdersChange: Change?,
@ -70,6 +75,7 @@ data class TokenMarketInfoResponse(
@Json(name = "networks")
val sourceNetworks: List<SourceNetwork>?,
) {
@JsonClass(generateAdapter = true)
data class SourceNetwork(
@Json(name = "network_id")
val id: String,
@ -78,6 +84,7 @@ data class TokenMarketInfoResponse(
)
}
@JsonClass(generateAdapter = true)
data class Change(
@Json(name = "24h")
val day: BigDecimal?,
@ -87,6 +94,7 @@ data class TokenMarketInfoResponse(
val month: BigDecimal?,
)
@JsonClass(generateAdapter = true)
data class Metrics(
@Json(name = "market_rating")
val marketRating: Int?,
@ -102,6 +110,7 @@ data class TokenMarketInfoResponse(
val fullyDilutedValuation: BigDecimal?,
)
@JsonClass(generateAdapter = true)
data class Links(
@Json(name = "official_links")
val officialLinks: List<Link>? = null,
@ -113,6 +122,7 @@ data class TokenMarketInfoResponse(
val blockchainSite: List<Link>? = null,
)
@JsonClass(generateAdapter = true)
data class Link(
@Json(name = "title")
val title: String,
@ -122,6 +132,7 @@ data class TokenMarketInfoResponse(
val link: String,
)
@JsonClass(generateAdapter = true)
data class PricePerformance(
@Json(name = "24h")
val day: Range?,
@ -131,6 +142,7 @@ data class TokenMarketInfoResponse(
val allTime: Range?,
)
@JsonClass(generateAdapter = true)
data class Range(
@Json(name = "low_price")
val low: BigDecimal?,

View file

@ -14,6 +14,8 @@ dependencies {
implementation(projects.core.datasource)
implementation(projects.core.utils)
implementation(projects.core.pagination)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.domain.legacy)
implementation(projects.domain.markets)

View file

@ -4,13 +4,16 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.compatibility.applyL2Compatibility
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.blockchainsdk.utils.fromNetworkId
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.common.currency.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetwork
import com.tangem.data.common.utils.retryOnError
import com.tangem.data.markets.analytics.MarketsDataAnalyticsEvent
import com.tangem.data.markets.converters.TokenChartConverter
import com.tangem.data.markets.converters.TokenMarketInfoConverter
import com.tangem.data.markets.converters.TokenMarketListConverter
import com.tangem.data.markets.converters.toRequestParam
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -31,6 +34,7 @@ internal class DefaultMarketsTokenRepository(
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val dispatcherProvider: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : MarketsTokenRepository {
private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher(
@ -62,10 +66,14 @@ internal class DefaultMarketsTokenRepository(
// we shouldn't infinitely retry on the first batch request
val res = if (isFirstBatchFetching) {
requestCall()
catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
requestCall()
}
} else {
retryOnError(priority = true) {
requestCall()
catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
requestCall()
}
}
}
@ -92,6 +100,9 @@ internal class DefaultMarketsTokenRepository(
val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher(
tangemTechApi = tangemTechApi,
marketsApi = marketsApi,
onApiError = {
analyticsEventHandler.send(MarketsDataAnalyticsEvent.List.Error.toEvent())
},
)
val atomicInteger = AtomicInteger(0)
@ -109,6 +120,7 @@ internal class DefaultMarketsTokenRepository(
fiatCurrencyCode: String,
interval: PriceChangeInterval,
tokenId: String,
tokenSymbol: String,
): TokenChart {
val mappedTokenId = getTokenIdIfL2Network(tokenId)
val response = marketsApi.getCoinChart(
@ -117,13 +129,23 @@ internal class DefaultMarketsTokenRepository(
interval = interval.toRequestParam(),
)
return TokenChartConverter.convert(interval, response.getOrThrow())
val result = catchApiErrorAndSendEvent(
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Chart,
tokenSymbol = tokenSymbol,
),
) {
response.getOrThrow()
}
return TokenChartConverter.convert(interval, result)
}
override suspend fun getChartPreview(
fiatCurrencyCode: String,
interval: PriceChangeInterval,
tokenId: String,
tokenSymbol: String,
): TokenChart {
val mappedTokenId = getTokenIdIfL2Network(tokenId)
val response = marketsApi.getCoinsListCharts(
@ -132,7 +154,11 @@ internal class DefaultMarketsTokenRepository(
interval = interval.toRequestParam(),
)
val chart = response.getOrThrow()[mappedTokenId] ?: error("No chart preview data for token $mappedTokenId")
val chart = catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
response.getOrThrow()[mappedTokenId] ?: error(
"No chart preview data for the token $mappedTokenId",
)
}
return TokenChartConverter.convert(interval, chart)
}
@ -140,6 +166,7 @@ internal class DefaultMarketsTokenRepository(
override suspend fun getTokenInfo(
fiatCurrencyCode: String,
tokenId: String,
tokenSymbol: String,
languageCode: String,
): TokenMarketInfo {
val response = marketsApi.getCoinMarketData(
@ -148,12 +175,22 @@ internal class DefaultMarketsTokenRepository(
language = languageCode,
)
val resultResponse = response.getOrThrow().applyL2Compatibility(tokenId)
val result = catchApiErrorAndSendEvent(
errorEvent = MarketsDataAnalyticsEvent.Details.Error(
request = MarketsDataAnalyticsEvent.Details.Error.Request.Info,
tokenSymbol = tokenSymbol,
),
) {
response.getOrThrow()
}
val resultResponse = result.applyL2Compatibility(tokenId)
return TokenMarketInfoConverter.convert(resultResponse)
}
override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes {
// TODO change method when backend is ready
// add error analytics event
val response = marketsApi.getCoinMarketData(
currency = fiatCurrencyCode,
coinId = tokenId,
@ -194,4 +231,16 @@ internal class DefaultMarketsTokenRepository(
)
}
}
private inline fun <T> catchApiErrorAndSendEvent(errorEvent: MarketsDataAnalyticsEvent, block: () -> T): T {
return try {
block()
} catch (e: ApiResponseError.HttpException) {
analyticsEventHandler.send(errorEvent.toEvent())
throw e
} catch (e: ApiResponseError.TimeoutException) {
analyticsEventHandler.send(errorEvent.toEvent())
throw e
}
}
}

View file

@ -4,6 +4,7 @@ import com.tangem.data.common.utils.retryOnError
import com.tangem.data.markets.converters.TokenMarketChartsConverter
import com.tangem.data.markets.converters.TokenQuotesShortConverter
import com.tangem.data.markets.converters.toRequestParam
import com.tangem.datasource.api.common.response.ApiResponseError
import com.tangem.datasource.api.common.response.getOrThrow
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse
@ -20,6 +21,7 @@ import kotlinx.coroutines.launch
internal class MarketsBatchUpdateFetcher(
private val marketsApi: TangemTechMarketsApi,
private val tangemTechApi: TangemTechApi,
private val onApiError: () -> Unit,
) : BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.fetchUpdateAsync(
@ -35,11 +37,13 @@ internal class MarketsBatchUpdateFetcher(
val updateTasks = idsToUpdate.map { batchIds ->
async {
retryOnError {
marketsApi.getCoinsListCharts(
coinIds = batchIds.second.joinToString(separator = ","),
interval = updateRequest.interval.toRequestParam(),
currency = updateRequest.currency,
).getOrThrow()
catchApiError(onApiError) {
marketsApi.getCoinsListCharts(
coinIds = batchIds.second.joinToString(separator = ","),
interval = updateRequest.interval.toRequestParam(),
currency = updateRequest.currency,
).getOrThrow()
}
}
}
}
@ -62,11 +66,13 @@ internal class MarketsBatchUpdateFetcher(
}
is TokenMarketUpdateRequest.UpdateQuotes -> {
val quotesRes = retryOnError {
tangemTechApi.getQuotes(
currencyId = updateRequest.currencyId,
coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","),
fields = quoteFields.joinToString(separator = ","),
).getOrThrow()
catchApiError(onApiError) {
tangemTechApi.getQuotes(
currencyId = updateRequest.currencyId,
coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","),
fields = quoteFields.joinToString(separator = ","),
).getOrThrow()
}
}
update {
@ -106,6 +112,15 @@ internal class MarketsBatchUpdateFetcher(
)
}
private inline fun <T> catchApiError(onError: () -> Unit, block: () -> T): T {
return try {
block()
} catch (e: ApiResponseError) {
onError()
throw e
}
}
companion object {
private val quoteFields = listOf(
"price",

View file

@ -0,0 +1,42 @@
package com.tangem.data.markets.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
sealed interface MarketsDataAnalyticsEvent {
sealed class List(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Markets", event = event, params = params), MarketsDataAnalyticsEvent {
data object Error : List(event = "Data Error")
}
sealed class Details(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params), MarketsDataAnalyticsEvent {
data class Error(
val request: Request,
val tokenSymbol: String,
) : Details(
event = "Data Error",
params = mapOf(
"Source" to request.source,
"Token" to tokenSymbol,
),
) {
enum class Request(val source: String) {
Chart("Chart"),
Info("Blocks"),
}
}
}
fun toEvent(): AnalyticsEvent = when (this) {
is List -> this
is Details -> this
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.data.markets.di
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.markets.DefaultMarketsTokenRepository
import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -24,12 +25,14 @@ internal object MarketsDataModule {
@DevTangemApi tangemTechApi: TangemTechApi,
userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,
): MarketsTokenRepository {
return DefaultMarketsTokenRepository(
marketsApi = marketsApi,
tangemTechApi = tangemTechApi,
dispatcherProvider = dispatchers,
userWalletsStore = userWalletsStore,
analyticsEventHandler = analyticsEventHandler,
)
}
}

View file

@ -9,11 +9,16 @@ class GetTokenMarketInfoUseCase(
private val marketsTokenRepository: MarketsTokenRepository,
) {
suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either<Unit, TokenMarketInfo> {
suspend operator fun invoke(
appCurrency: AppCurrency,
tokenId: String,
tokenSymbol: String,
): Either<Unit, TokenMarketInfo> {
return Either.catch {
marketsTokenRepository.getTokenInfo(
fiatCurrencyCode = appCurrency.code,
tokenId = tokenId,
tokenSymbol = tokenSymbol,
languageCode = SupportedLanguages.getCurrentSupportedLanguageCode(),
)
}.mapLeft {}

View file

@ -12,6 +12,7 @@ class GetTokenPriceChartUseCase(
appCurrency: AppCurrency,
interval: PriceChangeInterval,
tokenId: String,
tokenSymbol: String,
preview: Boolean,
): Either<Unit, TokenChart> {
return Either.catch {
@ -20,12 +21,14 @@ class GetTokenPriceChartUseCase(
fiatCurrencyCode = appCurrency.code,
interval = interval,
tokenId = tokenId,
tokenSymbol = tokenSymbol,
)
} else {
marketsTokenRepository.getChart(
fiatCurrencyCode = appCurrency.code,
interval = interval,
tokenId = tokenId,
tokenSymbol = tokenSymbol,
)
}
}.mapLeft {}

View file

@ -12,11 +12,26 @@ interface MarketsTokenRepository {
nextBatchSize: Int,
): TokenListBatchFlow
suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
suspend fun getChart(
fiatCurrencyCode: String,
interval: PriceChangeInterval,
tokenId: String,
tokenSymbol: String,
): TokenChart
suspend fun getChartPreview(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
suspend fun getChartPreview(
fiatCurrencyCode: String,
interval: PriceChangeInterval,
tokenId: String,
tokenSymbol: String,
): TokenChart
suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String, languageCode: String): TokenMarketInfo
suspend fun getTokenInfo(
fiatCurrencyCode: String,
tokenId: String,
tokenSymbol: String,
languageCode: String,
): TokenMarketInfo
suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes

View file

@ -20,6 +20,13 @@ interface MarketsTokenDetailsComponent : ComposableContentComponent {
val token: TokenMarketParams,
val appCurrency: AppCurrency,
val showPortfolio: Boolean,
val analyticsParams: AnalyticsParams?,
)
@Serializable
data class AnalyticsParams(
val blockchain: String?,
val source: String,
)
@Composable

View file

@ -11,11 +11,7 @@ interface TokenMarketBlockComponent : ComposableContentComponent {
@Serializable
data class Params(
val cryptoCurrencyID: CryptoCurrency.ID,
val tokenId: String,
val tokenName: String,
val tokenSymbol: String,
val tokenImageUrl: String?,
val cryptoCurrency: CryptoCurrency,
)
interface Factory {

View file

@ -63,6 +63,8 @@ dependencies {
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.featuretoggles)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
/* Common */
implementation(projects.common.ui)

View file

@ -7,6 +7,7 @@ import androidx.compose.ui.unit.Dp
import androidx.lifecycle.compose.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
@ -14,6 +15,7 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params
import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent
import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent
@ -29,6 +31,7 @@ import kotlinx.coroutines.launch
internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Params,
analyticsEventHandler: AnalyticsEventHandler,
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent {
@ -38,12 +41,17 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
id = getTokenIdIfL2Network(params.token.id),
),
)
private val analyticsParams = params.analyticsParams
private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams)
private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.showPortfolio) {
portfolioComponentFactory.create(
context = child("my_portfolio"),
params = MarketsPortfolioComponent.Params(updatedParams.token),
params = MarketsPortfolioComponent.Params(
updatedParams.token,
analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) },
),
)
} else {
null
@ -59,6 +67,18 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
}
}
}
// === Analytics ===
if (analyticsParams != null) {
analyticsEventHandler.send(
MarketDetailsAnalyticsEvent.EventBuilder(
token = params.token,
).screenOpened(
blockchain = analyticsParams.blockchain,
source = analyticsParams.source,
),
)
}
}
@Composable

View file

@ -0,0 +1,64 @@
package com.tangem.features.markets.details.impl.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketParams
internal class MarketDetailsAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) {
data class EventBuilder(
val token: TokenMarketParams,
) {
fun screenOpened(blockchain: String?, source: String) = MarketDetailsAnalyticsEvent(
event = "Token Chart Screen Opened",
params = buildMap {
put("Token", token.symbol)
blockchain?.let { put("blockchain", it) }
put("Source", source)
},
)
fun intervalChanged(intervalType: IntervalType, interval: PriceChangeInterval) = MarketDetailsAnalyticsEvent(
event = "Button - Period",
params = mapOf(
"Token" to token.symbol,
"Period" to interval.toAnalyticsString(),
"Source" to intervalType.source,
),
)
fun readMoreClicked() = MarketDetailsAnalyticsEvent(
event = "Button - Read More",
params = mapOf(
"Token" to token.symbol,
),
)
fun linkClicked(linkTitle: String) = MarketDetailsAnalyticsEvent(
event = "Button - Links",
params = mapOf(
"Token" to token.symbol,
"Link" to linkTitle,
),
)
}
enum class IntervalType(val source: String) {
Chart("Chart"),
PricePerformance("Price"),
Insights("Insights"),
}
}
private fun PriceChangeInterval.toAnalyticsString() = when (this) {
PriceChangeInterval.H24 -> "24h"
PriceChangeInterval.WEEK -> "7d"
PriceChangeInterval.MONTH -> "1m"
PriceChangeInterval.MONTH3 -> "3m"
PriceChangeInterval.MONTH6 -> "6m"
PriceChangeInterval.YEAR -> "1y"
PriceChangeInterval.ALL_TIME -> "All"
}

View file

@ -5,6 +5,7 @@ import arrow.core.getOrElse
import com.tangem.common.ui.charts.state.MarketChartData
import com.tangem.common.ui.charts.state.MarketChartDataProducer
import com.tangem.common.ui.charts.state.sorted
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -20,6 +21,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.*
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent
import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter
import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter
import com.tangem.features.markets.details.impl.model.formatter.*
@ -56,10 +58,12 @@ internal class MarketsTokenDetailsModel @Inject constructor(
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase,
private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private var quotesJob = JobHolder()
private val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>()
private val analyticsEventBuilder = MarketDetailsAnalyticsEvent.EventBuilder(token = params.token)
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
@ -75,14 +79,36 @@ internal class MarketsTokenDetailsModel @Inject constructor(
onInfoClick = {
showInfoBottomSheet(it)
},
onLinkClick = {
urlOpener.openUrl(it.url)
onLinkClick = { link ->
urlOpener.openUrl(link.url)
// === Analytics ===
analyticsEventHandler.send(analyticsEventBuilder.linkClicked(linkTitle = link.title))
},
// === Analytics ===
onPricePerformanceIntervalChanged = {
analyticsEventHandler.send(
analyticsEventBuilder.intervalChanged(
intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance,
interval = it,
),
)
},
onInsightsIntervalChanged = {
analyticsEventHandler.send(
analyticsEventBuilder.intervalChanged(
intervalType = MarketDetailsAnalyticsEvent.IntervalType.Insights,
interval = it,
),
)
},
// ==================
)
private val descriptionConverter = DescriptionConverter(
onReadModeClicked = {
showInfoBottomSheet(it)
// === Analytics ===
analyticsEventHandler.send(analyticsEventBuilder.readMoreClicked())
},
)
@ -167,6 +193,14 @@ internal class MarketsTokenDetailsModel @Inject constructor(
currentQuotes = currentQuotes,
lastUpdatedTimestamp = lastUpdatedTimestamp,
currentTokenInfo = currentTokenInfo,
onPricePerformanceIntervalChanged = {
analyticsEventHandler.send(
analyticsEventBuilder.intervalChanged(
intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance,
interval = it,
),
)
},
)
private val loadChartJobHolder = JobHolder()
@ -221,6 +255,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
appCurrency = currentAppCurrency.value,
interval = interval,
tokenId = params.token.id,
tokenSymbol = params.token.symbol,
preview = false,
)
@ -290,6 +325,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
val tokenMarketInfo = getTokenMarketInfoUseCase(
appCurrency = currentAppCurrency.value,
tokenId = params.token.id,
tokenSymbol = params.token.symbol,
)
tokenMarketInfo.fold(
@ -368,6 +404,15 @@ internal class MarketsTokenDetailsModel @Inject constructor(
private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
if (state.value.selectedInterval == interval) return
// === Analytics ===
analyticsEventHandler.send(
analyticsEventBuilder.intervalChanged(
intervalType = MarketDetailsAnalyticsEvent.IntervalType.Chart,
interval = interval,
),
)
// ==================
val quotes = currentQuotes.value
val priceChangePercent = quotes.getFormattedPercentByInterval(interval)

View file

@ -32,6 +32,7 @@ internal class DescriptionConverter(
),
),
body = stringReference(value.fullDescription ?: ""),
showGeneratedAINotification = true,
),
)
},

View file

@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
@ -21,6 +22,7 @@ import java.math.BigDecimal
internal class InsightsConverter(
private val appCurrency: Provider<AppCurrency>,
private val onInfoClick: (InfoBottomSheetContent) -> Unit,
private val onIntervalChanged: (PriceChangeInterval) -> Unit,
) : Converter<TokenMarketInfo.Insights, InsightsUM> {
override fun convert(value: TokenMarketInfo.Insights): InsightsUM {
@ -55,6 +57,7 @@ internal class InsightsConverter(
),
)
},
onIntervalChanged = onIntervalChanged,
)
}
}

View file

@ -1,9 +1,9 @@
package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.ui.state.LinksUM
import com.tangem.features.markets.details.impl.ui.state.LinksUM.Link
import com.tangem.features.markets.impl.R
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList
@ -25,7 +25,7 @@ internal class LinksConverter(
private fun TokenMarketInfo.Link.convert(): LinksUM.Link {
return LinksUM.Link(
title = stringReference(title),
title = title,
iconRes = getIconById(id),
url = link,
)

View file

@ -3,6 +3,7 @@ package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM
import com.tangem.utils.Provider
@ -13,6 +14,7 @@ import java.math.RoundingMode
@Stable
internal class PricePerformanceConverter(
private val appCurrency: Provider<AppCurrency>,
private val onIntervalChanged: (PriceChangeInterval) -> Unit,
) {
fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM {
@ -20,6 +22,7 @@ internal class PricePerformanceConverter(
h24 = value.day.convert(currentPrice),
month = value.month.convert(currentPrice),
all = value.allTime.convert(currentPrice),
onIntervalChanged = onIntervalChanged,
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.LinksUM
@ -14,15 +15,24 @@ internal class TokenMarketInfoConverter(
appCurrency: Provider<AppCurrency>,
onInfoClick: (InfoBottomSheetContent) -> Unit,
onLinkClick: (LinksUM.Link) -> Unit,
onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit,
onInsightsIntervalChanged: (PriceChangeInterval) -> Unit,
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.InformationBlocks> {
private val insightsConverter = InsightsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
private val insightsConverter = InsightsConverter(
appCurrency = appCurrency,
onInfoClick = onInfoClick,
onIntervalChanged = onInsightsIntervalChanged,
)
@Suppress("UnusedPrivateMember")
// TODO second markets iteration
private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick)
private val metricsConverter = MetricsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick)
private val pricePerformanceConverter = PricePerformanceConverter(appCurrency = appCurrency)
private val pricePerformanceConverter = PricePerformanceConverter(
appCurrency = appCurrency,
onIntervalChanged = onPricePerformanceIntervalChanged,
)
private val linksConverter = LinksConverter(onLinkClick = onLinkClick)
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks {

View file

@ -4,6 +4,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.event.triggeredEvent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenQuotes
import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter
@ -22,8 +23,12 @@ internal class QuotesStateUpdater(
private val currentQuotes: MutableStateFlow<TokenQuotes>,
private val lastUpdatedTimestamp: MutableStateFlow<Long>,
private val currentTokenInfo: MutableStateFlow<TokenMarketInfo?>,
private val onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit,
) {
private val pricePerformanceConverter = PricePerformanceConverter(currentAppCurrency)
private val pricePerformanceConverter = PricePerformanceConverter(
currentAppCurrency,
onIntervalChanged = onPricePerformanceIntervalChanged,
)
suspend fun updateQuotes(newQuotes: TokenQuotes) {
val triggerPriceChangeType = getFormattedPriceChange(

View file

@ -44,11 +44,13 @@ internal fun InfoBottomSheet(config: TangemBottomSheetConfig) {
),
)
AdditionalInfoNotification(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
if (it.showGeneratedAINotification) {
AdditionalInfoNotification(
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
)
}
SpacerH(bottomBarHeight)
}

View file

@ -47,7 +47,10 @@ internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) {
PriceChangeInterval.MONTH,
),
initialSelectedItem = PriceChangeInterval.H24,
onClick = { currentInterval = it },
onClick = {
currentInterval = it
state.onIntervalChanged(it)
},
) {
Box(
Modifier
@ -177,6 +180,7 @@ private fun ContentPreview() {
),
),
onInfoClick = {},
onIntervalChanged = {},
),
)
}

View file

@ -97,7 +97,7 @@ private fun SubBlock(
) {
links.fastForEach {
Chip(
text = it.title,
text = stringReference(it.title),
iconResId = it.iconRes,
onClick = { onLinkClick(it) },
)
@ -163,36 +163,36 @@ private fun ContentPreview() {
state = LinksUM(
officialLinks = persistentListOf(
LinksUM.Link(
title = stringReference("Website"),
title = "Website",
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
LinksUM.Link(
title = stringReference("Website"),
title = "Website",
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
LinksUM.Link(
title = stringReference("Website"),
title = "Website",
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
),
social = persistentListOf(
LinksUM.Link(
title = stringReference("Twitter"),
title = "Twitter",
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
LinksUM.Link(
title = stringReference("Facebook"),
title = "Facebook",
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),
),
repository = persistentListOf(
LinksUM.Link(
title = stringReference("Github"),
title = "Github",
iconRes = R.drawable.ic_plus_24,
url = "https://tangem.com",
),

View file

@ -54,7 +54,10 @@ internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier
PriceChangeInterval.ALL_TIME,
),
initialSelectedItem = PriceChangeInterval.H24,
onClick = { currentInterval = it },
onClick = {
currentInterval = it
state.onIntervalChanged(it)
},
) {
Box(
Modifier
@ -227,6 +230,7 @@ private fun ContentPreview() {
high = "\$580,5M",
indicatorFraction = 0.2f,
),
onIntervalChanged = {},
),
)
}

View file

@ -6,4 +6,5 @@ import com.tangem.core.ui.extensions.TextReference
internal data class InfoBottomSheetContent(
val title: TextReference,
val body: TextReference,
val showGeneratedAINotification: Boolean = false,
) : TangemBottomSheetConfigContent

View file

@ -1,5 +1,6 @@
package com.tangem.features.markets.details.impl.ui.state
import com.tangem.domain.markets.PriceChangeInterval
import kotlinx.collections.immutable.ImmutableList
internal data class InsightsUM(
@ -7,4 +8,5 @@ internal data class InsightsUM(
val weekInfo: ImmutableList<InfoPointUM>,
val monthInfo: ImmutableList<InfoPointUM>,
val onInfoClick: () -> Unit,
val onIntervalChanged: (PriceChangeInterval) -> Unit,
)

View file

@ -1,7 +1,6 @@
package com.tangem.features.markets.details.impl.ui.state
import androidx.annotation.DrawableRes
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
internal data class LinksUM(
@ -13,7 +12,7 @@ internal data class LinksUM(
) {
data class Link(
@DrawableRes val iconRes: Int,
val title: TextReference,
val title: String,
val url: String,
)
}

View file

@ -1,11 +1,13 @@
package com.tangem.features.markets.details.impl.ui.state
import androidx.annotation.FloatRange
import com.tangem.domain.markets.PriceChangeInterval
internal data class PricePerformanceUM(
val h24: Value,
val month: Value,
val all: Value,
val onIntervalChanged: (PriceChangeInterval) -> Unit,
) {
data class Value(
val low: String,

View file

@ -78,6 +78,10 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
token = token.toSerializableParam(),
appCurrency = appCurrency,
showPortfolio = true,
analyticsParams = MarketsTokenDetailsComponent.AnalyticsParams(
blockchain = null,
source = "Market",
),
),
),
)

View file

@ -11,7 +11,15 @@ import kotlinx.serialization.Serializable
interface MarketsPortfolioComponent : ComposableContentComponent {
@Serializable
data class Params(val token: TokenMarketParams)
data class Params(
val token: TokenMarketParams,
val analyticsParams: AnalyticsParams?,
)
@Serializable
data class AnalyticsParams(
val source: String,
)
fun setTokenNetworks(networks: List<TokenMarketInfo.Network>)

View file

@ -0,0 +1,50 @@
package com.tangem.features.markets.portfolio.impl.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.domain.markets.TokenMarketParams
import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM
internal class PortfolioAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) {
data class EventBuilder(
val token: TokenMarketParams,
val source: String?,
) {
fun addToPortfolioClicked() = PortfolioAnalyticsEvent(
event = "Button - Add To Portfolio",
params = mapOf(
"Token" to token.symbol,
),
)
fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected")
fun addToPortfolioContinue(blockchainNames: List<String>) = PortfolioAnalyticsEvent(
event = "Token Network Selected",
params = mapOf(
"Count" to blockchainNames.size.toString(),
"Token" to token.symbol,
"blockchain" to blockchainNames.joinToString(separator = ", "),
),
)
fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) =
PortfolioAnalyticsEvent(
event = when (actionUM) {
TokenActionsBSContentUM.Action.Buy -> "Button - Buy"
TokenActionsBSContentUM.Action.Receive -> "Button - Receive"
TokenActionsBSContentUM.Action.Exchange -> "Button - Swap"
else -> "error"
},
params = buildMap {
put("Token", token.symbol)
source?.let { put("Source", source) }
put("blockchain", blockchainName)
},
)
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.markets.portfolio.impl.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
@ -24,9 +25,11 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent
import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader
import com.tangem.features.markets.portfolio.impl.ui.WarningDialog
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
@ -49,12 +52,17 @@ internal class MarketsPortfolioModel @Inject constructor(
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
private val addToPortfolioManager: AddToPortfolioManager,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
val state: StateFlow<MyPortfolioUM> get() = _state
private val _state: MutableStateFlow<MyPortfolioUM> = MutableStateFlow(value = MyPortfolioUM.Loading)
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>()
private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder(
token = params.token,
source = params.analyticsParams?.source,
)
/** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */
private val selectedMultiWalletIdFlow = MutableStateFlow<UserWalletId?>(value = null)
@ -72,14 +80,37 @@ internal class MarketsPortfolioModel @Inject constructor(
)
private val factory = MyPortfolioUMFactory(
onAddClick = { onAddToPortfolioBSVisibilityChange(isShow = true) },
onAddClick = {
onAddToPortfolioBSVisibilityChange(isShow = true)
// === Analytics ===
analyticsEventHandler.send(
analyticsEventBuilder.addToPortfolioClicked(),
)
},
addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory(
token = params.token,
onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange,
onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange,
onNetworkSwitchClick = ::onNetworkSwitchClick,
onWalletSelect = ::onWalletSelect,
onContinueClick = ::onContinueClick,
onWalletSelect = {
onWalletSelect(it)
// === Analytics ===
analyticsEventHandler.send(
analyticsEventBuilder.addToPortfolioWalletChanged(),
)
},
onContinueClick = { selectedWalletId, addedNetworks ->
onContinueClick(selectedWalletId, addedNetworks)
// === Analytics ===
analyticsEventHandler.send(
analyticsEventBuilder.addToPortfolioContinue(
blockchainNames = addedNetworks.mapNotNull {
BlockchainUtils.getNetworkInfo(it.networkId)?.name
},
),
)
},
),
currentState = Provider { _state.value },
tokenActionsHandler = tokenActionsIntentsFactory.create(
@ -87,6 +118,14 @@ internal class MarketsPortfolioModel @Inject constructor(
updateTokenReceiveBSConfig = { updateBlock ->
updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) }
},
onHandleQuickAction = { handledAction ->
analyticsEventHandler.send(
analyticsEventBuilder.quickActionClick(
actionUM = handledAction.action,
blockchainName = handledAction.cryptoCurrencyData.status.currency.network.name,
),
)
},
),
updateTokens = { updateBlock ->
updateTokensState { state ->

View file

@ -34,10 +34,9 @@ internal class TokenActionsHandler @AssistedInject constructor(
private val clipboardManager: ClipboardManager,
private val uiMessageSender: UiMessageSender,
private val reduxStateHolder: ReduxStateHolder,
@Assisted
private val currentAppCurrency: Provider<AppCurrency>,
@Assisted("updateTokenReceiveBSConfig")
private val updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
@Assisted private val currentAppCurrency: Provider<AppCurrency>,
@Assisted private val updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
@Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit,
private val isDemoCardUseCase: IsDemoCardUseCase,
private val messageSender: UiMessageSender,
) {
@ -48,6 +47,12 @@ internal class TokenActionsHandler @AssistedInject constructor(
)
fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) {
onHandleQuickAction(
HandledQuickAction(
action = action,
cryptoCurrencyData = cryptoCurrencyData,
),
)
if (handleDemoMode(action, cryptoCurrencyData.userWallet)) return
when (action) {
@ -177,8 +182,13 @@ internal class TokenActionsHandler @AssistedInject constructor(
interface Factory {
fun create(
currentAppCurrency: Provider<AppCurrency>,
@Assisted("updateTokenReceiveBSConfig")
updateTokenReceiveBSConfig: ((TangemBottomSheetConfig) -> TangemBottomSheetConfig) -> Unit,
onHandleQuickAction: (HandledQuickAction) -> Unit,
): TokenActionsHandler
}
data class HandledQuickAction(
val action: TokenActionsBSContentUM.Action,
val cryptoCurrencyData: PortfolioData.CryptoCurrencyData,
)
}

View file

@ -40,6 +40,7 @@ internal class TokenMarketBlockModel @Inject constructor(
) : Model() {
private val params = paramsContainer.require<TokenMarketBlockComponent.Params>()
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = false)
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
@ -56,7 +57,7 @@ internal class TokenMarketBlockModel @Inject constructor(
val state = MutableStateFlow(
TokenMarketBlockUM(
currencySymbol = params.tokenSymbol,
currencySymbol = params.cryptoCurrency.symbol,
currentPrice = null,
h24Percent = null,
priceChangeType = PriceChangeType.NEUTRAL,
@ -72,7 +73,7 @@ internal class TokenMarketBlockModel @Inject constructor(
private fun startFetching() {
modelScope.launch {
getTokenQuotesUseCase(
currencyID = params.cryptoCurrencyID,
currencyID = params.cryptoCurrency.id,
interval = PriceChangeInterval.H24,
refresh = true,
).collect {
@ -100,9 +101,12 @@ internal class TokenMarketBlockModel @Inject constructor(
}
}.saveIn(quotesUpdateJobHolder)
val tokenId = params.cryptoCurrency.id.rawCurrencyId ?: return
modelScope.launch(dispatchers.main) {
val result = getTokenPriceChartUseCase(
tokenId = params.tokenId,
tokenId = tokenId,
tokenSymbol = params.cryptoCurrency.symbol,
interval = PriceChangeInterval.H24,
appCurrency = currentAppCurrency.value, // TODO get currency from quotes use case [REDACTED_TASK_KEY]
preview = true,
@ -128,12 +132,13 @@ internal class TokenMarketBlockModel @Inject constructor(
private fun navigateToMarketDetails() {
val quotes = quotesState ?: return
val tokenId = params.cryptoCurrency.id.rawCurrencyId ?: return
val tokenParam = TokenMarketParams(
id = params.tokenId,
name = params.tokenSymbol,
imageUrl = params.tokenImageUrl,
symbol = params.tokenSymbol,
id = tokenId,
name = params.cryptoCurrency.name,
imageUrl = params.cryptoCurrency.iconUrl,
symbol = params.cryptoCurrency.symbol,
tokenQuotes = TokenMarketParams.Quotes(
currentPrice = quotes.currentPrice,
h24Percent = quotes.h24Percent,
@ -147,6 +152,10 @@ internal class TokenMarketBlockModel @Inject constructor(
token = tokenParam,
appCurrency = currentAppCurrency.value,
showPortfolio = false,
analyticsParams = AppRoute.MarketsTokenDetails.AnalyticsParams(
blockchain = params.cryptoCurrency.network.name,
source = "Token",
),
),
)
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.markets.tokenlist.impl.analytics
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
internal sealed class MarketsListAnalyticsEvent(
event: String,
params: Map<String, String> = mapOf(),
) : AnalyticsEvent(category = "Markets", event = event, params = params) {
data object BottomSheetOpened : MarketsListAnalyticsEvent(event = "Markets Screen Opened")
data class SortBy(
val sortByTypeUM: SortByTypeUM,
val interval: MarketsListUM.TrendInterval,
) : MarketsListAnalyticsEvent(
event = "Sort By",
params = mapOf(
"Type" to when (sortByTypeUM) {
SortByTypeUM.Rating -> "Rating"
SortByTypeUM.Trending -> "Trending"
SortByTypeUM.ExperiencedBuyers -> "Buyers"
SortByTypeUM.TopGainers -> "Gainers"
SortByTypeUM.TopLosers -> "Losers"
},
"Period" to when (interval) {
MarketsListUM.TrendInterval.H24 -> "24h"
MarketsListUM.TrendInterval.D7 -> "7d"
MarketsListUM.TrendInterval.M1 -> "1m"
},
),
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.features.markets.tokenlist.impl.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
@ -9,8 +10,9 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.TokenMarket
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager
import com.tangem.features.markets.tokenlist.impl.analytics.MarketsListAnalyticsEvent
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListBatchFlowManager
import com.tangem.features.markets.tokenlist.impl.model.statemanager.MarketsListUMStateManager
import com.tangem.features.markets.tokenlist.impl.ui.state.ListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
@ -32,6 +34,7 @@ internal class MarketsListModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() {
private var updateQuotesJob = JobHolder()
@ -221,10 +224,30 @@ internal class MarketsListModel @Inject constructor(
}
}
// analytics
initAnalytics()
// initial loading
mainMarketsListManager.reload()
}
private fun initAnalytics() {
containerBottomSheetState
.onEach {
if (it == BottomSheetState.EXPANDED) {
analyticsEventHandler.send(MarketsListAnalyticsEvent.BottomSheetOpened)
}
}.launchIn(modelScope)
state
.filter { it.isInSearchMode.not() }
.map { MarketsListAnalyticsEvent.SortBy(it.selectedSortBy, it.selectedInterval) }
.distinctUntilChanged()
.onEach {
analyticsEventHandler.send(it)
}.launchIn(modelScope)
}
private fun onTokenUIClicked(token: MarketsListItemUM) {
modelScope.launch {
activeListManager.getTokenById(token.id)?.let { found ->

View file

@ -85,15 +85,9 @@ internal class TokenDetailsFragment : ComposeFragment() {
}
private fun CryptoCurrency.toParam(): TokenMarketBlockComponent.Params? {
val tokenId = id.rawCurrencyId ?: return null // token price is not available
id.rawCurrencyId ?: return null // token price is not available
return TokenMarketBlockComponent.Params(
cryptoCurrencyID = id,
tokenId = tokenId,
tokenName = name,
tokenSymbol = symbol,
tokenImageUrl = iconUrl,
)
return TokenMarketBlockComponent.Params(cryptoCurrency = this)
}
@Composable

View file

@ -403,6 +403,10 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor(
token = tokenMarketParams,
appCurrency = getSelectedAppCurrencyUseCase.unwrap(),
showPortfolio = false,
analyticsParams = AppRoute.MarketsTokenDetails.AnalyticsParams(
blockchain = cryptoCurrencyStatus.currency.network.name,
source = "Main",
),
),
)
}