Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-05 13:22:55 +04:00
commit 8cb8795f92
29 changed files with 495 additions and 179 deletions

View file

@ -1,5 +1,6 @@
package com.tangem.common.ui.tokens
import com.tangem.common.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.marketprice.PriceChangeType
@ -17,6 +18,7 @@ import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.toImmutableList
import java.math.BigDecimal
/**
@ -219,7 +221,14 @@ class TokenItemStateConverter(
-> {
TokenItemState.FiatAmountState.Content(
text = status.getFormattedFiatAmount(appCurrency = appCurrency, includeStaking = true),
hasStaked = !status.getStakedBalance().isZero(),
icons = buildList {
if (!status.getStakedBalance().isZero()) {
TokenItemState.FiatAmountState.Content.IconUM(
iconRes = R.drawable.ic_staking_24,
useAccentColor = true,
).let(::add)
}
}.toImmutableList(),
)
}
is CryptoCurrencyStatus.Unreachable,

View file

@ -3,7 +3,6 @@ package com.tangem.common.ui.userwallet
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CardColors
import androidx.compose.material3.Icon
@ -15,26 +14,32 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.common.ui.R
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.coil.RotationTransformation
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.common.ui.R
import com.tangem.core.ui.coil.RotationTransformation
import com.tangem.core.ui.components.block.TangemBlockCardColors
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.TangemThemePreview
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.collections.immutable.persistentListOf
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.StringsSigns.THREE_STARS
@Composable
fun UserWalletItem(
@ -61,6 +66,7 @@ fun UserWalletItem(
modifier = Modifier.weight(1f),
name = state.name,
information = state.information,
balance = state.balance,
)
when (state.endIcon) {
@ -85,7 +91,12 @@ fun UserWalletItem(
}
@Composable
private fun NameAndInfo(name: TextReference, information: TextReference, modifier: Modifier = Modifier) {
private fun NameAndInfo(
name: TextReference,
information: TextReference,
balance: UserWalletItemUM.Balance,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.heightIn(min = TangemTheme.dimens.size40),
horizontalAlignment = Alignment.Start,
@ -99,17 +110,37 @@ private fun NameAndInfo(name: TextReference, information: TextReference, modifie
overflow = TextOverflow.Ellipsis,
)
AnimatedContent(
targetState = information.resolveReference(),
label = "User wallet information",
) { information ->
Row(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = information,
text = information.resolveReference() + " $DOT ",
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
AnimatedContent(
targetState = balance,
label = "Balance content",
) { balance ->
val (balanceValue, isFlickering) = getBalanceValueAndFlickerState(balance)
if (balanceValue == null) {
TextShimmer(
style = TangemTheme.typography.caption2,
text = "aaaaa",
)
} else {
Text(
modifier = Modifier.flicker(isFlickering),
text = balanceValue,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
)
}
}
}
}
}
@ -150,16 +181,39 @@ private fun CardImage(imageUrl: String, modifier: Modifier = Modifier) {
)
}
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
fun getBalanceValueAndFlickerState(balance: UserWalletItemUM.Balance): Pair<String?, Boolean> {
return when (balance) {
is UserWalletItemUM.Balance.Failed -> DASH_SIGN to false
is UserWalletItemUM.Balance.Hidden -> THREE_STARS to false
is UserWalletItemUM.Balance.Loading -> null to false
is UserWalletItemUM.Balance.Locked -> stringResource(R.string.common_locked) to false
is UserWalletItemUM.Balance.Loaded -> balance.value to balance.isFlickering
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_UserWalletItem(
@PreviewParameter(UserWalletItemUMPreviewProvider::class) params: UserWalletItemUM,
) {
TangemThemePreview {
val list = persistentListOf(
UserWalletItem(
state = params,
)
}
}
private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWalletItemUM> {
override val values: Sequence<UserWalletItemUM>
get() = sequenceOf(
UserWalletItemUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
name = stringReference("My Wallet"),
information = getInformation(3, "4 496,75 $"),
information = getInformation(cardCount = 1),
balance = UserWalletItemUM.Balance.Locked,
imageUrl = "",
isEnabled = true,
onClick = {},
@ -167,7 +221,8 @@ private fun Preview() {
UserWalletItemUM(
id = UserWalletId("user_wallet_2".encodeToByteArray()),
name = stringReference("Old wallet"),
information = getInformation(3, "4 496,75 $"),
information = getInformation(cardCount = 2),
balance = UserWalletItemUM.Balance.Hidden,
imageUrl = "",
isEnabled = true,
onClick = {},
@ -176,7 +231,44 @@ private fun Preview() {
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = getInformation(3, "4 496,75 $"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Failed,
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Loaded(
value = "1.2345 BTC",
isFlickering = false,
),
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Loaded(
value = "1.2345 BTC",
isFlickering = true,
),
imageUrl = "",
isEnabled = false,
endIcon = UserWalletItemUM.EndIcon.Checkmark,
@ -184,30 +276,12 @@ private fun Preview() {
),
)
Column(
Modifier
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing12),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
list.fastForEach { userWalletItemUM ->
UserWalletItem(
modifier = Modifier.fillMaxWidth(),
state = userWalletItemUM,
)
}
}
private fun getInformation(cardCount: Int): TextReference {
return TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
}
}
private fun getInformation(cardCount: Int, totalBalance: String): TextReference {
val t1 = TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
val divider = stringReference(value = "")
val t2 = stringReference(totalBalance)
return TextReference.Combined(wrappedList(t1, divider, t2))
}
// endregion Preview

View file

@ -2,14 +2,16 @@ package com.tangem.common.ui.userwallet.converter
import com.tangem.common.ui.R
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.converter.Converter
/**
@ -37,12 +39,8 @@ class UserWalletItemUMConverter(
UserWalletItemUM(
id = walletId,
name = stringReference(name),
information = getInfo(
appCurrency = appCurrency,
balance = balance,
isBalanceHidden = isBalanceHidden,
isLoading = isLoading,
),
information = getInfo(userWallet = this),
balance = getBalanceInfo(userWallet = this),
imageUrl = artworkUrl,
isEnabled = !isLocked,
endIcon = endIcon,
@ -51,53 +49,40 @@ class UserWalletItemUMConverter(
}
}
private fun UserWallet.getInfo(
appCurrency: AppCurrency?,
balance: TotalFiatBalance?,
isBalanceHidden: Boolean,
isLoading: Boolean,
): TextReference {
val dividerRef = stringReference(value = " $DOT ")
val cardCount = getCardsCount() ?: 1
val cardCountRef = TextReference.PluralRes(
private fun getInfo(userWallet: UserWallet): TextReference {
val cardCount = userWallet.getCardsCount() ?: 1
return TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
return when {
isBalanceHidden -> combinedReference(cardCountRef, dividerRef, TextReference.STARS)
isLocked -> combinedReference(cardCountRef, dividerRef, resourceReference(R.string.common_locked))
isLoading -> cardCountRef
else -> getBalanceInfo(balance, appCurrency, cardCountRef, dividerRef)
}
}
private fun getBalanceInfo(
balance: TotalFiatBalance?,
appCurrency: AppCurrency?,
cardCountRef: TextReference,
dividerRef: TextReference,
): TextReference {
val amount = when (balance) {
is TotalFiatBalance.Loaded -> balance.amount
is TotalFiatBalance.Failed,
is TotalFiatBalance.Loading,
null,
-> null
}
private fun getBalanceInfo(userWallet: UserWallet): UserWalletItemUM.Balance {
return when {
isBalanceHidden -> UserWalletItemUM.Balance.Hidden
userWallet.isLocked -> UserWalletItemUM.Balance.Locked
balance == null -> UserWalletItemUM.Balance.Loading
else -> {
when (balance) {
is TotalFiatBalance.Loading -> UserWalletItemUM.Balance.Loading
is TotalFiatBalance.Failed -> UserWalletItemUM.Balance.Failed
is TotalFiatBalance.Loaded -> {
if (appCurrency != null) {
val formattedAmount = balance.amount.format {
fiat(appCurrency.code, appCurrency.symbol)
}
return if (amount != null && appCurrency != null) {
val formattedAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = amount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
val amountRef = stringReference(formattedAmount)
combinedReference(cardCountRef, dividerRef, amountRef)
} else {
combinedReference(cardCountRef, dividerRef, stringReference(BigDecimalFormatter.EMPTY_BALANCE_SIGN))
UserWalletItemUM.Balance.Loaded(
value = formattedAmount,
isFlickering = isLoading,
)
} else {
UserWalletItemUM.Balance.Failed
}
}
}
}
}
}
}

View file

@ -9,6 +9,7 @@ data class UserWalletItemUM(
val id: UserWalletId,
val name: TextReference,
val information: TextReference,
val balance: Balance,
val imageUrl: String,
val isEnabled: Boolean,
val endIcon: EndIcon = EndIcon.None,
@ -19,4 +20,20 @@ data class UserWalletItemUM(
Arrow,
Checkmark,
}
sealed class Balance {
data object Hidden : Balance()
data object Locked : Balance()
data object Failed : Balance()
data object Loading : Balance()
data class Loaded(
val value: String,
val isFlickering: Boolean,
) : Balance()
}
}

View file

@ -0,0 +1,79 @@
package com.tangem.core.ui.components
import android.content.res.Configuration
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Modifier that makes content flicker.
*
* @param isFlickering if `true`, content will flicker.
* @param targetTextAlpha target alpha value for flickering.
* @param animationDurationMillis duration of flickering animation.
* */
fun Modifier.flicker(
isFlickering: Boolean,
targetTextAlpha: Float = 0.4f,
animationDurationMillis: Int = 1200,
): Modifier = composed {
val infiniteTransition = rememberInfiniteTransition()
val defaultAlpha = remember { mutableFloatStateOf(value = 1f) }
val alpha: Float by if (isFlickering) {
infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = targetTextAlpha,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = animationDurationMillis,
easing = CubicBezierEasing(a = 0.45f, b = 0.0f, c = 0.55f, d = 1.0f),
),
repeatMode = RepeatMode.Reverse,
),
)
} else {
defaultAlpha
}
this.then(alpha(alpha))
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_FlickerText() {
TangemThemePreview {
var isFlickering by remember {
mutableStateOf(value = true)
}
Box(
modifier = Modifier
.fillMaxWidth()
.background(color = TangemTheme.colors.background.primary)
.clickable {
isFlickering = !isFlickering
},
) {
Text(
modifier = Modifier.flicker(isFlickering),
text = "Flicker text",
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body1,
)
}
}
}
// endregion Preview

View file

@ -23,12 +23,14 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.token.internal.*
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.rememberHapticFeedback
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.persistentListOf
import org.burnoutcrew.reorderable.ReorderableLazyListState
import java.util.UUID
import kotlin.math.max
@ -462,11 +464,18 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
text = stringReference(value = "PolygonPolygonPolygonPolygonPolygonPolygon"),
hasPending = true,
),
fiatAmountState = TokenItemState.FiatAmountState.Content(
fiatAmountState = FiatAmountState.Content(
text = "3213123123321312312312312312 $",
hasStaked = true,
icons = persistentListOf(
FiatAmountState.Content.IconUM(R.drawable.ic_error_sync_24, useAccentColor = false),
stakingIcon,
),
isFlickering = true,
),
subtitle2State = TokenItemState.Subtitle2State.TextContent(
text = "5,4123123213123123123123123123 MATIC",
isFlickering = true,
),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "5,4123123213123123123123123123 MATIC"),
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
price = "312 USD",
priceChangePercent = "42.0%",
@ -511,7 +520,10 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = false),
fiatAmountState = FiatAmountState.Content(
text = "321 $",
icons = persistentListOf(stakingIcon),
),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "5,412 MATIC"),
subtitleState = TokenItemState.SubtitleState.CryptoPriceContent(
price = "312 USD",
@ -525,7 +537,10 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = false),
fiatAmountState = FiatAmountState.Content(
text = "321 $",
icons = persistentListOf(stakingIcon),
),
subtitle2State = TokenItemState.Subtitle2State.LabelContent(
auditLabelUM = AuditLabelUM(
text = TextReference.Str(value = "Trusted"),
@ -565,6 +580,8 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
companion object {
val stakingIcon = FiatAmountState.Content.IconUM(R.drawable.ic_staking_24, useAccentColor = true)
val coinIconState
get() = CurrencyIconState.CoinIcon(
url = null,
@ -599,7 +616,10 @@ private class TokenItemStateProvider : CollectionPreviewParameterProvider<TokenI
text = stringReference(value = "Polygon"),
hasPending = true,
),
fiatAmountState = TokenItemState.FiatAmountState.Content(text = "321 $", hasStaked = true),
fiatAmountState = FiatAmountState.Content(
text = "321 $",
icons = persistentListOf(stakingIcon),
),
subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "5,412 MATIC"),
subtitleState = TokenItemState.SubtitleState.Unknown,
onItemClick = {},

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.audits.AuditLabel
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.stringResourceSafe
@ -25,15 +26,19 @@ internal fun TokenCryptoAmount(
when (state) {
is TokenCryptoAmountState.TextContent -> {
CryptoAmountText(
amount = state.text.orMaskWithStars(isBalanceHidden),
modifier = modifier,
amount = state.text.orMaskWithStars(isBalanceHidden),
isFlickering = state.isFlickering,
)
}
is TokenItemState.Subtitle2State.LabelContent -> {
AuditLabel(state = state.auditLabelUM, modifier = modifier)
}
is TokenCryptoAmountState.Unreachable -> {
CryptoAmountText(amount = stringResourceSafe(id = R.string.common_unreachable), modifier = modifier)
CryptoAmountText(
amount = stringResourceSafe(id = R.string.common_unreachable),
modifier = modifier,
)
}
is TokenCryptoAmountState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
@ -46,10 +51,10 @@ internal fun TokenCryptoAmount(
}
@Composable
private fun CryptoAmountText(amount: String, modifier: Modifier = Modifier) {
private fun CryptoAmountText(amount: String, modifier: Modifier = Modifier, isFlickering: Boolean = false) {
Text(
modifier = modifier.flicker(isFlickering),
text = amount,
modifier = modifier,
color = TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,

View file

@ -1,6 +1,7 @@
package com.tangem.core.ui.components.token.internal
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -14,9 +15,10 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState
@ -26,12 +28,13 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool
when (state) {
is TokenFiatAmountState.Content -> {
ContentFiatAmount(
text = state.text.orMaskWithStars(isBalanceHidden),
hasStaked = state.hasStaked,
modifier = modifier,
text = state.text.orMaskWithStars(isBalanceHidden),
icons = state.icons,
isAmountFlickering = state.isFlickering,
)
}
is TokenItemState.FiatAmountState.TextContent -> {
is TokenFiatAmountState.TextContent -> {
FiatAmountText(
text = state.text.orMaskWithStars(isBalanceHidden),
modifier = modifier,
@ -39,7 +42,7 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool
)
}
is TokenFiatAmountState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = TangemTheme.dimens.radius4)
RectangleShimmer(modifier = modifier.placeholderSize(), radius = 4.dp)
}
is TokenFiatAmountState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
@ -49,33 +52,53 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool
}
@Composable
private fun ContentFiatAmount(text: String, hasStaked: Boolean, modifier: Modifier = Modifier) {
private fun ContentFiatAmount(
text: String,
icons: List<TokenFiatAmountState.Content.IconUM>,
isAmountFlickering: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(
visible = hasStaked,
visible = icons.isNotEmpty(),
) {
Icon(
painter = rememberVectorPainter(image = ImageVector.vectorResource(R.drawable.ic_staking_24)),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing4)
.size(TangemTheme.dimens.size12),
)
Row(
modifier = Modifier.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
icons.fastForEach { icon ->
Icon(
modifier = Modifier.size(12.dp),
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
tint = if (icon.useAccentColor) {
TangemTheme.colors.icon.accent
} else {
TangemTheme.colors.icon.inactive
},
contentDescription = null,
)
}
}
}
FiatAmountText(text = text)
FiatAmountText(text = text, isFlickering = isAmountFlickering)
}
}
@Composable
private fun FiatAmountText(text: String, modifier: Modifier = Modifier, isAvailable: Boolean = true) {
private fun FiatAmountText(
text: String,
modifier: Modifier = Modifier,
isAvailable: Boolean = true,
isFlickering: Boolean = false,
) {
Text(
modifier = modifier.flicker(isFlickering),
text = text,
modifier = modifier,
color = if (isAvailable) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@ -85,6 +108,6 @@ private fun FiatAmountText(text: String, modifier: Modifier = Modifier, isAvaila
private fun Modifier.placeholderSize(): Modifier = composed {
return@composed this
.padding(vertical = TangemTheme.dimens.spacing4)
.size(width = TangemTheme.dimens.size40, height = TangemTheme.dimens.size12)
.padding(vertical = 4.dp)
.size(width = 40.dp, height = 12.dp)
}

View file

@ -5,6 +5,8 @@ import com.tangem.core.ui.components.audits.AuditLabelUM
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/** TokenItem component state */
@Immutable
@ -192,10 +194,18 @@ sealed class TokenItemState {
@Immutable
sealed class FiatAmountState {
data class Content(
val text: String,
val hasStaked: Boolean = false,
) : FiatAmountState()
val isFlickering: Boolean = false,
val icons: ImmutableList<IconUM> = persistentListOf(),
) : FiatAmountState() {
data class IconUM(
val iconRes: Int,
val useAccentColor: Boolean,
)
}
data class TextContent(val text: String, val isAvailable: Boolean = true) : FiatAmountState()
@ -207,7 +217,10 @@ sealed class TokenItemState {
@Immutable
sealed class Subtitle2State {
data class TextContent(val text: String) : Subtitle2State()
data class TextContent(
val text: String,
val isFlickering: Boolean = false,
) : Subtitle2State()
data class LabelContent(val auditLabelUM: AuditLabelUM) : Subtitle2State()

View file

@ -13,6 +13,7 @@ import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.tangem.core.res.getPluralStringSafe
import com.tangem.core.res.getStringSafe
import com.tangem.utils.StringsSigns.THREE_STARS
import java.util.Hashtable
/**
@ -79,6 +80,4 @@ fun String.capitalize(): String = replaceFirstChar { if (it.isLowerCase()) it.ti
fun String.orMaskWithStars(maskWithStars: Boolean): String {
return if (maskWithStars) THREE_STARS else this
}
internal const val THREE_STARS = "\u2217\u2217\u2217"
}

View file

@ -11,6 +11,7 @@ import androidx.compose.ui.text.AnnotatedString.Builder
import androidx.compose.ui.text.buildAnnotatedString
import com.tangem.core.res.getPluralStringSafe
import com.tangem.core.res.getStringSafe
import com.tangem.utils.StringsSigns.THREE_STARS
import org.intellij.markdown.MarkdownElementTypes
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
@ -72,9 +73,6 @@ sealed interface TextReference {
/** Empty string as [TextReference] */
val EMPTY: TextReference by lazy(mode = LazyThreadSafetyMode.NONE) { Str(value = "") }
/** Stars string as [TextReference] */
val STARS: TextReference by lazy(mode = LazyThreadSafetyMode.NONE) { Str(value = THREE_STARS) }
}
}

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M12,4C15.64,4,18.67,6.59,19.35,10.04C21.95,10.22,24,12.36,24,15C24,15.66,23.87,16.31,23.62,16.91C23.37,17.52,23,18.07,22.54,18.54C22.07,19,21.52,19.37,20.91,19.62C20.31,19.87,19.66,20,19,20H15.5V18H19C19.8,18,20.56,17.68,21.12,17.12C21.68,16.56,22,15.8,22,15C22,13.45,20.78,12.14,19.22,12.04L17.69,11.93L17.39,10.43C16.88,7.86,14.62,6,12,6C9.94,6,8.08,7.14,7.13,8.97L6.63,9.92L5.56,10.03C3.53,10.24,2,11.95,2,14C2,15.06,2.42,16.08,3.17,16.83C3.92,17.58,4.94,18,6,18H8.5V20H6C4.41,20,2.88,19.37,1.76,18.24C0.63,17.12,0,15.59,0,14C0,10.91,2.34,8.36,5.35,8.04C6.6,5.64,9.11,4,12,4Z" />
<path
android:fillColor="#000"
android:pathData="M11,19V14H13V19Z" />
<path
android:fillColor="#000"
android:pathData="M13,21V23H11V21Z" />
</vector>

View file

@ -11,4 +11,5 @@ object StringsSigns {
const val INFINITY_SIGN = ""
const val NON_BREAKING_SPACE = '\u00A0'
const val PERCENT = "%"
const val THREE_STARS = "\u2217\u2217\u2217"
}

View file

@ -1,6 +1,9 @@
package com.tangem.features.details.component.preview
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.TextReference
@ -13,55 +16,75 @@ import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
import com.tangem.features.details.ui.UserWalletListBlock
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
internal class PreviewUserWalletListComponent : UserWalletListComponent {
private val previewState = UserWalletListUM(
userWallets = persistentListOf(
UserWalletItemUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
name = stringReference("My Wallet"),
information = getInformation(3, "4 496,75 $"),
imageUrl = "",
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_2".encodeToByteArray()),
name = stringReference("Old wallet"),
information = getInformation(3, "4 496,75 $"),
imageUrl = "",
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = getInformation(3, "4 496,75 $"),
imageUrl = "",
isEnabled = false,
onClick = {},
private val previewState = mutableStateOf(
UserWalletListUM(
userWallets = persistentListOf(
UserWalletItemUM(
id = UserWalletId("user_wallet_1".encodeToByteArray()),
name = stringReference("My Wallet"),
information = getInformation(cardCount = 1),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_2".encodeToByteArray()),
name = stringReference("Old wallet"),
information = getInformation(cardCount = 2),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = getInformation(cardCount = 3),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = false,
onClick = {},
),
),
addNewWalletText = resourceReference(R.string.user_wallet_list_add_button),
isWalletSavingInProgress = false,
onAddNewWalletClick = {},
),
addNewWalletText = resourceReference(R.string.user_wallet_list_add_button),
isWalletSavingInProgress = false,
onAddNewWalletClick = {},
)
@Composable
override fun Content(modifier: Modifier) {
UserWalletListBlock(state = previewState, modifier = modifier)
val state by previewState
UserWalletListBlock(state = state, modifier = modifier)
LaunchedEffect(key1 = this) {
delay(timeMillis = 2_000)
previewState.value = state.copy(
userWallets = state.userWallets.map {
it.copy(
balance = UserWalletItemUM.Balance.Loaded(
value = "1.000 BTC",
isFlickering = true,
),
)
}.toImmutableList(),
)
}
}
private fun getInformation(cardCount: Int, totalBalance: String): TextReference {
val t1 = TextReference.PluralRes(
private fun getInformation(cardCount: Int): TextReference {
return TextReference.PluralRes(
id = R.plurals.card_label_card_count,
count = cardCount,
formatArgs = wrappedList(cardCount),
)
val divider = stringReference(value = "")
val t2 = stringReference(totalBalance)
return TextReference.Combined(wrappedList(t1, divider, t2))
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.details.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.CircularProgressIndicator
@ -10,11 +11,17 @@ import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.core.ui.components.block.BlockCard
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.details.component.UserWalletListComponent
import com.tangem.features.details.component.preview.PreviewUserWalletListComponent
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R
@ -85,4 +92,24 @@ private fun AddWalletButton(
)
}
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview_UserWalletListBlock(
@PreviewParameter(UserWalletListComponentPreviewProvider::class) component: UserWalletListComponent,
) {
TangemThemePreview {
component.Content(Modifier)
}
}
private class UserWalletListComponentPreviewProvider : PreviewParameterProvider<UserWalletListComponent> {
override val values: Sequence<UserWalletListComponent>
get() = sequenceOf(
PreviewUserWalletListComponent(),
)
}
// endregion Preview

View file

@ -16,9 +16,11 @@ import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.markets.impl.R
import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider
import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM
import com.tangem.utils.StringsSigns.DASH_SIGN
import kotlinx.collections.immutable.persistentListOf
import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState
@Composable
@ -96,7 +98,14 @@ private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider<Port
collection = listOf(
tokenUM.copy(
tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy(
fiatAmountState = contentFiatAmount.copy(hasStaked = true),
fiatAmountState = contentFiatAmount.copy(
icons = persistentListOf(
TokenItemState.FiatAmountState.Content.IconUM(
iconRes = R.drawable.ic_staking_24,
useAccentColor = true,
),
),
),
),
),
tokenUM.copy(

View file

@ -25,7 +25,8 @@ internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider
val userWallet = UserWalletItemUM(
id = UserWalletId("1"),
name = stringReference("Wallet 1"),
information = stringReference("3 cards, 10,123$"),
information = stringReference("3 cards"),
balance = UserWalletItemUM.Balance.Loading,
imageUrl = "",
isEnabled = true,
endIcon = UserWalletItemUM.EndIcon.Arrow,

View file

@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails
import androidx.compose.ui.graphics.Color
import androidx.paging.PagingData
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
@ -11,7 +12,6 @@ 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.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
@ -129,6 +129,7 @@ internal object TokenDetailsPreviewData {
displayCryptoBalance = "966,96 XLM",
displayFiatBalance = "91,50$",
isBalanceSelectorEnabled = true,
isBalanceFlickering = false,
)
val balanceError = TokenDetailsBalanceBlockState.Error(
actionButtons = actionButtons,

View file

@ -26,6 +26,7 @@ internal sealed class TokenDetailsBalanceBlockState {
val displayCryptoBalance: String,
val displayFiatBalance: String,
val isBalanceSelectorEnabled: Boolean,
val isBalanceFlickering: Boolean,
) : TokenDetailsBalanceBlockState()
data class Error(

View file

@ -121,6 +121,7 @@ internal class TokenDetailsLoadedBalanceConverter(
onBalanceSelect = clickIntents::onBalanceSelect,
selectedBalanceType = currentState.selectedBalanceType,
isBalanceSelectorEnabled = isBalanceSelectorEnabled,
isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA]
)
is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading(
actionButtons = currentState.actionButtons,

View file

@ -14,6 +14,7 @@ import androidx.constraintlayout.compose.ConstraintLayout
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.buttons.HorizontalActionChips
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
@ -113,7 +114,7 @@ private fun FiatBalance(
),
)
is TokenDetailsBalanceBlockState.Content -> Text(
modifier = modifier,
modifier = modifier.flicker(state.isBalanceFlickering),
text = state.displayFiatBalance.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
@ -141,7 +142,7 @@ private fun CryptoBalance(
),
)
is TokenDetailsBalanceBlockState.Content -> Text(
modifier = modifier,
modifier = modifier.flicker(state.isBalanceFlickering),
text = state.displayCryptoBalance.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
@ -202,6 +203,7 @@ private class TokenDetailsBalanceBlockStateProvider : CollectionPreviewParameter
collection = listOf(
TokenDetailsPreviewData.balanceLoading,
TokenDetailsPreviewData.balanceContent,
TokenDetailsPreviewData.balanceContent.copy(isBalanceFlickering = true),
TokenDetailsPreviewData.balanceError,
),
)

View file

@ -36,6 +36,7 @@ internal object WalletPreviewData {
onDeleteClick = {},
cardCount = 1,
isZeroBalance = false,
isBalanceFlickering = false,
)
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.common.preview
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.token.state.TokenItemState
@ -7,7 +8,6 @@ import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig
@ -105,6 +105,7 @@ internal object WalletScreenPreviewData {
onRenameClick = { _ -> },
onDeleteClick = {},
isZeroBalance = false,
isBalanceFlickering = false,
)
}
private val multiWalletState by lazy {

View file

@ -48,6 +48,7 @@ internal sealed interface WalletCardState {
override val imageResId: Int?,
override val onRenameClick: (UserWalletId) -> Unit,
override val onDeleteClick: (UserWalletId) -> Unit,
val isBalanceFlickering: Boolean,
val cardCount: Int?,
val balance: String,
val isZeroBalance: Boolean?,

View file

@ -78,6 +78,7 @@ internal class SetBalancesAndLimitsTransformer(
},
cardCount = userWallet.getCardsCount(),
isZeroBalance = visaCurrency.balances.available.isZero(),
isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA]
)
}
}

View file

@ -69,6 +69,7 @@ internal class SetTokenListErrorTransformer(
),
cardCount = selectedWallet.getCardsCount(),
isZeroBalance = true,
isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA]
)
}
}

View file

@ -61,6 +61,7 @@ internal class MultiWalletCardStateConverter(
),
isZeroBalance = fiatBalance.amount.isZero(),
cardCount = selectedWallet.getCardsCount(),
isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA]
)
}
}

View file

@ -65,6 +65,7 @@ internal class SingleWalletCardStateConverter(
balance = formatFiatAmount(status = status, appCurrency = appCurrency),
cardCount = selectedWallet.getCardsCount(),
isZeroBalance = status.fiatAmount?.isZero(),
isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA]
)
}

View file

@ -44,6 +44,7 @@ import androidx.constraintlayout.compose.Dimension
import com.tangem.core.ui.components.FontSizeRange
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.components.flicker
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveReference
@ -289,9 +290,11 @@ private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier:
when (walletCardState) {
is WalletCardState.Content -> {
ResizableText(
modifier = Modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size32)
.flicker(isFlickering = walletCardState.isBalanceFlickering),
text = walletCardState.balance.orMaskWithStars(isBalanceHidden),
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32),
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
@ -401,6 +404,9 @@ private fun Preview_WalletCard(
private class WalletCardStateProvider : CollectionPreviewParameterProvider<WalletCardState>(
collection = listOf(
WalletPreviewData.walletCardContentState,
WalletPreviewData.walletCardContentState.copy(
isBalanceFlickering = true,
),
WalletPreviewData.walletCardLoadingState,
WalletPreviewData.walletCardErrorState,
),