diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt index 7f9f95b4d3..aba734064b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt @@ -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 = persistentListOf(), + val startIcons: ImmutableList = persistentListOf(), + val endIcons: ImmutableList = persistentListOf(), val priceChangeUM: PriceChangeState = PriceChangeState.Unknown, ) : EndContentUM() diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt index ce68a8771d..a18f605cca 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt @@ -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), diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt deleted file mode 100644 index 376e670bda..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt +++ /dev/null @@ -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 { - override val values: Sequence - get() = sequenceOf( - TangemTokenRowPreviewData.bottomEndContentUM, - ) -} -// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt similarity index 64% rename from core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt rename to core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt index 131497424f..934b33aa8c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt @@ -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 { override val values: Sequence get() = sequenceOf( - TangemTokenRowPreviewData.topEndContentUM, + TangemTokenRowPreviewData.bottomEndContentUM, ) } // endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt index 93845dea18..6ab252c5b8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt @@ -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, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt index 150d6586d3..0906b9626d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt @@ -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) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt index c3d2a23e2a..b9b0c54c51 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt @@ -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 } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt index b838ad7601..93a3647d65 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt @@ -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, diff --git a/core/ui/src/main/res/drawable/shape_triangular.xml b/core/ui/src/main/res/drawable/shape_triangular.xml new file mode 100644 index 0000000000..c4baf11fb9 --- /dev/null +++ b/core/ui/src/main/res/drawable/shape_triangular.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index 9ecb89cabb..6efe15284e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -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 { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 27a1171ace..d50fe9e0bb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -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 = emptyMap(), private val stakingAvailabilityMap: Map = 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) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt new file mode 100644 index 0000000000..25f9f9df7b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt @@ -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, + val stakingApyMap: Map, +) : Converter { + + 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, + ): 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, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt new file mode 100644 index 0000000000..652591e155 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt @@ -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, + private val isAccountsModeEnabled: Boolean, + private val expandedAccounts: Set, + stakingAvailabilityMap: Map, + shouldShowMainPromo: Boolean, +) : Converter { + + 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() + .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 { + 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 { + 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 { + 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() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt index 26c9df46a3..a94d6b5b78 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt @@ -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 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt index cf83e0d256..2d7ba38899 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt @@ -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> = getAccountStatusListFlow() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index f4b38a9290..5c944d4473 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -85,6 +85,29 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { } } + protected fun updateState2( + accountList: AccountStatusList, + appCurrency: AppCurrency, + expandedAccounts: Set, + isAccountMode: Boolean, + yieldSupplyApyMap: Map = emptyMap(), + stakingAvailabilityMap: Map = 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, appCurrency: AppCurrency, @@ -141,6 +164,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { yieldSupplyApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, + isAccountsModeEnabled = false, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index 3fbfb3592d..929f5e07ad 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -143,6 +143,7 @@ internal abstract class BasicTokenListSubscriber( yieldSupplyApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, + isAccountsModeEnabled = false, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt index 1ddf7dd2cf..29616c5969 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt @@ -163,7 +163,7 @@ private fun LazyListScope.portfolioItem( @Suppress("MagicNumber") @Composable -private fun SlideInItemVisibility( +internal fun SlideInItemVisibility( visible: Boolean, currentIndex: Int, lastIndex: Int, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 7d1be99d66..c3755c12bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -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, 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,