Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-22 14:59:11 +03:00
parent d6735f937f
commit 95aa4b7a0a
35 changed files with 839 additions and 209 deletions

View file

@ -1,10 +1,8 @@
package com.tangem.tap.di.domain
import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase
import com.tangem.domain.markets.GetTokenMarketInfoUseCase
import com.tangem.domain.markets.GetTokenPriceChartUseCase
import com.tangem.domain.markets.GetTokenQuotesUseCase
import com.tangem.domain.markets.*
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.tokens.repository.QuotesRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -37,7 +35,13 @@ object MarketsDomainModule {
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenQuotesUseCase {
return GetTokenQuotesUseCase(marketsTokenRepository = marketsTokenRepository)
fun provideTokenFullQuotesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenFullQuotesUseCase {
return GetTokenFullQuotesUseCase(marketsTokenRepository = marketsTokenRepository)
}
@Provides
@Singleton
fun provideGetTokenQuotesUseCase(quotesRepository: QuotesRepository): GetTokenQuotesUseCase {
return GetTokenQuotesUseCase(quotesRepository = quotesRepository)
}
}

View file

@ -11,6 +11,7 @@ import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.disclaimer.api.components.DisclaimerComponent
import com.tangem.features.managetokens.ManageTokensToggles
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.pushnotifications.api.featuretoggles.PushNotificationsFeatureToggles
import com.tangem.features.pushnotifications.api.navigation.PushNotificationsRouter
import com.tangem.features.send.api.navigation.SendRouter
@ -50,6 +51,7 @@ internal class ChildFactory @Inject constructor(
private val walletSettingsComponentFactory: WalletSettingsComponent.Factory,
private val disclaimerComponentFactory: DisclaimerComponent.Factory,
private val manageTokensComponentFactory: ManageTokensComponent.Factory,
private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory,
private val sendRouter: SendRouter,
private val tokenDetailsRouter: TokenDetailsRouter,
private val walletRouter: WalletRouter,
@ -185,6 +187,16 @@ internal class ChildFactory @Inject constructor(
componentFactory = walletSettingsComponentFactory,
)
}
is AppRoute.MarketsTokenDetails -> {
route.asComponentChild(
contextProvider = contextProvider(route, contextFactory),
params = MarketsTokenDetailsComponent.Params(
token = route.token,
appCurrency = route.appCurrency,
),
componentFactory = marketsTokenDetailsComponentFactory,
)
}
}
}

View file

@ -19,6 +19,8 @@ dependencies {
implementation(projects.domain.tokens.models)
implementation(projects.domain.wallets.models)
implementation(projects.domain.staking.models)
implementation(projects.domain.markets.models)
implementation(projects.domain.appCurrency.models)
/* Libs - Other */
api(deps.kotlin.serialization)

View file

@ -5,6 +5,8 @@ import com.tangem.common.routing.bundle.RouteBundleParams
import com.tangem.common.routing.bundle.bundle
import com.tangem.common.routing.entity.SerializableIntent
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParam
import com.tangem.domain.qrscanning.models.SourceType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrency
@ -262,4 +264,10 @@ sealed class AppRoute(val path: String) : Route {
data class WalletSettings(
val userWalletId: UserWalletId,
) : AppRoute(path = "/wallet_settings/${userWalletId.stringValue}")
@Serializable
data class MarketsTokenDetails(
val token: TokenMarketParam,
val appCurrency: AppCurrency,
) : AppRoute(path = "/markets_token_details/${token.id}")
}

View file

@ -0,0 +1,51 @@
package com.tangem.common.routing.utils
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.decompose.navigation.Router
import kotlin.reflect.KClass
/**
* Temporary solution to convert [AppRouter] to [Router].
* (through manual ComponentContext creation).
*
* **Will be removed when all screens will be migrated to Decompose.**
*
* @return [Router] that wraps [AppRouter].
*/
fun AppRouter.asRouter(): Router {
return RouterProxy(appRouter = this)
}
private class RouterProxy(
private val appRouter: AppRouter,
) : Router {
override fun push(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
(route as? AppRoute)?.let {
appRouter.push(it, onComplete)
}
}
override fun replaceAll(vararg routes: Route, onComplete: (isSuccess: Boolean) -> Unit) {
routes.filterIsInstance<AppRoute>().let {
appRouter.replaceAll(*it.toTypedArray(), onComplete = onComplete)
}
}
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
appRouter.pop(onComplete)
}
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) {
(route as? AppRoute)?.let {
appRouter.popTo(route, onComplete)
}
}
@Suppress("UNCHECKED_CAST")
override fun popTo(routeClass: KClass<out Route>, onComplete: (isSuccess: Boolean) -> Unit) {
appRouter.popTo(routeClass as KClass<out AppRoute>, onComplete)
}
}

View file

@ -22,6 +22,11 @@ class PriceAndTimePointValuesConverter(
private val formatYValuesCache = mutableMapOf<Double, BigDecimal>()
private val formatXValuesCache = mutableMapOf<Double, BigDecimal>()
private data class Point(
val x: BigDecimal,
val y: BigDecimal,
)
override fun convert(data: MarketChartData.Data): MarketChartRawData {
formatYValuesCache.clear()
formatXValuesCache.clear()
@ -33,8 +38,10 @@ class PriceAndTimePointValuesConverter(
)
minMaxCache = cache
val normY = data.y.normalizeToDouble(min = cache.minY, max = cache.maxY)
val normX = data.x.normalizeTime(min = cache.minX, max = cache.maxX)
val points = data.x.zip(data.y) { x, y -> Point(x, y) }.sortedBy { it.x }
val normY = points.map { it.y }.normalizeToDouble(min = cache.minY, max = cache.maxY)
val normX = points.map { it.x }.normalizeTime(min = cache.minX, max = cache.maxX)
return if (normX.size > MAX_POINTS) {
LTThreeBuckets

View file

@ -19,6 +19,7 @@ class DefaultAppComponentContext(
messageHandler: UiMessageHandler,
override val dispatchers: CoroutineDispatcherProvider,
override val hiltComponentBuilder: DecomposeComponent.Builder,
private val replaceRouter: Router? = null,
) : AppComponentContext, ComponentContext by componentContext {
override val tags: HashMap<String, Any> = HashMap()
@ -31,5 +32,5 @@ class DefaultAppComponentContext(
get() = instanceKeeper.getOrCreate { DefaultAppNavigationProvider() }
override val router: Router
get() = instanceKeeper.getOrCreate { DefaultRouter(navigationProvider) }
get() = replaceRouter ?: instanceKeeper.getOrCreate { DefaultRouter(navigationProvider) }
}

View file

@ -1,6 +1,21 @@
package com.tangem.core.ui.components.marketprice
import java.math.BigDecimal
import java.math.RoundingMode
/** Price changing type */
enum class PriceChangeType {
UP, DOWN, NEUTRAL,
;
companion object {
@Suppress("MagicNumber")
fun fromBigDecimal(priceChangePercent: BigDecimal): PriceChangeType {
return when {
priceChangePercent < BigDecimal.ZERO -> DOWN
priceChangePercent.setScale(4, RoundingMode.HALF_UP) > BigDecimal.ZERO -> UP
else -> NEUTRAL
}
}
}
}

View file

@ -108,6 +108,22 @@ internal class DefaultMarketsTokenRepository(
return TokenChartConverter.convert(interval, response.getOrThrow())
}
override suspend fun getChartPreview(
fiatCurrencyCode: String,
interval: PriceChangeInterval,
tokenId: String,
): TokenChart {
val response = marketsApi.getCoinsListCharts(
coinIds = tokenId,
currency = fiatCurrencyCode,
interval = interval.toRequestParam(),
)
val chart = response.getOrThrow()[tokenId] ?: error("No chart preview data for token $tokenId")
return TokenChartConverter.convert(interval, chart)
}
override suspend fun getTokenInfo(
fiatCurrencyCode: String,
tokenId: String,

View file

@ -11,12 +11,15 @@ android {
dependencies {
/* Domain */
api(projects.domain.appCurrency.models)
api(projects.domain.core)
api(projects.core.pagination)
api(projects.domain.markets.models)
implementation(deps.kotlin.serialization)
implementation(projects.domain.tokens.models)
implementation(projects.domain.tokens)
/* Utils */
implementation(deps.kotlin.serialization)
implementation(projects.core.utils)
}

View file

@ -1,35 +1,32 @@
package com.tangem.features.markets.details
package com.tangem.domain.markets
import com.tangem.domain.core.serialization.SerializedBigDecimal
import com.tangem.domain.markets.TokenMarket
import kotlinx.serialization.Serializable
@Serializable
data class TokenMarketSerializable(
data class TokenMarketParam(
val id: String,
val name: String,
val symbol: String,
val marketCap: SerializedBigDecimal?,
val tokenQuotes: Quotes,
val imageUrl: String,
val imageUrl: String?,
) {
@Serializable
data class Quotes(
val currentPrice: SerializedBigDecimal,
val h24Percent: SerializedBigDecimal,
val weekPercent: SerializedBigDecimal,
val monthPercent: SerializedBigDecimal,
val weekPercent: SerializedBigDecimal?,
val monthPercent: SerializedBigDecimal?,
)
}
fun TokenMarket.toSerializable(): TokenMarketSerializable {
return TokenMarketSerializable(
fun TokenMarket.toSerializableParam(): TokenMarketParam {
return TokenMarketParam(
id = id,
name = name,
symbol = symbol,
marketCap = marketCap,
tokenQuotes = TokenMarketSerializable.Quotes(
tokenQuotes = TokenMarketParam.Quotes(
currentPrice = tokenQuotesShort.currentPrice,
h24Percent = tokenQuotesShort.h24ChangePercent,
weekPercent = tokenQuotesShort.weekChangePercent,

View file

@ -0,0 +1,18 @@
package com.tangem.domain.markets
import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.repositories.MarketsTokenRepository
class GetTokenFullQuotesUseCase(
private val marketsTokenRepository: MarketsTokenRepository,
) {
suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either<Unit, TokenQuotes> {
return Either.catch {
marketsTokenRepository.getTokenQuotes(
fiatCurrencyCode = appCurrency.code,
tokenId = tokenId,
)
}.mapLeft {}
}
}

View file

@ -12,13 +12,22 @@ class GetTokenPriceChartUseCase(
appCurrency: AppCurrency,
interval: PriceChangeInterval,
tokenId: String,
preview: Boolean,
): Either<Unit, TokenChart> {
return Either.catch {
marketsTokenRepository.getChart(
fiatCurrencyCode = appCurrency.code,
interval = interval,
tokenId = tokenId,
)
if (preview) {
marketsTokenRepository.getChartPreview(
fiatCurrencyCode = appCurrency.code,
interval = interval,
tokenId = tokenId,
)
} else {
marketsTokenRepository.getChart(
fiatCurrencyCode = appCurrency.code,
interval = interval,
tokenId = tokenId,
)
}
}.mapLeft {}
}
}

View file

@ -1,19 +1,26 @@
package com.tangem.domain.markets
import arrow.core.Either
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.repositories.MarketsTokenRepository
import com.tangem.domain.tokens.model.Quote
import com.tangem.domain.tokens.repository.QuotesRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@Suppress("UnusedPrivateMember")
class GetTokenQuotesUseCase(
private val marketsTokenRepository: MarketsTokenRepository,
private val quotesRepository: QuotesRepository,
) {
suspend operator fun invoke(appCurrency: AppCurrency, tokenId: String): Either<Unit, TokenQuotes> {
return Either.catch {
marketsTokenRepository.getTokenQuotes(
fiatCurrencyCode = appCurrency.code,
tokenId = tokenId,
)
}.mapLeft {}
operator fun invoke(tokenId: String, interval: PriceChangeInterval): Flow<Either<Unit, Quote>> {
return flowOf(
Either.catch {
Quote(
rawCurrencyId = "USD",
fiatRate = 100.toBigDecimal(), // mock
priceChange = 10.toBigDecimal(), // mock
)
}.mapLeft {},
)
// TODO implement quotes fetching from repository [REDACTED_TASK_KEY]
// quotesRepository.getQuotesUpdates()
}
}

View file

@ -12,6 +12,8 @@ interface MarketsTokenRepository {
suspend fun getChart(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
suspend fun getChartPreview(fiatCurrencyCode: String, interval: PriceChangeInterval, tokenId: String): TokenChart
suspend fun getTokenInfo(fiatCurrencyCode: String, tokenId: String, languageCode: String): TokenMarketInfo
suspend fun getTokenQuotes(fiatCurrencyCode: String, tokenId: String): TokenQuotes

View file

@ -5,17 +5,19 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarketParam
import com.tangem.features.markets.entry.BottomSheetState
import kotlinx.serialization.Serializable
@Stable
interface MarketsTokenDetailsComponent {
interface MarketsTokenDetailsComponent : ComposableContentComponent {
@Serializable
data class Params(
val token: TokenMarketSerializable,
val token: TokenMarketParam,
val appCurrency: AppCurrency,
)
@ -26,7 +28,5 @@ interface MarketsTokenDetailsComponent {
modifier: Modifier,
)
interface Factory {
fun create(context: AppComponentContext, params: Params, onBack: () -> Unit): MarketsTokenDetailsComponent
}
interface Factory : ComponentFactory<Params, MarketsTokenDetailsComponent>
}

View file

@ -3,7 +3,6 @@ package com.tangem.features.markets.token.block
import androidx.compose.runtime.Stable
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.tokens.model.CryptoCurrency
import kotlinx.serialization.Serializable
@Stable
@ -11,7 +10,10 @@ interface TokenMarketBlockComponent : ComposableContentComponent {
@Serializable
data class Params(
val cryptoCurrency: CryptoCurrency,
val tokenId: String,
val tokenName: String,
val tokenSymbol: String,
val tokenImageUrl: String?,
)
interface Factory {

View file

@ -51,4 +51,5 @@ dependencies {
/* Common */
implementation(projects.common.ui)
implementation(projects.common.uiCharts)
implementation(projects.common.routing)
}

View file

@ -8,11 +8,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.markets.entry.BottomSheetState
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.model.MarketsTokenDetailsModel
import com.tangem.features.markets.details.impl.model.state.TokenNetworksState
import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -23,8 +26,7 @@ import kotlinx.coroutines.launch
@Stable
internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: MarketsTokenDetailsComponent.Params,
@Assisted private val onBack: () -> Unit,
@Assisted params: Params,
portfolioComponentFactory: MarketsPortfolioComponent.Factory,
) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent {
@ -64,26 +66,50 @@ internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor(
val bsState by bottomSheetState
LaunchedEffect(bsState) {
model.containerBottomSheetState.value = bsState
model.isVisibleOnScreen.value = bsState == BottomSheetState.EXPANDED
}
MarketsTokenDetailsContent(
state = state,
onBackClick = onBack,
onHeaderSizeChange = onHeaderSizeChange,
portfolioBlock = { modifier ->
portfolioComponent.Content(modifier)
},
modifier = modifier,
backgroundColor = LocalMainBottomSheetColor.current.value,
addTopBarStatusBarPadding = false,
state = state,
onBackClick = ::navigateBack,
onHeaderSizeChange = onHeaderSizeChange,
portfolioBlock = { blockModifier ->
portfolioComponent.Content(blockModifier)
},
)
}
@Composable
override fun Content(modifier: Modifier) {
LifecycleStartEffect(Unit) {
model.isVisibleOnScreen.value = true
onStopOrDispose {
model.isVisibleOnScreen.value = false
}
}
val state by model.state.collectAsStateWithLifecycle()
MarketsTokenDetailsContent(
modifier = modifier,
backgroundColor = TangemTheme.colors.background.tertiary,
addTopBarStatusBarPadding = true,
state = state,
onBackClick = ::navigateBack,
onHeaderSizeChange = {},
portfolioBlock = { blockModifier ->
portfolioComponent.Content(blockModifier)
},
)
}
private fun navigateBack() = router.pop()
@AssistedFactory
interface Factory : MarketsTokenDetailsComponent.Factory {
override fun create(
context: AppComponentContext,
params: MarketsTokenDetailsComponent.Params,
onBack: () -> Unit,
): DefaultMarketsTokenDetailsComponent
override fun create(context: AppComponentContext, params: Params): DefaultMarketsTokenDetailsComponent
}
}

View file

@ -18,7 +18,6 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.*
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter
import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter
@ -52,7 +51,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase,
private val getTokenQuotesUseCase: GetTokenQuotesUseCase,
private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase,
private val urlOpener: UrlOpener,
) : Model() {
@ -118,7 +117,6 @@ internal class MarketsTokenDetailsModel @Inject constructor(
private var lastUpdatedTimestamp: Long = DateTime.now().millis
val containerBottomSheetState = MutableStateFlow(BottomSheetState.COLLAPSED)
val isVisibleOnScreen = MutableStateFlow(false)
val networksState = MutableStateFlow<TokenNetworksState>(TokenNetworksState.Loading)
@ -135,11 +133,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
percent = params.token.tokenQuotes.h24Percent,
useAbsoluteValue = true,
),
priceChangeType = if (params.token.tokenQuotes.h24Percent < BigDecimal.ZERO) {
PriceChangeType.DOWN
} else {
PriceChangeType.UP
},
priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(),
iconUrl = params.token.imageUrl,
chartState = MarketsTokenDetailsUM.ChartState(
dataProducer = chartDataProducer,
@ -184,7 +178,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
private fun loadQuotes() {
modelScope.launch {
val result = getTokenQuotesUseCase(
val result = getTokenFullQuotesUseCase(
tokenId = params.token.id,
appCurrency = currentAppCurrency.value,
)
@ -213,6 +207,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
appCurrency = currentAppCurrency.value,
interval = interval,
tokenId = params.token.id,
preview = false,
)
state.update {
@ -246,6 +241,11 @@ internal class MarketsTokenDetailsModel @Inject constructor(
chartState = it.chartState.copy(
status = MarketsTokenDetailsUM.ChartState.Status.DATA,
),
body = if (it.body is MarketsTokenDetailsUM.Body.Nothing) {
MarketsTokenDetailsUM.Body.Error(onLoadRetryClick = ::onLoadRetryClicked)
} else {
it.body
},
)
}
}.onLeft {
@ -488,7 +488,7 @@ internal class MarketsTokenDetailsModel @Inject constructor(
while (true) {
delay(timeMillis)
// Update quotes only when the container bottom sheet is in the expanded state
containerBottomSheetState.first { it == BottomSheetState.EXPANDED }
// and is visible on the screen
isVisibleOnScreen.first { it }

View file

@ -48,11 +48,13 @@ internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): Bi
}
}
@Suppress("MagicNumber")
internal fun BigDecimal?.percentChangeType(): PriceChangeType {
val scaled = this?.setScale(4, RoundingMode.HALF_UP)
return when {
this == null -> PriceChangeType.NEUTRAL
this > BigDecimal.ZERO -> PriceChangeType.UP
this < BigDecimal.ZERO -> PriceChangeType.DOWN
scaled == null -> PriceChangeType.NEUTRAL
scaled > BigDecimal.ZERO -> PriceChangeType.UP
scaled < BigDecimal.ZERO -> PriceChangeType.DOWN
else -> PriceChangeType.NEUTRAL
}
}

View file

@ -11,6 +11,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
@ -33,7 +34,6 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.disableNestedScroll
@ -45,10 +45,12 @@ import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.impl.R
import kotlinx.collections.immutable.persistentListOf
@Suppress("UnusedPrivateMember")
@Suppress("LongParameterList")
@Composable
internal fun MarketsTokenDetailsContent(
state: MarketsTokenDetailsUM,
backgroundColor: Color,
addTopBarStatusBarPadding: Boolean,
onBackClick: () -> Unit,
onHeaderSizeChange: (Dp) -> Unit,
portfolioBlock: @Composable (Modifier) -> Unit,
@ -56,41 +58,46 @@ internal fun MarketsTokenDetailsContent(
) {
Content(
modifier = modifier,
backgroundColor = backgroundColor,
state = state,
onBackClick = onBackClick,
onHeaderSizeChange = onHeaderSizeChange,
portfolioBlock = portfolioBlock,
addTopBarStatusBarInsets = addTopBarStatusBarPadding,
)
InfoBottomSheet(config = state.infoBottomSheet)
}
@Suppress("UnusedPrivateMember")
@Suppress("LongParameterList")
@Composable
private fun Content(
state: MarketsTokenDetailsUM,
backgroundColor: Color,
addTopBarStatusBarInsets: Boolean,
onBackClick: () -> Unit,
onHeaderSizeChange: (Dp) -> Unit,
portfolioBlock: @Composable (Modifier) -> Unit,
modifier: Modifier = Modifier,
) {
val backgroundColor = LocalMainBottomSheetColor.current.value
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }
Column(
modifier = modifier
.drawBehind { drawRect(backgroundColor) }
.let { if (addTopBarStatusBarInsets) it.statusBarsPadding() else it }
.fillMaxSize(),
) {
TangemTopAppBar(
modifier = Modifier.onGloballyPositioned {
if (it.size.height > 0) {
with(density) {
onHeaderSizeChange(it.size.height.toDp())
modifier = Modifier
.onGloballyPositioned {
if (it.size.height > 0) {
with(density) {
onHeaderSizeChange(it.size.height.toDp())
}
}
}
},
},
title = state.tokenName,
startButton = TopAppBarButtonUM.Back(onBackClick),
)
@ -125,6 +132,7 @@ private fun Content(
) {
MarketTokenDetailsChart(
modifier = Modifier.fillMaxWidth(),
backgroundColor = backgroundColor,
state = state.chartState,
)
}
@ -265,6 +273,7 @@ private fun Preview() {
TangemThemePreview {
Content(
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
addTopBarStatusBarInsets = false,
state = MarketsTokenDetailsUM(
tokenName = "Token Name",
priceText = "$0.00000000324",
@ -292,6 +301,7 @@ private fun Preview() {
),
onHeaderSizeChange = {},
onBackClick = {},
backgroundColor = TangemTheme.colors.background.tertiary,
portfolioBlock = {},
)
}

View file

@ -9,17 +9,21 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import com.tangem.common.ui.charts.MarketChart
import com.tangem.common.ui.charts.getMarketChartBottomAxisHeight
import com.tangem.common.ui.charts.state.MarketChartLook
import com.tangem.common.ui.charts.state.rememberMarketChartState
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM
import com.tangem.features.markets.tokenlist.impl.ui.components.UnableToLoadData
@Composable
internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, modifier: Modifier = Modifier) {
internal fun MarketTokenDetailsChart(
state: MarketsTokenDetailsUM.ChartState,
backgroundColor: Color,
modifier: Modifier = Modifier,
) {
val growingColor = TangemTheme.colors.icon.accent
val fallingColor = TangemTheme.colors.icon.warning
val neutralColor = TangemTheme.colors.icon.informative
@ -36,7 +40,6 @@ internal fun MarketTokenDetailsChart(state: MarketsTokenDetailsUM.ChartState, mo
onMarkerShown = state.onMarkerPointSelected,
)
val backgroundColor = LocalMainBottomSheetColor.current.value
val bottomChartAxisHeight = getMarketChartBottomAxisHeight()
Box(modifier) {

View file

@ -13,7 +13,7 @@ import java.math.BigDecimal
internal data class MarketsTokenDetailsUM(
val tokenName: String,
val priceText: String,
val iconUrl: String,
val iconUrl: String?,
val dateTimeText: TextReference,
val priceChangePercentText: String,
val priceChangeType: PriceChangeType,

View file

@ -1,27 +1,23 @@
package com.tangem.features.markets.entry.impl
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.*
import com.arkivanov.decompose.extensions.compose.jetpack.subscribeAsState
import com.arkivanov.decompose.router.stack.*
import com.arkivanov.decompose.value.Value
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.childByContext
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.features.markets.entry.BottomSheetState
import com.tangem.features.markets.entry.MarketsEntryComponent
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.details.toSerializable
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
import com.tangem.domain.markets.toSerializableParam
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child
import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -32,20 +28,22 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
private val marketsEntryChildFactory: MarketsEntryChildFactory,
) : MarketsEntryComponent, AppComponentContext by context {
private val stackNavigation = StackNavigation<MarketsEntryChildFactory.Child>()
private val stackNavigation = StackNavigation<Child>()
val stack: Value<ChildStack<MarketsEntryChildFactory.Child, Any>> = childStack(
val stack: Value<ChildStack<Child, Any>> = childStack(
key = "main",
source = stackNavigation,
serializer = MarketsEntryChildFactory.Child.serializer(),
initialConfiguration = MarketsEntryChildFactory.Child.TokenList,
serializer = Child.serializer(),
initialConfiguration = Child.TokenList,
handleBackButton = true,
childFactory = { configuration, componentContext ->
childFactory = { configuration, factoryContext ->
marketsEntryChildFactory.createChild(
child = configuration,
appComponentContext = childByContext(componentContext),
appComponentContext = childByContext(
componentContext = factoryContext,
router = createRouter(configuration),
),
onTokenSelected = ::marketsListTokenSelected,
onDetailsBack = ::onDetailsBack,
)
},
)
@ -57,103 +55,36 @@ internal class DefaultMarketsEntryComponent @AssistedInject constructor(
onHeaderSizeChange: (Dp) -> Unit,
modifier: Modifier,
) {
val primary = TangemTheme.colors.background.primary
val secondary = TangemTheme.colors.background.secondary
val backgroundColor = remember { Animatable(primary) }
val stackState = stack.subscribeAsState()
LocalMainBottomSheetColor.current.value = backgroundColor.value
Children(
stack = stackState.value,
animation = stackAnimation(slide()),
) {
when (it.configuration) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
(it.instance as MarketsTokenDetailsComponent).BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)
}
MarketsEntryChildFactory.Child.TokenList -> {
(it.instance as MarketsTokenListComponent).BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)
}
}
}
// order of LaunchedEffects is important here
val activeChild = stackState.value.active.configuration
LaunchedEffect(activeChild) {
when (activeChild) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.animateTo(
secondary,
animationSpec = tween(durationMillis = 500),
)
}
MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.animateTo(
primary,
animationSpec = tween(durationMillis = 500),
)
}
}
}
LaunchedEffect(bottomSheetState.value) {
if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) {
when (bottomSheetState.value) {
BottomSheetState.EXPANDED -> {
backgroundColor.animateTo(
secondary,
animationSpec = tween(durationMillis = 100),
)
}
BottomSheetState.COLLAPSED -> {
backgroundColor.animateTo(
primary,
animationSpec = tween(durationMillis = 100),
)
}
}
}
}
LaunchedEffect(primary, secondary) {
if (backgroundColor.isRunning) return@LaunchedEffect
when (activeChild) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.snapTo(secondary)
}
MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.snapTo(primary)
}
}
}
EntryBottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
stackState = stack.subscribeAsState(),
modifier = modifier,
)
}
@OptIn(ExperimentalDecomposeApi::class)
private fun marketsListTokenSelected(token: TokenMarket, appCurrency: AppCurrency) {
stackNavigation.pushNew(
configuration = MarketsEntryChildFactory.Child.TokenDetails(
configuration = Child.TokenDetails(
params = MarketsTokenDetailsComponent.Params(
token = token.toSerializable(),
token = token.toSerializableParam(),
appCurrency = appCurrency,
),
),
)
}
private fun onDetailsBack() {
stackNavigation.popWhile { it != MarketsEntryChildFactory.Child.TokenList }
private fun AppComponentContext.createRouter(child: Child): Router {
return when (child) {
is Child.TokenDetails -> {
MarketTokenDetailsRouter(
contextRouter = this.router,
stackNavigation = stackNavigation,
)
}
else -> this.router
}
}
@AssistedFactory

View file

@ -0,0 +1,21 @@
package com.tangem.features.markets.entry.impl
import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.popWhile
import com.tangem.core.decompose.navigation.Route
import com.tangem.core.decompose.navigation.Router
import kotlin.reflect.KClass
internal class MarketTokenDetailsRouter(
private val contextRouter: Router,
private val stackNavigation: StackNavigation<MarketsEntryChildFactory.Child>,
) : Router by contextRouter {
override fun pop(onComplete: (isSuccess: Boolean) -> Unit) {
stackNavigation.popWhile({ it != MarketsEntryChildFactory.Child.TokenList }, onComplete)
}
override fun popTo(route: Route, onComplete: (isSuccess: Boolean) -> Unit) = error("Not allowed")
override fun popTo(routeClass: KClass<out Route>, onComplete: (isSuccess: Boolean) -> Unit) = error("Not allowed")
}

View file

@ -5,6 +5,7 @@ import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.TokenMarket
import com.tangem.features.markets.details.MarketsTokenDetailsComponent
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
import kotlinx.serialization.Serializable
import javax.inject.Inject
@ -31,14 +32,12 @@ internal class MarketsEntryChildFactory @Inject constructor(
child: Child,
appComponentContext: AppComponentContext,
onTokenSelected: (TokenMarket, AppCurrency) -> Unit,
onDetailsBack: () -> Unit,
): Any {
return when (child) {
is Child.TokenDetails -> {
tokenDetailsComponentFactory.create(
context = appComponentContext,
params = child.params,
onBack = onDetailsBack,
)
}
is Child.TokenList -> {

View file

@ -0,0 +1,128 @@
package com.tangem.features.markets.entry.impl.ui
import androidx.compose.animation.Animatable
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.AnimationVector4D
import androidx.compose.animation.core.tween
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import com.arkivanov.decompose.extensions.compose.jetpack.stack.Children
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.slide
import com.arkivanov.decompose.extensions.compose.jetpack.stack.animation.stackAnimation
import com.arkivanov.decompose.router.stack.ChildStack
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.entry.BottomSheetState
import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory
import com.tangem.features.markets.tokenlist.api.MarketsTokenListComponent
@Composable
internal fun EntryBottomSheetContent(
bottomSheetState: State<BottomSheetState>,
onHeaderSizeChange: (Dp) -> Unit,
stackState: State<ChildStack<MarketsEntryChildFactory.Child, Any>>,
modifier: Modifier = Modifier,
) {
val primary = TangemTheme.colors.background.primary
val secondary = TangemTheme.colors.background.secondary
val backgroundColor = remember { Animatable(primary) }
LocalMainBottomSheetColor.current.value = backgroundColor.value
Children(
stack = stackState.value,
animation = stackAnimation(slide()),
) {
when (it.configuration) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
(it.instance as MarketsTokenDetailsComponent).BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)
}
MarketsEntryChildFactory.Child.TokenList -> {
(it.instance as MarketsTokenListComponent).BottomSheetContent(
bottomSheetState = bottomSheetState,
onHeaderSizeChange = onHeaderSizeChange,
modifier = modifier,
)
}
}
}
val activeChild = stackState.value.active.configuration
BackgroundColorEffects(
activeChild = activeChild,
backgroundColor = backgroundColor,
bottomSheetState = bottomSheetState,
)
}
@Composable
private fun BackgroundColorEffects(
activeChild: MarketsEntryChildFactory.Child,
backgroundColor: Animatable<Color, AnimationVector4D>,
bottomSheetState: State<BottomSheetState>,
) {
val primary = TangemTheme.colors.background.primary
val secondary = TangemTheme.colors.background.secondary
// Order of LaunchedEffects is important here
LaunchedEffect(activeChild) {
when (activeChild) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.animateTo(
secondary,
animationSpec = tween(durationMillis = 500),
)
}
MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.animateTo(
primary,
animationSpec = tween(durationMillis = 500),
)
}
}
}
LaunchedEffect(bottomSheetState.value) {
if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) {
when (bottomSheetState.value) {
BottomSheetState.EXPANDED -> {
backgroundColor.animateTo(
secondary,
animationSpec = tween(durationMillis = 100),
)
}
BottomSheetState.COLLAPSED -> {
backgroundColor.animateTo(
primary,
animationSpec = tween(durationMillis = 100),
)
}
}
}
}
LaunchedEffect(primary, secondary) {
if (backgroundColor.isRunning) return@LaunchedEffect
when (activeChild) {
is MarketsEntryChildFactory.Child.TokenDetails -> {
backgroundColor.snapTo(secondary)
}
MarketsEntryChildFactory.Child.TokenList -> {
backgroundColor.snapTo(primary)
}
}
}
}

View file

@ -2,7 +2,9 @@ package com.tangem.features.markets.token.block.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
@ -23,7 +25,12 @@ internal class DefaultTokenMarketBlockComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
TokenMarketBlock(modifier)
val state by model.state.collectAsStateWithLifecycle()
TokenMarketBlock(
modifier = modifier,
state = state,
)
}
@AssistedFactory

View file

@ -0,0 +1,8 @@
package com.tangem.features.markets.token.block.impl.model
import java.math.BigDecimal
internal class QuotesState(
val currentPrice: BigDecimal,
val h24Percent: BigDecimal,
)

View file

@ -1,11 +1,25 @@
package com.tangem.features.markets.token.block.impl.model
import androidx.compose.runtime.Stable
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.charts.state.MarketChartData
import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.markets.*
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ -13,9 +27,116 @@ import javax.inject.Inject
internal class TokenMarketBlockModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase,
private val getTokenQuotesUseCase: GetTokenQuotesUseCase,
getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
) : Model() {
val params = paramsContainer.require<TokenMarketBlockComponent.Params>()
private val params = paramsContainer.require<TokenMarketBlockComponent.Params>()
private val priceAndTimePointValuesConverter = PriceAndTimePointValuesConverter(needToFormatAxis = false)
// token price is available if cryptoCurrency.id.rawCurrencyId != null
private val currentAppCurrency = getSelectedAppCurrencyUseCase()
.map { maybeAppCurrency ->
maybeAppCurrency.getOrElse { AppCurrency.Default }
}.stateIn(
scope = modelScope,
started = SharingStarted.Eagerly,
initialValue = AppCurrency.Default,
)
private var quotesState: QuotesState? = null
val state = MutableStateFlow(
TokenMarketBlockUM(
currencySymbol = params.tokenSymbol,
currentPrice = null,
h24Percent = null,
priceChangeType = PriceChangeType.NEUTRAL,
chartData = null,
onClick = ::navigateToMarketDetails,
),
)
init {
startFetching()
}
private fun startFetching() {
modelScope.launch {
getTokenQuotesUseCase(
tokenId = params.tokenId,
interval = PriceChangeInterval.H24,
).collect {
it.onRight { res ->
quotesState = QuotesState(
currentPrice = res.fiatRate,
h24Percent = res.priceChange,
)
state.value = state.value.copy(
currentPrice = BigDecimalFormatter.formatFiatPriceUncapped(
fiatAmount = res.fiatRate,
// TODO get currency from quotes use case [REDACTED_TASK_KEY]
fiatCurrencyCode = currentAppCurrency.value.code,
// TODO get currency from quotes use case [REDACTED_TASK_KEY]
fiatCurrencySymbol = currentAppCurrency.value.symbol,
),
h24Percent = BigDecimalFormatter.formatPercent(
percent = res.priceChange,
useAbsoluteValue = true,
),
priceChangeType = PriceChangeType.fromBigDecimal(res.priceChange),
)
}
}
}
modelScope.launch(dispatchers.main) {
val result = getTokenPriceChartUseCase(
tokenId = params.tokenId,
interval = PriceChangeInterval.H24,
appCurrency = currentAppCurrency.value, // TODO get currency from quotes use case [REDACTED_TASK_KEY]
preview = true,
)
result.onRight { res ->
state.update { stateToUpdate ->
stateToUpdate.copy(
chartData = priceAndTimePointValuesConverter.convert(
MarketChartData.Data(
y = res.priceY.toImmutableList(),
x = res.timeStamps.sorted().map { it.toBigDecimal() }.toImmutableList(),
),
),
)
}
}
}
}
private fun navigateToMarketDetails() {
val quotes = quotesState ?: return
val tokenParam = TokenMarketParam(
id = params.tokenId,
name = params.tokenSymbol,
imageUrl = params.tokenImageUrl,
symbol = params.tokenSymbol,
tokenQuotes = TokenMarketParam.Quotes(
currentPrice = quotes.currentPrice,
h24Percent = quotes.h24Percent,
weekPercent = null,
monthPercent = null,
),
)
// FIXME navigation crash [REDACTED_TASK_KEY]
router.push(
AppRoute.MarketsTokenDetails(
token = tokenParam,
appCurrency = currentAppCurrency.value,
),
)
}
}

View file

@ -1,23 +1,209 @@
package com.tangem.features.markets.token.block.impl.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.block.information.InformationBlock
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.charts.MarketChartMini
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.marketprice.PriceChangeInPercent
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.details.impl.model.formatter.toChartType
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM
import kotlinx.collections.immutable.toImmutableList
import kotlin.random.Random
@Composable
internal fun TokenMarketBlock(modifier: Modifier = Modifier) {
InformationBlock(
internal fun TokenMarketBlock(state: TokenMarketBlockUM, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
title = {
Text(
text = "Token Market Block",
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
enabled = state.currentPrice != null,
onClick = state.onClick,
content = {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing12),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
LeftSide(
modifier = Modifier.weight(1f),
symbol = state.currencySymbol,
priceText = state.currentPrice,
percentText = state.h24Percent,
type = state.priceChangeType,
)
SpacerW8()
RightSide(
modifier = Modifier,
priceChangeType = state.priceChangeType,
chartRawData = state.chartData,
)
}
},
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun LeftSide(
symbol: String,
priceText: String?,
percentText: String?,
type: PriceChangeType,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
) {
Text("//TODO")
Text(
text = stringResource(id = R.string.wallet_marketplace_block_title, symbol),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
)
if (priceText != null && percentText != null) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
Text(
text = priceText,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
PriceChangeInPercent(
valueInPercent = percentText,
type = type,
)
Text(
text = stringResource(id = R.string.wallet_marketprice_block_update_time),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
}
} else {
TextShimmer(
modifier = Modifier.fillMaxWidth(fraction = 0.6f),
style = TangemTheme.typography.body2,
)
}
}
}
@Composable
fun RightSide(priceChangeType: PriceChangeType?, chartRawData: MarketChartRawData?, modifier: Modifier = Modifier) {
Row(
modifier = modifier.padding(vertical = TangemTheme.dimens.spacing10),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
verticalAlignment = Alignment.CenterVertically,
) {
if (chartRawData != null && priceChangeType != null) {
MarketChartMini(
rawData = chartRawData,
type = priceChangeType.toChartType(),
modifier = Modifier
.requiredSize(
width = TangemTheme.dimens.size56,
height = TangemTheme.dimens.size24,
),
)
} else {
RectangleShimmer(
modifier = Modifier
.padding(vertical = TangemTheme.dimens.spacing2)
.requiredSize(
width = TangemTheme.dimens.size56,
height = TangemTheme.dimens.size20,
),
)
}
if (priceChangeType != null) {
Icon(
modifier = Modifier.requiredSize(TangemTheme.dimens.size20),
imageVector = ImageVector.vectorResource(id = R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
} else {
RectangleShimmer(
modifier = Modifier
.requiredSize(
width = TangemTheme.dimens.size20,
height = TangemTheme.dimens.size20,
),
)
}
}
}
@Preview(widthDp = 360)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 360)
@Composable
private fun Preview() {
val data = MarketChartRawData(
x = List(20) { Random.nextFloat().toDouble() }.toImmutableList(),
y = List(20) { Random.nextFloat().toDouble() }.toImmutableList(),
)
val state = TokenMarketBlockUM(
currencySymbol = "XRP",
currentPrice = "0,5$",
h24Percent = "0,5%",
priceChangeType = PriceChangeType.UP,
chartData = data,
onClick = {},
)
TangemThemePreview {
Column(
modifier = Modifier.background(TangemTheme.colors.background.tertiary),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
TokenMarketBlock(
modifier = Modifier.fillMaxWidth(),
state = state,
)
TokenMarketBlock(
modifier = Modifier.fillMaxWidth(),
state = state.copy(
currentPrice = "0,0000000000012356786789$",
),
)
TokenMarketBlock(
modifier = Modifier.fillMaxWidth(),
state = state.copy(
currentPrice = null,
chartData = null,
),
)
TokenMarketBlock(
modifier = Modifier.fillMaxWidth(),
state = state.copy(
chartData = null,
),
)
}
}
}

View file

@ -0,0 +1,13 @@
package com.tangem.features.markets.token.block.impl.ui.state
import com.tangem.common.ui.charts.state.MarketChartRawData
import com.tangem.core.ui.components.marketprice.PriceChangeType
internal data class TokenMarketBlockUM(
val currencySymbol: String,
val currentPrice: String?,
val h24Percent: String?,
val priceChangeType: PriceChangeType,
val chartData: MarketChartRawData?,
val onClick: () -> Unit,
)

View file

@ -78,6 +78,7 @@ dependencies {
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.transaction)
implementation(projects.domain.staking)
implementation(projects.domain.markets.models)
/** Temp dependency to swap domain */
implementation(projects.features.swap.domain)

View file

@ -8,7 +8,9 @@ import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.defaultComponentContext
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.common.routing.bundle.unbundle
import com.tangem.common.routing.utils.asRouter
import com.tangem.core.decompose.context.DefaultAppComponentContext
import com.tangem.core.decompose.di.DecomposeComponent
import com.tangem.core.ui.UiDependencies
@ -46,6 +48,9 @@ internal class TokenDetailsFragment : ComposeFragment() {
@Inject
internal lateinit var tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory
@Inject
internal lateinit var appRouter: AppRouter
private var tokenMarketBlockComponent: TokenMarketBlockComponent? = null
private val internalTokenDetailsRouter: InnerTokenDetailsRouter
@ -57,25 +62,39 @@ internal class TokenDetailsFragment : ComposeFragment() {
super.onCreate(savedInstanceState)
if (marketsFeatureToggles.isFeatureEnabled) {
val cryptoCurrency: CryptoCurrency = arguments
?.getBundle(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY)
?.unbundle(CryptoCurrency.serializer())
?: error("Token Details screen can't open without `CryptoCurrency`")
val param = cryptoCurrency.toParam() ?: return
val appContext = DefaultAppComponentContext(
componentContext = defaultComponentContext(requireActivity().onBackPressedDispatcher),
messageHandler = uiDependencies.eventMessageHandler,
dispatchers = coroutineDispatcherProvider,
hiltComponentBuilder = componentBuilder,
replaceRouter = appRouter.asRouter(),
)
val cryptoCurrency: CryptoCurrency = arguments
?.getBundle(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY)
?.unbundle(CryptoCurrency.serializer())
?: error("This screen can't open without `CryptoCurrency`")
tokenMarketBlockComponent = tokenMarketBlockComponentFactory.create(
appComponentContext = appContext,
params = TokenMarketBlockComponent.Params(cryptoCurrency = cryptoCurrency),
params = param,
)
}
}
private fun CryptoCurrency.toParam(): TokenMarketBlockComponent.Params? {
val tokenId = id.rawCurrencyId ?: return null // token price is not available
return TokenMarketBlockComponent.Params(
tokenId = tokenId,
tokenName = name,
tokenSymbol = symbol,
tokenImageUrl = iconUrl,
)
}
@Composable
override fun ScreenContent(modifier: Modifier) {
val viewModel = hiltViewModel<TokenDetailsViewModel>()