From 70ee2fc177436431bb451b2b8d220649245f9d37 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 13 Aug 2026 16:03:42 -0400 Subject: [PATCH] feat(wallet): v2 card-stack collapse + appreciation pill - TokenCardStack: set collapsedReveal default to 0 dp so back cards hide completely behind the front card when fully collapsed (no slivers); fanned reveal stays at 64 dp. - CurrencyAppreciationLabel: add AppreciationStyle enum (Classic/Pill). Classic (default) keeps the existing sentiment-tinted fill so all current call sites are unchanged. Pill renders a neutral bordered box with secondary text colour for use in the v2 wallet header. - BalanceHeader: add topPadding/bottomPadding params (default: grid.x9 for both, preserving v1 behaviour) and appreciationStyle param. - WalletScreenContent: remove the leading x15 spacer; pass topPadding=96 dp, bottomPadding=44 dp, and AppreciationStyle.Pill to BalanceHeader to match Figma node 8966:1578. Parity with iOS PR #574. --- .../app/core/ui/CurrencyAppreciationLabel.kt | 74 +++++++++++++++---- .../flipcash/app/core/ui/TokenCardStack.kt | 5 +- .../core/src/main/res/values/strings.xml | 2 + .../balance/internal/WalletScreenContent.kt | 9 ++- .../internal/components/BalanceHeader.kt | 24 +++++- 5 files changed, 91 insertions(+), 23 deletions(-) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/CurrencyAppreciationLabel.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/CurrencyAppreciationLabel.kt index 00d7a06d1b..b70c68e9f0 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/CurrencyAppreciationLabel.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/CurrencyAppreciationLabel.kt @@ -1,6 +1,7 @@ package com.flipcash.app.core.ui import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding @@ -15,13 +16,32 @@ import com.flipcash.app.core.money.formattedAppreciation import com.flipcash.core.R import com.getcode.opencode.model.financial.Fiat import com.getcode.theme.CodeTheme +import com.getcode.theme.White20 import com.getcode.theme.extraSmall import com.getcode.ui.components.text.AnimatedNumberText +/** Controls the visual treatment of the appreciation badge. */ +enum class AppreciationStyle { + /** Sentiment-tinted filled pill (green/red). Used in v1 balance and token-info screens. */ + Classic, + /** + * Neutral bordered pill with secondary text colour. Used in the v2 wallet screen. + * Layout: `[signed delta in a bordered box] [label]`, both in secondary text. + */ + Pill, +} + +/** + * Displays a fiat appreciation/depreciation value with an optional label. + * + * [style] defaults to [AppreciationStyle.Classic] so existing call sites are unchanged. + * Pass [AppreciationStyle.Pill] in the v2 wallet header for the neutral bordered treatment. + */ @Composable fun CurrencyAppreciationLabel( appreciation: Fiat, modifier: Modifier = Modifier, + style: AppreciationStyle = AppreciationStyle.Classic, ) { val hasAppreciation = appreciation.toDouble() >= 0 val changeColor = if (hasAppreciation) { @@ -35,25 +55,49 @@ fun CurrencyAppreciationLabel( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(CodeTheme.dimens.grid.x1), ) { + val pillModifier = when (style) { + AppreciationStyle.Pill -> + Modifier + .border( + width = CodeTheme.dimens.border, + color = White20, + shape = MaterialTheme.shapes.extraSmall, + ) + .padding( + vertical = 3.dp, + horizontal = CodeTheme.dimens.grid.x1, + ) + AppreciationStyle.Classic -> + Modifier + .background( + color = changeColor.copy(0.20f), + shape = MaterialTheme.shapes.extraSmall, + ) + .padding( + vertical = 2.dp, + horizontal = CodeTheme.dimens.grid.x1, + ) + } AnimatedNumberText( - modifier = Modifier - .background( - color = changeColor.copy(0.20f), - shape = MaterialTheme.shapes.extraSmall, - ) - .padding( - vertical = 2.dp, - horizontal = CodeTheme.dimens.grid.x1 - ), + modifier = pillModifier, value = appreciation.formattedAppreciation(), style = CodeTheme.typography.textSmall, - color = changeColor, + color = if (style == AppreciationStyle.Pill) CodeTheme.colors.textSecondary else changeColor, ) - val label = if (hasAppreciation) { - stringResource(R.string.label_fromCurrencyAppreciation) - } else { - stringResource(R.string.label_fromCurrencyDepreciation) + val label = when (style) { + AppreciationStyle.Pill -> + if (hasAppreciation) { + stringResource(R.string.label_fromAppreciation) + } else { + stringResource(R.string.label_fromDepreciation) + } + AppreciationStyle.Classic -> + if (hasAppreciation) { + stringResource(R.string.label_fromCurrencyAppreciation) + } else { + stringResource(R.string.label_fromCurrencyDepreciation) + } } Text( text = label, @@ -61,4 +105,4 @@ fun CurrencyAppreciationLabel( color = CodeTheme.colors.textSecondary, ) } -} \ No newline at end of file +} diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenCardStack.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenCardStack.kt index 3667e156b6..1dd681c47a 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenCardStack.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/ui/TokenCardStack.kt @@ -20,6 +20,9 @@ import kotlin.collections.forEach * the viewport top (`-itemOffset`); it is read in the **placement** phase so scrolling only re-places * the cards and never re-measures/re-composes them. [pinInset] holds the collapsing deck below top * chrome (e.g. the status bar). Cards are drawn front-to-back so the last (highest-value) sits on top. + * + * When [collapsedReveal] is 0 (the default), back cards collapse completely behind the front card — + * no slivers are visible at rest — matching the iOS wallet card-stack behaviour. */ @Composable fun TokenCardStack( @@ -27,7 +30,7 @@ fun TokenCardStack( modifier: Modifier = Modifier, cardHeight: Dp = 224.dp, fannedReveal: Dp = 64.dp, - collapsedReveal: Dp = 12.dp, + collapsedReveal: Dp = 0.dp, pinInset: Dp = 0.dp, scrolledPast: () -> Float = { 0f }, onCardClick: (TokenWithLocalizedBalance) -> Unit = {}, diff --git a/apps/flipcash/core/src/main/res/values/strings.xml b/apps/flipcash/core/src/main/res/values/strings.xml index a866509dbe..b81693de38 100644 --- a/apps/flipcash/core/src/main/res/values/strings.xml +++ b/apps/flipcash/core/src/main/res/values/strings.xml @@ -590,6 +590,8 @@ from currency appreciation from currency depreciation + from appreciation + from depreciation This Will Take a Minute This transaction typically takes about a minute. You may leave the app while it completes diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt index 85be9e60d8..739c489a66 100644 --- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt +++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/WalletScreenContent.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.core.AppRoute +import com.flipcash.app.core.ui.AppreciationStyle import com.flipcash.app.core.ui.TokenCardStack import com.flipcash.app.balance.internal.components.BalanceHeader import com.flipcash.app.balance.internal.components.OnboardingFunnel @@ -82,15 +83,15 @@ internal fun WalletScreenContent( ) ) { item { - Spacer(Modifier.height(CodeTheme.dimens.grid.x15)) - } - - item { + // v2 wallet header: 96 dp top / 44 dp bottom per Figma node 8966:1578. BalanceHeader( modifier = Modifier .fillMaxWidth(), balance = tokenState.totalBalance, appreciation = tokenState.aggregateAppreciation, + topPadding = 96.dp, + bottomPadding = 44.dp, + appreciationStyle = AppreciationStyle.Pill, ) { dispatchEvent(WalletViewModel.Event.OpenCurrencySelection) } diff --git a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/BalanceHeader.kt b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/BalanceHeader.kt index 110e8df2b0..0ad8022c22 100644 --- a/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/BalanceHeader.kt +++ b/apps/flipcash/features/balance/src/main/kotlin/com/flipcash/app/balance/internal/components/BalanceHeader.kt @@ -7,6 +7,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import com.flipcash.app.core.ui.AppreciationStyle import com.flipcash.app.core.ui.CurrencyAppreciationLabel import com.getcode.opencode.compose.LocalExchange import com.getcode.opencode.model.financial.LocalFiat @@ -14,18 +16,31 @@ import com.getcode.theme.CodeTheme import com.getcode.ui.components.text.AmountArea import com.getcode.ui.theme.CodeCircularProgressIndicator +/** + * Balance header showing the total portfolio value and an optional appreciation pill. + * + * [topPadding] and [bottomPadding] allow callers to override the default symmetric vertical padding + * (both default to [CodeTheme.dimens.grid.x9], preserving v1 behaviour). The v2 wallet screen uses + * asymmetric values (~96 dp top, ~44 dp bottom) to match the Figma spec (node 8966:1578). + * + * [appreciationStyle] defaults to [AppreciationStyle.Classic] (sentiment-tinted fill); pass + * [AppreciationStyle.Pill] for the v2 neutral bordered treatment. + */ @Composable internal fun BalanceHeader( balance: LocalFiat?, appreciation: LocalFiat?, modifier: Modifier = Modifier, + topPadding: Dp = CodeTheme.dimens.grid.x9, + bottomPadding: Dp = CodeTheme.dimens.grid.x9, + appreciationStyle: AppreciationStyle = AppreciationStyle.Classic, onClick: () -> Unit ) { val exchange = LocalExchange.current Column( modifier = modifier .padding(horizontal = CodeTheme.dimens.inset) - .padding(vertical = CodeTheme.dimens.grid.x9), + .padding(top = topPadding, bottom = bottomPadding), horizontalAlignment = Alignment.CenterHorizontally, ) { if (balance == null) { @@ -45,8 +60,11 @@ internal fun BalanceHeader( ) if (appreciation != null) { - CurrencyAppreciationLabel(appreciation.nativeAmount) + CurrencyAppreciationLabel( + appreciation = appreciation.nativeAmount, + style = appreciationStyle, + ) } } } -} \ No newline at end of file +}