Updated on 2026-08-14
This commit is contained in:
parent
b32cc4a928
commit
2ba0f3d4a1
42 changed files with 581 additions and 74 deletions
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ internal class DescriptionConverter(
|
|||
),
|
||||
),
|
||||
body = stringReference(value.fullDescription ?: ""),
|
||||
showGeneratedAINotification = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -78,6 +78,10 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
|
|||
token = token.toSerializableParam(),
|
||||
appCurrency = appCurrency,
|
||||
showPortfolio = true,
|
||||
analyticsParams = MarketsTokenDetailsComponent.AnalyticsParams(
|
||||
blockchain = null,
|
||||
source = "Market",
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue