Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-17 11:50:14 +03:00
parent 5e12c2dc29
commit af8544e6d7
12 changed files with 1048 additions and 65 deletions

View file

@ -0,0 +1,315 @@
package com.tangem.common.ui.earn
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.innerShadow
import androidx.compose.ui.graphics.shadow.Shadow
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.CircleShimmer
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.ds.button.TangemButton
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.resolveAnnotatedReference
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.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.res.R as CoreResR
private const val TINTED_BACKGROUND_ALPHA = 0.1f
private const val TINTED_BORDER_ALPHA = 0.1f
private const val TINTED_INNER_SHADOW_ALPHA = 0.3f
private val BorderWidth = 1.dp
private val InnerShadowBlur = 20.dp
private val ShimmerSubtitleWidth = 78.dp
@Composable
fun EarnBlock(state: EarnBlockUM, modifier: Modifier = Modifier) {
when (state) {
is EarnBlockUM.Loading -> EarnBlockLoading(modifier)
is EarnBlockUM.Content -> EarnBlockContent(state, modifier)
}
}
@Composable
private fun EarnBlockLoading(modifier: Modifier = Modifier) {
val shape = RoundedCornerShape(TangemTheme.dimens2.x4)
TangemRowContainer(
modifier = modifier
.clip(shape)
.background(TangemTheme.colors2.surface.level3)
.border(width = BorderWidth, color = TangemTheme.colors2.border.neutral.primary, shape = shape),
contentPadding = PaddingValues(all = TangemTheme.dimens2.x3),
content = {
CircleShimmer(
modifier = Modifier
.layoutId(TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x3)
.size(TangemTheme.dimens2.x10),
)
RectangleShimmer(
modifier = Modifier
.layoutId(TangemRowLayoutId.START_TOP)
.size(width = TangemTheme.dimens2.x16, height = TangemTheme.dimens2.x5),
radius = TangemTheme.dimens2.x2,
)
RectangleShimmer(
modifier = Modifier
.layoutId(TangemRowLayoutId.START_BOTTOM)
.size(width = ShimmerSubtitleWidth, height = TangemTheme.dimens2.x4),
radius = TangemTheme.dimens2.x2,
)
},
)
}
@Composable
private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Modifier) {
val shape = RoundedCornerShape(TangemTheme.dimens2.x4)
val backgroundModifier = resolveBackgroundModifier(state.backgroundUM, shape)
val clickModifier = when (val trailing = state.trailingUM) {
is EarnBlockUM.TrailingUM.Balance -> Modifier.clickable(onClick = trailing.onClick)
else -> Modifier
}
TangemRowContainer(
modifier = modifier
.clip(shape)
.then(backgroundModifier)
.then(clickModifier),
contentPadding = PaddingValues(all = TangemTheme.dimens2.x3),
content = {
// Icon (HEAD)
EarnBlockIcon(
iconUM = state.iconUM,
modifier = Modifier
.layoutId(TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x3),
)
// Title (START_TOP)
Text(
text = state.titleUM.text.resolveReference(),
style = when (state.titleUM.style) {
EarnBlockUM.TitleUM.Style.Large -> TangemTheme.typography2.bodySemibold16
EarnBlockUM.TitleUM.Style.Small -> TangemTheme.typography2.captionMedium12
},
color = state.titleUM.color(),
modifier = Modifier
.layoutId(TangemRowLayoutId.START_TOP)
.padding(end = TangemTheme.dimens2.x2),
)
// Subtitle (START_BOTTOM)
val subtitle = state.subtitleUM
if (subtitle is EarnBlockUM.SubtitleUM.Text) {
Text(
text = subtitle.text.resolveReference(),
style = when (subtitle.style) {
EarnBlockUM.SubtitleUM.Style.Large -> TangemTheme.typography2.bodySemibold16
EarnBlockUM.SubtitleUM.Style.Small -> TangemTheme.typography2.captionMedium12
},
color = subtitle.color(),
modifier = Modifier
.layoutId(TangemRowLayoutId.START_BOTTOM)
.padding(end = TangemTheme.dimens2.x2),
)
}
// Trailing
EarnBlockTrailing(trailingUM = state.trailingUM)
},
)
}
@Composable
private fun resolveBackgroundModifier(backgroundUM: EarnBlockUM.BackgroundUM, shape: RoundedCornerShape): Modifier {
return when (backgroundUM) {
is EarnBlockUM.BackgroundUM.Surface -> Modifier
.background(TangemTheme.colors2.surface.level3)
.border(width = BorderWidth, color = TangemTheme.colors2.border.neutral.primary, shape = shape)
is EarnBlockUM.BackgroundUM.Tinted -> {
val tintColor = backgroundUM.color()
Modifier
.background(tintColor.copy(alpha = TINTED_BACKGROUND_ALPHA))
.border(width = BorderWidth, color = tintColor.copy(alpha = TINTED_BORDER_ALPHA), shape = shape)
.innerShadow(
shape = shape,
shadow = Shadow(
radius = InnerShadowBlur,
color = tintColor.copy(alpha = TINTED_INNER_SHADOW_ALPHA),
offset = DpOffset.Zero,
),
)
}
}
}
@Composable
private fun EarnBlockTrailing(trailingUM: EarnBlockUM.TrailingUM?) {
when (trailingUM) {
is EarnBlockUM.TrailingUM.Button -> {
TangemButton(
buttonUM = trailingUM.buttonUM,
modifier = Modifier.layoutId(TangemRowLayoutId.TAIL),
)
}
is EarnBlockUM.TrailingUM.Balance -> {
if (!trailingUM.isBalanceHidden) {
Text(
text = trailingUM.fiatValue.resolveAnnotatedReference(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
modifier = Modifier.layoutId(TangemRowLayoutId.END_TOP),
)
Text(
text = trailingUM.cryptoValue.resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
modifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM),
)
}
}
null -> Unit
}
}
@Composable
private fun EarnBlockIcon(iconUM: EarnBlockUM.IconUM, modifier: Modifier = Modifier) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier.size(TangemTheme.dimens2.x10),
) {
if (iconUM is EarnBlockUM.IconUM.Glowing) {
val glowShape = RoundedCornerShape(percent = 50)
Box(
modifier = Modifier
.size(TangemTheme.dimens2.x6)
.blur(radius = TangemTheme.dimens2.x4, edgeTreatment = BlurredEdgeTreatment.Unbounded)
.background(color = iconUM.glowColor().copy(alpha = 0.7f), shape = glowShape),
)
}
val iconRes = when (iconUM) {
is EarnBlockUM.IconUM.Glowing -> iconUM.iconRes
is EarnBlockUM.IconUM.Plain -> iconUM.iconRes
}
TangemIcon(
tangemIconUM = TangemIconUM.Image(imageRes = iconRes),
modifier = Modifier.size(TangemTheme.dimens2.x10),
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnBlock_Preview(@PreviewParameter(EarnBlockPreviewProvider::class) state: EarnBlockUM) {
TangemThemePreviewRedesign {
EarnBlock(
state = state,
modifier = Modifier.padding(TangemTheme.dimens2.x4),
)
}
}
private class EarnBlockPreviewProvider : CollectionPreviewParameterProvider<EarnBlockUM>(
collection = listOf(
EarnBlockUM.Loading,
EarnBlockUM.Content(
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Plain(iconRes = R.drawable.ic_staking_disable_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.staking_native),
style = EarnBlockUM.TitleUM.Style.Large,
color = { TangemTheme.colors2.text.neutral.tertiary },
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(CoreResR.string.staking_notification_network_error_text),
style = EarnBlockUM.SubtitleUM.Style.Small,
color = { TangemTheme.colors2.text.neutral.tertiary },
),
trailingUM = null,
),
EarnBlockUM.Content(
backgroundUM = EarnBlockUM.BackgroundUM.Tinted { TangemTheme.colors2.border.status.accent },
iconUM = EarnBlockUM.IconUM.Glowing(
iconRes = R.drawable.ic_staking_40,
glowColor = { TangemTheme.colors2.border.status.accent },
),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.token_details_staking_block_title),
style = EarnBlockUM.TitleUM.Style.Small,
color = { TangemTheme.colors2.text.status.accent },
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = stringReference("Average APR 5.24%"),
style = EarnBlockUM.SubtitleUM.Style.Large,
color = { TangemTheme.colors2.text.neutral.primary },
),
trailingUM = EarnBlockUM.TrailingUM.Button(
buttonUM = TangemButtonUM(
text = stringReference("Stake"),
type = TangemButtonType.Accent,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
onClick = {},
),
),
),
EarnBlockUM.Content(
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Glowing(
iconRes = R.drawable.ic_staking_40,
glowColor = { TangemTheme.colors2.text.status.accent },
),
titleUM = EarnBlockUM.TitleUM(
text = stringReference("Native staking"),
style = EarnBlockUM.TitleUM.Style.Large,
color = { TangemTheme.colors2.text.neutral.primary },
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = stringReference("$ 12.34 rewards"),
style = EarnBlockUM.SubtitleUM.Style.Small,
color = { TangemTheme.colors2.text.status.accent },
),
trailingUM = EarnBlockUM.TrailingUM.Balance(
fiatValue = stringReference("$ 500.17"),
cryptoValue = stringReference("500.00 SOL"),
isBalanceHidden = false,
onClick = {},
),
),
),
)
// endregion

View file

@ -0,0 +1,71 @@
package com.tangem.common.ui.earn
import androidx.annotation.DrawableRes
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.extensions.ColorReference2
import com.tangem.core.ui.extensions.TextReference
@Immutable
sealed interface EarnBlockUM {
data object Loading : EarnBlockUM
data class Content(
val backgroundUM: BackgroundUM,
val iconUM: IconUM,
val titleUM: TitleUM,
val subtitleUM: SubtitleUM?,
val trailingUM: TrailingUM?,
) : EarnBlockUM
@Immutable
sealed interface BackgroundUM {
data object Surface : BackgroundUM
data class Tinted(val color: ColorReference2) : BackgroundUM
}
@Immutable
sealed interface IconUM {
data class Glowing(
@DrawableRes val iconRes: Int,
val glowColor: ColorReference2,
) : IconUM
data class Plain(
@DrawableRes val iconRes: Int,
) : IconUM
}
@Immutable
data class TitleUM(
val text: TextReference,
val style: Style,
val color: ColorReference2,
) {
enum class Style { Large, Small }
}
@Immutable
sealed interface SubtitleUM {
data class Text(
val text: TextReference,
val style: Style,
val color: ColorReference2,
) : SubtitleUM
enum class Style { Large, Small }
}
@Immutable
sealed interface TrailingUM {
data class Button(val buttonUM: TangemButtonUM) : TrailingUM
data class Balance(
val fiatValue: TextReference,
val cryptoValue: TextReference,
val isBalanceHidden: Boolean,
val onClick: () -> Unit,
) : TrailingUM
}
}

View file

@ -47,7 +47,7 @@ fun TangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
iconPosition = buttonUM.iconPosition,
isEnabled = buttonUM.isEnabled,
isLoading = buttonUM.isLoading,
type = TangemButtonType.Positive,
type = TangemButtonType.Accent,
size = buttonUM.size,
shape = buttonUM.shape,
)

View file

@ -70,4 +70,20 @@ internal class TokenDetailsDialogFactory @Inject constructor(
fun showError(text: TextReference) {
uiMessageSender.send(DialogMessage(message = text))
}
fun showConfirmHideExpressStatus(onConfirm: () -> Unit) {
uiMessageSender.send(
DialogMessage(
title = resourceReference(R.string.express_status_hide_dialog_title),
message = resourceReference(R.string.express_status_hide_dialog_text),
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_hide),
onClick = onConfirm,
)
},
secondActionBuilder = { cancelAction() },
),
)
}
}

View file

@ -42,8 +42,6 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase
import com.tangem.domain.account.status.usecase.IsCryptoCurrencyCouldHideUseCase
@ -110,6 +108,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.transform
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetBalanceTransformer
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetTopBarTitleTransformer
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.ToggleBalanceTypeTransformer
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateStakingNotificationTransformer
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateNotificationsTransformer
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateTopBarMenuTransformer
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokendetails.impl.R
@ -128,6 +128,7 @@ import kotlinx.coroutines.launch
import javax.inject.Inject
@Suppress("LongParameterList", "LargeClass", "TooManyFunctions", "PropertyUsedBeforeDeclaration")
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
@Stable
@ModelScoped
internal class TokenDetailsModel @Inject constructor(
@ -204,6 +205,7 @@ internal class TokenDetailsModel @Inject constructor(
private val yieldSupplyBalanceJobHolder = JobHolder()
private val selectedAppCurrencyFlow: StateFlow<AppCurrency> = createSelectedAppCurrencyFlow()
private val redesignBalanceJobHolder = JobHolder()
private val redesignEarnJobHolder = JobHolder()
private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var account: Account.CryptoPortfolio? = null
@ -386,6 +388,13 @@ internal class TokenDetailsModel @Inject constructor(
val updatedState = stateFactory.getStateWithNotifications(warnings)
notificationsAnalyticsSender.send(uiState.value, updatedState.notifications)
uiState.value = updatedState
redesignStateController.update(
UpdateNotificationsTransformer(
warnings = warnings,
clickIntents = this@TokenDetailsModel,
),
)
}
.launchIn(modelScope)
.saveIn(warningsJobHolder)
@ -797,9 +806,9 @@ internal class TokenDetailsModel @Inject constructor(
)
if (canHide) {
showConfirmHideTokenDialog(cryptoCurrency)
dialogFactory.showConfirmHideToken(currency = cryptoCurrency, onConfirm = ::onHideConfirmed)
} else {
showLinkedTokensDialog(cryptoCurrency)
dialogFactory.showLinkedTokens(currency = cryptoCurrency)
}
}
}
@ -1021,7 +1030,7 @@ internal class TokenDetailsModel @Inject constructor(
}
}
if (message != null) {
showErrorDialog(stringReference(message))
dialogFactory.showError(text = stringReference(message))
TangemLogger.e(message)
}
},
@ -1065,7 +1074,7 @@ internal class TokenDetailsModel @Inject constructor(
}
if (message != null) {
showErrorDialog(message)
dialogFactory.showError(text = message)
}
},
ifRight = { uiState.value = stateFactory.getStateWithRemovedRequiredTrustlineNotification() },
@ -1080,7 +1089,9 @@ internal class TokenDetailsModel @Inject constructor(
blockchain = cryptoCurrency.network.name,
),
)
showDismissIncompleteTransactionConfirmDialog()
dialogFactory.showDismissIncompleteTransactionConfirm(
onConfirm = ::onConfirmDismissIncompleteTransactionClick,
)
}
override fun onConfirmDismissIncompleteTransactionClick() {
@ -1090,7 +1101,7 @@ internal class TokenDetailsModel @Inject constructor(
currency = cryptoCurrency,
).fold(
ifLeft = { e ->
showErrorDialog(stringReference(e.message.orEmpty()))
dialogFactory.showError(text = stringReference(e.message.orEmpty()))
TangemLogger.e("Error: $e")
},
ifRight = {
@ -1115,7 +1126,8 @@ internal class TokenDetailsModel @Inject constructor(
ifLeft = { e ->
when (e) {
is AssociateAssetError.NotEnoughBalance -> {
showErrorDialog(
dialogFactory.showError(
text =
resourceReference(
id = R.string.warning_hedera_token_association_not_enough_hbar_message,
formatArgs = wrappedList(e.feeCurrency.symbol),
@ -1123,7 +1135,7 @@ internal class TokenDetailsModel @Inject constructor(
)
}
is AssociateAssetError.DataError -> {
showErrorDialog(stringReference(e.message.orEmpty()))
dialogFactory.showError(text = stringReference(e.message.orEmpty()))
TangemLogger.e("Error: $e")
}
}
@ -1138,7 +1150,7 @@ internal class TokenDetailsModel @Inject constructor(
}
override fun onConfirmDisposeExpressStatus() {
showConfirmHideExpressStatusDialog()
dialogFactory.showConfirmHideExpressStatus(onConfirm = ::onDisposeExpressStatus)
}
override fun onDisposeExpressStatus() {
@ -1172,7 +1184,7 @@ internal class TokenDetailsModel @Inject constructor(
private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean {
if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false
showErrorDialog(unavailabilityReason.getUnavailabilityReasonText())
dialogFactory.showError(text = unavailabilityReason.getUnavailabilityReasonText())
return true
}
@ -1201,42 +1213,6 @@ internal class TokenDetailsModel @Inject constructor(
uiMessageSender.send(SnackbarMessage(resourceReference(R.string.staking_error_no_validators_title)))
}
private fun showConfirmHideTokenDialog(currency: CryptoCurrency) {
dialogFactory.showConfirmHideToken(currency = currency, onConfirm = ::onHideConfirmed)
}
private fun showLinkedTokensDialog(currency: CryptoCurrency) {
dialogFactory.showLinkedTokens(currency = currency)
}
private fun showDismissIncompleteTransactionConfirmDialog() {
dialogFactory.showDismissIncompleteTransactionConfirm(
onConfirm = ::onConfirmDismissIncompleteTransactionClick,
)
}
private fun showConfirmHideExpressStatusDialog() {
uiMessageSender.send(
DialogMessage(
title = resourceReference(R.string.express_status_hide_dialog_title),
message = resourceReference(R.string.express_status_hide_dialog_text),
firstActionBuilder = {
EventMessageAction(
title = resourceReference(R.string.common_hide),
onClick = {
onDisposeExpressStatus()
},
)
},
secondActionBuilder = { cancelAction() },
),
)
}
private fun showErrorDialog(text: TextReference) {
dialogFactory.showError(text = text)
}
private fun checkForActionUpdates() {
combine(
tokenDetailsDeepLinkActionListener.tokenDetailsActionFlow,
@ -1424,6 +1400,50 @@ internal class TokenDetailsModel @Inject constructor(
observeRedesignBalance()
updateRedesignTopBarMenu()
observeRedesignTopBarTitle()
observeRedesignStakingNotification()
}
private fun observeRedesignStakingNotification() {
val statusFlow = getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency)
.map { it.status }
.distinctUntilChanged()
val availabilityFlow = getStakingAvailabilityUseCase(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrency,
)
.map { it.getOrElse { StakingAvailability.Unavailable } }
.distinctUntilChanged()
val entryInfoFlow = availabilityFlow.mapLatest { availability ->
(availability as? StakingAvailability.Available)?.let { available ->
getStakingEntryInfoUseCase(
cryptoCurrencyId = cryptoCurrency.id,
symbol = cryptoCurrency.symbol,
stakingOption = available.option,
).getOrNull()
}
}
combine(
flow = statusFlow,
flow2 = availabilityFlow,
flow3 = entryInfoFlow,
flow4 = selectedAppCurrencyFlow,
) { status, availability, entryInfo, appCurrency ->
redesignStateController.update(
UpdateStakingNotificationTransformer(
cryptoCurrencyStatus = status,
stakingAvailability = availability,
stakingEntryInfo = entryInfo,
appCurrency = appCurrency,
clickIntents = this,
),
)
}
.flowOn(dispatchers.default)
.launchIn(modelScope)
.saveIn(redesignEarnJobHolder)
}
private fun initRedesignState() {

View file

@ -61,8 +61,9 @@ internal class TokenDetailsStateController @Inject constructor() {
tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
currencyIconState = CurrencyIconState.Loading,
),
notifications = persistentListOf(),
earnBlockState = null,
marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = ""),
stakingBlocksState = null,
pullToRefreshConfig = PullToRefreshConfig(
isRefreshing = false,
onRefresh = {},

View file

@ -5,7 +5,9 @@ import com.tangem.common.ui.account.AccountIconUM
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.common.ui.earn.EarnBlockUM
import com.tangem.core.ui.ds.image.DeviceIconUM
import com.tangem.core.ui.ds.message.TangemMessageUM
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
@ -13,8 +15,9 @@ import kotlinx.collections.immutable.ImmutableList
internal data class TokenDetailsUM(
val topAppBarUM: TokenDetailsTopAppBarUM,
val balanceBlockUM: TokenDetailsBalanceBlockUM,
val notifications: ImmutableList<TangemMessageUM>,
val earnBlockState: EarnBlockUM?,
val marketPriceBlockState: MarketPriceBlockState,
val stakingBlocksState: StakingBlockUM?,
val pullToRefreshConfig: PullToRefreshConfig,
val isBalanceHidden: Boolean,
val isMarketPriceAvailable: Boolean,

View file

@ -0,0 +1,225 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.message.TangemMessageButtonUM
import com.tangem.core.ui.ds.message.TangemMessageEffect
import com.tangem.core.ui.ds.message.TangemMessageUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
import com.tangem.domain.tokens.model.warnings.HederaWarnings
import com.tangem.domain.tokens.model.warnings.KaspaWarnings
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import com.tangem.core.res.R as CoreResR
internal class UpdateNotificationsTransformer(
private val warnings: Set<CryptoCurrencyWarning>,
private val clickIntents: TokenDetailsClickIntents,
) : Transformer<TokenDetailsUM> {
override fun transform(prevState: TokenDetailsUM): TokenDetailsUM {
val notifications = warnings.mapNotNull(::mapWarning).toImmutableList()
return prevState.copy(notifications = notifications)
}
@Suppress("LongMethod")
private fun mapWarning(warning: CryptoCurrencyWarning): TangemMessageUM? {
return when (warning) {
is CryptoCurrencyWarning.SomeNetworksUnreachable -> TangemMessageUM(
id = "networks_unreachable",
title = resourceReference(CoreResR.string.warning_network_unreachable_title),
subtitle = resourceReference(CoreResR.string.warning_network_unreachable_message),
messageEffect = TangemMessageEffect.None,
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
)
is CryptoCurrencyWarning.BalanceNotEnoughForFee -> createFeeWarning(
FeeWarningParams(
id = "balance_not_enough_for_fee",
currency = warning.tokenCurrency,
networkName = warning.coinCurrency.network.name,
feeCurrencyName = warning.coinCurrency.name,
feeCurrencySymbol = warning.coinCurrency.symbol,
buyCurrency = warning.coinCurrency,
),
)
is CryptoCurrencyWarning.CustomTokenNotEnoughForFee -> createFeeWarning(
FeeWarningParams(
id = "custom_token_not_enough_for_fee",
currency = warning.currency,
networkName = warning.feeCurrency?.network?.name ?: warning.networkName,
feeCurrencyName = warning.feeCurrencyName,
feeCurrencySymbol = warning.feeCurrencySymbol,
buyCurrency = warning.feeCurrency,
),
)
is CryptoCurrencyWarning.BeaconChainShutdown -> TangemMessageUM(
id = "beacon_chain_shutdown",
title = resourceReference(CoreResR.string.warning_beacon_chain_retirement_title),
subtitle = resourceReference(CoreResR.string.warning_beacon_chain_retirement_content),
messageEffect = TangemMessageEffect.None,
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
)
is HederaWarnings.AssociateWarning -> TangemMessageUM(
id = "hedera_associate",
title = resourceReference(CoreResR.string.warning_hedera_missing_token_association_title),
subtitle = resourceReference(CoreResR.string.warning_hedera_missing_token_association_message_brief),
messageEffect = TangemMessageEffect.None,
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(CoreResR.string.warning_hedera_missing_token_association_button_title),
type = TangemButtonType.Primary,
onClick = clickIntents::onAssociateClick,
),
),
)
is HederaWarnings.AssociateWarningWithFee -> TangemMessageUM(
id = "hedera_associate_fee",
title = resourceReference(CoreResR.string.warning_hedera_missing_token_association_title),
subtitle = resourceReference(
CoreResR.string.warning_hedera_missing_token_association_message,
wrappedList(
warning.fee.format { crypto(symbol = "", decimals = warning.feeCurrencyDecimals) },
warning.feeCurrencySymbol,
),
),
messageEffect = TangemMessageEffect.None,
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(CoreResR.string.warning_hedera_missing_token_association_button_title),
type = TangemButtonType.Primary,
onClick = clickIntents::onAssociateClick,
),
),
)
is CryptoCurrencyWarning.RequiredTrustline -> TangemMessageUM(
id = "required_trustline",
title = resourceReference(CoreResR.string.warning_token_trustline_title),
subtitle = resourceReference(
CoreResR.string.warning_token_trustline_subtitle,
wrappedList(
warning.requiredAmount.format { crypto(symbol = "", warning.currencyDecimals) }.trim(),
warning.currencySymbol,
),
),
messageEffect = TangemMessageEffect.None,
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(CoreResR.string.warning_token_trustline_button_title),
type = TangemButtonType.Primary,
onClick = clickIntents::onOpenTrustlineClick,
),
),
)
is KaspaWarnings.IncompleteTransaction -> TangemMessageUM(
id = "kaspa_incomplete",
title = resourceReference(CoreResR.string.warning_kaspa_unfinished_token_transaction_title),
subtitle = resourceReference(
CoreResR.string.warning_kaspa_unfinished_token_transaction_message,
wrappedList(
warning.amount.format { crypto(symbol = "", decimals = warning.currencyDecimals) },
warning.currencySymbol,
),
),
messageEffect = TangemMessageEffect.None,
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(CoreResR.string.alert_button_try_again),
type = TangemButtonType.Primary,
onClick = clickIntents::onRetryIncompleteTransactionClick,
),
),
onCloseClick = clickIntents::onDismissIncompleteTransactionClick,
)
is CryptoCurrencyWarning.MigrationMaticToPol -> TangemMessageUM(
id = "migration_matic_pol",
title = resourceReference(CoreResR.string.warning_matic_migration_title),
subtitle = resourceReference(CoreResR.string.warning_matic_migration_message),
messageEffect = TangemMessageEffect.None,
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
)
is CryptoCurrencyWarning.MigrationClore -> TangemMessageUM(
id = "migration_clore",
title = resourceReference(CoreResR.string.warning_clore_migration_title),
subtitle = resourceReference(CoreResR.string.warning_clore_migration_description),
messageEffect = TangemMessageEffect.None,
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
buttonsUM = persistentListOf(
TangemMessageButtonUM(
text = resourceReference(CoreResR.string.warning_clore_migration_button),
type = TangemButtonType.Primary,
onClick = clickIntents::onCloreMigrationClick,
),
),
)
// Non-warning types — skip for redesign
is CryptoCurrencyWarning.ExistentialDeposit,
is CryptoCurrencyWarning.Rent,
is CryptoCurrencyWarning.SomeNetworksNoAccount,
is CryptoCurrencyWarning.TopUpWithoutReserve,
is CryptoCurrencyWarning.SwapPromo,
is CryptoCurrencyWarning.FeeResourceInfo,
is CryptoCurrencyWarning.UsedOutdatedDataWarning,
-> null
}
}
private fun createFeeWarning(params: FeeWarningParams): TangemMessageUM {
val buttons = if (params.buyCurrency != null) {
persistentListOf(
TangemMessageButtonUM(
text = resourceReference(
CoreResR.string.common_buy_currency,
wrappedList(params.feeCurrencySymbol),
),
type = TangemButtonType.Primary,
onClick = { clickIntents.onBuyCoinClick(params.buyCurrency) },
),
)
} else {
persistentListOf()
}
return TangemMessageUM(
id = params.id,
title = resourceReference(
CoreResR.string.warning_send_blocked_funds_for_fee_title,
wrappedList(params.feeCurrencyName),
),
subtitle = resourceReference(
CoreResR.string.warning_send_blocked_funds_for_fee_message,
wrappedList(
params.currency.name,
params.networkName,
params.currency.name,
params.feeCurrencyName,
params.feeCurrencySymbol,
),
),
messageEffect = TangemMessageEffect.None,
iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24),
buttonsUM = buttons,
)
}
private data class FeeWarningParams(
val id: String,
val currency: CryptoCurrency,
val networkName: String,
val feeCurrencyName: String,
val feeCurrencySymbol: String,
val buyCurrency: CryptoCurrency?,
)
}

View file

@ -0,0 +1,298 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer
import androidx.compose.ui.text.SpanStyle
import com.tangem.common.getRewardStakingBalance
import com.tangem.common.getTotalStakingBalance
import com.tangem.common.ui.earn.EarnBlockUM
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.defaultAmount
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.formatStyled
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.staking.RewardBlockType
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.StakingOption
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
import com.tangem.features.tokendetails.impl.R
import com.tangem.lib.crypto.BlockchainUtils.isStakingRewardUnavailable
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal
import com.tangem.core.res.R as CoreResR
import com.tangem.core.ui.R as CoreUiR
internal class UpdateStakingNotificationTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val stakingAvailability: StakingAvailability,
private val stakingEntryInfo: StakingEntryInfo?,
private val appCurrency: AppCurrency,
private val clickIntents: TokenDetailsClickIntents,
) : Transformer<TokenDetailsUM> {
override fun transform(prevState: TokenDetailsUM): TokenDetailsUM {
return prevState.copy(earnBlockState = buildEarnBlock(prevState.isBalanceHidden))
}
private fun buildEarnBlock(isBalanceHidden: Boolean): EarnBlockUM? {
return when (val availability = stakingAvailability) {
StakingAvailability.TemporaryUnavailable -> buildTemporaryUnavailable()
StakingAvailability.Unavailable -> null
is StakingAvailability.Available -> getStakingInfoBlock(availability, isBalanceHidden)
}
}
private fun buildTemporaryUnavailable(): EarnBlockUM.Content {
return EarnBlockUM.Content(
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Plain(iconRes = CoreUiR.drawable.ic_staking_disable_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.staking_native),
style = EarnBlockUM.TitleUM.Style.Large,
color = { TangemTheme.colors2.text.neutral.tertiary },
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(CoreResR.string.staking_notification_network_error_text),
style = EarnBlockUM.SubtitleUM.Style.Small,
color = { TangemTheme.colors2.text.neutral.tertiary },
),
trailingUM = null,
)
}
private fun getStakingInfoBlock(
availability: StakingAvailability.Available,
isBalanceHidden: Boolean,
): EarnBlockUM? {
val status = cryptoCurrencyStatus
val stakingBalance = status.value.stakingBalance as? StakingBalance.Data
val stakingCryptoAmount = stakingBalance?.getTotalStakingBalance(status.currency.network.rawId)
return when {
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo != null -> {
val hasPendingBalances = stakingBalance.hasPendingBalances()
if (!hasPendingBalances) {
buildStakeAvailable(
availability = availability,
isEnabled = isStakingButtonEnabled(status),
)
} else {
buildActiveBlock(
stakingAmount = stakingBalance.getPendingAmount(),
rewardAmount = null,
isBalanceHidden = isBalanceHidden,
)
}
}
stakingCryptoAmount.isNullOrZero() && stakingEntryInfo == null -> null
else -> buildActiveBlock(
stakingAmount = stakingCryptoAmount,
rewardAmount = stakingBalance.getRewardAmount(),
isBalanceHidden = isBalanceHidden,
)
}
}
private fun isStakingButtonEnabled(status: CryptoCurrencyStatus): Boolean {
return status.value is CryptoCurrencyStatus.Loaded ||
status.value is CryptoCurrencyStatus.NoQuote ||
status.value is CryptoCurrencyStatus.Custom
}
private fun buildStakeAvailable(
availability: StakingAvailability.Available,
isEnabled: Boolean,
): EarnBlockUM.Content {
return EarnBlockUM.Content(
backgroundUM = EarnBlockUM.BackgroundUM.Tinted { TangemTheme.colors2.markers.backgroundTintedBlue },
iconUM = EarnBlockUM.IconUM.Glowing(
iconRes = CoreUiR.drawable.ic_staking_40,
glowColor = { TangemTheme.colors2.border.status.accent },
),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(id = R.string.token_details_staking_block_title),
style = EarnBlockUM.TitleUM.Style.Small,
color = { TangemTheme.colors2.text.status.accent },
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = stakeAvailableSubtitle(availability.option.displayApy),
style = EarnBlockUM.SubtitleUM.Style.Large,
color = { TangemTheme.colors2.text.neutral.primary },
),
trailingUM = EarnBlockUM.TrailingUM.Button(
buttonUM = TangemButtonUM(
text = resourceReference(R.string.common_stake),
type = TangemButtonType.Accent,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
isEnabled = isEnabled,
onClick = clickIntents::onStakeBannerClick,
),
),
)
}
private fun stakeAvailableSubtitle(apy: BigDecimal?): TextReference {
return if (apy != null) {
resourceReference(
CoreResR.string.token_details_earn_staking_subtitle,
wrappedList(apy.format { percent() }),
)
} else {
resourceReference(CoreResR.string.staking_notification_earn_rewards_text)
}
}
private fun buildActiveBlock(
stakingAmount: BigDecimal?,
rewardAmount: BigDecimal?,
isBalanceHidden: Boolean,
): EarnBlockUM.Content {
val status = cryptoCurrencyStatus
val fiatRate = status.value.fiatRate
val fiatAmount = stakingAmount?.let { fiatRate?.multiply(it) }
val rewardFiatAmount = rewardAmount?.let { fiatRate?.multiply(it) }
return EarnBlockUM.Content(
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Glowing(
iconRes = CoreUiR.drawable.ic_staking_40,
glowColor = { TangemTheme.colors2.border.status.accent },
),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.staking_native),
style = EarnBlockUM.TitleUM.Style.Large,
color = { TangemTheme.colors2.text.neutral.primary },
),
subtitleUM = getRewardSubtitle(status, rewardFiatAmount),
trailingUM = EarnBlockUM.TrailingUM.Balance(
fiatValue = fiatAmount.formatStyled {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
).defaultAmount(
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
)
},
cryptoValue = stringReference(
stakingAmount.format {
crypto(
symbol = status.currency.symbol,
decimals = status.currency.decimals,
)
},
),
isBalanceHidden = isBalanceHidden,
onClick = clickIntents::onStakeBannerClick,
),
)
}
private fun getRewardSubtitle(
status: CryptoCurrencyStatus,
stakingRewardAmount: BigDecimal?,
): EarnBlockUM.SubtitleUM? {
val blockchainId = status.currency.network.rawId
val isCoin = status.currency.id.isCoin
val stakingBalance = status.value.stakingBalance
val rewardBlockType = when {
stakingBalance is StakingBalance.Data.P2PEthPool -> {
if (stakingBalance.totalRewards.isNullOrZero()) {
RewardBlockType.RewardUnavailable.DefaultRewardUnavailable
} else {
RewardBlockType.EthereumEarnedRewards
}
}
isStakingRewardUnavailable(blockchainId, isCoin) -> {
RewardBlockType.RewardUnavailable.DefaultRewardUnavailable
}
stakingRewardAmount.isNullOrZero() -> RewardBlockType.NoRewards
else -> RewardBlockType.Rewards
}
val text = when (rewardBlockType) {
RewardBlockType.NoRewards -> resourceReference(R.string.staking_details_no_rewards_to_claim)
RewardBlockType.CardanoNoRewards -> resourceReference(R.string.staking_cardano_details_rewards_info_text)
RewardBlockType.RewardUnavailable.DefaultRewardUnavailable,
RewardBlockType.RewardUnavailable.SolanaRewardUnavailable,
-> return null
RewardBlockType.EthereumEarnedRewards -> {
val cryptoRewardAmount = (stakingBalance as? StakingBalance.Data.P2PEthPool)?.totalRewards
resourceReference(
R.string.staking_details_autocompound_rewards_earned,
wrappedList(
cryptoRewardAmount.format {
crypto(
symbol = status.currency.symbol,
decimals = status.currency.decimals,
)
},
),
)
}
RewardBlockType.RewardsRequirementsError,
RewardBlockType.Rewards,
-> resourceReference(
R.string.staking_details_rewards_to_claim,
wrappedList(
stakingRewardAmount.format { fiat(appCurrency.code, appCurrency.symbol) },
),
)
}
val isAccent = rewardBlockType == RewardBlockType.Rewards ||
rewardBlockType == RewardBlockType.RewardsRequirementsError ||
rewardBlockType == RewardBlockType.EthereumEarnedRewards
return EarnBlockUM.SubtitleUM.Text(
text = text,
style = EarnBlockUM.SubtitleUM.Style.Small,
color = if (isAccent) {
{ TangemTheme.colors2.text.status.accent }
} else {
{ TangemTheme.colors2.text.neutral.tertiary }
},
)
}
}
private fun StakingBalance.Data?.hasPendingBalances(): Boolean = when (this) {
is StakingBalance.Data.StakeKit -> balance.items.isNotEmpty()
is StakingBalance.Data.P2PEthPool -> !unstakingAmount.isNullOrZero()
null -> false
}
private fun StakingBalance.Data?.getPendingAmount(): BigDecimal = when (this) {
is StakingBalance.Data.StakeKit -> balance.items.sumOf { it.amount }
is StakingBalance.Data.P2PEthPool -> unstakingAmount
null -> BigDecimal.ZERO
}
private fun StakingBalance.Data?.getRewardAmount(): BigDecimal = when (this) {
is StakingBalance.Data.StakeKit -> getRewardStakingBalance()
is StakingBalance.Data.P2PEthPool -> totalRewards
null -> BigDecimal.ZERO
}
private val StakingOption.displayApy: BigDecimal?
get() = when (this) {
is StakingOption.StakeKit -> yield.preferredValidators
.mapNotNull { it.rewardInfo?.rate }
.maxOrNull()
is StakingOption.P2PEthPool -> apy
}

View file

@ -10,10 +10,13 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import com.tangem.common.ui.earn.EarnBlock
import com.tangem.common.ui.notifications.notifications
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
@ -59,6 +62,11 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi
partialCollapsedHeight = partialCollapsedHeight,
)
val rootBackground by LocalRootBackgroundColor.current
val notificationModifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens2.x4)
Box(
modifier = modifier.fillMaxSize(),
) {
@ -79,18 +87,18 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi
)
},
body = {
LazyColumn(
TokenDetailsBody(
tokenDetailsUM = tokenDetailsUM,
rootBackground = rootBackground,
modifier = Modifier
.fillMaxSize()
.nestedScroll(behavior.nestedScrollConnection),
) {
// TODO [REDACTED_TASK_KEY] Token Details Make Transaction History
}
itemModifier = notificationModifier,
)
},
)
}
val rootBackground by LocalRootBackgroundColor.current
val hazeIntensity by animateFloatAsState(
targetValue = (behavior.state.collapsedFraction * 2f).coerceIn(0f, 1f),
label = "TopBarHazeIntensity",
@ -110,6 +118,31 @@ internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifi
}
}
@Composable
private fun TokenDetailsBody(
tokenDetailsUM: TokenDetailsUM,
rootBackground: Color,
modifier: Modifier = Modifier,
itemModifier: Modifier = Modifier,
) {
LazyColumn(modifier = modifier) {
notifications(
notifications = tokenDetailsUM.notifications,
contentColor = rootBackground,
modifier = itemModifier,
)
tokenDetailsUM.earnBlockState?.let { earnBlock ->
item(key = "staking_block") {
EarnBlock(
state = earnBlock,
modifier = itemModifier,
)
}
}
// TODO [REDACTED_TASK_KEY] Token Details Make Transaction History
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@ -137,13 +170,14 @@ private fun TokenDetailsScreen_Preview() {
),
),
),
notifications = persistentListOf(),
earnBlockState = null,
balanceBlockUM = TokenDetailsBalanceBlockUM.Loading(
actionButtons = persistentListOf(),
tokenBalanceTypeUM = TokenBalanceTypeUM.Single,
currencyIconState = CurrencyIconState.Loading,
),
marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = "USDT"),
stakingBlocksState = null,
pullToRefreshConfig = PullToRefreshConfig(
isRefreshing = false,
onRefresh = {},

View file

@ -31,7 +31,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
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.express.ExpressStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlockLegacy
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.txhistory.entity.TxHistoryUM
@ -133,7 +133,7 @@ internal fun TokenDetailsScreenLegacy(
key = StakingBlockUM::class.java,
contentType = StakingBlockUM::class.java,
content = {
TokenStakingBlock(
TokenStakingBlockLegacy(
state = state.stakingBlocksState,
isBalanceHidden = state.isBalanceHidden,
modifier = itemModifier,

View file

@ -42,7 +42,7 @@ import com.tangem.features.tokendetails.impl.R
* @param modifier modifier
*/
@Composable
internal fun TokenStakingBlock(state: StakingBlockUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
internal fun TokenStakingBlockLegacy(state: StakingBlockUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
@ -60,16 +60,16 @@ internal fun TokenStakingBlock(state: StakingBlockUM, isBalanceHidden: Boolean,
targetState = state,
contentAlignment = Alignment.CenterStart,
label = "Staking block animation",
) {
when (it) {
) { stakingBlock ->
when (stakingBlock) {
is StakingBlockUM.TemporaryUnavailable -> StakingTemporaryUnavailableBlock()
is StakingBlockUM.Loading -> StakingLoading()
is StakingBlockUM.Staked -> StakingBalanceBlock(
state = it,
state = stakingBlock,
isBalanceHidden = isBalanceHidden,
)
is StakingBlockUM.StakeAvailable -> StakingAvailableContent(
state = it,
state = stakingBlock,
)
}
}
@ -165,7 +165,7 @@ private fun Preview_TokenStakingBlock(
state: StakingBlockUM,
) {
TangemThemePreview {
TokenStakingBlock(
TokenStakingBlockLegacy(
state = state,
isBalanceHidden = false,
)