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