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, token = route.token,
appCurrency = route.appCurrency, appCurrency = route.appCurrency,
showPortfolio = route.showPortfolio, showPortfolio = route.showPortfolio,
analyticsParams = route.analyticsParams?.let {
MarketsTokenDetailsComponent.AnalyticsParams(
blockchain = it.blockchain,
source = it.source,
)
},
), ),
componentFactory = marketsTokenDetailsComponentFactory, componentFactory = marketsTokenDetailsComponentFactory,
) )

View file

@ -272,5 +272,13 @@ sealed class AppRoute(val path: String) : Route {
val token: TokenMarketParams, val token: TokenMarketParams,
val appCurrency: AppCurrency, val appCurrency: AppCurrency,
val showPortfolio: Boolean, 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 package com.tangem.datasource.api.markets.models.response
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class TokenMarketChartResponse( data class TokenMarketChartResponse(
@Json(name = "prices") @Json(name = "prices")
val prices: Map<Long, BigDecimal>, val prices: Map<Long, BigDecimal>,

View file

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

View file

@ -14,6 +14,8 @@ dependencies {
implementation(projects.core.datasource) implementation(projects.core.datasource)
implementation(projects.core.utils) implementation(projects.core.utils)
implementation(projects.core.pagination) implementation(projects.core.pagination)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.domain.legacy) implementation(projects.domain.legacy)
implementation(projects.domain.markets) 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.applyL2Compatibility
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network
import com.tangem.blockchainsdk.utils.fromNetworkId 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.CryptoCurrencyFactory
import com.tangem.data.common.currency.getNetwork import com.tangem.data.common.currency.getNetwork
import com.tangem.data.common.utils.retryOnError 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.TokenChartConverter
import com.tangem.data.markets.converters.TokenMarketInfoConverter import com.tangem.data.markets.converters.TokenMarketInfoConverter
import com.tangem.data.markets.converters.TokenMarketListConverter import com.tangem.data.markets.converters.TokenMarketListConverter
import com.tangem.data.markets.converters.toRequestParam 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.common.response.getOrThrow
import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -31,6 +34,7 @@ internal class DefaultMarketsTokenRepository(
private val tangemTechApi: TangemTechApi, private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore, private val userWalletsStore: UserWalletsStore,
private val dispatcherProvider: CoroutineDispatcherProvider, private val dispatcherProvider: CoroutineDispatcherProvider,
private val analyticsEventHandler: AnalyticsEventHandler,
) : MarketsTokenRepository { ) : MarketsTokenRepository {
private fun createTokenMarketsFetcher(firstBatchSize: Int, nextBatchSize: Int) = LimitOffsetBatchFetcher( 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 // we shouldn't infinitely retry on the first batch request
val res = if (isFirstBatchFetching) { val res = if (isFirstBatchFetching) {
requestCall() catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
requestCall()
}
} else { } else {
retryOnError(priority = true) { retryOnError(priority = true) {
requestCall() catchApiErrorAndSendEvent(errorEvent = MarketsDataAnalyticsEvent.List.Error) {
requestCall()
}
} }
} }
@ -92,6 +100,9 @@ internal class DefaultMarketsTokenRepository(
val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher( val tokenMarketsUpdateFetcher = MarketsBatchUpdateFetcher(
tangemTechApi = tangemTechApi, tangemTechApi = tangemTechApi,
marketsApi = marketsApi, marketsApi = marketsApi,
onApiError = {
analyticsEventHandler.send(MarketsDataAnalyticsEvent.List.Error.toEvent())
},
) )
val atomicInteger = AtomicInteger(0) val atomicInteger = AtomicInteger(0)
@ -109,6 +120,7 @@ internal class DefaultMarketsTokenRepository(
fiatCurrencyCode: String, fiatCurrencyCode: String,
interval: PriceChangeInterval, interval: PriceChangeInterval,
tokenId: String, tokenId: String,
tokenSymbol: String,
): TokenChart { ): TokenChart {
val mappedTokenId = getTokenIdIfL2Network(tokenId) val mappedTokenId = getTokenIdIfL2Network(tokenId)
val response = marketsApi.getCoinChart( val response = marketsApi.getCoinChart(
@ -117,13 +129,23 @@ internal class DefaultMarketsTokenRepository(
interval = interval.toRequestParam(), 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( override suspend fun getChartPreview(
fiatCurrencyCode: String, fiatCurrencyCode: String,
interval: PriceChangeInterval, interval: PriceChangeInterval,
tokenId: String, tokenId: String,
tokenSymbol: String,
): TokenChart { ): TokenChart {
val mappedTokenId = getTokenIdIfL2Network(tokenId) val mappedTokenId = getTokenIdIfL2Network(tokenId)
val response = marketsApi.getCoinsListCharts( val response = marketsApi.getCoinsListCharts(
@ -132,7 +154,11 @@ internal class DefaultMarketsTokenRepository(
interval = interval.toRequestParam(), 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) return TokenChartConverter.convert(interval, chart)
} }
@ -140,6 +166,7 @@ internal class DefaultMarketsTokenRepository(
override suspend fun getTokenInfo( override suspend fun getTokenInfo(
fiatCurrencyCode: String, fiatCurrencyCode: String,
tokenId: String, tokenId: String,
tokenSymbol: String,
languageCode: String, languageCode: String,
): TokenMarketInfo { ): TokenMarketInfo {
val response = marketsApi.getCoinMarketData( val response = marketsApi.getCoinMarketData(
@ -148,12 +175,22 @@ internal class DefaultMarketsTokenRepository(
language = languageCode, 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) return TokenMarketInfoConverter.convert(resultResponse)
} }
override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes { override suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes {
// TODO change method when backend is ready // TODO change method when backend is ready
// add error analytics event
val response = marketsApi.getCoinMarketData( val response = marketsApi.getCoinMarketData(
currency = fiatCurrencyCode, currency = fiatCurrencyCode,
coinId = tokenId, 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.TokenMarketChartsConverter
import com.tangem.data.markets.converters.TokenQuotesShortConverter import com.tangem.data.markets.converters.TokenQuotesShortConverter
import com.tangem.data.markets.converters.toRequestParam 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.common.response.getOrThrow
import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse import com.tangem.datasource.api.markets.models.response.TokenMarketChartListResponse
@ -20,6 +21,7 @@ import kotlinx.coroutines.launch
internal class MarketsBatchUpdateFetcher( internal class MarketsBatchUpdateFetcher(
private val marketsApi: TangemTechMarketsApi, private val marketsApi: TangemTechMarketsApi,
private val tangemTechApi: TangemTechApi, private val tangemTechApi: TangemTechApi,
private val onApiError: () -> Unit,
) : BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> { ) : BatchUpdateFetcher<Int, List<TokenMarket>, TokenMarketUpdateRequest> {
override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.fetchUpdateAsync( override suspend fun BatchUpdateFetcher.UpdateContext<Int, List<TokenMarket>>.fetchUpdateAsync(
@ -35,11 +37,13 @@ internal class MarketsBatchUpdateFetcher(
val updateTasks = idsToUpdate.map { batchIds -> val updateTasks = idsToUpdate.map { batchIds ->
async { async {
retryOnError { retryOnError {
marketsApi.getCoinsListCharts( catchApiError(onApiError) {
coinIds = batchIds.second.joinToString(separator = ","), marketsApi.getCoinsListCharts(
interval = updateRequest.interval.toRequestParam(), coinIds = batchIds.second.joinToString(separator = ","),
currency = updateRequest.currency, interval = updateRequest.interval.toRequestParam(),
).getOrThrow() currency = updateRequest.currency,
).getOrThrow()
}
} }
} }
} }
@ -62,11 +66,13 @@ internal class MarketsBatchUpdateFetcher(
} }
is TokenMarketUpdateRequest.UpdateQuotes -> { is TokenMarketUpdateRequest.UpdateQuotes -> {
val quotesRes = retryOnError { val quotesRes = retryOnError {
tangemTechApi.getQuotes( catchApiError(onApiError) {
currencyId = updateRequest.currencyId, tangemTechApi.getQuotes(
coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","), currencyId = updateRequest.currencyId,
fields = quoteFields.joinToString(separator = ","), coinIds = idsToUpdate.map { it.second }.flatten().joinToString(separator = ","),
).getOrThrow() fields = quoteFields.joinToString(separator = ","),
).getOrThrow()
}
} }
update { 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 { companion object {
private val quoteFields = listOf( private val quoteFields = listOf(
"price", "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 package com.tangem.data.markets.di
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.data.markets.DefaultMarketsTokenRepository import com.tangem.data.markets.DefaultMarketsTokenRepository
import com.tangem.datasource.api.markets.TangemTechMarketsApi import com.tangem.datasource.api.markets.TangemTechMarketsApi
import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApi
@ -24,12 +25,14 @@ internal object MarketsDataModule {
@DevTangemApi tangemTechApi: TangemTechApi, @DevTangemApi tangemTechApi: TangemTechApi,
userWalletsStore: UserWalletsStore, userWalletsStore: UserWalletsStore,
dispatchers: CoroutineDispatcherProvider, dispatchers: CoroutineDispatcherProvider,
analyticsEventHandler: AnalyticsEventHandler,
): MarketsTokenRepository { ): MarketsTokenRepository {
return DefaultMarketsTokenRepository( return DefaultMarketsTokenRepository(
marketsApi = marketsApi, marketsApi = marketsApi,
tangemTechApi = tangemTechApi, tangemTechApi = tangemTechApi,
dispatcherProvider = dispatchers, dispatcherProvider = dispatchers,
userWalletsStore = userWalletsStore, userWalletsStore = userWalletsStore,
analyticsEventHandler = analyticsEventHandler,
) )
} }
} }

View file

@ -9,11 +9,16 @@ class GetTokenMarketInfoUseCase(
private val marketsTokenRepository: MarketsTokenRepository, 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 { return Either.catch {
marketsTokenRepository.getTokenInfo( marketsTokenRepository.getTokenInfo(
fiatCurrencyCode = appCurrency.code, fiatCurrencyCode = appCurrency.code,
tokenId = tokenId, tokenId = tokenId,
tokenSymbol = tokenSymbol,
languageCode = SupportedLanguages.getCurrentSupportedLanguageCode(), languageCode = SupportedLanguages.getCurrentSupportedLanguageCode(),
) )
}.mapLeft {} }.mapLeft {}

View file

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

View file

@ -12,11 +12,26 @@ interface MarketsTokenRepository {
nextBatchSize: Int, nextBatchSize: Int,
): TokenListBatchFlow ): 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 suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes

View file

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

View file

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

View file

@ -63,6 +63,8 @@ dependencies {
implementation(projects.core.decompose) implementation(projects.core.decompose)
implementation(projects.core.ui) implementation(projects.core.ui)
implementation(projects.core.featuretoggles) implementation(projects.core.featuretoggles)
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
/* Common */ /* Common */
implementation(projects.common.ui) 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.LifecycleStartEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network 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.AppComponentContext
import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel 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.core.ui.res.TangemTheme
import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params 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.MarketsTokenDetailsModel
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent
@ -29,6 +31,7 @@ import kotlinx.coroutines.launch
internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext, @Assisted appComponentContext: AppComponentContext,
@Assisted params: Params, @Assisted params: Params,
analyticsEventHandler: AnalyticsEventHandler,
portfolioComponentFactory: MarketsPortfolioComponent.Factory, portfolioComponentFactory: MarketsPortfolioComponent.Factory,
) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent { ) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent {
@ -38,12 +41,17 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
id = getTokenIdIfL2Network(params.token.id), id = getTokenIdIfL2Network(params.token.id),
), ),
) )
private val analyticsParams = params.analyticsParams
private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams) private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams)
private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.showPortfolio) { private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.showPortfolio) {
portfolioComponentFactory.create( portfolioComponentFactory.create(
context = child("my_portfolio"), context = child("my_portfolio"),
params = MarketsPortfolioComponent.Params(updatedParams.token), params = MarketsPortfolioComponent.Params(
updatedParams.token,
analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) },
),
) )
} else { } else {
null 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 @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.MarketChartData
import com.tangem.common.ui.charts.state.MarketChartDataProducer import com.tangem.common.ui.charts.state.MarketChartDataProducer
import com.tangem.common.ui.charts.state.sorted 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.di.ComponentScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer 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.appcurrency.model.AppCurrency
import com.tangem.domain.markets.* import com.tangem.domain.markets.*
import com.tangem.features.markets.details.MarketsTokenDetailsComponent 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.DescriptionConverter
import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter
import com.tangem.features.markets.details.impl.model.formatter.* import com.tangem.features.markets.details.impl.model.formatter.*
@ -56,10 +58,12 @@ internal class MarketsTokenDetailsModel @Inject constructor(
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase, private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase,
private val urlOpener: UrlOpener, private val urlOpener: UrlOpener,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() { ) : Model() {
private var quotesJob = JobHolder() private var quotesJob = JobHolder()
private val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>() private val params = paramsContainer.require<MarketsTokenDetailsComponent.Params>()
private val analyticsEventBuilder = MarketDetailsAnalyticsEvent.EventBuilder(token = params.token)
private val currentAppCurrency = getSelectedAppCurrencyUseCase() private val currentAppCurrency = getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency -> .map { maybeAppCurrency ->
@ -75,14 +79,36 @@ internal class MarketsTokenDetailsModel @Inject constructor(
onInfoClick = { onInfoClick = {
showInfoBottomSheet(it) showInfoBottomSheet(it)
}, },
onLinkClick = { onLinkClick = { link ->
urlOpener.openUrl(it.url) 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( private val descriptionConverter = DescriptionConverter(
onReadModeClicked = { onReadModeClicked = {
showInfoBottomSheet(it) showInfoBottomSheet(it)
// === Analytics ===
analyticsEventHandler.send(analyticsEventBuilder.readMoreClicked())
}, },
) )
@ -167,6 +193,14 @@ internal class MarketsTokenDetailsModel @Inject constructor(
currentQuotes = currentQuotes, currentQuotes = currentQuotes,
lastUpdatedTimestamp = lastUpdatedTimestamp, lastUpdatedTimestamp = lastUpdatedTimestamp,
currentTokenInfo = currentTokenInfo, currentTokenInfo = currentTokenInfo,
onPricePerformanceIntervalChanged = {
analyticsEventHandler.send(
analyticsEventBuilder.intervalChanged(
intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance,
interval = it,
),
)
},
) )
private val loadChartJobHolder = JobHolder() private val loadChartJobHolder = JobHolder()
@ -221,6 +255,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
appCurrency = currentAppCurrency.value, appCurrency = currentAppCurrency.value,
interval = interval, interval = interval,
tokenId = params.token.id, tokenId = params.token.id,
tokenSymbol = params.token.symbol,
preview = false, preview = false,
) )
@ -290,6 +325,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
val tokenMarketInfo = getTokenMarketInfoUseCase( val tokenMarketInfo = getTokenMarketInfoUseCase(
appCurrency = currentAppCurrency.value, appCurrency = currentAppCurrency.value,
tokenId = params.token.id, tokenId = params.token.id,
tokenSymbol = params.token.symbol,
) )
tokenMarketInfo.fold( tokenMarketInfo.fold(
@ -368,6 +404,15 @@ internal class MarketsTokenDetailsModel @Inject constructor(
private fun onSelectedIntervalChange(interval: PriceChangeInterval) { private fun onSelectedIntervalChange(interval: PriceChangeInterval) {
if (state.value.selectedInterval == interval) return if (state.value.selectedInterval == interval) return
// === Analytics ===
analyticsEventHandler.send(
analyticsEventBuilder.intervalChanged(
intervalType = MarketDetailsAnalyticsEvent.IntervalType.Chart,
interval = interval,
),
)
// ==================
val quotes = currentQuotes.value val quotes = currentQuotes.value
val priceChangePercent = quotes.getFormattedPercentByInterval(interval) val priceChangePercent = quotes.getFormattedPercentByInterval(interval)

View file

@ -32,6 +32,7 @@ internal class DescriptionConverter(
), ),
), ),
body = stringReference(value.fullDescription ?: ""), 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.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketInfo 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.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.InfoPointUM import com.tangem.features.markets.details.impl.ui.state.InfoPointUM
@ -21,6 +22,7 @@ import java.math.BigDecimal
internal class InsightsConverter( internal class InsightsConverter(
private val appCurrency: Provider<AppCurrency>, private val appCurrency: Provider<AppCurrency>,
private val onInfoClick: (InfoBottomSheetContent) -> Unit, private val onInfoClick: (InfoBottomSheetContent) -> Unit,
private val onIntervalChanged: (PriceChangeInterval) -> Unit,
) : Converter<TokenMarketInfo.Insights, InsightsUM> { ) : Converter<TokenMarketInfo.Insights, InsightsUM> {
override fun convert(value: 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 package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.markets.TokenMarketInfo 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
import com.tangem.features.markets.details.impl.ui.state.LinksUM.Link
import com.tangem.features.markets.impl.R import com.tangem.features.markets.impl.R
import com.tangem.utils.converter.Converter import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@ -25,7 +25,7 @@ internal class LinksConverter(
private fun TokenMarketInfo.Link.convert(): LinksUM.Link { private fun TokenMarketInfo.Link.convert(): LinksUM.Link {
return LinksUM.Link( return LinksUM.Link(
title = stringReference(title), title = title,
iconRes = getIconById(id), iconRes = getIconById(id),
url = link, url = link,
) )

View file

@ -3,6 +3,7 @@ package com.tangem.features.markets.details.impl.model.converters
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM
import com.tangem.utils.Provider import com.tangem.utils.Provider
@ -13,6 +14,7 @@ import java.math.RoundingMode
@Stable @Stable
internal class PricePerformanceConverter( internal class PricePerformanceConverter(
private val appCurrency: Provider<AppCurrency>, private val appCurrency: Provider<AppCurrency>,
private val onIntervalChanged: (PriceChangeInterval) -> Unit,
) { ) {
fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM { fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM {
@ -20,6 +22,7 @@ internal class PricePerformanceConverter(
h24 = value.day.convert(currentPrice), h24 = value.day.convert(currentPrice),
month = value.month.convert(currentPrice), month = value.month.convert(currentPrice),
all = value.allTime.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 androidx.compose.runtime.Stable
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketInfo 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.InfoBottomSheetContent
import com.tangem.features.markets.details.impl.ui.state.LinksUM import com.tangem.features.markets.details.impl.ui.state.LinksUM
@ -14,15 +15,24 @@ internal class TokenMarketInfoConverter(
appCurrency: Provider<AppCurrency>, appCurrency: Provider<AppCurrency>,
onInfoClick: (InfoBottomSheetContent) -> Unit, onInfoClick: (InfoBottomSheetContent) -> Unit,
onLinkClick: (LinksUM.Link) -> Unit, onLinkClick: (LinksUM.Link) -> Unit,
onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit,
onInsightsIntervalChanged: (PriceChangeInterval) -> Unit,
) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.InformationBlocks> { ) : Converter<TokenMarketInfo, MarketsTokenDetailsUM.InformationBlocks> {
private val insightsConverter = InsightsConverter(appCurrency = appCurrency, onInfoClick = onInfoClick) private val insightsConverter = InsightsConverter(
appCurrency = appCurrency,
onInfoClick = onInfoClick,
onIntervalChanged = onInsightsIntervalChanged,
)
@Suppress("UnusedPrivateMember") @Suppress("UnusedPrivateMember")
// TODO second markets iteration // TODO second markets iteration
private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick) private val securityScoreConverter = SecurityScoreConverter(onInfoClick = onInfoClick)
private val metricsConverter = MetricsConverter(appCurrency = appCurrency, 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) private val linksConverter = LinksConverter(onLinkClick = onLinkClick)
override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks { 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.consumedEvent
import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.event.triggeredEvent
import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.PriceChangeInterval
import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketInfo
import com.tangem.domain.markets.TokenQuotes import com.tangem.domain.markets.TokenQuotes
import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter
@ -22,8 +23,12 @@ internal class QuotesStateUpdater(
private val currentQuotes: MutableStateFlow<TokenQuotes>, private val currentQuotes: MutableStateFlow<TokenQuotes>,
private val lastUpdatedTimestamp: MutableStateFlow<Long>, private val lastUpdatedTimestamp: MutableStateFlow<Long>,
private val currentTokenInfo: MutableStateFlow<TokenMarketInfo?>, 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) { suspend fun updateQuotes(newQuotes: TokenQuotes) {
val triggerPriceChangeType = getFormattedPriceChange( val triggerPriceChangeType = getFormattedPriceChange(

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,15 @@ import kotlinx.serialization.Serializable
interface MarketsPortfolioComponent : ComposableContentComponent { interface MarketsPortfolioComponent : ComposableContentComponent {
@Serializable @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>) 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 androidx.compose.runtime.Stable
import arrow.core.getOrElse import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer 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.domain.wallets.usecase.GetSelectedWalletUseCase
import com.tangem.features.markets.impl.R import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent 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.loader.PortfolioDataLoader
import com.tangem.features.markets.portfolio.impl.ui.WarningDialog import com.tangem.features.markets.portfolio.impl.ui.WarningDialog
import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM 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.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -49,12 +52,17 @@ internal class MarketsPortfolioModel @Inject constructor(
private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase,
private val saveMarketTokensUseCase: SaveMarketTokensUseCase, private val saveMarketTokensUseCase: SaveMarketTokensUseCase,
private val addToPortfolioManager: AddToPortfolioManager, private val addToPortfolioManager: AddToPortfolioManager,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() { ) : Model() {
val state: StateFlow<MyPortfolioUM> get() = _state val state: StateFlow<MyPortfolioUM> get() = _state
private val _state: MutableStateFlow<MyPortfolioUM> = MutableStateFlow(value = MyPortfolioUM.Loading) private val _state: MutableStateFlow<MyPortfolioUM> = MutableStateFlow(value = MyPortfolioUM.Loading)
private val params = paramsContainer.require<MarketsPortfolioComponent.Params>() 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 */ /** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */
private val selectedMultiWalletIdFlow = MutableStateFlow<UserWalletId?>(value = null) private val selectedMultiWalletIdFlow = MutableStateFlow<UserWalletId?>(value = null)
@ -72,14 +80,37 @@ internal class MarketsPortfolioModel @Inject constructor(
) )
private val factory = MyPortfolioUMFactory( private val factory = MyPortfolioUMFactory(
onAddClick = { onAddToPortfolioBSVisibilityChange(isShow = true) }, onAddClick = {
onAddToPortfolioBSVisibilityChange(isShow = true)
// === Analytics ===
analyticsEventHandler.send(
analyticsEventBuilder.addToPortfolioClicked(),
)
},
addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory( addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory(
token = params.token, token = params.token,
onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange, onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange,
onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange, onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange,
onNetworkSwitchClick = ::onNetworkSwitchClick, onNetworkSwitchClick = ::onNetworkSwitchClick,
onWalletSelect = ::onWalletSelect, onWalletSelect = {
onContinueClick = ::onContinueClick, 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 }, currentState = Provider { _state.value },
tokenActionsHandler = tokenActionsIntentsFactory.create( tokenActionsHandler = tokenActionsIntentsFactory.create(
@ -87,6 +118,14 @@ internal class MarketsPortfolioModel @Inject constructor(
updateTokenReceiveBSConfig = { updateBlock -> updateTokenReceiveBSConfig = { updateBlock ->
updateTokensState { it.copy(tokenReceiveBSConfig = updateBlock(it.tokenReceiveBSConfig)) } 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 -> updateTokens = { updateBlock ->
updateTokensState { state -> updateTokensState { state ->

View file

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

View file

@ -40,6 +40,7 @@ internal class TokenMarketBlockModel @Inject constructor(
) : Model() { ) : Model() {
private val params = paramsContainer.require<TokenMarketBlockComponent.Params>() private val params = paramsContainer.require<TokenMarketBlockComponent.Params>()
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = false) private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = false)
private val currentAppCurrency = getSelectedAppCurrencyUseCase() private val currentAppCurrency = getSelectedAppCurrencyUseCase()
@ -56,7 +57,7 @@ internal class TokenMarketBlockModel @Inject constructor(
val state = MutableStateFlow( val state = MutableStateFlow(
TokenMarketBlockUM( TokenMarketBlockUM(
currencySymbol = params.tokenSymbol, currencySymbol = params.cryptoCurrency.symbol,
currentPrice = null, currentPrice = null,
h24Percent = null, h24Percent = null,
priceChangeType = PriceChangeType.NEUTRAL, priceChangeType = PriceChangeType.NEUTRAL,
@ -72,7 +73,7 @@ internal class TokenMarketBlockModel @Inject constructor(
private fun startFetching() { private fun startFetching() {
modelScope.launch { modelScope.launch {
getTokenQuotesUseCase( getTokenQuotesUseCase(
currencyID = params.cryptoCurrencyID, currencyID = params.cryptoCurrency.id,
interval = PriceChangeInterval.H24, interval = PriceChangeInterval.H24,
refresh = true, refresh = true,
).collect { ).collect {
@ -100,9 +101,12 @@ internal class TokenMarketBlockModel @Inject constructor(
} }
}.saveIn(quotesUpdateJobHolder) }.saveIn(quotesUpdateJobHolder)
val tokenId = params.cryptoCurrency.id.rawCurrencyId ?: return
modelScope.launch(dispatchers.main) { modelScope.launch(dispatchers.main) {
val result = getTokenPriceChartUseCase( val result = getTokenPriceChartUseCase(
tokenId = params.tokenId, tokenId = tokenId,
tokenSymbol = params.cryptoCurrency.symbol,
interval = PriceChangeInterval.H24, interval = PriceChangeInterval.H24,
appCurrency = currentAppCurrency.value, // TODO get currency from quotes use case [REDACTED_TASK_KEY] appCurrency = currentAppCurrency.value, // TODO get currency from quotes use case [REDACTED_TASK_KEY]
preview = true, preview = true,
@ -128,12 +132,13 @@ internal class TokenMarketBlockModel @Inject constructor(
private fun navigateToMarketDetails() { private fun navigateToMarketDetails() {
val quotes = quotesState ?: return val quotes = quotesState ?: return
val tokenId = params.cryptoCurrency.id.rawCurrencyId ?: return
val tokenParam = TokenMarketParams( val tokenParam = TokenMarketParams(
id = params.tokenId, id = tokenId,
name = params.tokenSymbol, name = params.cryptoCurrency.name,
imageUrl = params.tokenImageUrl, imageUrl = params.cryptoCurrency.iconUrl,
symbol = params.tokenSymbol, symbol = params.cryptoCurrency.symbol,
tokenQuotes = TokenMarketParams.Quotes( tokenQuotes = TokenMarketParams.Quotes(
currentPrice = quotes.currentPrice, currentPrice = quotes.currentPrice,
h24Percent = quotes.h24Percent, h24Percent = quotes.h24Percent,
@ -147,6 +152,10 @@ internal class TokenMarketBlockModel @Inject constructor(
token = tokenParam, token = tokenParam,
appCurrency = currentAppCurrency.value, appCurrency = currentAppCurrency.value,
showPortfolio = false, 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 androidx.compose.runtime.Stable
import arrow.core.getOrElse import arrow.core.getOrElse
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.decompose.di.ComponentScoped import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.Model
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase 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.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.TokenMarket import com.tangem.domain.markets.TokenMarket
import com.tangem.features.markets.entry.BottomSheetState 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.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.ListUM
import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM import com.tangem.features.markets.tokenlist.impl.ui.state.MarketsListItemUM
import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM import com.tangem.features.markets.tokenlist.impl.ui.state.SortByTypeUM
@ -32,6 +34,7 @@ internal class MarketsListModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider, override val dispatchers: CoroutineDispatcherProvider,
getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val analyticsEventHandler: AnalyticsEventHandler,
) : Model() { ) : Model() {
private var updateQuotesJob = JobHolder() private var updateQuotesJob = JobHolder()
@ -221,10 +224,30 @@ internal class MarketsListModel @Inject constructor(
} }
} }
// analytics
initAnalytics()
// initial loading // initial loading
mainMarketsListManager.reload() 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) { private fun onTokenUIClicked(token: MarketsListItemUM) {
modelScope.launch { modelScope.launch {
activeListManager.getTokenById(token.id)?.let { found -> activeListManager.getTokenById(token.id)?.let { found ->

View file

@ -85,15 +85,9 @@ internal class TokenDetailsFragment : ComposeFragment() {
} }
private fun CryptoCurrency.toParam(): TokenMarketBlockComponent.Params? { 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( return TokenMarketBlockComponent.Params(cryptoCurrency = this)
cryptoCurrencyID = id,
tokenId = tokenId,
tokenName = name,
tokenSymbol = symbol,
tokenImageUrl = iconUrl,
)
} }
@Composable @Composable

View file

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