Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-12 21:29:05 +03:00
parent 7e42240fc6
commit 530b2d93ad
13 changed files with 306 additions and 193 deletions

View file

@ -49,6 +49,7 @@ 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.extensions.wrappedList
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.res.R as CoreResR
@ -123,10 +124,9 @@ private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Mo
.padding(end = TangemTheme.dimens2.x3),
)
Text(
text = state.titleUM.text.resolveReference(),
style = state.titleUM.style.textStyle,
color = state.titleUM.tone.color(state.type),
EarnBlockTitle(
titleUM = state.titleUM,
type = state.type,
modifier = Modifier
.layoutId(TangemRowLayoutId.START_TOP)
.padding(end = TangemTheme.dimens2.x2),
@ -182,7 +182,7 @@ private fun EarnBlockTrailing(type: Type, trailingUM: EarnBlockUM.TrailingUM?, o
TangemButton(
buttonUM = TangemButtonUM(
text = trailingUM.text,
type = type.buttonType(),
type = trailingUM.style.buttonType(type),
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
isEnabled = trailingUM.isEnabled,
@ -207,21 +207,37 @@ private fun EarnBlockTrailing(type: Type, trailingUM: EarnBlockUM.TrailingUM?, o
)
}
}
is EarnBlockUM.TrailingUM.Icon -> {
TangemIcon(
tangemIconUM = TangemIconUM.Icon(
iconRes = trailingUM.tone.iconRes(),
tintReference = { trailingUM.tone.tint() },
),
modifier = Modifier
.layoutId(TangemRowLayoutId.TAIL)
.size(TangemTheme.dimens2.x6),
)
}
null -> Unit
}
}
@Composable
private fun EarnBlockTitle(titleUM: EarnBlockUM.TitleUM, type: Type, modifier: Modifier = Modifier) {
val titleText: @Composable () -> Unit = {
Text(
text = titleUM.text.resolveReference(),
style = titleUM.style.textStyle,
color = titleUM.tone.color(type),
)
}
val icon = titleUM.iconUM
if (icon == null) {
Box(modifier = modifier) { titleText() }
return
}
Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) {
titleText()
Spacer(modifier = Modifier.width(TangemTheme.dimens2.x1))
TangemIcon(
tangemIconUM = TangemIconUM.Icon(
iconRes = icon.tone.iconRes(),
tintReference = { icon.tone.tint() },
),
modifier = Modifier.size(TangemTheme.dimens2.x4),
)
}
}
@Composable
private fun EarnBlockSubtitle(subtitle: EarnBlockUM.SubtitleUM.Text, type: Type, modifier: Modifier = Modifier) {
val textStyle = subtitle.style.textStyle
@ -297,9 +313,12 @@ private fun Type.accentStrongTint(): Color = when (this) {
Type.YieldSupply -> TangemTheme.colors2.text.status.positive
}
private fun Type.buttonType(): TangemButtonType = when (this) {
Type.Staking -> TangemButtonType.Accent
Type.YieldSupply -> TangemButtonType.Positive
private fun EarnBlockUM.TrailingUM.Button.Style.buttonType(type: Type): TangemButtonType = when (this) {
EarnBlockUM.TrailingUM.Button.Style.Default -> when (type) {
Type.Staking -> TangemButtonType.Accent
Type.YieldSupply -> TangemButtonType.Positive
}
EarnBlockUM.TrailingUM.Button.Style.Secondary -> TangemButtonType.Secondary
}
@Composable
@ -319,16 +338,16 @@ private fun EarnBlockUM.SubtitleUM.Tone.color(type: Type): Color = when (this) {
EarnBlockUM.SubtitleUM.Tone.Accent -> type.accentText()
}
private fun EarnBlockUM.TrailingUM.IconTone.iconRes(): Int = when (this) {
EarnBlockUM.TrailingUM.IconTone.Warning -> R.drawable.ic_alert_triangle_20
EarnBlockUM.TrailingUM.IconTone.Info -> R.drawable.ic_alert_circle_red_20
private fun EarnBlockUM.TitleUM.IconTone.iconRes(): Int = when (this) {
EarnBlockUM.TitleUM.IconTone.Warning -> R.drawable.ic_attention_default_24
EarnBlockUM.TitleUM.IconTone.Info -> R.drawable.ic_alert_circle_24
}
@Composable
@ReadOnlyComposable
private fun EarnBlockUM.TrailingUM.IconTone.tint(): Color = when (this) {
EarnBlockUM.TrailingUM.IconTone.Warning -> TangemTheme.colors2.graphic.status.attention
EarnBlockUM.TrailingUM.IconTone.Info -> TangemTheme.colors2.fill.neutral.secondary
private fun EarnBlockUM.TitleUM.IconTone.tint(): Color = when (this) {
EarnBlockUM.TitleUM.IconTone.Warning -> TangemTheme.colors2.graphic.status.attention
EarnBlockUM.TitleUM.IconTone.Info -> TangemTheme.colors2.graphic.neutral.secondary
}
@Composable
@ -359,7 +378,7 @@ private val EarnBlockUM.SubtitleUM.Style.textStyle: TextStyle
@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) {
private fun EarnBlock_Staking_Preview(@PreviewParameter(EarnBlockStakingPreviewProvider::class) state: EarnBlockUM) {
TangemThemePreviewRedesign {
EarnBlock(
state = state,
@ -368,7 +387,21 @@ private fun EarnBlock_Preview(@PreviewParameter(EarnBlockPreviewProvider::class)
}
}
private class EarnBlockPreviewProvider : CollectionPreviewParameterProvider<EarnBlockUM>(
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun EarnBlock_YieldSupply_Preview(
@PreviewParameter(EarnBlockYieldSupplyPreviewProvider::class) state: EarnBlockUM,
) {
TangemThemePreviewRedesign {
EarnBlock(
state = state,
modifier = Modifier.padding(TangemTheme.dimens2.x4),
)
}
}
private class EarnBlockStakingPreviewProvider : CollectionPreviewParameterProvider<EarnBlockUM>(
collection = listOf(
EarnBlockUM.Loading,
EarnBlockUM.Content(
@ -376,7 +409,7 @@ private class EarnBlockPreviewProvider : CollectionPreviewParameterProvider<Earn
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Plain(iconRes = R.drawable.ic_staking_disable_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.staking_native),
text = resourceReference(CoreResR.string.common_stake),
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Disabled,
),
@ -392,17 +425,17 @@ private class EarnBlockPreviewProvider : CollectionPreviewParameterProvider<Earn
backgroundUM = EarnBlockUM.BackgroundUM.AccentSoft,
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_staking_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.token_details_staking_block_title),
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Accent,
text = resourceReference(CoreResR.string.common_staking),
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = stringReference("Average APR 5.24%"),
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Primary,
style = EarnBlockUM.SubtitleUM.Style.Small,
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
),
trailingUM = EarnBlockUM.TrailingUM.Button(
text = stringReference("Stake"),
text = resourceReference(CoreResR.string.common_stake),
),
onClick = {},
),
@ -411,7 +444,7 @@ private class EarnBlockPreviewProvider : CollectionPreviewParameterProvider<Earn
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_staking_40),
titleUM = EarnBlockUM.TitleUM(
text = stringReference("Native staking"),
text = resourceReference(CoreResR.string.staking_enabled),
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
@ -427,25 +460,146 @@ private class EarnBlockPreviewProvider : CollectionPreviewParameterProvider<Earn
),
onClick = {},
),
),
)
private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterProvider<EarnBlockUM>(
collection = listOf(
// Available — promo entry: AccentSoft background, "More" button
EarnBlockUM.Content(
type = Type.YieldSupply,
backgroundUM = EarnBlockUM.BackgroundUM.AccentSoft,
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
titleUM = EarnBlockUM.TitleUM(
text = stringReference("Earn yield"),
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Accent,
text = resourceReference(
id = CoreResR.string.yield_module_token_details_earn_notification_subtitle,
formatArgs = wrappedList("5.24"),
),
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = stringReference("Start earning · 5.24%"),
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Primary,
text = resourceReference(
CoreResR.string.yield_module_token_details_earn_notification_description,
),
style = EarnBlockUM.SubtitleUM.Style.Small,
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
),
trailingUM = EarnBlockUM.TrailingUM.Button(
text = stringReference("More"),
text = resourceReference(CoreResR.string.common_more),
),
onClick = {},
),
// Content — yield enabled, "Details" button
EarnBlockUM.Content(
type = Type.YieldSupply,
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.yield_module_transaction_enter),
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(
id = CoreResR.string.yield_module_average_apy,
formatArgs = wrappedList("5.24"),
),
style = EarnBlockUM.SubtitleUM.Style.Small,
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
),
trailingUM = EarnBlockUM.TrailingUM.Button(
text = resourceReference(CoreResR.string.details_title),
style = EarnBlockUM.TrailingUM.Button.Style.Secondary,
),
onClick = {},
),
// Content with Warning title icon
EarnBlockUM.Content(
type = Type.YieldSupply,
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.common_yield_mode),
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Primary,
iconUM = EarnBlockUM.TitleUM.IconUM(tone = EarnBlockUM.TitleUM.IconTone.Warning),
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(
id = CoreResR.string.yield_module_average_apy,
formatArgs = wrappedList("5.24"),
),
style = EarnBlockUM.SubtitleUM.Style.Small,
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
),
trailingUM = EarnBlockUM.TrailingUM.Button(
text = resourceReference(CoreResR.string.details_title),
style = EarnBlockUM.TrailingUM.Button.Style.Secondary,
),
onClick = {},
),
// Content with Info title icon
EarnBlockUM.Content(
type = Type.YieldSupply,
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.common_yield_mode),
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Primary,
iconUM = EarnBlockUM.TitleUM.IconUM(tone = EarnBlockUM.TitleUM.IconTone.Info),
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(
id = CoreResR.string.yield_module_average_apy,
formatArgs = wrappedList("5.24"),
),
style = EarnBlockUM.SubtitleUM.Style.Small,
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
),
trailingUM = EarnBlockUM.TrailingUM.Button(
text = resourceReference(CoreResR.string.details_title),
style = EarnBlockUM.TrailingUM.Button.Style.Secondary,
),
onClick = {},
),
// Processing.Enter — enabling, no trailing
EarnBlockUM.Content(
type = Type.YieldSupply,
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.common_yield_mode),
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(CoreResR.string.common_enabling),
style = EarnBlockUM.SubtitleUM.Style.Small,
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
loader = EarnBlockUM.SubtitleUM.Loader(tone = EarnBlockUM.SubtitleUM.LoaderTone.Positive),
),
trailingUM = null,
),
// Processing.Exit — disabling, no trailing, plain icon
EarnBlockUM.Content(
type = Type.YieldSupply,
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Plain(iconRes = R.drawable.ic_yield_disabling_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.common_yield_mode),
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(CoreResR.string.common_disabling),
style = EarnBlockUM.SubtitleUM.Style.Small,
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
loader = EarnBlockUM.SubtitleUM.Loader(tone = EarnBlockUM.SubtitleUM.LoaderTone.Muted),
),
trailingUM = null,
),
),
)
// endregion

View file

@ -39,9 +39,13 @@ sealed interface EarnBlockUM {
val text: TextReference,
val style: Style,
val tone: Tone,
val iconUM: IconUM? = null,
) {
enum class Style { Large, Small }
enum class Tone { Primary, Secondary, Disabled, Accent }
data class IconUM(val tone: IconTone)
enum class IconTone { Warning, Info }
}
@Immutable
@ -65,18 +69,15 @@ sealed interface EarnBlockUM {
data class Button(
val text: TextReference,
val isEnabled: Boolean = true,
) : TrailingUM
val style: Style = Style.Default,
) : TrailingUM {
enum class Style { Default, Secondary }
}
data class Balance(
val fiatValue: TextReference,
val cryptoValue: TextReference,
val isBalanceHidden: Boolean,
) : TrailingUM
data class Icon(
val tone: IconTone,
) : TrailingUM
enum class IconTone { Warning, Info }
}
}

View file

@ -1471,6 +1471,7 @@
<string name="staking_max_amount_requirement_error">Maximum amount: %s</string>
<string name="staking_migrate">Migrate</string>
<string name="staking_native">Native staking</string>
<string name="staking_enabled">Staking enabled</string>
<string name="staking_no_validators_error_message">No active validators available for staking at the moment. Please try again later.</string>
<string name="staking_notification_additional_ada_deposit_text">When staking on the Cardano network, your entire balance is used. An additional 2 ADA will be reserved and returned after unstaking. Your ADA remains unlocked while staking.</string>
<string name="staking_notification_additional_ada_deposit_title">ADA Staking Details</string>

View file

@ -67,7 +67,6 @@ fun TransactionItem(state: TransactionItemUM, isBalanceHidden: Boolean, modifier
private fun ContentItem(state: TransactionItemUM.Content, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
val rowModifier = modifier
.fillMaxWidth()
.background(TangemTheme.colors2.surface.level1)
.clickable(onClick = state.onClick)
TangemRowContainer(

View file

@ -45,7 +45,6 @@ internal fun TransactionStatusPill(
Row(
modifier = modifier
.fillMaxWidth()
.background(TangemTheme.colors2.surface.level1)
.clickable(onClick = state.onClick)
.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x2),
horizontalArrangement = Arrangement.Center,

View file

@ -102,7 +102,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta
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.transformer.InitializeWithCryptoCurrencyTransformer
import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetBalanceLoadingTransformer
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
@ -829,11 +828,9 @@ internal class TokenDetailsModel @Inject constructor(
override fun onRefreshSwipe(isRefreshing: Boolean) {
uiState.value = stateFactory.getRefreshingState()
redesignStateController.update(
SetBalanceLoadingTransformer(
currencyIconState = redesignStateController.value.balanceBlockUM.currencyIconState,
),
)
redesignStateController.update { state ->
state.copy(pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = true))
}
modelScope.launch(dispatchers.main) {
listOf(
@ -846,6 +843,9 @@ internal class TokenDetailsModel @Inject constructor(
},
).awaitAll()
uiState.value = stateFactory.getRefreshedState()
redesignStateController.update { state ->
state.copy(pullToRefreshConfig = state.pullToRefreshConfig.copy(isRefreshing = false))
}
}.saveIn(refreshStateJobHolder)
}
@ -1336,6 +1336,7 @@ internal class TokenDetailsModel @Inject constructor(
InitializeWithCryptoCurrencyTransformer(
cryptoCurrency = cryptoCurrency,
onBackClick = ::onBackClick,
onRefreshSwipe = ::onRefreshSwipe,
),
)
}

View file

@ -11,6 +11,7 @@ import com.tangem.utils.transformer.Transformer
internal class InitializeWithCryptoCurrencyTransformer(
private val cryptoCurrency: CryptoCurrency,
private val onBackClick: () -> Unit,
private val onRefreshSwipe: (Boolean) -> Unit,
) : Transformer<TokenDetailsUM> {
override fun transform(prevState: TokenDetailsUM): TokenDetailsUM {
@ -23,6 +24,9 @@ internal class InitializeWithCryptoCurrencyTransformer(
),
balanceBlockUM = prevState.balanceBlockUM.copyCurrencyIconState(iconState),
marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = cryptoCurrency.symbol),
pullToRefreshConfig = prevState.pullToRefreshConfig.copy(
onRefresh = { onRefreshSwipe(it.value) },
),
)
}
}

View file

@ -58,7 +58,7 @@ internal class UpdateStakingNotificationTransformer(
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Plain(iconRes = CoreUiR.drawable.ic_staking_disable_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.staking_native),
text = resourceReference(CoreResR.string.common_staking),
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Disabled,
),
@ -119,17 +119,17 @@ internal class UpdateStakingNotificationTransformer(
backgroundUM = EarnBlockUM.BackgroundUM.AccentSoft,
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = CoreUiR.drawable.ic_staking_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(id = R.string.token_details_staking_block_title),
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Accent,
text = resourceReference(id = CoreResR.string.common_staking),
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = stakeAvailableSubtitle(availability.option.displayApy),
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Primary,
style = EarnBlockUM.SubtitleUM.Style.Small,
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
),
trailingUM = EarnBlockUM.TrailingUM.Button(
text = resourceReference(R.string.common_stake),
text = resourceReference(CoreResR.string.common_stake),
isEnabled = isEnabled,
),
onClick = clickIntents::onStakeBannerClick,
@ -161,7 +161,7 @@ internal class UpdateStakingNotificationTransformer(
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = CoreUiR.drawable.ic_staking_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.staking_native),
text = resourceReference(CoreResR.string.staking_enabled),
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),

View file

@ -1,7 +1,7 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui
import android.content.res.Configuration
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.PaddingValues
@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
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 androidx.compose.foundation.lazy.LazyListScope
@ -26,9 +25,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
@ -39,13 +36,11 @@ import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState
import com.tangem.core.ui.components.BottomFade
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshSlidingContainer
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar
import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.themedColor
import com.tangem.core.ui.res.LocalRootBackgroundColor
@ -58,15 +53,12 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlockHeight
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.tokendetails.ExpressTransactionsComponent
import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.txhistory.entity.TxHistoryItemsUM
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import dev.chrisbanes.haze.HazeProgressive
import dev.chrisbanes.haze.HazeTint
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
@ -86,67 +78,45 @@ internal fun TokenDetailsScreen(
) {
val expressState by expressTransactionsComponent.state.collectAsStateWithLifecycle()
val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() }
val partialCollapsedHeight = TopBarHeight + statusBarHeight
val expandedHeight = TokenDetailsBalanceBlockHeight + partialCollapsedHeight
val behavior = rememberTangemExitUntilCollapsedScrollBehavior(
expandedHeight = expandedHeight,
partialCollapsedHeight = partialCollapsedHeight,
)
val topBarTotalHeight = TopBarHeight + statusBarHeight
val rootBackground by LocalRootBackgroundColor.current
var marketBlockHeight by remember { mutableStateOf(0.dp) }
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
val fadeFloorHeight = TangemTheme.dimens.size100 + bottomBarHeight
val effectiveBottomPadding = maxOf(partialCollapsedHeight + marketBlockHeight, fadeFloorHeight)
val effectiveBottomPadding = marketBlockHeight + TangemTheme.dimens2.x4
Box(
modifier = modifier.fillMaxSize(),
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors2.surface.level2),
) {
Box(
modifier = Modifier
.fillMaxSize()
.hazeSourceTangem(zIndex = -2f),
) {
TangemCollapsingTopBar(
state = behavior.state,
collapsingPart = {
TokenDetailsBalanceBlock(
balanceBlockUM = tokenDetailsUM.balanceBlockUM,
behavior = behavior,
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.padding(top = TopBarHeight),
)
},
body = {
TokenDetailsBody(
tokenDetailsUM = tokenDetailsUM,
yieldSupplyComponent = yieldSupplyComponent,
txHistoryComponent = txHistoryComponent,
expressTransactionsComponent = expressTransactionsComponent,
expressTransactionsToDisplay = expressState.transactionsToDisplay,
rootBackground = rootBackground,
bottomContentPadding = effectiveBottomPadding,
modifier = Modifier
.fillMaxSize()
.nestedScroll(behavior.nestedScrollConnection),
)
},
)
TangemPullToRefreshSlidingContainer(
config = tokenDetailsUM.pullToRefreshConfig,
indicatorOffset = topBarTotalHeight,
) {
TokenDetailsBody(
tokenDetailsUM = tokenDetailsUM,
yieldSupplyComponent = yieldSupplyComponent,
txHistoryComponent = txHistoryComponent,
expressTransactionsComponent = expressTransactionsComponent,
expressTransactionsToDisplay = expressState.transactionsToDisplay,
rootBackground = rootBackground,
topContentPadding = topBarTotalHeight,
bottomContentPadding = effectiveBottomPadding,
modifier = Modifier.fillMaxSize(),
)
}
}
TokenDetailsTopBarOverlay(
topAppBarUM = tokenDetailsUM.topAppBarUM,
collapsedFraction = behavior.state.collapsedFraction,
rootBackground = rootBackground,
)
TokenDetailsTopBarOverlay(topAppBarUM = tokenDetailsUM.topAppBarUM)
if (tokenMarketBlockComponent != null) {
TokenDetailsMarketBlockOverlay(
component = tokenMarketBlockComponent,
rootBackground = rootBackground,
onHeightChange = { marketBlockHeight = it },
)
}
@ -156,24 +126,9 @@ internal fun TokenDetailsScreen(
}
@Composable
private fun TokenDetailsTopBarOverlay(
topAppBarUM: TokenDetailsTopAppBarUM,
collapsedFraction: Float,
rootBackground: Color,
) {
val hazeIntensity by animateFloatAsState(
targetValue = (collapsedFraction * 2f).coerceIn(0f, 1f),
label = "TopBarHazeIntensity",
)
private fun TokenDetailsTopBarOverlay(topAppBarUM: TokenDetailsTopAppBarUM) {
Box(
modifier = Modifier.hazeEffectTangem {
fallbackTint = HazeTint(rootBackground.copy(alpha = hazeIntensity / 2f))
progressive = HazeProgressive.verticalGradient(
startIntensity = hazeIntensity,
endIntensity = 0f,
preferPerformance = true,
)
},
modifier = Modifier.background(TangemTheme.colors2.surface.level2),
) {
TokenDetailsTopBar(topAppBarUM = topAppBarUM)
}
@ -182,18 +137,12 @@ private fun TokenDetailsTopBarOverlay(
@Composable
private fun BoxScope.TokenDetailsMarketBlockOverlay(
component: TokenMarketBlockComponent,
rootBackground: Color,
onHeightChange: (Dp) -> Unit,
) {
val density = LocalDensity.current
BottomFade(
gradientBrush = Brush.verticalGradient(
colors = listOf(
rootBackground.copy(alpha = 0f),
rootBackground,
),
),
backgroundColor = TangemTheme.colors2.surface.level2,
modifier = Modifier.align(Alignment.BottomCenter),
)
@ -220,6 +169,7 @@ private fun TokenDetailsBody(
expressTransactionsComponent: ExpressTransactionsComponent,
expressTransactionsToDisplay: PersistentList<ExpressTransactionStateUM>,
rootBackground: Color,
topContentPadding: Dp,
bottomContentPadding: Dp,
modifier: Modifier = Modifier,
) {
@ -236,8 +186,14 @@ private fun TokenDetailsBody(
LazyColumn(
modifier = modifier,
state = listState,
contentPadding = PaddingValues(bottom = bottomContentPadding),
contentPadding = PaddingValues(top = topContentPadding, bottom = bottomContentPadding),
) {
item(key = "balance_block") {
TokenDetailsBalanceBlock(
balanceBlockUM = tokenDetailsUM.balanceBlockUM,
modifier = Modifier.fillMaxWidth(),
)
}
notifications(
notifications = tokenDetailsUM.notifications,
contentColor = rootBackground,

View file

@ -16,8 +16,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.scale
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
@ -33,13 +31,9 @@ import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.button.action.ActionButtons
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior
import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior
import com.tangem.core.ui.ds.topbar.collapsing.snapToExitUntilCollapsed
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.LocalRootBackgroundColor
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM
@ -49,27 +43,12 @@ import kotlinx.collections.immutable.persistentListOf
private val CurrencyIconSize: Dp = 70.dp
private val NetworkBadgeSize: Dp = 24.dp
internal val TokenDetailsBalanceBlockHeight: Dp = 404.dp
private const val MIN_SCALE = 0.75f
private const val MAX_SCALE = 1f
@Composable
internal fun TokenDetailsBalanceBlock(
balanceBlockUM: TokenDetailsBalanceBlockUM,
behavior: TangemCollapsingAppBarBehavior,
modifier: Modifier = Modifier,
) {
val rootBackground by LocalRootBackgroundColor.current
val collapsedFraction = behavior.state.collapsedFraction
val alpha = 1f - collapsedFraction
val scale = alpha.coerceIn(MIN_SCALE, MAX_SCALE)
internal fun TokenDetailsBalanceBlock(balanceBlockUM: TokenDetailsBalanceBlockUM, modifier: Modifier = Modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.alpha(alpha)
.scale(scale)
.snapToExitUntilCollapsed(behavior)
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens2.x10),
) {
@ -78,7 +57,7 @@ internal fun TokenDetailsBalanceBlock(
shouldDisplayNetwork = true,
iconSize = CurrencyIconSize,
networkBadgeSize = NetworkBadgeSize,
networkBadgeBackground = rootBackground,
networkBadgeBackground = TangemTheme.colors2.surface.level2,
)
SpacerH(TangemTheme.dimens2.x3)
when (balanceBlockUM) {
@ -183,7 +162,6 @@ private fun TokenDetailsBalanceBlock_Preview(
TangemThemePreviewRedesign {
TokenDetailsBalanceBlock(
balanceBlockUM = params,
behavior = rememberTangemExitUntilCollapsedScrollBehavior(),
modifier = Modifier.background(TangemTheme.colors2.surface.level2),
)
}

View file

@ -23,6 +23,7 @@ class InitializeWithCryptoCurrencyTransformerTest {
every { symbol } returns TOKEN_SYMBOL
}
private val onBackClick: () -> Unit = mockk(relaxed = true)
private val onRefreshSwipe: (Boolean) -> Unit = mockk(relaxed = true)
@Test
fun `GIVEN crypto currency WHEN transform THEN top bar title is Simple with token name`() {
@ -30,6 +31,7 @@ class InitializeWithCryptoCurrencyTransformerTest {
val transformer = InitializeWithCryptoCurrencyTransformer(
cryptoCurrency = cryptoCurrency,
onBackClick = onBackClick,
onRefreshSwipe = onRefreshSwipe,
)
// WHEN
@ -45,6 +47,7 @@ class InitializeWithCryptoCurrencyTransformerTest {
val transformer = InitializeWithCryptoCurrencyTransformer(
cryptoCurrency = cryptoCurrency,
onBackClick = onBackClick,
onRefreshSwipe = onRefreshSwipe,
)
// WHEN
@ -60,6 +63,7 @@ class InitializeWithCryptoCurrencyTransformerTest {
val transformer = InitializeWithCryptoCurrencyTransformer(
cryptoCurrency = cryptoCurrency,
onBackClick = onBackClick,
onRefreshSwipe = onRefreshSwipe,
)
// WHEN
@ -76,6 +80,7 @@ class InitializeWithCryptoCurrencyTransformerTest {
val transformer = InitializeWithCryptoCurrencyTransformer(
cryptoCurrency = cryptoCurrency,
onBackClick = onBackClick,
onRefreshSwipe = onRefreshSwipe,
)
// WHEN
@ -93,21 +98,39 @@ class InitializeWithCryptoCurrencyTransformerTest {
val transformer = InitializeWithCryptoCurrencyTransformer(
cryptoCurrency = cryptoCurrency,
onBackClick = onBackClick,
onRefreshSwipe = onRefreshSwipe,
)
// WHEN
val result = transformer.transform(state)
// THEN — only top bar title/subtitle/onBackClick and marketPriceBlockState are touched
// THEN — only top bar title/subtitle/onBackClick, marketPriceBlockState and pullToRefresh.onRefresh are touched
assertThat(result.topAppBarUM.menuItems).isEqualTo(state.topAppBarUM.menuItems)
assertThat(result.balanceBlockUM.actionButtons).isEqualTo(state.balanceBlockUM.actionButtons)
assertThat(result.balanceBlockUM.tokenBalanceTypeUM).isEqualTo(state.balanceBlockUM.tokenBalanceTypeUM)
assertThat(result.earnBlockState).isEqualTo(state.earnBlockState)
assertThat(result.pullToRefreshConfig).isSameInstanceAs(state.pullToRefreshConfig)
assertThat(result.pullToRefreshConfig.isRefreshing).isEqualTo(state.pullToRefreshConfig.isRefreshing)
assertThat(result.isBalanceHidden).isEqualTo(state.isBalanceHidden)
assertThat(result.isMarketPriceAvailable).isEqualTo(state.isMarketPriceAvailable)
}
@Test
fun `GIVEN onRefreshSwipe WHEN pull-to-refresh callback invoked THEN onRefreshSwipe is dispatched`() {
// GIVEN
val transformer = InitializeWithCryptoCurrencyTransformer(
cryptoCurrency = cryptoCurrency,
onBackClick = onBackClick,
onRefreshSwipe = onRefreshSwipe,
)
// WHEN
val result = transformer.transform(initialState())
result.pullToRefreshConfig.onRefresh(PullToRefreshConfig.ShowRefreshState(value = true))
// THEN
verify(exactly = 1) { onRefreshSwipe.invoke(true) }
}
private fun initialState(): TokenDetailsUM = TokenDetailsUM(
topAppBarUM = TokenDetailsTopAppBarUM(
titleState = TitleState.Simple(tokenName = ""),
@ -123,7 +146,7 @@ class InitializeWithCryptoCurrencyTransformerTest {
notifications = persistentListOf(),
marketPriceBlockState = mockk<MarketPriceBlockState>(relaxed = true),
earnBlockState = null,
pullToRefreshConfig = mockk<PullToRefreshConfig>(relaxed = true),
pullToRefreshConfig = PullToRefreshConfig(isRefreshing = false, onRefresh = {}),
isBalanceHidden = false,
isMarketPriceAvailable = false,
)

View file

@ -61,6 +61,7 @@ internal class YieldSupplyToEarnBlockConverter : Converter<YieldSupplyUM, EarnBl
},
style = EarnBlockUM.TitleUM.Style.Large,
tone = EarnBlockUM.TitleUM.Tone.Primary,
iconUM = buildTitleIcon(value),
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(
@ -70,21 +71,18 @@ internal class YieldSupplyToEarnBlockConverter : Converter<YieldSupplyUM, EarnBl
style = EarnBlockUM.SubtitleUM.Style.Small,
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
),
trailingUM = buildContentTrailing(value),
trailingUM = EarnBlockUM.TrailingUM.Button(
text = resourceReference(CoreResR.string.details_title),
style = EarnBlockUM.TrailingUM.Button.Style.Secondary,
),
onClick = value.onClick,
)
}
private fun buildContentTrailing(value: YieldSupplyUM.Content): EarnBlockUM.TrailingUM? = when {
value.showWarningIcon -> EarnBlockUM.TrailingUM.Icon(
tone = EarnBlockUM.TrailingUM.IconTone.Warning,
)
value.showInfoIcon -> EarnBlockUM.TrailingUM.Icon(
tone = EarnBlockUM.TrailingUM.IconTone.Info,
)
else -> EarnBlockUM.TrailingUM.Button(
text = resourceReference(CoreResR.string.details_title),
)
private fun buildTitleIcon(value: YieldSupplyUM.Content): EarnBlockUM.TitleUM.IconUM? = when {
value.showWarningIcon -> EarnBlockUM.TitleUM.IconUM(tone = EarnBlockUM.TitleUM.IconTone.Warning)
value.showInfoIcon -> EarnBlockUM.TitleUM.IconUM(tone = EarnBlockUM.TitleUM.IconTone.Info)
else -> null
}
private fun buildProcessingEnter(): EarnBlockUM.Content {

View file

@ -87,7 +87,7 @@ internal class YieldSupplyToEarnBlockConverterTest {
}
@Test
fun `GIVEN Content with showWarningIcon WHEN convert THEN trailing Warning Icon`() {
fun `GIVEN Content with showWarningIcon WHEN convert THEN title Warning Icon`() {
val content = YieldSupplyUM.Content(
apy = "5.1",
title = stringReference("Yield Mode"),
@ -102,13 +102,13 @@ internal class YieldSupplyToEarnBlockConverterTest {
assertThat(result).isInstanceOf(EarnBlockUM.Content::class.java)
val earnBlock = result as EarnBlockUM.Content
assertThat(earnBlock.trailingUM).isInstanceOf(EarnBlockUM.TrailingUM.Icon::class.java)
val icon = earnBlock.trailingUM as EarnBlockUM.TrailingUM.Icon
assertThat(icon.tone).isEqualTo(EarnBlockUM.TrailingUM.IconTone.Warning)
assertThat(earnBlock.titleUM.iconUM).isNotNull()
assertThat(earnBlock.titleUM.iconUM?.tone).isEqualTo(EarnBlockUM.TitleUM.IconTone.Warning)
assertThat(earnBlock.trailingUM).isInstanceOf(EarnBlockUM.TrailingUM.Button::class.java)
}
@Test
fun `GIVEN Content with showInfoIcon WHEN convert THEN trailing Info Icon`() {
fun `GIVEN Content with showInfoIcon WHEN convert THEN title Info Icon`() {
val content = YieldSupplyUM.Content(
apy = "5.1",
title = stringReference("Yield Mode"),
@ -123,9 +123,9 @@ internal class YieldSupplyToEarnBlockConverterTest {
assertThat(result).isInstanceOf(EarnBlockUM.Content::class.java)
val earnBlock = result as EarnBlockUM.Content
assertThat(earnBlock.trailingUM).isInstanceOf(EarnBlockUM.TrailingUM.Icon::class.java)
val icon = earnBlock.trailingUM as EarnBlockUM.TrailingUM.Icon
assertThat(icon.tone).isEqualTo(EarnBlockUM.TrailingUM.IconTone.Info)
assertThat(earnBlock.titleUM.iconUM).isNotNull()
assertThat(earnBlock.titleUM.iconUM?.tone).isEqualTo(EarnBlockUM.TitleUM.IconTone.Info)
assertThat(earnBlock.trailingUM).isInstanceOf(EarnBlockUM.TrailingUM.Button::class.java)
}
@Test
@ -143,9 +143,8 @@ internal class YieldSupplyToEarnBlockConverterTest {
val result = converter.convert(content)
val earnBlock = result as EarnBlockUM.Content
assertThat(earnBlock.trailingUM).isInstanceOf(EarnBlockUM.TrailingUM.Icon::class.java)
val icon = earnBlock.trailingUM as EarnBlockUM.TrailingUM.Icon
assertThat(icon.tone).isEqualTo(EarnBlockUM.TrailingUM.IconTone.Warning)
assertThat(earnBlock.titleUM.iconUM).isNotNull()
assertThat(earnBlock.titleUM.iconUM?.tone).isEqualTo(EarnBlockUM.TitleUM.IconTone.Warning)
}
@Test