Updated on 2026-08-14

This commit is contained in:
Tangem 2026-06-15 22:43:52 +03:00
commit d6f9f59866
1729 changed files with 67614 additions and 9361 deletions

View file

@ -7,11 +7,6 @@ plugins {
android {
namespace = "com.tangem.common.ui"
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
dependencies {
api(projects.common)
@ -56,5 +51,4 @@ dependencies {
/** Tests */
testImplementation(projects.test.core)
testRuntimeOnly(deps.test.junit5.engine)
}

View file

@ -8,8 +8,6 @@
<ID>BooleanPropertyNaming:GiveTxPermissionState.kt$CancelPermissionButton$val enabled: Boolean</ID>
<ID>BooleanPropertyNaming:NotificationUM.kt$NotificationUM.Error.ExceedsBalance$val mergeFeeNetworkName: Boolean = false</ID>
<ID>BooleanPropertyNaming:NotificationsFactory.kt$NotificationsFactory$val showNotification = sendingAmount + feeAmount &gt; balance - minimumRequirement.orZero()</ID>
<ID>BooleanPropertyNaming:TokenReceiveBottomSheetConfig.kt$TokenReceiveBottomSheetConfig$val showMemoDisclaimer: Boolean</ID>
<ID>MultilineLambdaItParameter:ExpressStatusItems.kt${ val itemInfo = expressTxs[it].info val (iconRes, tint) = when (itemInfo.iconState) { ExpressTransactionStateIconUM.Warning -&gt; { R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention } ExpressTransactionStateIconUM.Error -&gt; { R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning } ExpressTransactionStateIconUM.None -&gt; null to null } ExpressStatusItem( title = itemInfo.title, fromTokenIconState = itemInfo.fromCurrencyIcon, toTokenIconState = itemInfo.toCurrencyIcon, fromAmount = itemInfo.fromAmount, fromSymbol = itemInfo.fromAmountSymbol, toAmount = itemInfo.toAmount, toSymbol = itemInfo.toAmountSymbol, onClick = itemInfo.onClick, infoIconRes = iconRes, infoIconTint = tint, modifier = modifier.animateItem(), ) }</ID>
<ID>MultilineLambdaItParameter:TokenItemStateConverter.kt$TokenItemStateConverter.Companion${ it.key.equals( other = token.yieldSupplyKey(), ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId), ) }</ID>
<ID>NoNameShadowing:NavigationButtonsBlock.kt$navigationUM</ID>
<ID>NoNameShadowing:UserWalletItem.kt$balance</ID>

View file

@ -66,6 +66,12 @@ fun AccountIcon(
fun AccountIcon(name: TextReference, icon: AccountIconUM, size: AccountIconSize, modifier: Modifier = Modifier) {
when (icon) {
is AccountIconUM.Payment -> PaymentAccountIcon(size = size, modifier = modifier)
is AccountIconUM.Virtual -> AccountResIcon(
resId = icon.icon.getResId(),
color = icon.color.getUiColor(),
size = size,
modifier = modifier,
)
is AccountIconUM.CryptoPortfolio -> AccountIcon(
name = name,
icon = icon,

View file

@ -7,4 +7,9 @@ sealed class AccountIconUM {
data class CryptoPortfolio(val value: Icon, val color: Color) : AccountIconUM()
data object Payment : AccountIconUM()
data object Virtual : AccountIconUM() {
val icon: Icon = Icon.Safe
val color: Color = Color.VitalGreen
}
}

View file

@ -90,6 +90,10 @@ private fun PreviewAccountTitle() {
accountTitleUM = AccountTitleUM.Account.payment(prefixText = stringReference(StringsSigns.DOT)),
modifier = Modifier.padding(4.dp),
)
AccountTitle(
accountTitleUM = AccountTitleUM.Account.virtual(prefixText = stringReference(StringsSigns.DOT)),
modifier = Modifier.padding(4.dp),
)
}
}
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
/**
* A sealed interface representing the title of an account, which can be either a simple text
@ -31,6 +32,14 @@ sealed interface AccountTitleUM {
icon = AccountIconUM.Payment,
)
}
fun virtual(prefixText: TextReference = TextReference.EMPTY): Account {
return Account(
prefixText = prefixText,
name = stringReference("Virtual account"),
icon = AccountIconUM.Virtual,
)
}
}
}
}

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
@ -100,6 +101,7 @@ private fun TokenHeader(
text = tokenName.text.resolveReference(),
style = TangemTheme.typography2.headingSemibold28,
color = TangemTheme.colors2.text.neutral.primary,
textAlign = TextAlign.Center,
)
}
TokenItemState.TitleState.Loading -> {

View file

@ -31,6 +31,7 @@ class AmountAccountConverter(
return when (account) {
is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(account.icon)
is Account.Payment -> AccountIconUM.Payment
is Account.Virtual -> AccountIconUM.Virtual
}
}
}

View file

@ -40,6 +40,7 @@ import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.currency.fiaticon.FiatIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.fields.AmountTextField
import com.tangem.core.ui.components.fields.TangemAmountTextFieldColors
import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
@ -89,6 +90,7 @@ fun AmountFieldV2(
} else {
amountUM.amountTextField.cryptoAmount to amountUM.amountTextField.value
}
val colors = TangemAmountTextFieldColors
AmountTextField(
value = primaryValue,
decimals = primaryAmount.decimals,
@ -97,9 +99,10 @@ fun AmountFieldV2(
symbol = primaryAmount.currencySymbol,
currencyCode = currencyCode,
decimalFormat = decimalFormat,
symbolColor = TangemTheme.colors.text.disabled,
symbolColor = colors.disabledTextColor,
),
onValueChange = onValueChange,
colors = colors,
keyboardOptions = amountUM.amountTextField.keyboardOptions,
keyboardActions = amountUM.amountTextField.keyboardActions,
textStyle = TangemTheme.typography.head.copy(

View file

@ -4,13 +4,7 @@ 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.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Text
@ -22,11 +16,16 @@ 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.platform.testTag
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.shadow.Shadow
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -36,22 +35,15 @@ import com.tangem.common.ui.earn.EarnBlockUM.Type
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.button.*
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.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.TokenDetailsScreenTestTags
import com.tangem.core.res.R as CoreResR
private const val TINTED_BACKGROUND_ALPHA = 0.1f
@ -69,12 +61,13 @@ fun EarnBlock(state: EarnBlockUM, modifier: Modifier = Modifier) {
when (state) {
is EarnBlockUM.Loading -> EarnBlockLoading(modifier)
is EarnBlockUM.Content -> EarnBlockContent(state, modifier)
is EarnBlockUM.Promo -> EarnBlockPromo(state, modifier)
}
}
@Composable
private fun EarnBlockLoading(modifier: Modifier = Modifier) {
val shape = RoundedCornerShape(TangemTheme.dimens2.x4)
val shape = RoundedCornerShape(TangemTheme.dimens2.x5)
TangemRowContainer(
modifier = modifier
.clip(shape)
@ -91,13 +84,13 @@ private fun EarnBlockLoading(modifier: Modifier = Modifier) {
RectangleShimmer(
modifier = Modifier
.layoutId(TangemRowLayoutId.START_TOP)
.size(width = TangemTheme.dimens2.x16, height = TangemTheme.dimens2.x5),
.size(width = ShimmerSubtitleWidth, height = TangemTheme.dimens2.x4),
radius = TangemTheme.dimens2.x2,
)
RectangleShimmer(
modifier = Modifier
.layoutId(TangemRowLayoutId.START_BOTTOM)
.size(width = ShimmerSubtitleWidth, height = TangemTheme.dimens2.x4),
.size(width = TangemTheme.dimens2.x16, height = TangemTheme.dimens2.x5),
radius = TangemTheme.dimens2.x2,
)
},
@ -106,7 +99,7 @@ private fun EarnBlockLoading(modifier: Modifier = Modifier) {
@Composable
private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Modifier) {
val shape = RoundedCornerShape(TangemTheme.dimens2.x4)
val shape = RoundedCornerShape(TangemTheme.dimens2.x5)
val clickModifier = state.onClick?.let { Modifier.clickable(onClick = it) } ?: Modifier
@ -114,7 +107,7 @@ private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Mo
modifier = modifier
.clip(shape)
.then(clickModifier.backgroundModifier(state.type, state.backgroundUM, shape)),
contentPadding = PaddingValues(all = TangemTheme.dimens2.x3),
contentPadding = PaddingValues(all = TangemTheme.dimens2.x4),
content = {
EarnBlockIcon(
type = state.type,
@ -132,15 +125,21 @@ private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Mo
.padding(end = TangemTheme.dimens2.x2),
)
val subtitle = state.subtitleUM
if (subtitle is EarnBlockUM.SubtitleUM.Text) {
EarnBlockSubtitle(
val subtitleModifier = Modifier
.layoutId(TangemRowLayoutId.START_BOTTOM)
.padding(end = TangemTheme.dimens2.x2)
when (val subtitle = state.subtitleUM) {
is EarnBlockUM.SubtitleUM.Text -> EarnBlockSubtitle(
subtitle = subtitle,
type = state.type,
modifier = Modifier
.layoutId(TangemRowLayoutId.START_BOTTOM)
.padding(end = TangemTheme.dimens2.x2),
modifier = subtitleModifier,
)
is EarnBlockUM.SubtitleUM.AccentedText -> EarnBlockAccentedSubtitle(
subtitle = subtitle,
type = state.type,
modifier = subtitleModifier,
)
null -> Unit
}
EarnBlockTrailing(type = state.type, trailingUM = state.trailingUM, onClick = state.onClick)
@ -148,6 +147,74 @@ private fun EarnBlockContent(state: EarnBlockUM.Content, modifier: Modifier = Mo
)
}
@Composable
private fun EarnBlockPromo(state: EarnBlockUM.Promo, modifier: Modifier = Modifier) {
val shape = RoundedCornerShape(TangemTheme.dimens2.x5)
Column(
modifier = modifier
.clip(shape)
.backgroundModifier(state.type, state.backgroundUM, shape)
.padding(all = TangemTheme.dimens2.x4),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x3),
) {
Row(verticalAlignment = Alignment.CenterVertically) {
EarnBlockIcon(
type = state.type,
iconUM = state.iconUM,
modifier = Modifier.padding(end = TangemTheme.dimens2.x3),
)
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
modifier = Modifier.weight(1f),
) {
Text(
text = state.title.resolveAnnotatedReference(),
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
)
Text(
text = state.subtitle.resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = state.type.accentText(),
)
}
}
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2)) {
EarnBlockPromoButton(
text = resourceReference(CoreResR.string.common_learn_more),
type = TangemButtonType.Secondary,
onClick = state.onSecondaryClick,
modifier = Modifier.weight(1f),
)
EarnBlockPromoButton(
text = resourceReference(CoreResR.string.common_activate),
type = EarnBlockUM.TrailingUM.Button.Style.Default.buttonType(state.type),
onClick = state.onPrimaryClick,
modifier = Modifier.weight(1f),
)
}
}
}
@Composable
private fun EarnBlockPromoButton(
text: TextReference,
type: TangemButtonType,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
TangemButton(
buttonUM = TangemButtonUM(
text = text,
type = type,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
onClick = onClick,
),
modifier = modifier,
)
}
@Composable
private fun Modifier.backgroundModifier(
type: Type,
@ -192,20 +259,24 @@ private fun EarnBlockTrailing(type: Type, trailingUM: EarnBlockUM.TrailingUM?, o
)
}
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),
)
val fiatModifier = Modifier.layoutId(TangemRowLayoutId.END_TOP).let {
if (type == Type.Staking) it.testTag(TokenDetailsScreenTestTags.STAKING_FIAT_AMOUNT) else it
}
val cryptoModifier = Modifier.layoutId(TangemRowLayoutId.END_BOTTOM).let {
if (type == Type.Staking) it.testTag(TokenDetailsScreenTestTags.STAKING_TOKEN_AMOUNT) else it
}
Text(
text = trailingUM.fiatValue.orMaskWithStars(trailingUM.isBalanceHidden).resolveAnnotatedReference(),
style = TangemTheme.typography2.bodySemibold16,
color = TangemTheme.colors2.text.neutral.primary,
modifier = fiatModifier,
)
Text(
text = trailingUM.cryptoValue.orMaskWithStars(trailingUM.isBalanceHidden).resolveReference(),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
modifier = cryptoModifier,
)
}
null -> Unit
}
@ -258,6 +329,29 @@ private fun EarnBlockSubtitle(subtitle: EarnBlockUM.SubtitleUM.Text, type: Type,
}
}
@Composable
private fun EarnBlockAccentedSubtitle(
subtitle: EarnBlockUM.SubtitleUM.AccentedText,
type: Type,
modifier: Modifier = Modifier,
) {
val baseText = subtitle.text.resolveReference()
val accentText = subtitle.accent.resolveReference()
val accentColor = type.accentText()
Text(
text = buildAnnotatedString {
append(baseText)
if (baseText.isNotEmpty() && !baseText.last().isWhitespace()) append(' ')
withStyle(SpanStyle(color = accentColor)) {
append(accentText)
}
},
style = subtitle.style.textStyle,
color = TangemTheme.colors2.text.neutral.tertiary,
modifier = modifier,
)
}
@Composable
private fun EarnBlockIcon(type: Type, iconUM: EarnBlockUM.IconUM, modifier: Modifier = Modifier) {
Box(
@ -361,7 +455,7 @@ private val EarnBlockUM.TitleUM.Style.textStyle: TextStyle
@Composable
@ReadOnlyComposable
get() = when (this) {
EarnBlockUM.TitleUM.Style.Large -> TangemTheme.typography2.bodySemibold16
EarnBlockUM.TitleUM.Style.Large -> TangemTheme.typography2.bodyMedium16
EarnBlockUM.TitleUM.Style.Small -> TangemTheme.typography2.captionMedium12
}
@ -369,7 +463,7 @@ private val EarnBlockUM.SubtitleUM.Style.textStyle: TextStyle
@Composable
@ReadOnlyComposable
get() = when (this) {
EarnBlockUM.SubtitleUM.Style.Large -> TangemTheme.typography2.bodySemibold16
EarnBlockUM.SubtitleUM.Style.Large -> TangemTheme.typography2.bodyMedium16
EarnBlockUM.SubtitleUM.Style.Small -> TangemTheme.typography2.captionMedium12
}
// endregion
@ -410,12 +504,12 @@ private class EarnBlockStakingPreviewProvider : CollectionPreviewParameterProvid
iconUM = EarnBlockUM.IconUM.Plain(iconRes = R.drawable.ic_staking_disable_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.common_stake),
style = EarnBlockUM.TitleUM.Style.Large,
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Disabled,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(CoreResR.string.staking_notification_network_error_text),
style = EarnBlockUM.SubtitleUM.Style.Small,
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
),
trailingUM = null,
@ -426,12 +520,12 @@ private class EarnBlockStakingPreviewProvider : CollectionPreviewParameterProvid
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_staking_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.common_staking),
style = EarnBlockUM.TitleUM.Style.Large,
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = stringReference("Average APR 5.24%"),
style = EarnBlockUM.SubtitleUM.Style.Small,
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
),
trailingUM = EarnBlockUM.TrailingUM.Button(
@ -445,12 +539,12 @@ private class EarnBlockStakingPreviewProvider : CollectionPreviewParameterProvid
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_staking_40),
titleUM = EarnBlockUM.TitleUM(
text = resourceReference(CoreResR.string.staking_enabled),
style = EarnBlockUM.TitleUM.Style.Large,
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = stringReference("$ 12.34 rewards"),
style = EarnBlockUM.SubtitleUM.Style.Small,
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
),
trailingUM = EarnBlockUM.TrailingUM.Balance(
@ -465,6 +559,22 @@ private class EarnBlockStakingPreviewProvider : CollectionPreviewParameterProvid
private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterProvider<EarnBlockUM>(
collection = listOf(
// Promo — boosted APY offer: AccentSoft background, two buttons below
EarnBlockUM.Promo(
type = Type.YieldSupply,
backgroundUM = EarnBlockUM.BackgroundUM.AccentSoft,
iconUM = EarnBlockUM.IconUM.Glowing(iconRes = R.drawable.ic_yield_40),
title = annotatedReference(
buildAnnotatedString {
append("Special offer for Yield mode\nAPY ")
withStyle(SpanStyle(textDecoration = TextDecoration.LineThrough)) { append("5.1%") }
append(" x3 → 15.3%")
},
),
subtitle = stringReference("First time activation bonus!"),
onPrimaryClick = {},
onSecondaryClick = {},
),
// Available — promo entry: AccentSoft background, "More" button
EarnBlockUM.Content(
type = Type.YieldSupply,
@ -475,14 +585,14 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
id = CoreResR.string.yield_module_token_details_earn_notification_subtitle,
formatArgs = wrappedList("5.24"),
),
style = EarnBlockUM.TitleUM.Style.Large,
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(
CoreResR.string.yield_module_token_details_earn_notification_description,
),
style = EarnBlockUM.SubtitleUM.Style.Small,
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
),
trailingUM = EarnBlockUM.TrailingUM.Button(
@ -497,7 +607,7 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
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,
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
@ -505,7 +615,7 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
id = CoreResR.string.yield_module_average_apy,
formatArgs = wrappedList("5.24"),
),
style = EarnBlockUM.SubtitleUM.Style.Small,
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
),
trailingUM = EarnBlockUM.TrailingUM.Button(
@ -521,7 +631,7 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
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,
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Primary,
iconUM = EarnBlockUM.TitleUM.IconUM(tone = EarnBlockUM.TitleUM.IconTone.Warning),
),
@ -530,7 +640,7 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
id = CoreResR.string.yield_module_average_apy,
formatArgs = wrappedList("5.24"),
),
style = EarnBlockUM.SubtitleUM.Style.Small,
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
),
trailingUM = EarnBlockUM.TrailingUM.Button(
@ -546,7 +656,7 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
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,
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Primary,
iconUM = EarnBlockUM.TitleUM.IconUM(tone = EarnBlockUM.TitleUM.IconTone.Info),
),
@ -555,7 +665,7 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
id = CoreResR.string.yield_module_average_apy,
formatArgs = wrappedList("5.24"),
),
style = EarnBlockUM.SubtitleUM.Style.Small,
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
),
trailingUM = EarnBlockUM.TrailingUM.Button(
@ -571,12 +681,12 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
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,
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(CoreResR.string.common_enabling),
style = EarnBlockUM.SubtitleUM.Style.Small,
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Accent,
loader = EarnBlockUM.SubtitleUM.Loader(tone = EarnBlockUM.SubtitleUM.LoaderTone.Positive),
),
@ -589,12 +699,12 @@ private class EarnBlockYieldSupplyPreviewProvider : CollectionPreviewParameterPr
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,
style = EarnBlockUM.TitleUM.Style.Small,
tone = EarnBlockUM.TitleUM.Tone.Primary,
),
subtitleUM = EarnBlockUM.SubtitleUM.Text(
text = resourceReference(CoreResR.string.common_disabling),
style = EarnBlockUM.SubtitleUM.Style.Small,
style = EarnBlockUM.SubtitleUM.Style.Large,
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
loader = EarnBlockUM.SubtitleUM.Loader(tone = EarnBlockUM.SubtitleUM.LoaderTone.Muted),
),

View file

@ -19,6 +19,21 @@ sealed interface EarnBlockUM {
val onClick: (() -> Unit)? = null,
) : EarnBlockUM
/**
* Promo variant a column with the [iconUM] + annotated multiline [title] + [subtitle] row on top
* and a pair of full-width buttons below. Button labels are fixed and hardcoded in the composable,
* so only their click handlers ([onSecondaryClick], [onPrimaryClick]) are exposed here.
*/
data class Promo(
val type: Type,
val backgroundUM: BackgroundUM,
val iconUM: IconUM,
val title: TextReference,
val subtitle: TextReference,
val onPrimaryClick: () -> Unit,
val onSecondaryClick: () -> Unit,
) : EarnBlockUM
enum class Type { Staking, YieldSupply }
@Immutable
@ -57,6 +72,12 @@ sealed interface EarnBlockUM {
val loader: Loader? = null,
) : SubtitleUM
data class AccentedText(
val text: TextReference,
val accent: TextReference,
val style: Style,
) : SubtitleUM
data class Loader(val tone: LoaderTone)
enum class Style { Large, Small }

View file

@ -2,9 +2,9 @@ package com.tangem.common.ui.expressStatus
import androidx.compose.runtime.Composable
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.res.TangemTheme
data class ExpressStatusBottomSheetConfig(
@ -18,7 +18,7 @@ fun ExpressStatusBottomSheet(config: TangemBottomSheetConfig) {
containerColor = TangemTheme.colors.background.tertiary,
) { content: ExpressStatusBottomSheetConfig ->
when (val state = content.value) {
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state)
is ExpressTransactionStateUM.OnrampUM -> OnrampStatusBottomSheetContent(state, false)
}
}
}

View file

@ -3,13 +3,7 @@ package com.tangem.common.ui.expressStatus
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@ -18,14 +12,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.common.ui.R
import com.tangem.common.ui.expressStatus.state.ExpressLinkUM
import com.tangem.common.ui.expressStatus.state.ExpressStatusUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.common.ui.expressStatus.state.*
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.components.atoms.text.TextEllipsis
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
@ -35,6 +26,7 @@ import com.tangem.core.ui.extensions.resolveReference
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.ui.test.TokenDetailsScreenTestTags
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
@ -78,7 +70,8 @@ private fun ExpressTransactionItem(
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors2.surface.level3)
.clickable(onClick = info.onClick)
.padding(TangemTheme.dimens2.x4),
.padding(TangemTheme.dimens2.x4)
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM),
) {
TitleRow(
title = info.title.resolveReference(),
@ -104,7 +97,9 @@ private fun TitleRow(title: String, infoIconRes: Int?, infoIconTint: Color?) {
text = title,
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors3.text.primary,
modifier = Modifier.weight(1f),
modifier = Modifier
.weight(1f)
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TITLE),
)
if (infoIconRes != null && infoIconTint != null) {
Icon(
@ -126,32 +121,42 @@ private fun AmountsRow(info: ExpressTransactionStateInfoUM) {
CurrencyIcon(
state = info.fromCurrencyIcon,
shouldDisplayNetwork = false,
modifier = Modifier.size(TangemTheme.dimens.size18),
modifier = Modifier
.size(TangemTheme.dimens.size18)
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_FROM_ICON),
)
EllipsisText(
text = info.fromAmount.resolveReference(),
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors3.text.primary,
ellipsis = TextEllipsis.OffsetEnd(info.fromAmountSymbol.length),
modifier = Modifier.weight(weight = 1f, fill = false),
modifier = Modifier
.weight(weight = 1f, fill = false)
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_FROM_AMOUNT),
)
Icon(
painter = painterResource(R.drawable.ic_forward_24),
contentDescription = null,
tint = TangemTheme.colors3.icon.tertiary,
modifier = Modifier.size(TangemTheme.dimens.size18),
modifier = Modifier
.size(TangemTheme.dimens.size18)
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_SWAP_ICON),
)
CurrencyIcon(
state = info.toCurrencyIcon,
shouldDisplayNetwork = false,
modifier = Modifier.size(TangemTheme.dimens.size18),
modifier = Modifier
.size(TangemTheme.dimens.size18)
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_ICON),
)
EllipsisText(
text = info.toAmount.resolveReference(),
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors3.text.primary,
ellipsis = TextEllipsis.OffsetEnd(info.toAmountSymbol.length),
modifier = Modifier.weight(weight = 1f, fill = false),
modifier = Modifier
.weight(weight = 1f, fill = false)
.testTag(TokenDetailsScreenTestTags.EXPRESS_STATUS_ITEM_TO_AMOUNT),
)
}
}
@ -206,13 +211,17 @@ private val PreviewExpressTransactionState: ExpressTransactionStateUM = object :
onDisposeExpressStatus = {},
iconState = ExpressTransactionStateIconUM.None,
toAmount = stringReference("0,11441958 BTC"),
toAmountValue = "0.11441958".toBigDecimal(),
toFiatAmount = null,
toAmountSymbol = "BTC",
toCurrencyIcon = CurrencyIconState.Loading,
toAddress = "0x",
fromAmount = stringReference("100 SOL"),
fromAmountValue = "100".toBigDecimal(),
fromFiatAmount = null,
fromAmountSymbol = "SOL",
fromCurrencyIcon = CurrencyIconState.Loading,
fromAddress = "0x",
)
}
// endregion

View file

@ -9,18 +9,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH10
import com.tangem.core.ui.components.SpacerH12
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@Composable
fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM, isExpressShareButtonEnabled: Boolean) {
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
@ -70,6 +68,10 @@ fun OnrampStatusBottomSheetContent(state: ExpressTransactionStateUM.OnrampUM) {
isAutoDisposable = state.activeStatus.isAutoDisposable,
onClick = state.info.onDisposeExpressStatus,
)
SpacerH24()
if (isExpressShareButtonEnabled) {
SpacerH(80.dp)
} else {
SpacerH24()
}
}
}

View file

@ -1,10 +1,13 @@
package com.tangem.common.ui.expressStatus.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.onramp.model.OnrampStatus
import java.math.BigDecimal
@Immutable
interface ExpressTransactionStateUM {
val info: ExpressTransactionStateInfoUM
@ -35,13 +38,17 @@ data class ExpressTransactionStateInfoUM(
val onDisposeExpressStatus: () -> Unit,
val iconState: ExpressTransactionStateIconUM,
val toAmount: TextReference,
val toAmountValue: BigDecimal,
val toFiatAmount: TextReference?,
val toAmountSymbol: String,
val toCurrencyIcon: CurrencyIconState,
val toAddress: String,
val fromAmount: TextReference,
val fromAmountValue: BigDecimal,
val fromFiatAmount: TextReference?,
val fromAmountSymbol: String,
val fromCurrencyIcon: CurrencyIconState,
val fromAddress: String?,
) {
val subtitle: TextReference
get() = buildExpressStatusSubtitle(activeStatus = activeStatus, date = timestampAgoFormatted)

View file

@ -14,10 +14,13 @@ 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.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.row.TangemRowContainer
@ -61,15 +64,14 @@ fun TokenActionRow(
val accentColor = accentColor(isEnabled)
TangemRowContainer(
modifier = modifier
.background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(TangemTheme.dimens2.x5),
)
.clip(RoundedCornerShape(TangemTheme.dimens2.x5))
.background(color = TangemTheme.colors2.surface.level3)
.clickableWithHaptic(
onClick = onClick,
onLongClick = onLongClick,
hapticManager = hapticManager,
),
)
.semantics { if (!isEnabled) disabled() },
) {
LeadingIcon(iconRes = iconRes, accentColor = accentColor)
Text(