From d454dacbb1ab61afbd6d9279b167d99ef4934778 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 14 Sep 2026 17:18:34 -0400 Subject: [PATCH 1/3] feat(nav): scan wears the tip card, You wears a people circle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The scanner tab drew a viewfinder and the You tab drew the tip card, which had the card standing for the account rather than for the thing the scanner points at. Node 10000:111297 swaps them: the scanner gets the tip card glyph, and the You tab falls back to a people circle when the user has no photo. The avatar branch is untouched — a photo still wins. The design file draws the You tab only as an outline, so the selected weight comes from Material instead. Icons.Filled.AccountCircle is the same glyph solid, and its disc lands on ic_people_circle's outer edge (r 10 at 24x24, against the outline's 9.25 centreline plus a 1.5 stroke); only the head moves, 0.5 smaller. Knocking the person out of a solid body is what ic_nav_wallet_selected and ic_nav_tipcard_selected already do. That makes the icon map return a Painter rather than a drawable id, since one of the four now comes from an ImageVector. ic_nav_scan and ic_nav_scan_selected lose their last caller and go with it. --- .../com/flipcash/app/core/ui/NavigationBar.kt | 55 +++++++++++++++---- .../app/core/ui/onboarding/NewUserTutorial.kt | 2 +- .../src/main/res/drawable/ic_nav_scan.xml | 25 --------- .../res/drawable/ic_nav_scan_selected.xml | 16 ------ 4 files changed, 45 insertions(+), 53 deletions(-) delete mode 100644 apps/flipcash/core/src/main/res/drawable/ic_nav_scan.xml delete mode 100644 apps/flipcash/core/src/main/res/drawable/ic_nav_scan_selected.xml diff --git a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/NavigationBar.kt b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/NavigationBar.kt index df0f2764b9..d9a1aa7ec0 100644 --- a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/NavigationBar.kt +++ b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/NavigationBar.kt @@ -1,6 +1,5 @@ package com.flipcash.app.core.ui -import androidx.annotation.DrawableRes import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState @@ -21,6 +20,8 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState @@ -32,6 +33,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview @@ -219,7 +222,7 @@ fun NavigationBar( modifier = Modifier .size(iconSize) .graphicsLayer { alpha = iconAlpha }, - painter = painterResource(button.icon(selected)), + painter = button.icon(selected), colorFilter = ColorFilter.tint(Color.White), contentDescription = null, ) @@ -255,16 +258,32 @@ internal val NavBarButton.testTag: String } /** - * The glyph for [this] tab at the weight its selection calls for. Every tab is drawn as an outline - * until it is selected, where it fills in; the dimming on top of that is the caller's alpha. The - * unsuffixed drawable is the outline, so `ic_nav_scan` stays the outline the tutorial card reuses. + * The glyph for [this] tab at the weight its selection calls for (node 10000:111297). Every tab is + * drawn as an outline until it is selected, where it fills in; the dimming on top of that is the + * caller's alpha. The unsuffixed drawable is the outline, so `ic_nav_tipcard` is also the outline + * the tutorial card reuses. + * + * The scanner tab carries the tip card glyph because scanning one is what that tab is for. The You + * tab draws the user's own photo when there is one (see the avatar branch above) and falls back to + * a people circle, not to the tip card — the tab is the account, and the card is only its front. + * + * That fallback's selected weight comes from Material rather than from the design file, which draws + * the You tab only as an outline. `Icons.Filled.AccountCircle` is the same glyph solid: its disc + * lands on `ic_people_circle`'s outer edge (r 10 at 24x24, against the outline's 9.25 centreline + * plus a 1.5 stroke), and knocking the person out of a solid body is what `ic_nav_wallet_selected` + * and `ic_nav_tipcard_selected` already do. The head is the one thing that moves, 0.5 smaller. */ -@DrawableRes -private fun NavBarButton.icon(selected: Boolean): Int = when (this) { - NavBarButton.Scanner -> if (selected) R.drawable.ic_nav_scan_selected else R.drawable.ic_nav_scan - NavBarButton.Wallet -> if (selected) R.drawable.ic_nav_wallet_selected else R.drawable.ic_nav_wallet - NavBarButton.Chats -> if (selected) R.drawable.ic_nav_chat_selected else R.drawable.ic_nav_chat - NavBarButton.TipCard -> if (selected) R.drawable.ic_nav_tipcard_selected else R.drawable.ic_nav_tipcard +@Composable +private fun NavBarButton.icon(selected: Boolean): Painter = when (this) { + NavBarButton.Scanner -> + painterResource(if (selected) R.drawable.ic_nav_tipcard_selected else R.drawable.ic_nav_tipcard) + NavBarButton.Wallet -> + painterResource(if (selected) R.drawable.ic_nav_wallet_selected else R.drawable.ic_nav_wallet) + NavBarButton.Chats -> + painterResource(if (selected) R.drawable.ic_nav_chat_selected else R.drawable.ic_nav_chat) + NavBarButton.TipCard -> + if (selected) rememberVectorPainter(Icons.Filled.AccountCircle) + else painterResource(R.drawable.ic_people_circle) } @Preview @@ -302,3 +321,17 @@ private fun NavigationBarAvatarSelectedPreview() { avatar = { modifier -> Box(modifier.background(Color(0xFF8E6E5B))) }, ) } + +/** + * The You tab with no photo to draw (node 10000:111297) — the people-circle fallback, selected, so + * the filled twin gets a preview of its own rather than only appearing in the screenshot test. + */ +@Preview(name = "No photo, You tab selected") +@PreviewWrapper(FlipcashThemeWrapper::class) +@Composable +private fun NavigationBarNoAvatarSelectedPreview() { + NavigationBar( + state = rememberNavigationBarState(selectedTab = NavBarButton.TipCard), + onButtonClick = { }, + ) +} diff --git a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/onboarding/NewUserTutorial.kt b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/onboarding/NewUserTutorial.kt index 633ecbeae4..0811bafcc4 100644 --- a/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/onboarding/NewUserTutorial.kt +++ b/apps/flipcash/core-ui/src/main/kotlin/com/flipcash/app/core/ui/onboarding/NewUserTutorial.kt @@ -72,7 +72,7 @@ sealed interface TutorialItem { override val description: String @Composable get() = stringResource(R.string.subtitle_scanTipCard) override val icon: Painter - @Composable get() = painterResource(R.drawable.ic_nav_scan) + @Composable get() = painterResource(R.drawable.ic_nav_tipcard) } class ProfilePicture(override val isCompleted: Boolean) : Profile { diff --git a/apps/flipcash/core/src/main/res/drawable/ic_nav_scan.xml b/apps/flipcash/core/src/main/res/drawable/ic_nav_scan.xml deleted file mode 100644 index 5924299475..0000000000 --- a/apps/flipcash/core/src/main/res/drawable/ic_nav_scan.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - diff --git a/apps/flipcash/core/src/main/res/drawable/ic_nav_scan_selected.xml b/apps/flipcash/core/src/main/res/drawable/ic_nav_scan_selected.xml deleted file mode 100644 index 030b1dde94..0000000000 --- a/apps/flipcash/core/src/main/res/drawable/ic_nav_scan_selected.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - From 234d80a5a1f750080240963ac25ed491beb03660 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 14 Sep 2026 17:19:16 -0400 Subject: [PATCH 2/3] test(nav): capture the people-circle fallback selected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The capture had three bars: the photo slot in both selection states, plus the no-photo glyph unselected. The state it was missing is the one the design file never drew — the people circle selected — which is now the only glyph in the bar coming from Material rather than from the drawables. A fourth bar puts it next to the outline it replaces, so the two can be compared in one image. --- .../app/core/ui/NavigationBarAvatarScreenshotTest.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/flipcash/core-ui/src/test/kotlin/com/flipcash/app/core/ui/NavigationBarAvatarScreenshotTest.kt b/apps/flipcash/core-ui/src/test/kotlin/com/flipcash/app/core/ui/NavigationBarAvatarScreenshotTest.kt index 604f65ee91..634a3d3e0b 100644 --- a/apps/flipcash/core-ui/src/test/kotlin/com/flipcash/app/core/ui/NavigationBarAvatarScreenshotTest.kt +++ b/apps/flipcash/core-ui/src/test/kotlin/com/flipcash/app/core/ui/NavigationBarAvatarScreenshotTest.kt @@ -72,6 +72,11 @@ class NavigationBarAvatarScreenshotTest { NavigationBar( state = rememberNavigationBarState(selectedTab = NavBarButton.Wallet), ) + // No photo, You tab selected — the filled people-circle fallback, which is the + // one state neither the photo bars nor the unselected fallback shows. + NavigationBar( + state = rememberNavigationBarState(selectedTab = NavBarButton.TipCard), + ) } } } From 3bf3f7fab870e687496c42fc2077a837d3b7f79c Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Mon, 14 Sep 2026 17:54:38 -0400 Subject: [PATCH 3/3] feat(nav): move chats to the second tab Chats sits beside the scanner rather than after the wallet. Scanning a tip card now lands in the chat, so the two tabs that make up the messaging path are adjacent instead of separated by the wallet. NavBarButton.tabs is the only place bar order is expressed; NavigationBar reads its indices from that list, so nothing else moves. --- .../kotlin/com/flipcash/app/core/navigation/NavBarButton.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarButton.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarButton.kt index 2c06d17054..8fffa63638 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarButton.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarButton.kt @@ -8,7 +8,7 @@ enum class NavBarButton { ; companion object { - /** The fixed tab set, in bar order. */ - val tabs = listOf(Scanner, Wallet, Chats, TipCard) + /** The fixed tab set, in bar order. Chats sits second, beside the scanner it now feeds. */ + val tabs = listOf(Scanner, Chats, Wallet, TipCard) } }