From 7c3b952ee842aa8a5c403cf611485ceff758d1f4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 3 Jul 2026 15:33:29 +0500 Subject: [PATCH] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 4 + .../ds2/messagebanner/TangemMessageBanner.kt | 85 +++++++++++++++++-- .../foryou/impl/components/DonutChart.kt | 37 ++++---- .../foryou/impl/components/MarketChart.kt | 57 ++++--------- .../impl/components/state/DonutSegmentUM.kt | 29 ++++++- .../impl/components/state/MarketChartUM.kt | 8 +- .../foryou/impl/model/ForYouNotification.kt | 26 ++++++ 7 files changed, 170 insertions(+), 76 deletions(-) create mode 100644 features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/model/ForYouNotification.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 5f233f9e05..022e5924b1 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -287,6 +287,10 @@ Approve Approved Approving + + %d asset + %d assets + Attention Available networks Backup diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds2/messagebanner/TangemMessageBanner.kt b/core/ui/src/main/java/com/tangem/core/ui/ds2/messagebanner/TangemMessageBanner.kt index b186ea78cd..7cf3172777 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds2/messagebanner/TangemMessageBanner.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds2/messagebanner/TangemMessageBanner.kt @@ -3,14 +3,7 @@ package com.tangem.core.ui.ds2.messagebanner import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -29,6 +22,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.image.TangemIcon import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds2.button.TangemButton import com.tangem.core.ui.ds2.glowring.TangemGlowRing @@ -143,6 +137,49 @@ fun TangemMessageBanner( } } +/** + * Design-system v2 (DS3) **Message Banner** — title/description header with optional slots and an + * action-button row. + * + * [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5475-7680&m=dev) + * + * @param state component state model + * @param modifier component Modifier + * @param extraBottomSlot Slot under the description, inside the text column. + */ +@Suppress("LongParameterList") +@Composable +fun TangemMessageBanner( + state: TangemMessageBanner.State, + modifier: Modifier = Modifier, + extraBottomSlot: (@Composable ColumnScope.() -> Unit)? = null, +) { + TangemMessageBanner( + modifier = modifier, + variant = state.variant, + showGlowRing = state.isShowGlowRing, + secondaryButton = state.secondaryButton, + primaryButton = state.primaryButton, + ) { + MessageBannerContentRow( + title = state.title, + description = state.description, + contentAlign = state.contentAlign, + slotStart = if (state.iconStart != null) { + { TangemIcon(state.iconStart) } + } else { + null + }, + slotEnd = if (state.iconEnd != null) { + { TangemIcon(state.iconEnd) } + } else { + null + }, + extraBottomSlot = extraBottomSlot, + ) + } +} + @Suppress("LongParameterList") @Composable private fun MessageBannerContentRow( @@ -168,7 +205,11 @@ private fun MessageBannerContentRow( horizontalArrangement = Arrangement.spacedBy(12.dp), ) { slotStart?.let { slot -> Box(modifier = Modifier.align(Alignment.Top)) { slot() } } - textWrapper(Modifier.weight(1f).align(Alignment.Top)) + textWrapper( + Modifier + .weight(1f) + .align(Alignment.Top), + ) slotEnd?.let { slot -> Box(modifier = Modifier.align(Alignment.Top)) { slot() } } } TangemMessageBanner.ContentAlign.Center -> Box(modifier = Modifier.fillMaxWidth()) { @@ -293,6 +334,32 @@ object TangemMessageBanner { val isEnabled: Boolean = true, val isLoading: Boolean = false, ) + + /** + * A model holding configuration data for TangemMessageBanner + * + * @param title Banner headline. + * @param variant Visual appearance — background color + glow ring. + * @param contentAlign Horizontal alignment of the text block. + * @param isShowGlowRing Whether the glow ring is drawn around the banner. `false` shows only the + * background. + * @param description Secondary line under the [title]. `null` hides it. + * @param secondaryButton Start action. `null` hides it. + * @param primaryButton End action. `null` hides it. + * @param iconStart Leading icon before the title. `null` hides it. + * @param iconEnd Trailing icon after the title (e.g. the [CloseButton] preset). `null` hides it. + */ + data class State( + val title: TextReference, + val variant: Variant = Variant.Default, + val contentAlign: ContentAlign = ContentAlign.Start, + val isShowGlowRing: Boolean = true, + val description: TextReference? = null, + val secondaryButton: Button? = null, + val primaryButton: Button? = null, + val iconStart: TangemIconUM? = null, + val iconEnd: TangemIconUM? = null, + ) } /** diff --git a/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/DonutChart.kt b/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/DonutChart.kt index 962d1c96cf..44d22d09f9 100644 --- a/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/DonutChart.kt +++ b/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/DonutChart.kt @@ -8,18 +8,9 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectTapGestures -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -43,7 +34,10 @@ import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.features.foryou.impl.R +import com.tangem.features.foryou.impl.components.state.DonutSegmentColor import com.tangem.features.foryou.impl.components.state.DonutSegmentUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal import kotlin.math.atan2 import kotlin.math.hypot @@ -88,7 +82,7 @@ import kotlin.math.min @Suppress("MagicNumber", "LongParameterList", "LongMethod", "NamedArguments") @Composable internal fun DonutChart( - segments: List, + segments: ImmutableList, modifier: Modifier = Modifier, selectedIndex: Int? = null, onSegmentClick: ((index: Int?) -> Unit)? = null, @@ -100,6 +94,11 @@ internal fun DonutChart( val strokePx = with(LocalDensity.current) { strokeWidth.toPx() } val dimOverlayColor = TangemTheme.colors3.border.inverse.tertiary + // Resolve each slice's themed colour once, here in composition (the palette reads TangemTheme, which + // isn't available inside the drawBehind DrawScope). Order is preserved 1:1 with [segments] so slice i + // keeps the colour its producer assigned by rank — the draw pass below indexes by i, not by paint order. + val segmentColors = segments.map { it.color.getColor() } + // Fade the dim in/out in step with the segment tooltip's pop-in (same spring as DonutSegmentTooltip). val dimProgress by animateFloatAsState( targetValue = if (selectedIndex != null) 1f else 0f, @@ -175,7 +174,7 @@ internal fun DonutChart( for (i in segments.indices.reversed()) { if (sweeps[i] <= 0f) continue drawArc( - color = segments[i].color, + color = segmentColors[i], startAngle = starts[i], sweepAngle = sweeps[i], useCenter = false, @@ -352,28 +351,28 @@ private fun PreviewDonutChart() { modifier = Modifier.size(260.dp), selectedIndex = selectedIndex, onSegmentClick = { index -> selectedIndex = index.takeIf { it != selectedIndex } }, - segments = listOf( + segments = persistentListOf( DonutSegmentUM( weight = BigDecimal(0.55), - color = TangemTheme.colors3.border.brand, + color = DonutSegmentColor.Brand, title = stringReference("Ethereum"), fiatValue = stringReference("$5,720.22"), ), DonutSegmentUM( weight = BigDecimal(0.07), - color = TangemTheme.colors3.border.accent.violet, + color = DonutSegmentColor.Violet, title = stringReference("Solana"), fiatValue = stringReference("$728.30"), ), DonutSegmentUM( weight = BigDecimal(0.06), - color = TangemTheme.colors3.border.accent.red, + color = DonutSegmentColor.Red, title = stringReference("Polkadot"), fiatValue = stringReference("$624.26"), ), DonutSegmentUM( weight = BigDecimal(0.05), - color = TangemTheme.colors3.border.accent.green, + color = DonutSegmentColor.Green, title = stringReference("Tether"), fiatValue = stringReference("$520.18"), ), @@ -409,7 +408,7 @@ private fun PreviewDonutChartEmpty() { ) { DonutChart( modifier = Modifier.size(260.dp), - segments = emptyList(), + segments = persistentListOf(), ) { Text( text = stringResourceSafe(R.string.market_chart_bubble_no_data), diff --git a/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/MarketChart.kt b/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/MarketChart.kt index 582c492702..0da5cb11a4 100644 --- a/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/MarketChart.kt +++ b/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/MarketChart.kt @@ -8,27 +8,10 @@ import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.runtime.withFrameNanos +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset @@ -51,21 +34,14 @@ import com.tangem.core.ui.components.haze.hazeSourceTangem import com.tangem.core.ui.ds.button.SecondaryTangemButton import com.tangem.core.ui.ds.button.TangemButtonSize import com.tangem.core.ui.ds2.surface.TangemSurface -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.pluralStringResourceSafe -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.features.foryou.impl.R -import com.tangem.features.foryou.impl.components.state.AiInsightUM -import com.tangem.features.foryou.impl.components.state.DonutChartUM -import com.tangem.features.foryou.impl.components.state.DonutSegmentUM -import com.tangem.features.foryou.impl.components.state.MarketChartUM +import com.tangem.features.foryou.impl.components.state.* +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.math.BigDecimal @@ -80,7 +56,7 @@ internal fun MarketChart(marketChart: MarketChartUM, modifier: Modifier = Modifi .onGloballyPositioned { cardBoundsInWindow = it.boundsInWindow() }, color = TangemTheme.colors3.bg.secondary, ) { - Column { + Column(modifier = Modifier.fillMaxWidth()) { DonutChartBlock(marketChart.donutChart, cardBoundsInWindow) Spacer(modifier = Modifier.height(16.dp)) if (marketChart is MarketChartUM.Loaded) { @@ -353,10 +329,7 @@ private fun MarketChart_Preview( } } -/** - * Maps a [scenario] to the state shown. Built inside a `@Composable` (not the [PreviewParameterProvider]) - * because the segment colors come from [TangemTheme.colors3], which can only be read in composition. - */ +/** Maps a [scenario] to the state shown in the preview. */ @Suppress("MagicNumber") @Composable private fun previewMarketChartState(scenario: MarketChartPreviewScenario): MarketChartUM = when (scenario) { @@ -378,16 +351,16 @@ private fun previewMarketChartState(scenario: MarketChartPreviewScenario): Marke aiInsight = AiInsightUM.Hide, donutChart = DonutChartUM.Loaded( totalAmount = "$10,12345678912.1333", - donutSegmentList = listOf( + donutSegmentList = persistentListOf( DonutSegmentUM( weight = BigDecimal(0.55), - color = TangemTheme.colors3.border.brand, + color = DonutSegmentColor.Brand, title = stringReference("Ethereum"), fiatValue = stringReference("$5,720.22"), ), DonutSegmentUM( weight = BigDecimal(0.45), - color = TangemTheme.colors3.border.accent.green, + color = DonutSegmentColor.Green, title = stringReference("Solana"), fiatValue = stringReference("$728.30"), ), @@ -401,28 +374,28 @@ private fun previewMarketChartState(scenario: MarketChartPreviewScenario): Marke @Composable private fun previewLoadedDonut(): DonutChartUM.Loaded = DonutChartUM.Loaded( totalAmount = "$10,123456.1333", - donutSegmentList = listOf( + donutSegmentList = persistentListOf( DonutSegmentUM( weight = BigDecimal(0.55), - color = TangemTheme.colors3.border.brand, + color = DonutSegmentColor.Brand, title = stringReference("Ethereum"), fiatValue = stringReference("$5,720.22"), ), DonutSegmentUM( weight = BigDecimal(0.077), - color = TangemTheme.colors3.border.accent.violet, + color = DonutSegmentColor.Violet, title = stringReference("Solana"), fiatValue = stringReference("$728.30"), ), DonutSegmentUM( weight = BigDecimal(0.0666), - color = TangemTheme.colors3.border.accent.red, + color = DonutSegmentColor.Red, title = stringReference("Polkadot"), fiatValue = stringReference("$624.26"), ), DonutSegmentUM( weight = BigDecimal(0.05), - color = TangemTheme.colors3.border.accent.green, + color = DonutSegmentColor.Green, title = stringReference("Tether"), fiatValue = stringReference("$520.18"), ), diff --git a/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/state/DonutSegmentUM.kt b/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/state/DonutSegmentUM.kt index df7e33719c..cd6563427d 100644 --- a/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/state/DonutSegmentUM.kt +++ b/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/state/DonutSegmentUM.kt @@ -1,8 +1,11 @@ package com.tangem.features.foryou.impl.components.state +import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable +import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme import java.math.BigDecimal /** @@ -12,7 +15,8 @@ import java.math.BigDecimal * contiguously; whatever is left after `sum(weight)` shows through as the track. * For a portfolio where slices sum to 1f the ring fills completely and no track is visible. * Also doubles as the slice's share for the selection tooltip (rendered as `weight * 100%`). - * @param color Solid fill of the slice. + * @param color Palette entry for the slice's solid fill. Assigned by the producer in segment order, so the + * slice's colour follows its rank; [DonutChart] resolves it to a themed [Color] in composition. * @param title Human-readable name of the asset this slice represents (e.g. `"Ethereum"`). Shown in the * selection tooltip. Empty by default for slices that don't need a label. * @param fiatValue Pre-formatted fiat value of the slice (e.g. `"$5,720.22"`). Shown in the selection @@ -21,8 +25,27 @@ import java.math.BigDecimal @Immutable internal data class DonutSegmentUM( - val color: Color, + val color: DonutSegmentColor, val weight: BigDecimal, val title: TextReference, val fiatValue: TextReference, -) \ No newline at end of file +) + +internal enum class DonutSegmentColor { + Brand, + Violet, + Red, + Green, + ; + + @ReadOnlyComposable + @Composable + fun getColor(): Color { + return when (this) { + Brand -> TangemTheme.colors3.border.brand + Violet -> TangemTheme.colors3.border.accent.violet + Red -> TangemTheme.colors3.border.accent.red + Green -> TangemTheme.colors3.border.accent.green + } + } +} \ No newline at end of file diff --git a/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/state/MarketChartUM.kt b/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/state/MarketChartUM.kt index ce825b8336..7440dda859 100644 --- a/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/state/MarketChartUM.kt +++ b/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/components/state/MarketChartUM.kt @@ -2,6 +2,8 @@ package com.tangem.features.foryou.impl.components.state import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf @Immutable internal sealed class MarketChartUM( @@ -27,14 +29,14 @@ internal sealed class MarketChartUM( @Immutable internal sealed class DonutChartUM( - open val donutSegmentList: List, + open val donutSegmentList: ImmutableList, ) { data class Loaded( val totalAmount: String, - override val donutSegmentList: List, + override val donutSegmentList: ImmutableList, ) : DonutChartUM(donutSegmentList = donutSegmentList) - data object NoData : DonutChartUM(donutSegmentList = emptyList()) + data object NoData : DonutChartUM(donutSegmentList = persistentListOf()) } @Immutable diff --git a/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/model/ForYouNotification.kt b/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/model/ForYouNotification.kt new file mode 100644 index 0000000000..612fe713bb --- /dev/null +++ b/features/for-you/impl/src/main/java/com/tangem/features/foryou/impl/model/ForYouNotification.kt @@ -0,0 +1,26 @@ +package com.tangem.features.foryou.impl.model + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.generated.icons.Icons +import com.tangem.core.ui.res.generated.icons.ic_cloud_exclamation_20 +import com.tangem.features.foryou.impl.R + +@Immutable +internal sealed class ForYouNotification(val state: TangemMessageBanner.State) { + + data object UsedOutdatedData : ForYouNotification( + state = TangemMessageBanner.State( + title = resourceReference(R.string.warning_some_token_balances_not_updated), + iconEnd = TangemIconUM.Icon( + imageVector = Icons.ic_cloud_exclamation_20, + tintReference = { TangemTheme.colors3.icon.primary }, + ), + variant = TangemMessageBanner.Variant.Warning, + isShowGlowRing = false, + ), + ) +} \ No newline at end of file