Updated on 2026-08-14

This commit is contained in:
Tangem 2026-02-24 19:28:48 +05:00
commit 3edb222a84
22 changed files with 1381 additions and 222 deletions

View file

@ -72,14 +72,14 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
*/
@Composable
fun TangemBadge(
text: TextReference,
modifier: Modifier = Modifier,
text: TextReference? = null,
@DrawableRes iconRes: Int? = null,
size: TangemBadgeSize = X9,
shape: TangemBadgeShape = TangemBadgeShape.Default,
color: TangemBadgeColor = TangemBadgeColor.Gray,
type: TangemBadgeType = TangemBadgeType.Solid,
iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start,
iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.None,
onClick: (() -> Unit)? = null,
) {
val iconColor = getIconColor(type = type, color = color)
@ -94,7 +94,7 @@ fun TangemBadge(
.clickableSingle(enabled = onClick != null, onClick = { onClick?.invoke() }),
) {
AnimatedVisibility(
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.Start,
visible = iconRes != null && iconPosition != TangemBadgeIconPosition.End,
modifier = Modifier.size(size = size.toContentSize()),
label = "Start Icon Visibility",
) {
@ -105,13 +105,18 @@ fun TangemBadge(
tint = iconColor,
)
}
Text(
text = text.resolveReference(),
style = size.toTextStyle(),
maxLines = 1,
color = getTextColor(type = type, color = color),
)
AnimatedVisibility(
visible = text != null,
label = "Text Visibility",
) {
val wrappedText = remember(this) { requireNotNull(text) }
Text(
text = wrappedText.resolveReference(),
style = size.toTextStyle(),
maxLines = 1,
color = getTextColor(type = type, color = color),
)
}
AnimatedVisibility(
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.End,
modifier = Modifier.size(size = size.toContentSize()),
@ -178,14 +183,17 @@ enum class TangemBadgeSize {
X4 -> when (position) {
TangemBadgeIconPosition.Start -> PaddingValues(start = 4.dp, end = 6.dp)
TangemBadgeIconPosition.End -> PaddingValues(start = 6.dp, end = 4.dp)
TangemBadgeIconPosition.None -> PaddingValues(start = 6.dp, end = 6.dp)
}
X6 -> when (position) {
TangemBadgeIconPosition.Start -> PaddingValues(start = 8.dp, end = 12.dp)
TangemBadgeIconPosition.End -> PaddingValues(start = 12.dp, end = 8.dp)
TangemBadgeIconPosition.None -> PaddingValues(start = 12.dp, end = 12.dp)
}
X9 -> when (position) {
TangemBadgeIconPosition.Start -> PaddingValues(start = 12.dp, end = 16.dp)
TangemBadgeIconPosition.End -> PaddingValues(start = 16.dp, end = 12.dp)
TangemBadgeIconPosition.None -> PaddingValues(start = 16.dp, end = 16.dp)
}
}
@ -222,6 +230,7 @@ enum class TangemBadgeSize {
enum class TangemBadgeIconPosition {
Start,
End,
None,
}
/**
@ -240,6 +249,7 @@ enum class TangemBadgeColor {
Blue,
Red,
Gray,
Green,
}
@ReadOnlyComposable
@ -258,6 +268,12 @@ private fun getIconColor(type: TangemBadgeType, color: TangemBadgeColor) = when
-> TangemTheme.colors2.markers.iconRed
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
}
TangemBadgeColor.Green -> when (type) {
TangemBadgeType.Outline,
TangemBadgeType.Tinted,
-> TangemTheme.colors2.markers.iconGreen
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
}
}
@ReadOnlyComposable
@ -276,8 +292,15 @@ private fun getTextColor(type: TangemBadgeType, color: TangemBadgeColor) = when
-> TangemTheme.colors2.markers.textRed
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
}
TangemBadgeColor.Green -> when (type) {
TangemBadgeType.Outline,
TangemBadgeType.Tinted,
-> TangemTheme.colors2.markers.textGreen
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
}
}
@Suppress("CyclomaticComplexMethod")
@ReadOnlyComposable
@Composable
private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadgeColor, shape: Shape) = when (type) {
@ -286,6 +309,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundSolidGray
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundSolidBlue
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundSolidRed
TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundSolidGreen
},
)
TangemBadgeType.Tinted -> background(
@ -293,6 +317,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundTintedGray
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundTintedBlue
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundTintedRed
TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundTintedGreen
},
)
TangemBadgeType.Outline -> {
@ -301,6 +326,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.borderGray
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.borderTintedBlue
TangemBadgeColor.Red -> TangemTheme.colors2.markers.borderTintedRed
TangemBadgeColor.Green -> TangemTheme.colors2.markers.borderTintedGreen
},
shape = shape,
width = 1.dp,
@ -320,16 +346,16 @@ private fun TangemBadge_Preview(@PreviewParameter(TangemBadgePreviewProvider::cl
.background(TangemTheme.colors2.surface.level1)
.padding(8.dp),
) {
repeat(2) { yIndex ->
repeat(3) { yIndex ->
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
repeat(TangemBadgeType.entries.size) { index ->
TangemBadge(
text = stringReference("Title"),
text = stringReference("Title").takeIf { yIndex < 2 },
iconRes = R.drawable.ic_information_24,
type = TangemBadgeType.entries[index],
color = params,
shape = TangemBadgeShape.entries[yIndex % 2],
iconPosition = TangemBadgeIconPosition.entries[yIndex % 2],
iconPosition = TangemBadgeIconPosition.entries[yIndex],
)
}
}
@ -344,6 +370,7 @@ private class TangemBadgePreviewProvider : PreviewParameterProvider<TangemBadgeC
TangemBadgeColor.Gray,
TangemBadgeColor.Blue,
TangemBadgeColor.Red,
TangemBadgeColor.Green,
)
}
// endregion

View file

@ -17,7 +17,7 @@ import kotlin.math.max
* A custom layout composable that arranges its children in a row with specific layout IDs.
*/
enum class TangemRowLayoutId {
HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP
HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP, EXTRA_BOTTOM
}
/**
@ -37,6 +37,7 @@ fun TangemRowContainer(
val density = LocalDensity.current
val localDirection = LocalLayoutDirection.current
val verticalPadding = with(density) { TangemTheme.dimens2.x1.roundToPx() }
val extraContentPadding = with(density) { TangemTheme.dimens2.x2.roundToPx() }
val contentTopPadding = with(density) { contentPadding.calculateTopPadding().roundToPx() }
val contentBottomPadding = with(density) { contentPadding.calculateBottomPadding().roundToPx() }
val contentStartPadding = with(density) { contentPadding.calculateLeftPadding(localDirection).roundToPx() }
@ -110,6 +111,10 @@ fun TangemRowContainer(
layoutId = TangemRowLayoutId.EXTRA_TOP,
constraints = constraints,
)
val extraBottomPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.EXTRA_BOTTOM,
constraints = constraints,
)
val mainLayoutHeight = maxOf(
headPlaceable.heightOrZero(),
@ -124,7 +129,13 @@ fun TangemRowContainer(
contentTopPadding
}
val layoutHeight = mainLayoutHeight + mainContentTopPadding + contentBottomPadding
val mainContentBottomPadding = if (extraBottomPlaceable != null) {
extraBottomPlaceable.heightOrZero() + contentBottomPadding
} else {
contentBottomPadding
}
val layoutHeight = mainLayoutHeight + mainContentTopPadding + mainContentBottomPadding
layout(width = constraints.maxWidth, height = layoutHeight) {
extraTopPlaceable?.placeRelative(x = 0, y = 0)
@ -174,6 +185,11 @@ fun TangemRowContainer(
x = layoutWidth - tailPlaceable.width + contentEndPadding,
y = mainContentTopPadding + (mainLayoutHeight - tailPlaceable.height).div(other = 2),
)
extraBottomPlaceable?.placeRelative(
x = 0,
y = mainContentTopPadding + mainLayoutHeight + extraContentPadding,
)
}
}
}

View file

@ -52,15 +52,6 @@ fun TangemTokenRow(
.testTag(tag = TokenElementsTestTags.TOKEN_ICON),
)
TokenRowPromoBanner(
promoBannerUM = tokenRowUM.promoBannerUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.EXTRA_TOP)
.testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER)
.padding(horizontal = TangemTheme.dimens2.x3)
.fillMaxWidth(),
)
TokenRowTitle(
titleUM = tokenRowUM.titleUM,
modifier = Modifier
@ -77,17 +68,21 @@ fun TangemTokenRow(
.testTag(tag = TokenElementsTestTags.TOKEN_PRICE),
)
TokenRowEndTopContent(
TokenRowEndContent(
endContentUM = tokenRowUM.topEndContentUM,
isBalanceHidden = isBalanceHidden,
textStyle = TangemTheme.typography2.bodySemibold16,
textColor = TangemTheme.colors2.text.neutral.primary,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
)
TokenRowEndBottomContent(
TokenRowEndContent(
endContentUM = tokenRowUM.bottomEndContentUM,
isBalanceHidden = isBalanceHidden,
textStyle = TangemTheme.typography2.captionSemibold12,
textColor = TangemTheme.colors2.text.neutral.secondary,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
@ -100,6 +95,15 @@ fun TangemTokenRow(
.layoutId(layoutId = TangemRowLayoutId.TAIL)
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
)
TokenRowPromoBanner(
promoBannerUM = tokenRowUM.promoBannerUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.EXTRA_BOTTOM)
.testTag(tag = TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER)
.padding(start = TangemTheme.dimens2.x10, bottom = TangemTheme.dimens2.x2)
.fillMaxWidth(),
)
},
modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM),
)
@ -159,17 +163,21 @@ fun TangemTokenRow(
.testTag(tag = TokenElementsTestTags.TOKEN_PRICE),
)
TokenRowEndTopContent(
TokenRowEndContent(
endContentUM = tokenRowUM.topEndContentUM,
isBalanceHidden = isBalanceHidden,
textStyle = TangemTheme.typography2.bodySemibold16,
textColor = TangemTheme.colors2.text.neutral.primary,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
)
TokenRowEndBottomContent(
TokenRowEndContent(
endContentUM = tokenRowUM.bottomEndContentUM,
isBalanceHidden = isBalanceHidden,
textStyle = TangemTheme.typography2.captionSemibold12,
textColor = TangemTheme.colors2.text.neutral.secondary,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),

View file

@ -133,7 +133,8 @@ sealed class TangemTokenRowUM : TangemRowUM {
val text: TextReference,
val isAvailable: Boolean = true,
val isFlickering: Boolean = false,
val icons: ImmutableList<TangemIconUM.Icon> = persistentListOf(),
val startIcons: ImmutableList<TangemIconUM.Icon> = persistentListOf(),
val endIcons: ImmutableList<TangemIconUM.Icon> = persistentListOf(),
val priceChangeUM: PriceChangeState = PriceChangeState.Unknown,
) : EndContentUM()

View file

@ -142,7 +142,7 @@ internal object TangemTokenRowPreviewData {
)
}),
),
icons = persistentListOf(
startIcons = persistentListOf(
TangemIconUM.Icon(R.drawable.ic_staking_mini_10),
TangemIconUM.Icon(R.drawable.ic_attention_12),
TangemIconUM.Icon(R.drawable.ic_error_sync_24),

View file

@ -1,100 +0,0 @@
package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowEndBottomContent(
endContentUM: TangemTokenRowUM.EndContentUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (endContentUM) {
is TangemTokenRowUM.EndContentUM.Content -> Content(
modifier = modifier,
endContentUM = endContentUM,
isBalanceHidden = isBalanceHidden,
)
TangemTokenRowUM.EndContentUM.Empty -> Unit
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
style = TangemTheme.typography2.captionSemibold12,
modifier = modifier.width(TangemTheme.dimens2.x10),
radius = TangemTheme.dimens2.x25,
)
}
}
@Composable
private fun Content(
endContentUM: TangemTokenRowUM.EndContentUM.Content,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
isEnabled = endContentUM.isFlickering,
textColor = if (endContentUM.isAvailable) {
TangemTheme.colors2.text.neutral.secondary
} else {
TangemTheme.colors2.text.status.disabled
},
),
)
when (val priceChangeUM = endContentUM.priceChangeUM) {
is PriceChangeState.Content -> TokenRowPriceChangeContent(
priceChangeState = priceChangeUM,
isFlickering = endContentUM.isFlickering,
isAvailable = endContentUM.isAvailable,
)
PriceChangeState.Unknown -> Unit
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenRowEndBottomContent_Preview(
@PreviewParameter(TokenRowEndBottomContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
) {
TangemThemePreviewRedesign {
TokenRowEndBottomContent(
endContentUM = params,
isBalanceHidden = false,
)
}
}
private class TokenRowEndBottomContentPreviewProvider : PreviewParameterProvider<TangemTokenRowUM.EndContentUM> {
override val values: Sequence<TangemTokenRowUM.EndContentUM>
get() = sequenceOf(
TangemTokenRowPreviewData.bottomEndContentUM,
)
}
// endregion

View file

@ -8,15 +8,18 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.orMaskWithStars
@ -25,9 +28,11 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowEndTopContent(
internal fun TokenRowEndContent(
endContentUM: TangemTokenRowUM.EndContentUM,
isBalanceHidden: Boolean,
textStyle: TextStyle,
textColor: Color,
modifier: Modifier = Modifier,
) {
when (endContentUM) {
@ -35,11 +40,13 @@ internal fun TokenRowEndTopContent(
modifier = modifier,
endContentUM = endContentUM,
isBalanceHidden = isBalanceHidden,
textStyle = textStyle,
textColor = textColor,
)
TangemTokenRowUM.EndContentUM.Empty -> Unit
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
style = TangemTheme.typography2.bodySemibold16,
modifier = modifier.width(TangemTheme.dimens2.x18),
style = textStyle,
modifier = modifier.width(TangemTheme.dimens2.x10),
radius = TangemTheme.dimens2.x25,
)
}
@ -48,6 +55,8 @@ internal fun TokenRowEndTopContent(
@Composable
private fun Content(
endContentUM: TangemTokenRowUM.EndContentUM.Content,
textStyle: TextStyle,
textColor: Color,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
@ -56,14 +65,14 @@ private fun Content(
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(
visible = endContentUM.icons.isNotEmpty(),
visible = endContentUM.startIcons.isNotEmpty(),
) {
Row(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x1),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
endContentUM.icons.fastForEach { icon ->
endContentUM.startIcons.fastForEach { icon ->
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x3),
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
@ -75,11 +84,11 @@ private fun Content(
}
Text(
modifier = Modifier,
text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.bodySemibold16.applyBladeBrush(
color = textColor,
style = textStyle.applyBladeBrush(
isEnabled = endContentUM.isFlickering,
textColor = if (endContentUM.isAvailable) {
TangemTheme.colors2.text.neutral.primary
@ -88,6 +97,34 @@ private fun Content(
},
),
)
AnimatedVisibility(
visible = endContentUM.endIcons.isNotEmpty(),
) {
Row(
modifier = Modifier.padding(start = TangemTheme.dimens2.x0_5),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
endContentUM.endIcons.fastForEach { icon ->
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x3),
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
tint = icon.tintReference(),
contentDescription = null,
)
}
}
}
when (val priceChangeUM = endContentUM.priceChangeUM) {
is PriceChangeState.Content -> TokenRowPriceChangeContent(
priceChangeState = priceChangeUM,
isFlickering = endContentUM.isFlickering,
isAvailable = endContentUM.isAvailable,
)
PriceChangeState.Unknown -> Unit
}
}
}
@ -95,13 +132,15 @@ private fun Content(
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenRowEndTopContent_Preview(
private fun TokenRowEndContent_Preview(
@PreviewParameter(TokenRowEndContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
) {
TangemThemePreviewRedesign {
TokenRowEndTopContent(
TokenRowEndContent(
endContentUM = params,
isBalanceHidden = false,
textColor = TangemTheme.colors2.text.neutral.primary,
textStyle = TangemTheme.typography2.captionSemibold12,
)
}
}
@ -109,7 +148,7 @@ private fun TokenRowEndTopContent_Preview(
private class TokenRowEndContentPreviewProvider : PreviewParameterProvider<TangemTokenRowUM.EndContentUM> {
override val values: Sequence<TangemTokenRowUM.EndContentUM>
get() = sequenceOf(
TangemTokenRowPreviewData.topEndContentUM,
TangemTokenRowPreviewData.bottomEndContentUM,
)
}
// endregion

View file

@ -3,15 +3,12 @@ package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
@ -19,6 +16,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.badge.*
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -40,55 +38,52 @@ internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM.C
LaunchedEffect(promoBannerUM) {
promoBannerUM.onPromoShown()
}
val bgColor = TangemTheme.colors.control.default
Column(modifier = modifier) {
val bgColor = TangemTheme.colors2.markers.backgroundTintedGreen
Column(
modifier = modifier,
) {
Icon(
painter = painterResource(id = R.drawable.shape_triangular),
contentDescription = null,
tint = bgColor,
modifier = Modifier.padding(start = TangemTheme.dimens2.x5),
)
Row(
modifier = Modifier
.background(color = bgColor, shape = RoundedCornerShape(TangemTheme.dimens2.x4))
.clickable(onClick = promoBannerUM.onPromoBannerClick)
.padding(horizontal = TangemTheme.dimens2.x3, vertical = TangemTheme.dimens2.x2)
.fillMaxWidth(),
.padding(
start = TangemTheme.dimens2.x2_5,
end = TangemTheme.dimens2.x0_5,
top = TangemTheme.dimens2.x0_5,
bottom = TangemTheme.dimens2.x0_5,
),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
tint = TangemTheme.colors2.markers.textGreen,
modifier = Modifier
.padding(end = TangemTheme.dimens2.x2)
.size(TangemTheme.dimens2.x4),
.padding(vertical = TangemTheme.dimens2.x0_5)
.size(TangemTheme.dimens2.x3),
)
Text(
text = promoBannerUM.title.resolveReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.secondary,
style = TangemTheme.typography2.captionSemibold11,
color = TangemTheme.colors2.markers.textGreen,
modifier = Modifier
.weight(1f)
.padding(end = TangemTheme.dimens2.x2),
.padding(vertical = TangemTheme.dimens2.x0_5),
)
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
contentDescription = null,
tint = TangemTheme.colors2.text.neutral.secondary,
modifier = Modifier
.size(TangemTheme.dimens2.x4)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onClick = { promoBannerUM.onCloseClick() },
),
)
}
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_rectangle_bottom),
contentDescription = null,
tint = bgColor,
modifier = Modifier
.size(width = TangemTheme.dimens2.x3, height = TangemTheme.dimens2.x2),
TangemBadge(
size = TangemBadgeSize.X4,
shape = TangemBadgeShape.Rounded,
color = TangemBadgeColor.Green,
type = TangemBadgeType.Tinted,
iconRes = R.drawable.ic_close_24,
iconPosition = TangemBadgeIconPosition.None,
onClick = promoBannerUM.onCloseClick,
)
}
}

View file

@ -59,20 +59,45 @@ object TangemColorPalette {
val DarkGreen = Color(0xFF06311F)
// endregion Green
// region Blue
// region Azure
val Azure = Color(0xFF0099FF)
// endregion Blue
val Azure_50 = Color(0x800099FF)
val Azure_10 = Color(0x1A0099FF)
// endregion Azure
// region Red
// region Amaranth
val Amaranth = Color(0xFFFF3333)
val Amaranth_50 = Color(0x80FF3333)
val Amaranth_20 = Color(0x33FF3333)
val Amaranth_10 = Color(0x1AFF3333)
// endregion Amaranth
// region Flamingo
val Flamingo = Color(0xFFFF5B5B)
// endregion Red
val Flamingo_50 = Color(0x80FF5B5B)
val Flamingo_20 = Color(0x33FF5B5B)
val Flamingo_10 = Color(0x1AFF5B5B)
// endregion Flamingo
// region Yellow
val Tangerine = Color(0xFFFFB71B)
val Mustard = Color(0xFFFDDE55)
// endregion Yellow
// region Emerald
val Emerald = Color(0xFF34DF12)
val Emerald_50 = Color(0x8034DF12)
val Emerald_20 = Color(0x3334DF12)
val Emerald_10 = Color(0x1A34DF12)
// endregion Emerald
// region Eucalyptus
val Eucalyptus = Color(0xFF0C9F3D)
val Eucalyptus_50 = Color(0x800C9F3D)
val Eucalyptus_20 = Color(0x330C9F3D)
val Eucalyptus_10 = Color(0x1A0C9F3D)
// endregion Eucalyptus
// region Overlay
val Overlay1 = Color(0x66000000)
val Overlay2 = Color(0xB2000000)

View file

@ -448,24 +448,36 @@ class TangemColors2 internal constructor(
@Stable
class Markers internal constructor(
backgroundSolidGray: Color,
backgroundDisabled: Color,
backgroundSolidBlue: Color,
textGray: Color,
textDisabled: Color,
iconGray: Color,
iconDisabled: Color,
backgroundDisabled: Color,
textGray: Color,
iconGray: Color,
borderGray: Color,
backgroundTintedBlue: Color,
backgroundSolidGray: Color,
backgroundTintedGray: Color,
textBlue: Color,
iconBlue: Color,
borderTintedBlue: Color,
backgroundSolidBlue: Color,
backgroundTintedBlue: Color,
textRed: Color,
iconRed: Color,
borderTintedRed: Color,
backgroundSolidRed: Color,
backgroundTintedRed: Color,
iconBlue: Color,
iconRed: Color,
textRed: Color,
backgroundTintedGray: Color,
borderTintedBlue: Color,
borderTintedRed: Color,
textGreen: Color,
iconGreen: Color,
borderTintedGreen: Color,
borderSolidColor: Color,
backgroundTintedGreen: Color,
backgroundSolidGreen: Color,
textGreenAlt: Color,
iconGreenAlt: Color,
borderTintedGreenAlt: Color,
borderSolidColorAlt: Color,
backgroundTintedGreenAlt: Color,
backgroundSolidGreenAlt: Color,
) {
var backgroundSolidGray by mutableStateOf(backgroundSolidGray)
private set
@ -504,6 +516,32 @@ class TangemColors2 internal constructor(
var borderTintedRed by mutableStateOf(borderTintedRed)
private set
var textGreen by mutableStateOf(textGreen)
private set
var iconGreen by mutableStateOf(iconGreen)
private set
var borderTintedGreen by mutableStateOf(borderTintedGreen)
private set
var borderSolidColor by mutableStateOf(borderSolidColor)
private set
var backgroundTintedGreen by mutableStateOf(backgroundTintedGreen)
private set
var backgroundSolidGreen by mutableStateOf(backgroundSolidGreen)
private set
var textGreenAlt by mutableStateOf(textGreenAlt)
private set
var iconGreenAlt by mutableStateOf(iconGreenAlt)
private set
var borderTintedGreenAlt by mutableStateOf(borderTintedGreenAlt)
private set
var borderSolidColorAlt by mutableStateOf(borderSolidColorAlt)
private set
var backgroundTintedGreenAlt by mutableStateOf(backgroundTintedGreenAlt)
private set
var backgroundSolidGreenAlt by mutableStateOf(backgroundSolidGreenAlt)
private set
fun update(other: Markers) {
backgroundSolidGray = other.backgroundSolidGray
backgroundDisabled = other.backgroundDisabled
@ -523,6 +561,18 @@ class TangemColors2 internal constructor(
backgroundTintedGray = other.backgroundTintedGray
borderTintedBlue = other.borderTintedBlue
borderTintedRed = other.borderTintedRed
textGreen = other.textGreen
iconGreen = other.iconGreen
borderTintedGreen = other.borderTintedGreen
borderSolidColor = other.borderSolidColor
backgroundTintedGreen = other.backgroundTintedGreen
backgroundSolidGreen = other.backgroundSolidGreen
textGreenAlt = other.textGreenAlt
iconGreenAlt = other.iconGreenAlt
borderTintedGreenAlt = other.borderTintedGreenAlt
borderSolidColorAlt = other.borderSolidColorAlt
backgroundTintedGreenAlt = other.backgroundTintedGreenAlt
backgroundSolidGreenAlt = other.backgroundSolidGreenAlt
}
}

View file

@ -154,16 +154,28 @@ private fun lightThemeColors2(): TangemColors2 {
iconGray = TangemColorPalette.Dark1,
iconDisabled = TangemColorPalette.Light2,
borderGray = TangemColorPalette.Light3,
backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
backgroundTintedBlue = TangemColorPalette.Azure_10,
textBlue = text.status.accent,
backgroundSolidRed = TangemColorPalette.Amaranth,
backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
backgroundTintedRed = TangemColorPalette.Amaranth_10,
iconBlue = TangemColorPalette.Azure,
iconRed = TangemColorPalette.Amaranth,
textRed = TangemColorPalette.Amaranth,
backgroundTintedGray = TangemColorPalette.Dark6.copy(alpha = 0.1f),
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
borderTintedBlue = TangemColorPalette.Azure_10,
borderTintedRed = TangemColorPalette.Amaranth_10,
textGreen = TangemColorPalette.Emerald,
iconGreen = TangemColorPalette.Emerald,
borderTintedGreen = TangemColorPalette.Emerald_10,
borderSolidColor = TangemColorPalette.Emerald_50,
backgroundTintedGreen = TangemColorPalette.Emerald_10,
backgroundSolidGreen = TangemColorPalette.Emerald,
textGreenAlt = TangemColorPalette.Eucalyptus,
iconGreenAlt = TangemColorPalette.Eucalyptus,
borderTintedGreenAlt = TangemColorPalette.Eucalyptus_10,
borderSolidColorAlt = TangemColorPalette.Eucalyptus_50,
backgroundTintedGreenAlt = TangemColorPalette.Eucalyptus_10,
backgroundSolidGreenAlt = TangemColorPalette.Eucalyptus,
)
val tabs = TangemColors2.Tabs(
textPrimary = TangemColorPalette.Light2,
@ -304,7 +316,7 @@ private fun darkThemeColors2(): TangemColors2 {
iconGray = TangemColorPalette.Dark2,
iconDisabled = TangemColorPalette.Dark5,
borderGray = TangemColorPalette.White.copy(alpha = 0.2f),
backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
backgroundTintedBlue = TangemColorPalette.Azure_10,
textBlue = text.status.accent,
backgroundSolidRed = TangemColorPalette.Amaranth,
backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
@ -312,8 +324,20 @@ private fun darkThemeColors2(): TangemColors2 {
iconRed = TangemColorPalette.Flamingo,
textRed = TangemColorPalette.Flamingo,
backgroundTintedGray = TangemColorPalette.White.copy(alpha = 0.1f),
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
borderTintedBlue = TangemColorPalette.Azure_10,
borderTintedRed = TangemColorPalette.Amaranth_10,
textGreen = TangemColorPalette.Emerald,
iconGreen = TangemColorPalette.Emerald,
borderTintedGreen = TangemColorPalette.Emerald_10,
borderSolidColor = TangemColorPalette.Emerald_50,
backgroundTintedGreen = TangemColorPalette.Emerald_10,
backgroundSolidGreen = TangemColorPalette.Emerald,
textGreenAlt = TangemColorPalette.Emerald,
iconGreenAlt = TangemColorPalette.Emerald,
borderTintedGreenAlt = TangemColorPalette.Emerald_10,
borderSolidColorAlt = TangemColorPalette.Emerald_50,
backgroundTintedGreenAlt = TangemColorPalette.Emerald_10,
backgroundSolidGreenAlt = TangemColorPalette.Emerald,
)
val tabs = TangemColors2.Tabs(
textPrimary = TangemColorPalette.Dark4,

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="8dp"
android:height="8dp"
android:viewportWidth="8"
android:viewportHeight="8">
<path
android:fillColor="#34DF12"
android:pathData="M8,8L0,8L0,0C0,0 2,6 8,8Z" />
</vector>

View file

@ -7,10 +7,7 @@ import com.tangem.domain.card.common.util.getCardsCount
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons
import timber.log.Timber
import java.math.BigDecimal
@ -53,7 +50,17 @@ internal class SetTokenListErrorTransformer(
}
override fun transform(walletUM: WalletUM): WalletUM {
return walletUM // todo redesign main
return when (walletUM) {
is WalletUM.Content -> {
walletUM.copy(
tokensListUM = WalletTokensListUM.Empty,
)
}
is WalletUM.Locked -> {
Timber.w("Impossible to load tokens list for locked wallet")
walletUM
}
}
}
private fun WalletCardState.toLoadedState(): WalletCardState {

View file

@ -5,12 +5,10 @@ import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMTransformer
import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons
import timber.log.Timber
import java.math.BigDecimal
@ -23,6 +21,7 @@ internal class SetTokenListTransformer(
private val yieldSupplyApyMap: Map<String, BigDecimal> = emptyMap(),
private val stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
private val shouldShowMainPromo: Boolean,
private val isAccountsModeEnabled: Boolean,
) : WalletStateTransformer(userWallet.walletId) {
override fun transform(prevState: WalletState): WalletState {
@ -47,7 +46,17 @@ internal class SetTokenListTransformer(
}
override fun transform(walletUM: WalletUM): WalletUM {
return walletUM // todo redesign main
return when (walletUM) {
is WalletUM.Content -> {
walletUM.copy(
tokensListUM = toLoadedState(),
)
}
is WalletUM.Locked -> {
Timber.w("Impossible to load tokens list for locked wallet")
walletUM
}
}
}
private fun WalletCardState.toLoadedState(): WalletCardState {
@ -73,4 +82,19 @@ internal class SetTokenListTransformer(
shouldShowMainPromo = shouldShowMainPromo,
).convert(value = this)
}
private fun toLoadedState(): WalletTokensListUM {
if (params !is TokenConverterParams.Account) return WalletTokensListUM.Empty
return WalletTokensListUMTransformer(
selectedWallet = userWallet,
appCurrency = appCurrency,
clickIntents = clickIntents,
yieldModuleApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
isAccountsModeEnabled = isAccountsModeEnabled,
expandedAccounts = params.expandedAccounts,
).convert(value = params.accountList)
}
}

View file

@ -0,0 +1,139 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.common.ui.R
import com.tangem.common.ui.tokens.TokenItemStateConverter
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.yieldSupplyKey
import com.tangem.domain.models.staking.StakingBalance
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingOption
import com.tangem.domain.staking.model.common.RewardInfo
import com.tangem.domain.staking.model.common.RewardType
import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.EarnApyConverter.EarnApyInfo
import com.tangem.lib.crypto.BlockchainUtils
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
internal class EarnApyConverter(
val yieldModuleApyMap: Map<String, BigDecimal>,
val stakingApyMap: Map<CryptoCurrency, StakingAvailability>,
) : Converter<CryptoCurrencyStatus, EarnApyInfo?> {
override fun convert(value: CryptoCurrencyStatus): EarnApyInfo? {
val token = value.currency as? CryptoCurrency.Token
if (token != null && yieldModuleApyMap.isNotEmpty()) {
val yieldSupplyApy = yieldModuleApyMap.entries.firstOrNull { apy ->
apy.key.equals(
other = token.yieldSupplyKey(),
ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId),
)
}?.value
if (yieldSupplyApy != null) {
val isActive = value.value.yieldSupplyStatus?.isActive == false
return EarnApyInfo(
text = resourceReference(
R.string.yield_module_earn_badge,
wrappedList(yieldSupplyApy),
),
isActive = isActive,
apy = yieldSupplyApy.toString(),
source = TokenItemStateConverter.ApySource.YIELD_SUPPLY,
)
}
}
if (stakingApyMap.isNotEmpty()) {
val stakingInfo = findStakingRate(
currencyStatus = value,
stakingApyMap = stakingApyMap,
)
val rewardTypeRes = when (stakingInfo.rewardType) {
RewardType.APR -> R.string.staking_apr_earn_badge
RewardType.UNKNOWN,
RewardType.APY,
null,
-> R.string.yield_module_earn_badge
}
if (stakingInfo.rate != null) {
val apyString = stakingInfo.rate.format { percent(withPercentSign = false) }
return EarnApyInfo(
text = resourceReference(
rewardTypeRes,
wrappedList(apyString),
),
isActive = stakingInfo.isActive,
apy = apyString,
source = TokenItemStateConverter.ApySource.STAKING,
)
}
}
return null
}
private fun findStakingRate(
currencyStatus: CryptoCurrencyStatus,
stakingApyMap: Map<CryptoCurrency, StakingAvailability>,
): StakingLocalInfo {
val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available
?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null)
val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data
val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit
val p2pEthPoolBalance = stakingBalance as? StakingBalance.Data.P2PEthPool
val rateInfo = when (val stakingOptions = stakingAvailability.option) {
is StakingOption.P2PEthPool -> {
RewardInfo(
rate = stakingOptions.apy,
type = RewardType.APY,
)
}
is StakingOption.StakeKit -> if (stakeKitBalance != null) {
val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address }
stakeKitBalance.balance.items
.mapNotNull { it.validatorAddress }
.firstNotNullOfOrNull { address ->
validatorsByAddress[address]?.rewardInfo
} ?: stakingOptions.yield.validators
.filter { it.preferred }
.mapNotNull { validator ->
validator.rewardInfo
}
.maxByOrNull { it.rate }
} else {
stakingOptions.yield.validators
.filter { it.preferred }
.mapNotNull { validator ->
validator.rewardInfo
}
.maxByOrNull { it.rate }
}
}
return StakingLocalInfo(
rate = rateInfo?.rate,
isActive = stakeKitBalance != null || p2pEthPoolBalance != null,
rewardType = rateInfo?.type,
)
}
data class StakingLocalInfo(
val rate: BigDecimal?,
val isActive: Boolean,
val rewardType: RewardType?,
)
data class EarnApyInfo(
val text: TextReference?,
val isActive: Boolean,
val apy: String?,
val source: TokenItemStateConverter.ApySource,
)
}

View file

@ -0,0 +1,521 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import androidx.compose.ui.text.SpanStyle
import com.tangem.common.getTotalCryptoAmount
import com.tangem.common.getTotalFiatAmount
import com.tangem.common.ui.account.AccountIconItemStateConverter
import com.tangem.common.ui.account.toUM
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.ds.badge.*
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM.EndContentUM
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.models.StatusSource
import com.tangem.domain.models.TotalFiatBalance
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.tokenlist.TokenList
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.TokensListItemUM2
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.addIf
import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
@Suppress("LargeClass", "LongParameterList")
internal class WalletTokensListUMTransformer(
private val appCurrency: AppCurrency,
private val selectedWallet: UserWallet,
private val clickIntents: WalletClickIntents,
private val yieldModuleApyMap: Map<String, BigDecimal>,
private val isAccountsModeEnabled: Boolean,
private val expandedAccounts: Set<AccountId>,
stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability>,
shouldShowMainPromo: Boolean,
) : Converter<AccountStatusList, WalletTokensListUM> {
private val yieldSupplyPromoBannerConverter = YieldSupplyPromoBannerConverter(
yieldModuleApyMap,
shouldShowMainPromo,
)
private val currencyToIconStateConverter = CryptoCurrencyToIconStateConverter()
private val earnApyConverter = EarnApyConverter(
yieldModuleApyMap = yieldModuleApyMap,
stakingApyMap = stakingAvailabilityMap,
)
override fun convert(value: AccountStatusList): WalletTokensListUM {
val promoCryptoCurrency = yieldSupplyPromoBannerConverter.convert2(value = value)
return if (value.accountStatuses.isEmpty()) {
WalletTokensListUM.Empty
} else {
val isCollapsable = value.accountStatuses.count {
it is AccountStatus.CryptoPortfolio && it.account.tokensCount > 0
} > 1
val tokenListUM = value.accountStatuses
.filterIsInstance<AccountStatus.CryptoPortfolio>()
.asSequence()
.flatMap { accountStatus ->
if (isAccountsModeEnabled) {
val isExpanded = expandedAccounts.contains(accountStatus.account.accountId)
sequenceOf(
TokensListItemUM2.Portfolio(
tokenRowUM = toAccountRow(accountStatus, isExpanded),
isExpanded = isExpanded || !isCollapsable,
isCollapsable = isCollapsable,
tokenList = getTokenListItems(
accountStatus.tokenList,
promoCryptoCurrency,
).toPersistentList(),
),
)
} else {
getTokenListItems(accountStatus.tokenList, promoCryptoCurrency)
}
}.toPersistentList()
WalletTokensListUM.Content(
tokenList = tokenListUM,
organizeButtonUM = getOrganizeButtonUM(value),
)
}
}
private fun getTokenListItems(
tokenList: TokenList,
promoCryptoCurrency: CryptoCurrencyStatus?,
): Sequence<TokensListItemUM2> {
return when (tokenList) {
TokenList.Empty -> emptySequence()
is TokenList.GroupedByNetwork -> {
tokenList.groups.asSequence().flatMap { (network, currencies) ->
buildList {
add(
TokensListItemUM2.GroupTitle(
tokenRowUM = toGroupRow(network),
),
)
addAll(
currencies.asSequence().map { currencyStatus ->
val shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id
TokensListItemUM2.Token(
tokenRowUM = toCurrencyRow(
currencyStatus = currencyStatus,
shouldShowPromo = shouldShowPromo,
),
)
}.toList(),
)
}
}
}
is TokenList.Ungrouped -> {
tokenList.currencies.asSequence().map { currencyStatus ->
TokensListItemUM2.Token(
toCurrencyRow(
currencyStatus = currencyStatus,
shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id,
),
)
}
}
}
}
private fun toAccountRow(accountStatus: AccountStatus.CryptoPortfolio, isExpanded: Boolean): TangemTokenRowUM {
val account = accountStatus.account
val (topEndContent, bottomEndContent) = when (val accountBalance = accountStatus.tokenList.totalFiatBalance) {
TotalFiatBalance.Failed -> toFailedAccountRow()
is TotalFiatBalance.Loaded -> toLoadedAccountRow(accountStatus, accountBalance)
TotalFiatBalance.Loading -> EndContentUM.Loading to EndContentUM.Loading
}
return TangemTokenRowUM.Content(
id = accountStatus.account.accountId.value,
headIconUM = TangemIconUM.Currency(
currencyIconState = AccountIconItemStateConverter(size = AccountIconSize.ExtraSmall).convert(account),
),
titleUM = TangemTokenRowUM.TitleUM.Content(
text = account.accountName.toUM().value,
),
subtitleUM = TangemTokenRowUM.SubtitleUM.Content(
text = pluralReference(
R.plurals.common_tokens_count,
count = account.tokensCount,
formatArgs = wrappedList(account.tokensCount),
),
),
topEndContentUM = topEndContent,
bottomEndContentUM = bottomEndContent,
onItemClick = {
if (isExpanded) {
clickIntents.onAccountCollapseClick(account)
} else {
clickIntents.onAccountExpandClick(account)
}
},
onItemLongClick = null,
)
}
private fun toFailedAccountRow(): Pair<EndContentUM.Content, EndContentUM.Content> {
return EndContentUM.Content(
text = stringReference(StringsSigns.DASH_SIGN),
) to EndContentUM.Content(
text = styledResourceReference(
id = R.string.common_unreachable,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) },
),
endIcons = persistentListOf(
TangemIconUM.Icon(
iconRes = R.drawable.ic_attention_default_24,
tintReference = { TangemTheme.colors2.graphic.status.attention },
),
),
)
}
private fun toLoadedAccountRow(
accountStatus: AccountStatus.CryptoPortfolio,
accountBalance: TotalFiatBalance.Loaded,
): Pair<EndContentUM.Content, EndContentUM> {
val priceChange = accountStatus.priceChangeLce.getOrNull()
return EndContentUM.Content(
text = accountBalance.amount.formatStyled {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
)
},
) to if (priceChange != null) {
val priceChangeType = PriceChangeType.fromBigDecimal(priceChange.value)
EndContentUM.Content(
text = stringReference(
priceChange.value.format {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
},
),
priceChangeUM = PriceChangeState.Content(
type = priceChangeType,
valueInPercent = priceChange.value.format { percent() },
),
)
} else {
EndContentUM.Empty
}
}
private fun toGroupRow(network: Network): TangemHeaderRowUM {
return TangemHeaderRowUM(
id = network.hashCode().toString(),
title = resourceReference(
id = R.string.wallet_network_group_title,
formatArgs = wrappedList(network.name),
),
)
}
private fun toCurrencyRow(currencyStatus: CryptoCurrencyStatus, shouldShowPromo: Boolean): TangemTokenRowUM {
val earnApyInfo = earnApyConverter.convert(currencyStatus)
return TangemTokenRowUM.Content(
id = currencyStatus.currency.id.value,
headIconUM = TangemIconUM.Currency(
currencyIconState = currencyToIconStateConverter.convert(currencyStatus),
),
titleUM = toCurrencyRowTitle(currencyStatus, earnApyInfo),
subtitleUM = toCurrencyRowSubtitle(currencyStatus),
topEndContentUM = toCurrencyRowTopEnd(currencyStatus),
bottomEndContentUM = toCurrencyRowBottomEnd(currencyStatus),
promoBannerUM = toPromoBannerUM(
currencyStatus,
earnApyInfo.takeIf { shouldShowPromo },
),
onItemClick = when (currencyStatus.value) {
CryptoCurrencyStatus.Loading,
is CryptoCurrencyStatus.MissedDerivation,
-> null
else -> {
{
clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus)
}
}
},
onItemLongClick = when (currencyStatus.value) {
CryptoCurrencyStatus.Loading -> null
else -> {
{
clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus)
}
}
},
)
}
private fun toCurrencyRowTitle(
currencyStatus: CryptoCurrencyStatus,
earnApyInfo: EarnApyConverter.EarnApyInfo?,
): TangemTokenRowUM.TitleUM = when (val value = currencyStatus.value) {
is CryptoCurrencyStatus.Loading,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> {
TangemTokenRowUM.TitleUM.Content(
text = stringReference(currencyStatus.currency.name),
)
}
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> {
TangemTokenRowUM.TitleUM.Content(
text = stringReference(currencyStatus.currency.name),
hasPending = value.hasCurrentNetworkTransactions,
badge = if (earnApyInfo != null && earnApyInfo.text != null) {
TangemBadgeUM(
type = TangemBadgeType.Solid,
color = when {
earnApyInfo.isActive -> TangemBadgeColor.Blue
else -> TangemBadgeColor.Gray
},
shape = TangemBadgeShape.Rounded,
size = TangemBadgeSize.X4,
text = earnApyInfo.text,
onClick = if (earnApyInfo.apy != null) {
{
clickIntents.onApyLabelClick(
userWalletId = selectedWallet.walletId,
currencyStatus = currencyStatus,
apySource = earnApyInfo.source,
apy = earnApyInfo.apy,
)
}
} else {
null
},
)
} else {
null
},
)
}
}
private fun toCurrencyRowSubtitle(currencyStatus: CryptoCurrencyStatus): TangemTokenRowUM.SubtitleUM {
return when (currencyStatus.value) {
is CryptoCurrencyStatus.Loading -> TangemTokenRowUM.SubtitleUM.Loading
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> TangemTokenRowUM.SubtitleUM.Content(
text = stringReference(
currencyStatus.value.fiatRate.format {
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
},
),
priceChangeUM = PriceChangeState.Content(
type = PriceChangeType.fromBigDecimal(currencyStatus.value.priceChange.orZero()),
valueInPercent = currencyStatus.value.priceChange.format { percent() },
),
isFlickering = currencyStatus.value.isFlickering(),
)
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> TangemTokenRowUM.SubtitleUM.Empty
}
}
private fun toCurrencyRowTopEnd(currencyStatus: CryptoCurrencyStatus): EndContentUM {
val yieldSupply = currencyStatus.value.yieldSupplyStatus
return when (currencyStatus.value) {
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> {
EndContentUM.Content(
text = currencyStatus.getTotalFiatAmount().formatStyled {
fiat(
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) },
)
},
isFlickering = currencyStatus.value.isFlickering(),
startIcons = buildList {
addIf(
element = TangemIconUM.Icon(
iconRes = R.drawable.ic_attention_default_24,
tintReference = { TangemTheme.colors2.graphic.status.attention },
),
condition = yieldSupply?.isActive == true && !yieldSupply.isAllowedToSpend,
)
addIf(
element = TangemIconUM.Icon(
iconRes = R.drawable.ic_error_sync_default_24,
tintReference = { TangemTheme.colors2.graphic.neutral.tertiary },
),
condition = currencyStatus.value.sources.total == StatusSource.ONLY_CACHE,
)
}.toImmutableList(),
)
}
is CryptoCurrencyStatus.Loading -> EndContentUM.Loading
is CryptoCurrencyStatus.MissedDerivation -> EndContentUM.Content(
text = stringReference(StringsSigns.DASH_SIGN),
)
is CryptoCurrencyStatus.Unreachable -> EndContentUM.Content(
text = styledResourceReference(
id = R.string.common_unreachable,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) },
),
endIcons = persistentListOf(
TangemIconUM.Icon(
iconRes = R.drawable.ic_attention_default_24,
tintReference = { TangemTheme.colors2.graphic.status.attention },
),
),
)
is CryptoCurrencyStatus.NoAmount,
-> EndContentUM.Empty
}
}
private fun toCurrencyRowBottomEnd(currencyStatus: CryptoCurrencyStatus): EndContentUM {
return when (currencyStatus.value) {
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.NoQuote,
is CryptoCurrencyStatus.NoAccount,
-> EndContentUM.Content(
text = stringReference(
currencyStatus.getTotalCryptoAmount().format {
crypto(cryptoCurrency = currencyStatus.currency)
},
),
isFlickering = currencyStatus.value.isFlickering(),
)
is CryptoCurrencyStatus.Loading -> EndContentUM.Loading
is CryptoCurrencyStatus.MissedDerivation -> EndContentUM.Content(
text = styledResourceReference(
id = R.string.common_no_address,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) },
),
endIcons = persistentListOf(
TangemIconUM.Icon(
iconRes = R.drawable.ic_attention_default_24,
tintReference = { TangemTheme.colors2.graphic.status.attention },
),
),
)
is CryptoCurrencyStatus.Unreachable -> EndContentUM.Content(
text = styledResourceReference(
id = R.string.common_unreachable,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) },
),
endIcons = persistentListOf(
TangemIconUM.Icon(
iconRes = R.drawable.ic_attention_default_24,
tintReference = { TangemTheme.colors2.graphic.status.attention },
),
),
)
is CryptoCurrencyStatus.NoAmount,
-> EndContentUM.Empty
}
}
private fun toPromoBannerUM(
currencyStatus: CryptoCurrencyStatus,
earnApyInfo: EarnApyConverter.EarnApyInfo?,
): TangemTokenRowUM.PromoBannerUM {
val currency = currencyStatus.currency
val isTokenCurrency = currency is CryptoCurrency.Token
val isCurrencyStatusLoaded = currencyStatus.value is CryptoCurrencyStatus.Loaded
val isApyInfoNotNull = earnApyInfo != null && earnApyInfo.apy != null
if (!isTokenCurrency || !isCurrencyStatusLoaded || !isApyInfoNotNull) {
return TangemTokenRowUM.PromoBannerUM.Empty
}
return TangemTokenRowUM.PromoBannerUM.Content(
title = resourceReference(
R.string.yield_module_main_screen_promo_banner_message,
wrappedList(earnApyInfo.apy),
),
onPromoBannerClick = {
clickIntents.onYieldPromoClicked(currency)
clickIntents.onApyLabelClick(
userWalletId = selectedWallet.walletId,
currencyStatus = currencyStatus,
apySource = earnApyInfo.source,
apy = earnApyInfo.apy,
)
},
onCloseClick = clickIntents::onYieldPromoCloseClick,
onPromoShown = {
clickIntents.onYieldPromoShown(currency)
},
)
}
private fun getOrganizeButtonUM(accountList: AccountStatusList): TangemButtonUM? {
return if (accountList.flattenCurrencies().size > 1 && !isSingleCurrencyWalletWithToken()) {
TangemButtonUM(
text = resourceReference(R.string.organize_tokens_title),
isEnabled = accountList.totalFiatBalance !is TotalFiatBalance.Loading,
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
type = TangemButtonType.PrimaryInverse,
iconRes = R.drawable.ic_filter_default_24,
onClick = clickIntents::onOrganizeTokensClick,
)
} else {
null
}
}
private fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = sources.total == StatusSource.CACHE
private fun isSingleCurrencyWalletWithToken(): Boolean {
return selectedWallet is UserWallet.Cold &&
selectedWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken()
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.currency.yieldSupplyKey
@ -28,6 +29,35 @@ internal class YieldSupplyPromoBannerConverter(
if (cryptoCurrencyStatuses.any { it.value.yieldSupplyStatus?.isActive == true }) return null
if (yieldModuleApyMap.isEmpty()) return null
val max = cryptoCurrencyStatuses.asSequence()
.mapNotNull { status ->
val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null
val shouldIgnoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId)
val tokenKey = "${token.network.rawId}_${token.contractAddress}"
val matchedKey = yieldModuleApyMap.keys.firstOrNull { mapKey ->
mapKey.equals(tokenKey, shouldIgnoreCase)
} ?: return@mapNotNull null
status to matchedKey
}
.maxByOrNull { (status, _) -> status.value.amount ?: BigDecimal.ZERO }
return max?.first
}
fun convert2(value: AccountStatusList): CryptoCurrencyStatus? {
if (!shouldShowMainPromo) return null
val currencies = value.flattenCurrencies().filter { status ->
status.value is CryptoCurrencyStatus.Loaded
}
val cryptoCurrencyStatuses = currencies.filter { it.currency is CryptoCurrency.Token }
if (cryptoCurrencyStatuses.any { it.value.yieldSupplyStatus?.isActive == true }) return null
if (yieldModuleApyMap.isEmpty()) return null
val max = cryptoCurrencyStatuses.asSequence()
.mapNotNull { status ->
val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null

View file

@ -1,5 +1,6 @@
package com.tangem.feature.wallet.presentation.wallet.subscribers
import com.tangem.core.ui.DesignFeatureToggles
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -37,6 +38,7 @@ internal class AccountListSubscriber @AssistedInject constructor(
private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase,
private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase,
private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase,
private val designFeatureToggles: DesignFeatureToggles,
) : BasicAccountListSubscriber() {
override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7(
@ -51,15 +53,27 @@ internal class AccountListSubscriber @AssistedInject constructor(
accountList, appCurrency, expandedAccounts, isAccountMode,
yieldSupplyApyMap, shouldShowMainPromo, stakingAvailabilityMap,
->
updateState(
accountList = accountList,
appCurrency = appCurrency,
expandedAccounts = expandedAccounts,
isAccountMode = isAccountMode,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
)
if (designFeatureToggles.isRedesignEnabled) {
updateState2(
accountList = accountList,
appCurrency = appCurrency,
expandedAccounts = expandedAccounts,
isAccountMode = isAccountMode,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
)
} else {
updateState(
accountList = accountList,
appCurrency = appCurrency,
expandedAccounts = expandedAccounts,
isAccountMode = isAccountMode,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
)
}
}
private fun stakingAvailabilityFlow(): Flow<Map<CryptoCurrency, StakingAvailability>> = getAccountStatusListFlow()

View file

@ -85,6 +85,29 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
}
}
protected fun updateState2(
accountList: AccountStatusList,
appCurrency: AppCurrency,
expandedAccounts: Set<AccountId>,
isAccountMode: Boolean,
yieldSupplyApyMap: Map<String, BigDecimal> = emptyMap(),
stakingAvailabilityMap: Map<CryptoCurrency, StakingAvailability> = emptyMap(),
shouldShowMainPromo: Boolean = false,
) {
stateController.update(
SetTokenListTransformer(
params = TokenConverterParams.Account(accountList, expandedAccounts),
userWallet = userWallet,
appCurrency = appCurrency,
clickIntents = clickIntents,
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
isAccountsModeEnabled = isAccountMode,
),
)
}
private fun singleAccountTransform(
maybeTokenList: Lce<TokenListError, TokenList>,
appCurrency: AppCurrency,
@ -141,6 +164,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() {
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
isAccountsModeEnabled = false,
),
)
}

View file

@ -143,6 +143,7 @@ internal abstract class BasicTokenListSubscriber(
yieldSupplyApyMap = yieldSupplyApyMap,
stakingAvailabilityMap = stakingAvailabilityMap,
shouldShowMainPromo = shouldShowMainPromo,
isAccountsModeEnabled = false,
),
)
}

View file

@ -163,7 +163,7 @@ private fun LazyListScope.portfolioItem(
@Suppress("MagicNumber")
@Composable
private fun SlideInItemVisibility(
internal fun SlideInItemVisibility(
visible: Boolean,
currentIndex: Int,
lastIndex: Int,

View file

@ -1,5 +1,9 @@
package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency
import androidx.compose.animation.*
import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@ -8,22 +12,37 @@ import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.lerp
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.account.AccountIconSize
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.tokenlist.TokenListItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.row.header.TangemHeaderRow
import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM
import com.tangem.core.ui.ds.row.token.TangemTokenRow
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.ds.row.token.internal.TokenRowTitle
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.MainScreenTestTags
import com.tangem.core.ui.utils.ProvideSharedTransitionScope
import com.tangem.core.ui.utils.lazyListItemPosition
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.TokensListItemUM2
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM
import kotlinx.collections.immutable.ImmutableList
internal const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST"
@ -59,6 +78,180 @@ internal fun LazyListScope.tokensListItems(
}
}
/**
* LazyList extension for [WalletTokensListState]
*
* @param walletTokensListUM state
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
internal fun LazyListScope.tokensListItems2(
walletTokensListUM: WalletTokensListUM,
modifier: Modifier = Modifier,
isBalanceHidden: Boolean,
) {
when (walletTokensListUM) {
is WalletTokensListUM.Loading,
is WalletTokensListUM.Content,
-> {
walletTokensListUM.tokenList.fastForEachIndexed { index, listItem ->
when (listItem) {
is TokensListItemUM2.GroupTitle,
is TokensListItemUM2.Token,
-> tokenItem(
listItem = listItem,
index = index,
lastIndex = walletTokensListUM.tokenList.lastIndex,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
)
is TokensListItemUM2.Portfolio -> portfolioItem(
listItem = listItem,
index = index,
isBalanceHidden = isBalanceHidden,
modifier = modifier,
)
}
}
}
WalletTokensListUM.Empty -> nonContentItem(modifier = modifier)
}
}
private fun LazyListScope.tokenItem(
listItem: TokensListItemUM2,
index: Int,
lastIndex: Int,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
item(
key = listItem.tokenRowUM.id,
contentType = listItem.tokenRowUM::class.java,
) {
val itemModifier = modifier
.testTag(MainScreenTestTags.TOKEN_LIST_ITEM)
.semantics { lazyListItemPosition = index }
.padding(top = if (index == 0) TangemTheme.dimens2.x3 else 0.dp)
.roundedShapeItemDecoration(
radius = 18.dp,
currentIndex = index,
addDefaultPadding = false,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors2.surface.level3,
)
when (val tokenRowUM = listItem.tokenRowUM) {
is TangemTokenRowUM -> TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = isBalanceHidden,
reorderableTokenListState = null,
modifier = itemModifier,
)
is TangemHeaderRowUM -> TangemHeaderRow(
headerRowUM = tokenRowUM,
modifier = itemModifier,
)
}
}
}
private fun LazyListScope.portfolioItem(
listItem: TokensListItemUM2.Portfolio,
index: Int,
isBalanceHidden: Boolean,
modifier: Modifier,
) {
val lastIndex = listItem.tokenList.lastIndex + 1
accountItem(
listItem = listItem,
modifier = modifier,
index = index,
lastIndex = lastIndex,
isBalanceHidden = isBalanceHidden,
)
itemsIndexed(
items = listItem.tokenList,
key = { _, item -> item.tokenRowUM.id },
contentType = { _, item -> item::class.java },
itemContent = { tokenIndex, item ->
SlideInItemVisibility(
currentIndex = tokenIndex + 1,
lastIndex = lastIndex,
modifier = modifier
.animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null)
.roundedShapeItemDecoration(
radius = 18.dp,
currentIndex = tokenIndex + 1,
addDefaultPadding = false,
lastIndex = lastIndex,
backgroundColor = TangemTheme.colors2.surface.level3,
),
visible = listItem.isExpanded,
) {
val itemModifier = Modifier
.testTag(MainScreenTestTags.TOKEN_LIST_ITEM)
.semantics { lazyListItemPosition = tokenIndex + 1 }
when (val tokenRowUM = item.tokenRowUM) {
is TangemTokenRowUM -> TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = isBalanceHidden,
reorderableTokenListState = null,
modifier = itemModifier,
)
is TangemHeaderRowUM -> TangemHeaderRow(
headerRowUM = tokenRowUM,
modifier = itemModifier,
)
}
}
},
)
}
private fun LazyListScope.accountItem(
listItem: TokensListItemUM2.Portfolio,
modifier: Modifier,
index: Int,
lastIndex: Int,
isBalanceHidden: Boolean,
) {
item(
key = listItem.tokenRowUM.id,
contentType = listItem.tokenRowUM::class.java,
) {
val portfolioModifier = modifier
.padding(top = if (index != 0) TangemTheme.dimens2.x2 else TangemTheme.dimens2.x3)
.testTag(MainScreenTestTags.TOKEN_LIST_ITEM)
.semantics { lazyListItemPosition = index }
.roundedShapeItemDecoration(
currentIndex = 0,
radius = 18.dp,
addDefaultPadding = false,
lastIndex = if (listItem.isExpanded) lastIndex else 0,
backgroundColor = TangemTheme.colors2.surface.level3,
)
if (listItem.isCollapsable) {
PortfolioRowItem(
item = listItem,
isBalanceHidden = isBalanceHidden,
modifier = portfolioModifier,
)
} else {
TangemHeaderRow(
title = (listItem.tokenRowUM.titleUM as? TangemTokenRowUM.TitleUM.Content)?.text.orEmpty(),
subtitle = (listItem.tokenRowUM.topEndContentUM as? TangemTokenRowUM.EndContentUM.Content)
?.text?.orMaskWithStars(isBalanceHidden),
headTangemIconUM = listItem.tokenRowUM.headIconUM,
modifier = portfolioModifier,
)
}
}
}
private fun LazyListScope.contentItems(
items: ImmutableList<TokensListItemUM>,
modifier: Modifier = Modifier,
@ -77,6 +270,7 @@ private fun LazyListScope.contentItems(
currentIndex = index,
lastIndex = items.lastIndex,
backgroundColor = TangemTheme.colors.background.primary,
radius = 18.dp,
)
.testTag(MainScreenTestTags.TOKEN_LIST_ITEM)
.semantics { lazyListItemPosition = index },
@ -85,6 +279,117 @@ private fun LazyListScope.contentItems(
)
}
@Suppress("MagicNumber", "ReusedModifierInstance", "LongMethod")
@Composable
internal fun PortfolioRowItem(
item: TokensListItemUM2.Portfolio,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
// TangemSharedTransitionLayout {
ProvideSharedTransitionScope(modifier) {
val iconSharedContentState = rememberSharedContentState(key = "icon")
val titleSharedContentState = rememberSharedContentState(key = "title")
val boundsTransform = BoundsTransform { _, _ -> tween(250) }
AnimatedContent(
item.isExpanded,
transitionSpec = {
fadeIn(animationSpec = tween(350, delayMillis = 90))
.togetherWith(fadeOut(animationSpec = tween(350)))
},
) { isExpandedWrapped ->
val animatedContentScope = this
val composables = remember {
SharedTokenRowComposables(
icon = { modifier ->
val size = if (isExpandedWrapped) AccountIconSize.ExtraSmall else AccountIconSize.Default
val currencyIconState =
when (val currencyIconState = item.tokenRowUM.headIconUM.currencyIconState) {
is CurrencyIconState.CryptoPortfolio.Icon ->
currencyIconState.copy(size = size)
is CurrencyIconState.CryptoPortfolio.Letter ->
currencyIconState.copy(size = size)
else -> currencyIconState
}
TangemIcon(
tangemIconUM = item.tokenRowUM.headIconUM.copy(currencyIconState = currencyIconState),
modifier = modifier.sharedBounds(
sharedContentState = iconSharedContentState,
animatedVisibilityScope = animatedContentScope,
boundsTransform = boundsTransform,
),
)
},
title = { modifier ->
val targetAnimationFraction = if (isExpandedWrapped) 0f else 1f
val animationFraction = animateFloatAsState(
targetValue = targetAnimationFraction,
animationSpec = tween(durationMillis = 350),
)
val startStyle = TangemTheme.typography2.captionSemibold12
val stopStyle = TangemTheme.typography2.bodySemibold16
val textStyle by remember(animationFraction.value) {
derivedStateOf { lerp(startStyle, stopStyle, animationFraction.value) }
}
val resizedTitle = when (val titleUM = item.tokenRowUM.titleUM) {
is TangemTokenRowUM.TitleUM.Content -> titleUM.copy(
text = styledStringReference(
titleUM.text.resolveReference(),
{ textStyle.toSpanStyle() },
),
)
else -> titleUM
}
TokenRowTitle(
titleUM = resizedTitle,
modifier = modifier.sharedBounds(
sharedContentState = titleSharedContentState,
animatedVisibilityScope = animatedContentScope,
boundsTransform = boundsTransform,
resizeMode = scaleToBounds(ContentScale.Fit, Alignment.CenterStart),
),
)
},
)
}
if (isExpandedWrapped) {
TangemHeaderRow(
subtitle = (item.tokenRowUM.topEndContentUM as? TangemTokenRowUM.EndContentUM.Content)
?.text?.orMaskWithStars(isBalanceHidden),
titleContent = composables.title,
headContent = composables.icon,
footerTangemIconRes = R.drawable.ic_minimize_24,
onItemClick = item.tokenRowUM.onItemClick,
)
} else {
TangemTokenRow(
tokenRowUM = item.tokenRowUM,
headComponent = composables.icon,
titleComponent = composables.title,
isBalanceHidden = isBalanceHidden,
reorderableTokenListState = null,
)
}
}
// }
}
}
@Stable
class SharedTokenRowComposables(
val title: @Composable (Modifier) -> Unit,
val icon: @Composable (Modifier) -> Unit,
)
private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) {
item(
key = NON_CONTENT_TOKENS_LIST_KEY,