Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-02 12:52:47 +03:00
parent e23c6cf3e2
commit ab6832b5e9
10 changed files with 619 additions and 419 deletions

View file

@ -7,10 +7,12 @@ 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.core.ui.res.LocalRedesignEnabled
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.markets.token.block.TokenMarketBlockComponent.Params
import com.tangem.features.markets.token.block.impl.model.TokenMarketBlockModel
import com.tangem.features.markets.token.block.impl.ui.TokenMarketBlock
import com.tangem.features.markets.token.block.impl.ui.TokenMarketBlockLegacy
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -27,10 +29,17 @@ internal class DefaultTokenMarketBlockComponent @AssistedInject constructor(
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
TokenMarketBlock(
modifier = modifier,
state = state,
)
if (LocalRedesignEnabled.current) {
TokenMarketBlock(
tokenMarketBlockUM = state,
modifier = modifier,
)
} else {
TokenMarketBlockLegacy(
state = state,
modifier = modifier,
)
}
}
@AssistedFactory

View file

@ -1,216 +1,25 @@
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.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
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.common.ui.tokens.TokenPriceText
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.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.token.block.impl.model.formatter.toChartType
import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM
import kotlinx.collections.immutable.toImmutableList
import kotlin.random.Random
@Suppress("UnusedParameter")
@Composable
internal fun TokenMarketBlock(state: TokenMarketBlockUM, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
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),
internal fun TokenMarketBlock(tokenMarketBlockUM: TokenMarketBlockUM, modifier: Modifier = Modifier) {
Box(
modifier = modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResourceSafe(id = R.string.wallet_marketplace_block_title, symbol),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
text = "Market Block Redesign",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
if (priceText != null && percentText != null) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
TokenPriceText(
modifier = Modifier.alignByBaseline(),
price = priceText,
priceChangeType = type,
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
PriceChangeInPercent(
modifier = Modifier.alignByBaseline(),
valueInPercent = percentText,
type = type,
)
Text(
modifier = Modifier.alignByBaseline(),
text = stringResourceSafe(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
private 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,216 @@
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 androidx.compose.ui.graphics.vector.ImageVector
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.common.ui.tokens.TokenPriceText
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.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.token.block.impl.model.formatter.toChartType
import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM
import kotlinx.collections.immutable.toImmutableList
import kotlin.random.Random
@Composable
internal fun TokenMarketBlockLegacy(state: TokenMarketBlockUM, modifier: Modifier = Modifier) {
BlockCard(
modifier = modifier,
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(
text = stringResourceSafe(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),
) {
TokenPriceText(
modifier = Modifier.alignByBaseline(),
price = priceText,
priceChangeType = type,
)
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
PriceChangeInPercent(
modifier = Modifier.alignByBaseline(),
valueInPercent = percentText,
type = type,
)
Text(
modifier = Modifier.alignByBaseline(),
text = stringResourceSafe(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
private 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),
) {
TokenMarketBlockLegacy(
modifier = Modifier.fillMaxWidth(),
state = state,
)
TokenMarketBlockLegacy(
modifier = Modifier.fillMaxWidth(),
state = state.copy(
currentPrice = "0,0000000000012356786789$",
),
)
TokenMarketBlockLegacy(
modifier = Modifier.fillMaxWidth(),
state = state.copy(
currentPrice = null,
chartData = null,
),
)
TokenMarketBlockLegacy(
modifier = Modifier.fillMaxWidth(),
state = state.copy(
chartData = null,
),
)
}
}
}

View file

@ -15,11 +15,13 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.details.NavigationAction
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsModel
import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreenLegacy
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokenreceive.TokenReceiveComponent
@ -84,15 +86,27 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
val bottomSheet by bottomSheetSlot.subscribeAsState()
NavigationBar3ButtonsScrim()
TokenDetailsScreen(
state = state,
tokenMarketBlockComponent = tokenMarketBlockComponent,
txHistoryComponent = txHistoryComponent,
yieldSupplyComponent = yieldSupplyComponent,
)
if (LocalRedesignEnabled.current) {
val tokenDetailsUM by model.redesignUiState.collectAsStateWithLifecycle()
TokenDetailsScreen(
tokenDetailsUM = tokenDetailsUM,
tokenMarketBlockComponent = tokenMarketBlockComponent,
modifier = modifier,
)
} else {
val state by model.uiState.collectAsStateWithLifecycle()
TokenDetailsScreenLegacy(
state = state,
tokenMarketBlockComponent = tokenMarketBlockComponent,
txHistoryComponent = txHistoryComponent,
yieldSupplyComponent = yieldSupplyComponent,
)
}
bottomSheet.child?.instance?.BottomSheet()
}

View file

@ -27,6 +27,9 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -87,7 +90,11 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.Token
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender
import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.TokenDetailsExpressStatusFactory
import com.tangem.features.tokendetails.TokenDetailsComponent
@ -99,6 +106,7 @@ import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import com.tangem.utils.extensions.isZero
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -197,6 +205,9 @@ internal class TokenDetailsModel @Inject constructor(
private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency))
val uiState: StateFlow<TokenDetailsState> = internalUiState
private val internalRedesignUiState = MutableStateFlow(createInitialRedesignState())
val redesignUiState: StateFlow<TokenDetailsUM> = internalRedesignUiState
// region Clore migration
// TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY])
private val cloreMigrationModel by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -1275,6 +1286,29 @@ internal class TokenDetailsModel @Inject constructor(
// endregion Clore migration
private fun createInitialRedesignState(): TokenDetailsUM {
return TokenDetailsUM(
topAppBarUM = TokenDetailsTopAppBarUM(
title = stringReference(cryptoCurrency.name),
subtitle = stringReference(cryptoCurrency.symbol),
menuItems = persistentListOf(),
),
balanceBlockUM = TokenDetailsBalanceBlockUM.Loading(
actionButtons = persistentListOf(),
tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
currencyIconState = CurrencyIconState.Loading,
),
marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = cryptoCurrency.symbol),
stakingBlocksState = null,
pullToRefreshConfig = PullToRefreshConfig(
isRefreshing = false,
onRefresh = {},
),
isBalanceHidden = false,
isMarketPriceAvailable = false,
)
}
private companion object {
const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L
}

View file

@ -0,0 +1,64 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@Immutable
internal sealed class TokenDetailsBalanceBlockUM {
abstract val actionButtons: ImmutableList<TangemButtonUM>
abstract val tokenBalanceTypeUM: TokenBalanceTypeUM
abstract val currencyIconState: CurrencyIconState
data class Loading(
override val actionButtons: ImmutableList<TangemButtonUM>,
override val tokenBalanceTypeUM: TokenBalanceTypeUM,
override val currencyIconState: CurrencyIconState,
) : TokenDetailsBalanceBlockUM()
data class Content(
override val actionButtons: ImmutableList<TangemButtonUM>,
override val tokenBalanceTypeUM: TokenBalanceTypeUM,
override val currencyIconState: CurrencyIconState,
val displayCryptoBalance: TextReference,
val displayFiatBalance: TextReference,
val isBalanceFlickering: Boolean,
) : TokenDetailsBalanceBlockUM()
data class Error(
override val actionButtons: ImmutableList<TangemButtonUM>,
override val tokenBalanceTypeUM: TokenBalanceTypeUM,
override val currencyIconState: CurrencyIconState,
) : TokenDetailsBalanceBlockUM()
fun copyActionButtons(buttons: ImmutableList<TangemButtonUM>): TokenDetailsBalanceBlockUM {
return when (this) {
is Content -> this.copy(actionButtons = buttons)
is Error -> this.copy(actionButtons = buttons)
is Loading -> this.copy(actionButtons = buttons)
}
}
}
internal sealed class TokenBalanceTypeUM {
abstract val type: Type
data object Single : TokenBalanceTypeUM() {
override val type = Type.ALL
}
data class Multiple(
override val type: Type,
val availableTypes: ImmutableList<Type>,
val onSelect: (Type) -> Unit,
) : TokenBalanceTypeUM()
enum class Type {
ALL,
AVAILABLE,
}
}

View file

@ -0,0 +1,24 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.compose.runtime.Stable
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@Stable
internal data class TokenDetailsUM(
val topAppBarUM: TokenDetailsTopAppBarUM,
val balanceBlockUM: TokenDetailsBalanceBlockUM,
val marketPriceBlockState: MarketPriceBlockState,
val stakingBlocksState: StakingBlockUM?,
val pullToRefreshConfig: PullToRefreshConfig,
val isBalanceHidden: Boolean,
val isMarketPriceAvailable: Boolean,
)
internal data class TokenDetailsTopAppBarUM(
val title: TextReference,
val subtitle: TextReference,
val menuItems: ImmutableList<TextReference>,
)

View file

@ -1,218 +1,30 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheet
import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.expressTransactionsItems
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
// TODO: Split to blocks [REDACTED_JIRA]
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Suppress("UnusedParameter")
@Composable
internal fun TokenDetailsScreen(
state: TokenDetailsState,
tokenDetailsUM: TokenDetailsUM,
tokenMarketBlockComponent: TokenMarketBlockComponent?,
txHistoryComponent: TxHistoryComponent,
yieldSupplyComponent: YieldSupplyComponent,
modifier: Modifier = Modifier,
) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
Scaffold(
topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) },
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars),
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->
val listState = rememberLazyListState()
val txHistoryComponentState by txHistoryComponent.txHistoryState.collectAsStateWithLifecycle()
val dialogConfig = state.dialogConfig
val betweenItemsPadding = TangemTheme.dimens.spacing12
val horizontalPadding = TangemTheme.dimens.spacing16
val itemModifier = Modifier
.padding(top = betweenItemsPadding)
.padding(horizontal = horizontalPadding)
TangemPullToRefreshContainer(
config = state.pullToRefreshConfig,
modifier = Modifier.padding(scaffoldPaddings),
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.testTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER),
state = listState,
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing16 + bottomBarHeight,
),
) {
item {
TokenInfoBlock(
modifier = Modifier.padding(horizontal = horizontalPadding),
state = state.tokenInfoBlockState,
)
}
item {
TokenDetailsBalanceBlock(
modifier = itemModifier,
isBalanceHidden = state.isBalanceHidden,
state = state.tokenBalanceBlockState,
)
}
items(
items = state.notifications,
key = { it::class.java },
contentType = { it.config::class.java },
itemContent = {
Notification(
modifier = itemModifier.animateItem(),
config = it.config,
iconTint = when (it) {
is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent
is TokenDetailsNotification.UsedOutdatedData -> TangemTheme.colors.text.attention
else -> null
},
)
},
)
when {
tokenMarketBlockComponent != null -> {
item(
key = TokenMarketBlockComponent::class.java,
contentType = TokenMarketBlockComponent::class.java,
content = { tokenMarketBlockComponent.Content(modifier = itemModifier) },
)
}
state.isMarketPriceAvailable -> {
item(
key = MarketPriceBlockState::class.java,
contentType = MarketPriceBlockState::class.java,
content = {
MarketPriceBlock(
modifier = itemModifier,
state = state.marketPriceBlockState,
)
},
)
}
}
if (state.stakingBlocksState != null) {
item(
key = StakingBlockUM::class.java,
contentType = StakingBlockUM::class.java,
content = {
TokenStakingBlock(
state = state.stakingBlocksState,
isBalanceHidden = state.isBalanceHidden,
modifier = itemModifier,
)
},
)
}
item {
yieldSupplyComponent.Content(modifier = itemModifier)
}
expressTransactionsItems(
expressTxs = state.expressTxsToDisplay,
modifier = itemModifier,
)
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryComponentState) }
}
}
if (dialogConfig != null) {
TokenDetailsDialogs(dialogConfig = dialogConfig)
}
state.bottomSheetConfig?.let { config ->
when (config.content) {
is ChooseAddressBottomSheetConfig -> {
ChooseAddressBottomSheet(config = config)
}
is ExpressStatusBottomSheetConfig -> {
ExpressStatusBottomSheet(config = config)
}
is CloreMigrationBottomSheetConfig -> {
CloreMigrationBottomSheet(config = config)
}
}
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TokenDetailsScreenPreview(
@PreviewParameter(TokenDetailsScreenParameterProvider::class) state: TokenDetailsState,
) {
TangemThemePreview {
TokenDetailsScreen(
state = state,
tokenMarketBlockComponent = null,
txHistoryComponent = object : TxHistoryComponent {
override val txHistoryState: StateFlow<TxHistoryUM> = MutableStateFlow(
value = TxHistoryUM.Empty(isBalanceHidden = false, onExploreClick = {}),
)
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) = Unit
},
yieldSupplyComponent = object : YieldSupplyComponent {
@Composable
override fun Content(modifier: Modifier) {
}
},
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
text = "Token Details Redesign",
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
}
}
private class TokenDetailsScreenParameterProvider : CollectionPreviewParameterProvider<TokenDetailsState>(
collection = listOf(
TokenDetailsPreviewData.tokenDetailsState_1,
TokenDetailsPreviewData.tokenDetailsState_2,
TokenDetailsPreviewData.tokenDetailsState_3,
),
)
// endregion Preview
}

View file

@ -0,0 +1,218 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.*
import androidx.compose.material3.Scaffold
import androidx.compose.material3.ScaffoldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheet
import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig
import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig
import com.tangem.common.ui.expressStatus.expressTransactionsItems
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer
import com.tangem.core.ui.components.marketprice.MarketPriceBlock
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockUM
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlockLegacy
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.express.ExpressStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
// TODO: Split to blocks [REDACTED_JIRA]
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
internal fun TokenDetailsScreenLegacy(
state: TokenDetailsState,
tokenMarketBlockComponent: TokenMarketBlockComponent?,
txHistoryComponent: TxHistoryComponent,
yieldSupplyComponent: YieldSupplyComponent,
) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
Scaffold(
topBar = { TokenDetailsTopAppBar(config = state.topAppBarConfig) },
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars),
containerColor = TangemTheme.colors.background.secondary,
) { scaffoldPaddings ->
val listState = rememberLazyListState()
val txHistoryComponentState by txHistoryComponent.txHistoryState.collectAsStateWithLifecycle()
val dialogConfig = state.dialogConfig
val betweenItemsPadding = TangemTheme.dimens.spacing12
val horizontalPadding = TangemTheme.dimens.spacing16
val itemModifier = Modifier
.padding(top = betweenItemsPadding)
.padding(horizontal = horizontalPadding)
TangemPullToRefreshContainer(
config = state.pullToRefreshConfig,
modifier = Modifier.padding(scaffoldPaddings),
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.testTag(TokenDetailsScreenTestTags.SCREEN_CONTAINER),
state = listState,
contentPadding = PaddingValues(
bottom = TangemTheme.dimens.spacing16 + bottomBarHeight,
),
) {
item {
TokenInfoBlock(
modifier = Modifier.padding(horizontal = horizontalPadding),
state = state.tokenInfoBlockState,
)
}
item {
TokenDetailsBalanceBlockLegacy(
modifier = itemModifier,
isBalanceHidden = state.isBalanceHidden,
state = state.tokenBalanceBlockState,
)
}
items(
items = state.notifications,
key = { it::class.java },
contentType = { it.config::class.java },
itemContent = { notification ->
Notification(
modifier = itemModifier.animateItem(),
config = notification.config,
iconTint = when (notification) {
is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent
is TokenDetailsNotification.UsedOutdatedData -> TangemTheme.colors.text.attention
else -> null
},
)
},
)
when {
tokenMarketBlockComponent != null -> {
item(
key = TokenMarketBlockComponent::class.java,
contentType = TokenMarketBlockComponent::class.java,
content = { tokenMarketBlockComponent.Content(modifier = itemModifier) },
)
}
state.isMarketPriceAvailable -> {
item(
key = MarketPriceBlockState::class.java,
contentType = MarketPriceBlockState::class.java,
content = {
MarketPriceBlock(
modifier = itemModifier,
state = state.marketPriceBlockState,
)
},
)
}
}
if (state.stakingBlocksState != null) {
item(
key = StakingBlockUM::class.java,
contentType = StakingBlockUM::class.java,
content = {
TokenStakingBlock(
state = state.stakingBlocksState,
isBalanceHidden = state.isBalanceHidden,
modifier = itemModifier,
)
},
)
}
item {
yieldSupplyComponent.Content(modifier = itemModifier)
}
expressTransactionsItems(
expressTxs = state.expressTxsToDisplay,
modifier = itemModifier,
)
with(txHistoryComponent) { txHistoryContent(listState = listState, state = txHistoryComponentState) }
}
}
if (dialogConfig != null) {
TokenDetailsDialogs(dialogConfig = dialogConfig)
}
state.bottomSheetConfig?.let { config ->
when (config.content) {
is ChooseAddressBottomSheetConfig -> {
ChooseAddressBottomSheet(config = config)
}
is ExpressStatusBottomSheetConfig -> {
ExpressStatusBottomSheet(config = config)
}
is CloreMigrationBottomSheetConfig -> {
CloreMigrationBottomSheet(config = config)
}
}
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TokenDetailsScreenPreview(
@PreviewParameter(TokenDetailsScreenParameterProvider::class) state: TokenDetailsState,
) {
TangemThemePreview {
TokenDetailsScreenLegacy(
state = state,
tokenMarketBlockComponent = null,
txHistoryComponent = object : TxHistoryComponent {
override val txHistoryState: StateFlow<TxHistoryUM> = MutableStateFlow(
value = TxHistoryUM.Empty(isBalanceHidden = false, onExploreClick = {}),
)
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) = Unit
},
yieldSupplyComponent = object : YieldSupplyComponent {
@Composable
override fun Content(modifier: Modifier) {
}
},
)
}
}
private class TokenDetailsScreenParameterProvider : CollectionPreviewParameterProvider<TokenDetailsState>(
collection = listOf(
TokenDetailsPreviewData.tokenDetailsState_1,
TokenDetailsPreviewData.tokenDetailsState_2,
TokenDetailsPreviewData.tokenDetailsState_3,
),
)
// endregion Preview

View file

@ -39,7 +39,7 @@ import kotlinx.collections.immutable.toImmutableList
@Suppress("DestructuringDeclarationWithTooManyEntries")
@Composable
internal fun TokenDetailsBalanceBlock(
internal fun TokenDetailsBalanceBlockLegacy(
state: TokenDetailsBalanceBlockState,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
@ -264,7 +264,7 @@ private fun Preview_TokenDetailsBalanceBlock(
@PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState,
) {
TangemThemePreview {
TokenDetailsBalanceBlock(state = state, isBalanceHidden = false)
TokenDetailsBalanceBlockLegacy(state = state, isBalanceHidden = false)
}
}