From 865d57c2d7f792a8860b7762a304829ec7a01e50 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 23 Sep 2026 14:16:46 -0400 Subject: [PATCH 1/2] refactor(nav): move the nav bar homes into AppRoute.Tabs The Chats tab's home was AppRoute.Sheets.Tips. It is not a sheet and no longer shows tips, and Wallet and Menu had the same problem under Sheets. resolveRoutes had to check asNavBarTab() on every Sheets route so that these three were not wrapped in Main.Sheet. AppRoute.Tabs now holds Scanner, Wallet, Chats and Menu. Everything left under Sheets is a real sheet, so resolveRoutes wraps any Sheets route and the special case is gone. The tab-home checks in navigateAll, AppContent and NavTabBarInsetEntryDecorator become `is AppRoute.Tabs`. Chats also drops the tipping flow it was hosted in. TipFlowViewModel only ever set its steps to listOf(TipStep.Tips), so TipStep.Intro never ran, TipInfoScreen was never shown, and nothing built Tips(resumed = true), the only way to reach TipStep.TipCard. The ShareTipCard event was never sent. Tabs.Chats is a data object that renders ChatsScreen (was TipsScreen) backed by ChatsViewModel, which keeps only the chat-feed mapping. TippingFlowScreen, TipCardScreen, TipInfoScreen, TipStep, TipResult and ic_tipping_hand_large are deleted. The screen-root tag follows the route name, so tips_screen becomes chats_screen in the Maestro flows and BaselineProfileGenerator. The other tabs keep their tags. The back stack is held in remember, not saved state, so the renamed serializable routes have nothing to restore from. --- .../com/flipcash/app/internal/ui/App.kt | 2 +- .../app/internal/ui/navigation/AppContent.kt | 8 +- .../ui/navigation/AppScreenContent.kt | 22 +-- .../NavTabBarInsetEntryDecorator.kt | 3 +- .../navigation/BuildNavGraphForLaunchTest.kt | 34 ++-- .../benchmark/BaselineProfileGenerator.kt | 4 +- .../kotlin/com/flipcash/app/core/AppRoute.kt | 38 +++-- .../app/core/extensions/CodeNavigator.kt | 22 +-- .../flipcash/app/core/navigation/HomeRoute.kt | 2 +- .../app/core/navigation/NavBarRoutes.kt | 18 +-- .../flipcash/app/core/tipping/TipResult.kt | 16 -- .../com/flipcash/app/core/tipping/TipStep.kt | 24 --- .../res/drawable/ic_tipping_hand_large.xml | 28 ---- .../app/core/navigation/HomeRouteTest.kt | 2 +- .../app/core/navigation/NavBarRoutesTest.kt | 17 +- .../balance/internal/WalletScreenContent.kt | 2 +- .../flipcash/app/deposit/DepositFlowScreen.kt | 2 +- .../app/menu/internal/MenuScreenViewModel.kt | 8 +- .../internal/PurchaseAccountScreenContent.kt | 2 +- .../flipcash/app/scanner/internal/Scanner.kt | 6 +- .../tipping/{TipsScreen.kt => ChatsScreen.kt} | 20 ++- .../com/flipcash/app/tipping/TipCardScreen.kt | 49 ------ .../flipcash/app/tipping/TippingFlowScreen.kt | 73 --------- .../app/tipping/internal/ChatsViewModel.kt | 79 ++++++++++ .../app/tipping/internal/TipFlowViewModel.kt | 149 ------------------ .../tipping/internal/screens/TipInfoScreen.kt | 115 -------------- .../app/withdrawal/WithdrawalFlowScreen.kt | 2 +- .../app/bills/decor/PayableDecorator.kt | 2 +- .../app/notifications/ChatTapTargetPlanner.kt | 2 +- .../NotificationPermissionScreen.kt | 8 +- .../flipcash/app/router/internal/AppRouter.kt | 12 +- .../app/router/internal/AppRouterTest.kt | 18 +-- .../app/router/internal/NavigateToTest.kt | 38 ++--- .../app/router/internal/ResolveRoutesTest.kt | 30 ++-- docs/architecture/features/README.md | 2 +- maestro/README.md | 2 +- maestro/blocking.yaml | 4 +- maestro/subflows/create_account.yaml | 2 +- maestro/subflows/login.yaml | 2 +- maestro/subflows/login_usdf_only.yaml | 2 +- maestro/subflows/login_with_deeplink.yaml | 2 +- maestro/subflows/open_tip_chat.yaml | 2 +- maestro/tipping_setup.yaml | 2 +- 43 files changed, 247 insertions(+), 630 deletions(-) delete mode 100644 apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tipping/TipResult.kt delete mode 100644 apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tipping/TipStep.kt delete mode 100644 apps/flipcash/core/src/main/res/drawable/ic_tipping_hand_large.xml rename apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/{TipsScreen.kt => ChatsScreen.kt} (91%) delete mode 100644 apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipCardScreen.kt delete mode 100644 apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TippingFlowScreen.kt create mode 100644 apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/ChatsViewModel.kt delete mode 100644 apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/TipFlowViewModel.kt delete mode 100644 apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipInfoScreen.kt diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt index e30f8ee7d0..2fe637eb96 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/App.kt @@ -280,7 +280,7 @@ internal fun App( // gives. Pushing it instead reads as a modal on a stack // the user never navigated. Mirrors iOS // DeepLinkController's requestedCardMint. - codeNavigator.navigateAll(listOf(AppRoute.Sheets.Wallet)) + codeNavigator.navigateAll(listOf(AppRoute.Tabs.Wallet)) cardExpansion.beginExpanded(action.mint) } diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt index 629ee3e2c6..87b47cfde2 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt @@ -32,7 +32,6 @@ import com.flipcash.app.core.navigation.DeeplinkAction import com.flipcash.app.core.navigation.LocalTabBarVisibility import com.flipcash.app.core.navigation.NavBarButton import com.flipcash.app.core.navigation.TabBarVisibilityController -import com.flipcash.app.core.navigation.asNavBarTab import com.flipcash.app.core.navigation.destinationRoute import com.flipcash.app.core.ui.transitions.CardExpandTransition import com.flipcash.app.core.tokens.TokenInfoEntry @@ -102,8 +101,7 @@ internal fun AppContent( // A tab press replaces the whole back stack (tab-bar semantics — see AppNavigationBar), so every // tab home was destroyed and rebuilt on each switch: the wallet re-fetched its balances and the // chat list scrolled back to the top. Hold each tab home's ViewModels and scroll state outside the - // back stack so a switch shows what the tab last had. Only the four routes a tab press produces - // are held, so variants of a tab route (a resumed Tips, say) still get a fresh screen. + // back stack so a switch shows what the tab last had. val tabHomeKeys = remember { NavBarButton.entries.map { it.destinationRoute().toString() }.toSet() } @@ -113,7 +111,7 @@ internal fun AppContent( // push keeps its tab home on the stack, so this fires on a replaceAll away from the tabs. LaunchedEffect(entryState, codeNavigator.backStack) { snapshotFlow { - codeNavigator.backStack.any { (it as? AppRoute)?.asNavBarTab() != null } + codeNavigator.backStack.any { it is AppRoute.Tabs } }.collect { onTabs -> if (!onTabs) entryState.releaseAll() } } @@ -166,7 +164,7 @@ internal fun AppContent( // into a detail screen and slides in. Pops always slide back out (a pop is always // leaving a detail). Sheets/overlays keep their own (no) transition. transitionSpec = { - val landsOnTab = (codeNavigator.currentRouteKey as? AppRoute)?.asNavBarTab() != null + val landsOnTab = codeNavigator.currentRouteKey is AppRoute.Tabs when { targetState is OverlayScene<*> || initialState is OverlayScene<*> -> EnterTransition.None togetherWith ExitTransition.None diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt index f3e67ebf0b..a83807714f 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppScreenContent.kt @@ -28,7 +28,7 @@ import com.flipcash.app.currencycreator.CurrencyCreatorFlowScreen import com.flipcash.app.tipping.FindByUsernameScreen import com.flipcash.app.tipping.NewChatScreen import com.flipcash.app.tipping.NewGroupFlowScreen -import com.flipcash.app.tipping.TippingFlowScreen +import com.flipcash.app.tipping.ChatsScreen import com.flipcash.shared.transactionhistory.ActivityHistoryScreen import com.flipcash.app.core.AppRoute import com.flipcash.app.core.navigation.DeeplinkAction @@ -90,26 +90,26 @@ fun appEntryProvider( SheetContent(key, resultStateRegistry, barManager) } annotatedEntry { key -> AppRestrictedScreen(key.restrictionType) } - annotatedEntry { ScannerScreen() } annotatedEntry { RegionSelectionScreen() } annotatedEntry { key -> InviteContactScreen(key.phoneNumber) } - // Sheets (inner content — wrapped in Main.Sheet by navigateTo()) - // Route type is `Give` but the screen is the Cash/Give screen the flows call cash_screen. - annotatedEntry(testTag = "cash_screen") { key -> CashScreen(key.mint, key.fromTokenInfo) } - annotatedEntry { key -> - TippingFlowScreen(route = key, resultStateRegistry = resultStateRegistry) - } - annotatedEntry { key -> TokenSelectScreen(key.purpose) } - annotatedEntry { + // Tabs (nav bar homes — flat on the root backstack, never wrapped in Main.Sheet) + annotatedEntry { ScannerScreen() } + annotatedEntry { // The wallet hosts the card-expand overlay in-entry so a pushed action (Give/Convert/Withdraw) // covers the expanded currency-info with correct z-order (iOS WalletScreen structure). CardExpandHost { WalletScreen() } } + annotatedEntry { ChatsScreen() } + annotatedEntry { MenuScreen() } + + // Sheets (inner content — wrapped in Main.Sheet by navigateTo()) + // Route type is `Give` but the screen is the Cash/Give screen the flows call cash_screen. + annotatedEntry(testTag = "cash_screen") { key -> CashScreen(key.mint, key.fromTokenInfo) } + annotatedEntry { key -> TokenSelectScreen(key.purpose) } annotatedEntry { ShareAppScreen() } annotatedEntry { ActivityHistoryScreen() } annotatedEntry { key -> TransactionDetailsScreen(key.id) } - annotatedEntry { MenuScreen() } // Messaging annotatedEntry { key -> diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/decorators/NavTabBarInsetEntryDecorator.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/decorators/NavTabBarInsetEntryDecorator.kt index f245b05e67..2a0e922bac 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/decorators/NavTabBarInsetEntryDecorator.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/decorators/NavTabBarInsetEntryDecorator.kt @@ -10,7 +10,6 @@ import androidx.navigation3.runtime.NavEntryDecorator import androidx.navigation3.runtime.NavKey import com.flipcash.app.core.AppRoute import com.flipcash.app.core.navigation.LocalTabBarPadding -import com.flipcash.app.core.navigation.asNavBarTab import com.getcode.navigation.core.CodeNavigator /** @@ -41,7 +40,7 @@ fun NavTabBarInsetEntryDecorator( val route = remember(entry.contentKey) { navigator.backStack.firstOrNull { it.toString() == entry.contentKey } as? AppRoute } - val isTabHome = route?.asNavBarTab() != null + val isTabHome = route is AppRoute.Tabs val padding = if (isTabHome) PaddingValues(bottom = tabBarHeight.value) else PaddingValues() CompositionLocalProvider(LocalTabBarPadding provides padding) { diff --git a/apps/flipcash/app/src/test/kotlin/com/flipcash/app/internal/ui/navigation/BuildNavGraphForLaunchTest.kt b/apps/flipcash/app/src/test/kotlin/com/flipcash/app/internal/ui/navigation/BuildNavGraphForLaunchTest.kt index 3027814154..0f98384de1 100644 --- a/apps/flipcash/app/src/test/kotlin/com/flipcash/app/internal/ui/navigation/BuildNavGraphForLaunchTest.kt +++ b/apps/flipcash/app/src/test/kotlin/com/flipcash/app/internal/ui/navigation/BuildNavGraphForLaunchTest.kt @@ -54,16 +54,16 @@ class BuildNavGraphForLaunchTest { @Test fun `logged in without deeplink opens on the Chats tab`() { val result = build(AuthState.Ready)!! - assertEquals(listOf(AppRoute.Sheets.Tips()), result.baseRoutes) + assertEquals(listOf(AppRoute.Tabs.Chats), result.baseRoutes) assertTrue(result.deeplinkRoutes.isEmpty()) - assertEquals(listOf(AppRoute.Sheets.Tips()), result.resolvedBackStack()) + assertEquals(listOf(AppRoute.Tabs.Chats), result.resolvedBackStack()) } @Test fun `logged in with Navigate deeplink includes deeplink routes`() { - val routes = listOf(AppRoute.Main.Scanner) + val routes = listOf(AppRoute.Tabs.Scanner) val result = buildReady(DeeplinkAction.Navigate(routes)) - assertEquals(listOf(AppRoute.Sheets.Wallet), result.baseRoutes) + assertEquals(listOf(AppRoute.Tabs.Wallet), result.baseRoutes) assertEquals(routes, result.deeplinkRoutes) } @@ -71,7 +71,7 @@ class BuildNavGraphForLaunchTest { fun `logged in with OpenCashLink fires eagerly via pendingAction`() { val action = DeeplinkAction.OpenCashLink("testEntropy") val result = buildReady(action) - assertEquals(listOf(AppRoute.Sheets.Wallet), result.baseRoutes) + assertEquals(listOf(AppRoute.Tabs.Wallet), result.baseRoutes) assertTrue(result.deeplinkRoutes.isEmpty()) assertEquals(action, result.pendingAction) } @@ -80,7 +80,7 @@ class BuildNavGraphForLaunchTest { fun `logged in with Login action fires eagerly via pendingAction`() { val action = DeeplinkAction.Login("seed") val result = buildReady(action) - assertEquals(listOf(AppRoute.Sheets.Wallet), result.baseRoutes) + assertEquals(listOf(AppRoute.Tabs.Wallet), result.baseRoutes) assertTrue(result.deeplinkRoutes.isEmpty()) assertEquals(action, result.pendingAction) } @@ -91,10 +91,10 @@ class BuildNavGraphForLaunchTest { // no pushed screen, so nothing lands in deeplinkRoutes. See DeeplinkAction.OpenToken. val action = DeeplinkAction.OpenToken( mint = Mint(MINT), - routes = listOf(AppRoute.Sheets.Wallet, AppRoute.Token.Info(Mint(MINT))), + routes = listOf(AppRoute.Tabs.Wallet, AppRoute.Token.Info(Mint(MINT))), ) val result = buildReady(action) - assertEquals(listOf(AppRoute.Sheets.Wallet), result.baseRoutes) + assertEquals(listOf(AppRoute.Tabs.Wallet), result.baseRoutes) assertTrue(result.deeplinkRoutes.isEmpty()) assertEquals(action, result.pendingAction) } @@ -102,7 +102,7 @@ class BuildNavGraphForLaunchTest { @Test fun `logged in with None action opens the Chats tab without deeplink routes`() { val result = buildReady(DeeplinkAction.None) - assertEquals(listOf(AppRoute.Sheets.Tips()), result.baseRoutes) + assertEquals(listOf(AppRoute.Tabs.Chats), result.baseRoutes) assertTrue(result.deeplinkRoutes.isEmpty()) } @@ -195,13 +195,13 @@ class BuildNavGraphForLaunchTest { fun `token deeplink pushes token info onto the Wallet tab without a sheet`() { val result = buildReady( DeeplinkAction.Navigate( - listOf(AppRoute.Sheets.Wallet, AppRoute.Token.Info(mint, TokenInfoEntry.Deeplink)) + listOf(AppRoute.Tabs.Wallet, AppRoute.Token.Info(mint, TokenInfoEntry.Deeplink)) ) ) val stack = result.resolvedBackStack() assertEquals(2, stack.size) - assertEquals(AppRoute.Sheets.Wallet, stack[0]) + assertEquals(AppRoute.Tabs.Wallet, stack[0]) assertIs(stack[1]) assertTrue(stack.none { it is AppRoute.Main.Sheet }, "a tab home must never be wrapped in a sheet") } @@ -211,7 +211,7 @@ class BuildNavGraphForLaunchTest { val result = buildReady( DeeplinkAction.Navigate( listOf( - AppRoute.Sheets.Tips(), + AppRoute.Tabs.Chats, AppRoute.Messaging.Chat(ChatIdentifier.ByChatId(ChatId(listOf(1, 2, 3, 4)))), ) ) @@ -219,10 +219,10 @@ class BuildNavGraphForLaunchTest { val stack = result.resolvedBackStack() assertEquals(2, stack.size) - assertIs(stack[0]) + assertIs(stack[0]) assertIs(stack[1]) // The launch home must be replaced by the target tab, not left underneath it. - assertTrue(stack.none { it == AppRoute.Sheets.Wallet }) + assertTrue(stack.none { it == AppRoute.Tabs.Wallet }) assertTrue(stack.none { it is AppRoute.Main.Sheet }) } @@ -231,7 +231,7 @@ class BuildNavGraphForLaunchTest { val result = buildReady( DeeplinkAction.Navigate( listOf( - AppRoute.Sheets.Menu, + AppRoute.Tabs.Menu, AppRoute.Menu.MyAccount, AppRoute.Verification( origin = AppRoute.Menu.MyAccount, @@ -245,7 +245,7 @@ class BuildNavGraphForLaunchTest { val stack = result.resolvedBackStack() assertEquals(3, stack.size) - assertEquals(AppRoute.Sheets.Menu, stack[0]) + assertEquals(AppRoute.Tabs.Menu, stack[0]) assertTrue(stack.none { it is AppRoute.Main.Sheet }) } @@ -253,7 +253,7 @@ class BuildNavGraphForLaunchTest { fun `pending actions still launch on the Wallet tab`() { val action = DeeplinkAction.OpenCashLink("testEntropy") val result = buildReady(action) - assertEquals(listOf(AppRoute.Sheets.Wallet), result.baseRoutes) + assertEquals(listOf(AppRoute.Tabs.Wallet), result.baseRoutes) assertEquals(action, result.pendingAction) } } diff --git a/apps/flipcash/benchmark/src/main/kotlin/com/flipcash/benchmark/BaselineProfileGenerator.kt b/apps/flipcash/benchmark/src/main/kotlin/com/flipcash/benchmark/BaselineProfileGenerator.kt index 6daa5412b6..b0a203fe2d 100644 --- a/apps/flipcash/benchmark/src/main/kotlin/com/flipcash/benchmark/BaselineProfileGenerator.kt +++ b/apps/flipcash/benchmark/src/main/kotlin/com/flipcash/benchmark/BaselineProfileGenerator.kt @@ -114,7 +114,7 @@ class BaselineProfileGenerator { // The chats tab lists conversations; open the first one and send a text message. A // message is fund-free (money-safety: never tap Send Cash / confirm a spend). The // conversation is not hardcoded — send_contact_row resolves to the first row. - openTab("nav_chats", "tips_screen") + openTab("nav_chats", "chats_screen") device.wait(Until.findObject(By.res("send_contact_row")), TIMEOUT)?.click() device.wait(Until.findObject(By.res("chat_screen")), LOGIN_TIMEOUT) @@ -132,7 +132,7 @@ class BaselineProfileGenerator { // Back to the conversation list, fling it for coverage, then back to the scanner. device.pressBack() - device.wait(Until.findObject(By.res("tips_screen")), TIMEOUT) + device.wait(Until.findObject(By.res("chats_screen")), TIMEOUT) device.waitForIdle() flingScroll("chat_list", Direction.UP, 2) flingScroll("chat_list", Direction.DOWN, 1) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt index 91894db4e8..d3abefd2b2 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/AppRoute.kt @@ -110,8 +110,6 @@ sealed interface AppRoute : NavKey, Parcelable { @Serializable data class AppRestricted(val restrictionType: RestrictionType) : Main - @Serializable - data object Scanner : Main // TODO: is there a better place for this to live? @Serializable @@ -168,20 +166,36 @@ sealed interface AppRoute : NavKey, Parcelable { get() = steps } + /** + * The homes of the hoisted nav bar's tabs. They sit flat on the root backstack, so the bar stays + * visible over them, and a route list leading with one is applied as a tab switch (see + * `navigateAll`). `NavBarRoutes.kt` maps each to its `NavBarButton`. + */ @Serializable @Parcelize - sealed interface Sheets : AppRoute { + sealed interface Tabs : AppRoute { @Serializable - data class TokenSelection(val purpose: TokenPurpose) : Sheets + data object Scanner : Tabs + @Serializable - data class Give(val mint: Mint? = null, val fromTokenInfo: Boolean = false) : Sheets + data object Wallet : Tabs + /** Tip DMs and groups. */ @Serializable - data class Tips(val resumed: Boolean = false): Sheets { - } + data object Chats : Tabs + /** Settings, plus the user's tip card. The nav bar labels it "You". */ @Serializable - data object Wallet : Sheets + data object Menu : Tabs + } + + @Serializable + @Parcelize + sealed interface Sheets : AppRoute { + @Serializable + data class TokenSelection(val purpose: TokenPurpose) : Sheets + @Serializable + data class Give(val mint: Mint? = null, val fromTokenInfo: Boolean = false) : Sheets /** Full unified paged activity history — the "dive in" from the wallet's recent-activity preview. */ @Serializable @@ -197,9 +211,6 @@ sealed interface AppRoute : NavKey, Parcelable { @Serializable data class TransactionDetails(val id: ID) : Sheets - @Serializable - data object Menu : Sheets - @Serializable data object ShareApp : Sheets @@ -335,9 +346,8 @@ sealed interface AppRoute : NavKey, Parcelable { /** * Node 10127:117987 — the two ways to start a chat, reached from the "+" on the Chats list. * - * A top-level route rather than a step of the tipping flow, even though the Chats list it - * is reached from is one: the flow is a tab home, so a step pushed inside it keeps the tab - * bar. This covers it, the way [Chat] does. [FindByUsername] and [NewGroup] are pushed on + * A top-level route rather than something drawn inside [Tabs.Chats]: that is a tab home, so + * anything inside it keeps the tab bar. This covers it, the way [Chat] does. [FindByUsername] and [NewGroup] are pushed on * top of it for the same reason. */ @Serializable diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/extensions/CodeNavigator.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/extensions/CodeNavigator.kt index dd38e8b767..5088133f48 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/extensions/CodeNavigator.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/extensions/CodeNavigator.kt @@ -3,7 +3,6 @@ package com.flipcash.app.core.extensions import androidx.compose.runtime.snapshots.Snapshot import androidx.navigation3.runtime.NavKey import com.flipcash.app.core.AppRoute -import com.flipcash.app.core.navigation.asNavBarTab import com.getcode.navigation.core.CodeNavigator import com.getcode.navigation.core.NavOptions @@ -35,12 +34,12 @@ fun CodeNavigator.openAsSheet(route: AppRoute, innerRoutes: List = emp } /** - * True when [routes] leads with a route that is a tab home. + * True when [routes] leads with an [AppRoute.Tabs] home. * Such a list is applied as a *tab switch* (the leading route replaces the stack) rather than * stacked on top of whatever tab the user was on. */ private fun leadsWithTab(routes: List): Boolean = - (routes.firstOrNull() as? AppRoute)?.asNavBarTab() != null + routes.firstOrNull() is AppRoute.Tabs /** * Navigate to multiple routes, wrapping [AppRoute.Sheets] in [AppRoute.Main.Sheet]. @@ -50,9 +49,8 @@ private fun leadsWithTab(routes: List): Boolean = * If a sheet is already open and the new routes include a sheet, the current sheet * is animated closed before the new one opens. * - * The tab homes — `Sheets.Wallet`, `Sheets.Tips`, `Sheets.Menu` — are *not* sheets, so a route - * list leading with one switches to that tab (replacing the stack) and pushes the rest on top - * of it. See [resolveRoutes]. + * A route list leading with an [AppRoute.Tabs] home switches to that tab (replacing the stack) + * and pushes the rest on top of it. See [resolveRoutes]. */ fun CodeNavigator.navigateAll( routes: List, @@ -100,10 +98,9 @@ fun CodeNavigator.navigateAll( * [AppRoute.Main.Sheet] with inner routes, mirroring what [navigateAll] pushes * onto the backstack. Useful for predicting the resulting stack without navigating. * - * The tab homes — `Sheets.Wallet`, `Sheets.Tips`, `Sheets.Menu` — are top-level tab destinations - * rather than modals, so they stay flat on the root backstack (which keeps the hoisted nav bar - * visible and lets back/pop behave like a tab stack). Anything after the tab route is resolved - * independently, so a genuine sheet later in the list still wraps. + * [AppRoute.Tabs] homes are not sheets, so they stay flat on the root backstack (which keeps the + * hoisted nav bar visible and lets back/pop behave like a tab stack). A sheet later in the list + * still wraps, along with everything after it. */ fun resolveRoutes(routes: List): List { if (routes.isEmpty()) return emptyList() @@ -112,11 +109,6 @@ fun resolveRoutes(routes: List): List { if (sheetIndex < 0) return routes val sheetRoute = routes[sheetIndex] as AppRoute.Sheets - - if (sheetRoute.asNavBarTab() != null) { - return routes.take(sheetIndex + 1) + resolveRoutes(routes.drop(sheetIndex + 1)) - } - val before = routes.take(sheetIndex) val innerRoutes = routes.drop(sheetIndex + 1).filterIsInstance() return before + AppRoute.Main.Sheet(sheetRoute, innerRoutes) diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/HomeRoute.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/HomeRoute.kt index d11582d4fb..394b164aab 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/HomeRoute.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/HomeRoute.kt @@ -10,7 +10,7 @@ import com.flipcash.app.core.AppRoute * [com.getcode.navigation.core.NavOptions.PopUpTo.ClearAll], exactly like a tab switch from the * nav bar. */ -val homeRoute: AppRoute = AppRoute.Sheets.Wallet +val homeRoute: AppRoute = AppRoute.Tabs.Wallet /** * The tab a cold launch opens on when no deeplink directs it elsewhere: Chats. Built from the tab's diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarRoutes.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarRoutes.kt index ebccaa40ec..ba5bcc7676 100644 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarRoutes.kt +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/NavBarRoutes.kt @@ -9,20 +9,18 @@ import com.flipcash.app.core.AppRoute /** The top-level route a tab switches to. */ fun NavBarButton.destinationRoute(): AppRoute = when (this) { - NavBarButton.Scanner -> AppRoute.Main.Scanner - NavBarButton.Wallet -> AppRoute.Sheets.Wallet - // The chats tab routes through the tipping flow seeded at the list. - NavBarButton.Chats -> AppRoute.Sheets.Tips(resumed = false) + NavBarButton.Scanner -> AppRoute.Tabs.Scanner + NavBarButton.Wallet -> AppRoute.Tabs.Wallet + NavBarButton.Chats -> AppRoute.Tabs.Chats // The "You" tab is the menu (settings) surface, augmented with the tip card + share. - NavBarButton.TipCard -> AppRoute.Sheets.Menu + NavBarButton.TipCard -> AppRoute.Tabs.Menu } /** The tab a top-level route belongs to, or null if the route isn't a tab home. */ fun AppRoute.asNavBarTab(): NavBarButton? = when (this) { - AppRoute.Main.Scanner -> NavBarButton.Scanner - AppRoute.Sheets.Wallet -> NavBarButton.Wallet - // The tipping flow is home to the chats tab (the tip card moved to the You/menu tab). - is AppRoute.Sheets.Tips -> NavBarButton.Chats - AppRoute.Sheets.Menu -> NavBarButton.TipCard + AppRoute.Tabs.Scanner -> NavBarButton.Scanner + AppRoute.Tabs.Wallet -> NavBarButton.Wallet + AppRoute.Tabs.Chats -> NavBarButton.Chats + AppRoute.Tabs.Menu -> NavBarButton.TipCard else -> null } diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tipping/TipResult.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tipping/TipResult.kt deleted file mode 100644 index e6a9ff3a5e..0000000000 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tipping/TipResult.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.flipcash.app.core.tipping - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import kotlinx.serialization.Serializable - -@Serializable -sealed interface TipResult : Parcelable { - @Parcelize - @Serializable - data object Success : TipResult - - @Parcelize - @Serializable - data object Canceled : TipResult -} diff --git a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tipping/TipStep.kt b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tipping/TipStep.kt deleted file mode 100644 index d3b30dcfd7..0000000000 --- a/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/tipping/TipStep.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.flipcash.app.core.tipping - -import android.os.Parcelable -import com.getcode.navigation.NonDismissableRoute -import com.getcode.navigation.NonDraggableRoute -import com.getcode.navigation.flow.FlowStep -import kotlinx.parcelize.Parcelize -import kotlinx.serialization.Serializable - -@Serializable -sealed interface TipStep : FlowStep, Parcelable { - - @Parcelize - @Serializable - data object Intro : TipStep - - @Parcelize - @Serializable - data object TipCard: TipStep - - @Parcelize - @Serializable - data object Tips : TipStep -} diff --git a/apps/flipcash/core/src/main/res/drawable/ic_tipping_hand_large.xml b/apps/flipcash/core/src/main/res/drawable/ic_tipping_hand_large.xml deleted file mode 100644 index 66f3de265f..0000000000 --- a/apps/flipcash/core/src/main/res/drawable/ic_tipping_hand_large.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - diff --git a/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/HomeRouteTest.kt b/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/HomeRouteTest.kt index a1c1e42283..3ac2379b17 100644 --- a/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/HomeRouteTest.kt +++ b/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/HomeRouteTest.kt @@ -12,7 +12,7 @@ class HomeRouteTest { @Test fun `home is the wallet tab`() { - assertEquals(AppRoute.Sheets.Wallet, homeRoute) + assertEquals(AppRoute.Tabs.Wallet, homeRoute) } @Test diff --git a/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/NavBarRoutesTest.kt b/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/NavBarRoutesTest.kt index 47e8fa433b..aee8c35743 100644 --- a/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/NavBarRoutesTest.kt +++ b/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/NavBarRoutesTest.kt @@ -6,30 +6,27 @@ import kotlin.test.assertEquals /** * The "You" tab is the menu (settings) surface. Mirrors iOS `YouTabRoutingTests`: the TipCard tab - * routes to the menu, and the menu route belongs to the TipCard tab — the tipping flow is now home - * only to the Chats tab. + * routes to the menu, and the menu route belongs to the TipCard tab. */ class NavBarRoutesTest { @Test fun `the TipCard (You) tab routes to the menu`() { - assertEquals(AppRoute.Sheets.Menu, NavBarButton.TipCard.destinationRoute()) + assertEquals(AppRoute.Tabs.Menu, NavBarButton.TipCard.destinationRoute()) } @Test fun `the menu route belongs to the TipCard (You) tab`() { - assertEquals(NavBarButton.TipCard, AppRoute.Sheets.Menu.asNavBarTab()) + assertEquals(NavBarButton.TipCard, AppRoute.Tabs.Menu.asNavBarTab()) } @Test - fun `the Chats tab still routes through the tipping flow`() { - assertEquals(AppRoute.Sheets.Tips(resumed = false), NavBarButton.Chats.destinationRoute()) + fun `the Chats tab routes to the chat list`() { + assertEquals(AppRoute.Tabs.Chats, NavBarButton.Chats.destinationRoute()) } @Test - fun `the tipping flow maps back to the Chats tab, never TipCard`() { - // Even the post-setup resumed form is the Chats tab now — the tip card moved to the You tab. - assertEquals(NavBarButton.Chats, AppRoute.Sheets.Tips(resumed = true).asNavBarTab()) - assertEquals(NavBarButton.Chats, AppRoute.Sheets.Tips(resumed = false).asNavBarTab()) + fun `the chat list maps back to the Chats tab, never TipCard`() { + assertEquals(NavBarButton.Chats, AppRoute.Tabs.Chats.asNavBarTab()) } } 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 e3578ecf20..6151e29d58 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 @@ -221,7 +221,7 @@ internal fun WalletScreenContent( dispatchEvent(WalletViewModel.Event.PresentDepositOptions) } is TutorialItem.ScanTipCard -> { - dispatchEvent(WalletViewModel.Event.OpenScreen(AppRoute.Main.Scanner)) + dispatchEvent(WalletViewModel.Event.OpenScreen(AppRoute.Tabs.Scanner)) } } } diff --git a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/DepositFlowScreen.kt b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/DepositFlowScreen.kt index 543d162f48..dd12134da0 100644 --- a/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/DepositFlowScreen.kt +++ b/apps/flipcash/features/deposit/src/main/kotlin/com/flipcash/app/deposit/DepositFlowScreen.kt @@ -70,7 +70,7 @@ fun DepositFlowScreen( ) when (result) { DepositResult.Success -> { - outerNavigator.popUntil { it == AppRoute.Sheets.Menu } + outerNavigator.popUntil { it == AppRoute.Tabs.Menu } } DepositResult.Canceled -> { outerNavigator.pop() diff --git a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt index 8062d41b18..d8624dbd55 100644 --- a/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt +++ b/apps/flipcash/features/menu/src/main/kotlin/com/flipcash/app/menu/internal/MenuScreenViewModel.kt @@ -326,7 +326,7 @@ internal class MenuScreenViewModel @Inject constructor( UsernameProgress.Unlocked -> dispatchEvent( Event.OpenScreen( AppRoute.UpdateUserProfile( - origin = AppRoute.Sheets.Menu, + origin = AppRoute.Tabs.Menu, steps = listOf(UpdateProfileStep.Username), ) ) @@ -373,7 +373,7 @@ internal class MenuScreenViewModel @Inject constructor( dispatchEvent( Event.OpenScreen( AppRoute.UpdateUserProfile( - origin = AppRoute.Sheets.Menu, + origin = AppRoute.Tabs.Menu, // A name is all a tip card needs. steps = listOf(UpdateProfileStep.Name(DisplayNameSource.TipCardSetup)), ) @@ -388,7 +388,7 @@ internal class MenuScreenViewModel @Inject constructor( dispatchEvent( Event.OpenScreen( AppRoute.UpdateUserProfile( - origin = AppRoute.Sheets.Menu, + origin = AppRoute.Tabs.Menu, // The account already has a name and a card by the time this // checklist is drawn, so the flow reduces to the one step. steps = listOf(UpdateProfileStep.Photo), @@ -404,7 +404,7 @@ internal class MenuScreenViewModel @Inject constructor( dispatchEvent( Event.OpenScreen( AppRoute.UpdateUserProfile( - origin = AppRoute.Sheets.Menu, + origin = AppRoute.Tabs.Menu, steps = listOf(UpdateProfileStep.MinimumTip), ) ) diff --git a/apps/flipcash/features/purchase/src/main/kotlin/com/flipcash/app/purchase/internal/PurchaseAccountScreenContent.kt b/apps/flipcash/features/purchase/src/main/kotlin/com/flipcash/app/purchase/internal/PurchaseAccountScreenContent.kt index 092c55c052..029a81f4ba 100644 --- a/apps/flipcash/features/purchase/src/main/kotlin/com/flipcash/app/purchase/internal/PurchaseAccountScreenContent.kt +++ b/apps/flipcash/features/purchase/src/main/kotlin/com/flipcash/app/purchase/internal/PurchaseAccountScreenContent.kt @@ -51,7 +51,7 @@ fun PurchaseAccountScreen(viewModel: PurchaseAccountViewModel) { viewModel.eventFlow .filterIsInstance() .onEach { - navigator.replaceAll(AppRoute.Main.Scanner) + navigator.replaceAll(AppRoute.Tabs.Scanner) }.launchIn(this) } diff --git a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt index d48645eb35..4573b0e28a 100644 --- a/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt +++ b/apps/flipcash/features/scanner/src/main/kotlin/com/flipcash/app/scanner/internal/Scanner.kt @@ -110,13 +110,13 @@ internal fun Scanner() { is DeeplinkType.Navigatable -> { val routes: List = when (deeplink) { is DeeplinkType.TokenInfo -> listOf( - AppRoute.Sheets.Wallet, + AppRoute.Tabs.Wallet, Info(deeplink.mint, TokenInfoEntry.Deeplink) ) // Scanned tip-DM code — same destination as the // /tip/chat/{id} deeplink. is DeeplinkType.TipChat -> listOf( - AppRoute.Sheets.Tips(), + AppRoute.Tabs.Chats, AppRoute.Messaging.Chat(deeplink.identifier), ) else -> emptyList() @@ -209,7 +209,7 @@ internal fun Scanner() { session.tipCardEvents.collect { event -> when (event) { TipCardEvent.OwnCardScanned -> - navigator.navigateAll(listOf(AppRoute.Sheets.Menu)) + navigator.navigateAll(listOf(AppRoute.Tabs.Menu)) } } } diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipsScreen.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/ChatsScreen.kt similarity index 91% rename from apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipsScreen.kt rename to apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/ChatsScreen.kt index 1285641bbe..526951387b 100644 --- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipsScreen.kt +++ b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/ChatsScreen.kt @@ -29,18 +29,18 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewWrapper import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.flipcash.app.core.AppRoute import com.flipcash.app.core.chat.ChatIdentifier import com.flipcash.app.core.data.isLoaded import com.flipcash.app.core.navigation.LocalTabBarPadding import com.flipcash.app.theme.FlipcashThemeWrapper -import com.flipcash.app.tipping.internal.TipFlowViewModel +import com.flipcash.app.tipping.internal.ChatsViewModel import com.flipcash.app.tipping.internal.components.TipChatRow import com.flipcash.features.tipping.R import com.flipcash.shared.chat.ui.ConversationReference import com.getcode.navigation.core.LocalCodeNavigator -import com.getcode.navigation.flow.flowSharedViewModel import com.getcode.theme.CodeTheme import com.getcode.ui.components.AppBarDefaults import com.getcode.ui.components.AppBarWithTitle @@ -49,18 +49,16 @@ import com.getcode.ui.theme.CodeScaffold import com.getcode.ui.theme.ScaffoldBarPlacement /** - * The tip DM conversation list — always a step in the tipping [TippingFlowScreen] flow, so it shares - * the flow's [TipFlowViewModel]. It is the "Chats" root tab: the standard centred screen title over - * the list, no dismiss affordance (the root nav bar is the chrome); the tip card lives on its own - * tab. + * The "Chats" root tab: tip DMs and groups under the standard centred screen title, with no dismiss + * affordance (the root nav bar is the chrome). The tip card lives on the "You" tab. */ @Composable -fun TipsScreen() { - val viewModel = flowSharedViewModel() +fun ChatsScreen() { + val viewModel = hiltViewModel() val state by viewModel.stateFlow.collectAsStateWithLifecycle() val navigator = LocalCodeNavigator.current - val chats = state.tipChats + val chats = state.chats val listState = rememberLazyListState() CodeScaffold( @@ -90,8 +88,8 @@ fun TipsScreen() { // Start title sits at the inset and reads as off-centre against the Add button. titleAlignment = Alignment.CenterHorizontally, // Node 9442:5779 — the only way to start a chat with someone who has never paid - // you. A pushed route, not a step of this flow: this list is a tab home, and a step - // pushed inside it would leave the tab bar over the entry screen. + // you. A pushed route rather than an in-place screen: this list is a tab home, and + // anything drawn inside it would leave the tab bar over the entry screen. endContent = { AppBarDefaults.Add { navigator.push(AppRoute.Messaging.NewChat) } }, diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipCardScreen.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipCardScreen.kt deleted file mode 100644 index d971114d75..0000000000 --- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TipCardScreen.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.flipcash.app.tipping - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.flipcash.app.bills.ScannableRenderer -import com.flipcash.app.bills.components.cards.LocalTipCardBaseAlpha -import com.flipcash.app.bills.components.cards.LocalTipCardColor -import com.flipcash.app.core.bill.Scannable -import com.flipcash.app.tipping.internal.TipFlowViewModel -import com.getcode.navigation.flow.flowSharedViewModel - -/** - * The user's own tip card — a step in the tipping [TippingFlowScreen] flow, so it shares the flow's - * [TipFlowViewModel]. It is the post-profile-setup landing (the flow seeded at TipCard): the card - * full-bleed, no chrome. The primary home for the card is the "You" tab (the menu), which also owns - * settings. - */ -@Composable -fun TipCardScreen() { - val viewModel = flowSharedViewModel() - val state by viewModel.stateFlow.collectAsStateWithLifecycle() - - Box(modifier = Modifier.fillMaxSize()) { - TipCardArt(card = state.tipCard, modifier = Modifier.fillMaxSize()) - } -} - -@Composable -private fun TipCardArt(card: Scannable.TipCard?, modifier: Modifier = Modifier) { - if (card == null) return - CompositionLocalProvider( - // Static display (no camera behind the card), so render it opaque at the design's flattened - // colour rather than the translucent frosted fill — the 36% alpha otherwise multiplies the - // app's Brand background and reads incorrectly. Figma flattens the card to rgb(16,16,17). - LocalTipCardColor provides Color(0xFF101011), - LocalTipCardBaseAlpha provides 1f, - ) { - Box(modifier = modifier, contentAlignment = Alignment.Center) { - ScannableRenderer(scannable = card) - } - } -} diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TippingFlowScreen.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TippingFlowScreen.kt deleted file mode 100644 index 083999085e..0000000000 --- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/TippingFlowScreen.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.flipcash.app.tipping - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.hilt.navigation.compose.hiltViewModel -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation3.runtime.NavEntry -import androidx.navigation3.runtime.NavKey -import androidx.navigation3.runtime.entryProvider -import com.flipcash.app.core.AppRoute -import com.flipcash.app.core.tipping.TipResult -import com.flipcash.app.core.tipping.TipStep -import com.flipcash.app.tipping.internal.TipFlowViewModel -import com.flipcash.app.tipping.internal.screens.TipInfoScreen -import com.getcode.navigation.annotatedEntry -import com.getcode.navigation.flow.FlowExitReason -import com.getcode.navigation.flow.FlowHost -import com.getcode.navigation.flow.flowSharedViewModel -import com.getcode.navigation.results.NavResultStateRegistry -import com.getcode.navigation.scenes.LocalBottomSheetDismissDispatcher -import com.getcode.ui.utils.rememberKeyboardController - -@Composable -fun TippingFlowScreen( - route: AppRoute.Sheets.Tips, - resultStateRegistry: NavResultStateRegistry, -) { - val sheetDismiss = LocalBottomSheetDismissDispatcher.current - val viewModel = hiltViewModel() - val state by viewModel.stateFlow.collectAsStateWithLifecycle() - val keyboard = rememberKeyboardController() - - // How the flow was entered gates the step list (post-setup handoff → TipCard alone). - LaunchedEffect(route.resumed) { - viewModel.dispatchEvent(TipFlowViewModel.Event.OnResumed(route.resumed)) - } - - // `resumed` is deterministically the tip card, so seed it synchronously rather than waiting for - // the VM's async steps to resolve. Otherwise the tip-card tab renders the default first step - // (the chats list) for a frame — briefly visible when a tab switch crossfades into it. - val steps = if (route.resumed) listOf(TipStep.TipCard) else state.steps - - FlowHost( - steps = steps, - resumeAt = 0, - resultStateRegistry = resultStateRegistry, - // Proceeding off the end (TipCard's Done, or backing out at root) completes the flow. - completedResult = TipResult.Success, - onExit = { reason, _ -> - when (reason) { - is FlowExitReason.Completed, - FlowExitReason.BackedOutOfRoot, - FlowExitReason.Canceled -> keyboard.hideIfVisible { sheetDismiss() } - } - }, - entryProvider = tippingEntryProvider(), - ) -} - -private fun tippingEntryProvider(): (NavKey) -> NavEntry = entryProvider { - annotatedEntry { SyncStep(it); TipInfoScreen() } - annotatedEntry { SyncStep(it); TipCardScreen() } - annotatedEntry { SyncStep(it); TipsScreen() } -} - -@Composable -private fun SyncStep(step: TipStep) { - val viewModel = flowSharedViewModel() - LaunchedEffect(step) { - viewModel.dispatchEvent(TipFlowViewModel.Event.OnStepChanged(step)) - } -} diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/ChatsViewModel.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/ChatsViewModel.kt new file mode 100644 index 0000000000..1daad4dcdf --- /dev/null +++ b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/ChatsViewModel.kt @@ -0,0 +1,79 @@ +package com.flipcash.app.tipping.internal + +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import com.flipcash.app.core.data.Loadable +import com.flipcash.libs.coroutines.DispatcherProvider +import com.flipcash.app.tokens.TokenCoordinator +import com.flipcash.services.models.chat.ChatType +import com.flipcash.services.user.UserManager +import com.flipcash.shared.chat.ChatCoordinator +import com.flipcash.shared.chat.ChatSummary +import com.flipcash.shared.chat.ui.ConversationReference +import com.flipcash.shared.chat.ui.toConversationReference +import com.getcode.opencode.model.financial.Token +import com.getcode.util.resources.ResourceHelper +import com.getcode.view.BaseViewModel +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +/** Backs the "Chats" tab: the user's tip DMs and groups. */ +@HiltViewModel +internal class ChatsViewModel @Inject constructor( + chatCoordinator: ChatCoordinator, + userManager: UserManager, + tokenCoordinator: TokenCoordinator, + private val resources: ResourceHelper, + dispatchers: DispatcherProvider, +) : BaseViewModel( + initialState = State(), + updateStateForEvent = updateStateForEvent, +) { + + data class State( + // Loading until the chat feed emits — distinguishes "still loading" from "loaded, none", so + // the empty state doesn't flash on top of a list that's about to arrive. + val chats: Loadable> = Loadable.Loading(), + ) + + sealed interface Event { + data class ChatsUpdated(val chats: Loadable>) : Event + } + + init { + fun conversations(summaries: List, tokens: List): List { + val selfId = userManager.accountId + val tokensByMint = tokens.associateBy { it.address } + return summaries.map { it.toConversationReference(selfId, tokensByMint, resources) } + } + + // On a cold launch the feed is usually built before this screen is, so draw it on the first + // frame rather than waiting for the collector below to get a turn on the main thread. + chatCoordinator.currentFeed(ChatType.TIP_DM, ChatType.GROUP)?.let { summaries -> + dispatchEvent( + Event.ChatsUpdated(Loadable.Loaded(conversations(summaries, tokenCoordinator.cachedTokens()))) + ) + } + + combine( + chatCoordinator.feed(ChatType.TIP_DM, ChatType.GROUP), + tokenCoordinator.tokens, + ::conversations, + ) + // Off the main thread: on a cold launch the first mapping lands while the main thread + // is drawing the app's first screens, and waiting for it holds the Chats tab blank. + .flowOn(dispatchers.Default) + .onEach { dispatchEvent(Event.ChatsUpdated(Loadable.Loaded(it))) }.launchIn(viewModelScope) + } + + internal companion object { + private val updateStateForEvent: (Event) -> (State.() -> State) = { event -> + when (event) { + is Event.ChatsUpdated -> { state -> state.copy(chats = event.chats) } + } + } + } +} diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/TipFlowViewModel.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/TipFlowViewModel.kt deleted file mode 100644 index ea5c968248..0000000000 --- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/TipFlowViewModel.kt +++ /dev/null @@ -1,149 +0,0 @@ -package com.flipcash.app.tipping.internal - -import androidx.lifecycle.viewModelScope -import dagger.hilt.android.lifecycle.HiltViewModel -import com.flipcash.app.core.bill.Scannable -import com.flipcash.app.core.data.Loadable -import com.flipcash.libs.coroutines.DispatcherProvider -import com.flipcash.app.core.extensions.onResult -import com.flipcash.app.core.tipping.TipStep -import com.flipcash.app.shareable.ShareSheetController -import com.flipcash.app.shareable.Shareable -import com.flipcash.app.bills.share.TipCodePreviewCache -import com.flipcash.app.tokens.TokenCoordinator -import com.flipcash.features.tipping.R -import com.flipcash.services.models.chat.ChatType -import com.flipcash.services.user.UserManager -import com.flipcash.shared.chat.ChatCoordinator -import com.flipcash.shared.chat.ChatSummary -import com.flipcash.shared.chat.ui.ConversationReference -import com.flipcash.shared.chat.ui.toConversationReference -import com.flipcash.shared.tipping.TippingCoordinator -import com.getcode.opencode.model.financial.Token -import com.getcode.util.resources.ResourceHelper -import com.getcode.view.BaseViewModel -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filterIsInstance -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.onEach -import javax.inject.Inject - -@HiltViewModel -internal class TipFlowViewModel @Inject constructor( - chatCoordinator: ChatCoordinator, - userManager: UserManager, - tippingCoordinator: TippingCoordinator, - tokenCoordinator: TokenCoordinator, - shareable: ShareSheetController, - tipCodePreviewCache: TipCodePreviewCache, - private val resources: ResourceHelper, - dispatchers: DispatcherProvider, -) : BaseViewModel( - initialState = State(), - updateStateForEvent = updateStateForEvent, -) { - - data class State( - /** - * The steps the flow opens on. - * - * This flow *is* the "Chats" root tab: it always shows the list (and its empty state) - * whatever the profile looks like, so a nameless account gets the same chrome as any other, - * and the claim-your-tip-card prompt lives on the You tab instead. Swapping a root tab out - * for a setup screen would also strand that screen's Close, which has no sheet to dismiss. - * The post-setup handoff is the one exception, and it is seeded from the route - * (see TippingFlowScreen). - */ - val steps: List = listOf(TipStep.Tips), - // true when re-entering right after the user-profile setup handoff (Tips(resumed = true)). - val resumed: Boolean = false, - val currentStep: TipStep? = null, - // Loading until the chat feed emits — distinguishes "still loading" from "loaded, none", so - // the empty state doesn't flash on top of a list that's about to arrive. - val tipChats: Loadable> = Loadable.Loading(), - val tipCard: Scannable.TipCard? = null, - ) - - sealed interface Event { - data class OnStepChanged(val step: TipStep) : Event - data class ChatsUpdated(val tips: Loadable>) : Event - - /** How the flow was entered — [resumed] is true for the post-setup handoff re-entry. */ - data class OnResumed(val resumed: Boolean) : Event - data class OnTipCardPopulated(val card: Scannable.TipCard) : Event - data object ShareTipCard: Event - } - - init { - // Rebuild the current user's tip card whenever their profile becomes available/changes. - userManager.state - .mapNotNull { it.userProfile } - .distinctUntilChanged() - .map { tippingCoordinator.resolveTipCard() } - .onResult(onSuccess = { card -> - dispatchEvent(Event.OnTipCardPopulated(card)) - // Render the Sharesheet preview eagerly so it's ready by the time the user taps share. - tippingCoordinator.currentUserId?.let { tipCodePreviewCache.prepare(it, card) } - }) - .launchIn(viewModelScope) - - fun conversations(summaries: List, tokens: List): List { - val selfId = userManager.accountId - val tokensByMint = tokens.associateBy { it.address } - return summaries.map { it.toConversationReference(selfId, tokensByMint, resources) } - } - - // On a cold launch the feed is usually built before this screen is, so draw it on the first - // frame rather than waiting for the collector below to get a turn on the main thread. - chatCoordinator.currentFeed(ChatType.TIP_DM, ChatType.GROUP)?.let { summaries -> - dispatchEvent( - Event.ChatsUpdated(Loadable.Loaded(conversations(summaries, tokenCoordinator.cachedTokens()))) - ) - } - - combine( - chatCoordinator.feed(ChatType.TIP_DM, ChatType.GROUP), - tokenCoordinator.tokens, - ::conversations, - ) - // Off the main thread: on a cold launch the first mapping lands while the main thread - // is drawing the app's first screens, and waiting for it holds the Chats tab blank. - .flowOn(dispatchers.Default) - .onEach { dispatchEvent(Event.ChatsUpdated(Loadable.Loaded(it))) }.launchIn(viewModelScope) - - eventFlow - .filterIsInstance() - .mapNotNull { tippingCoordinator.currentUserId } - .map { userId -> - // Title shown above the link, e.g. "Tip X" (same label as the card). - val title = stateFlow.value.tipCard?.user?.displayName - ?.let { resources.getString(R.string.label_tipUser, it) } - // Attach the eagerly-rendered preview if it's ready; null shares the URL alone. - shareable.present( - Shareable.TipCard( - userId = userId, - preview = tipCodePreviewCache.get(userId), - title = title, - username = stateFlow.value.tipCard?.user?.username, - ) - ) - } - .launchIn(viewModelScope) - } - - internal companion object { - private val updateStateForEvent: (Event) -> (State.() -> State) = { event -> - when (event) { - is Event.OnStepChanged -> { state -> state.copy(currentStep = event.step) } - is Event.ChatsUpdated -> { state -> state.copy(tipChats = event.tips) } - is Event.OnResumed -> { state -> state.copy(resumed = event.resumed) } - is Event.OnTipCardPopulated -> { state -> state.copy(tipCard = event.card) } - is Event.ShareTipCard -> { state -> state } - } - } - } -} diff --git a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipInfoScreen.kt b/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipInfoScreen.kt deleted file mode 100644 index 2d6ee799e8..0000000000 --- a/apps/flipcash/features/tipping/src/main/kotlin/com/flipcash/app/tipping/internal/screens/TipInfoScreen.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.flipcash.app.tipping.internal.screens - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewWrapper -import com.flipcash.app.core.AppRoute -import com.flipcash.app.core.DisplayNameSource -import com.flipcash.app.core.userprofile.UpdateProfileStep -import com.flipcash.app.core.LocalUserManager -import com.flipcash.app.core.tipping.TipResult -import com.flipcash.app.core.tipping.TipStep -import com.flipcash.app.theme.FlipcashThemeWrapper -import com.flipcash.core.R -import com.getcode.navigation.flow.LocalFlowNavigator -import com.getcode.navigation.flow.PreviewFlowNavigator -import com.getcode.navigation.flow.rememberFlowNavigator -import com.getcode.theme.CodeTheme -import com.getcode.ui.components.AppBarDefaults -import com.getcode.ui.components.AppBarWithTitle -import com.getcode.ui.theme.ButtonState -import com.getcode.ui.theme.CodeButton -import com.getcode.ui.theme.CodeScaffold - -@Composable -internal fun TipInfoScreen() { - val flowNavigator = rememberFlowNavigator() - val userManager = LocalUserManager.current - CodeScaffold( - topBar = { - AppBarWithTitle( - endContent = { AppBarDefaults.Close { flowNavigator.exitCanceled() }}) - }, - bottomBar = { - CodeButton( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = CodeTheme.dimens.inset) - .navigationBarsPadding() - .padding(vertical = CodeTheme.dimens.grid.x3), - text = stringResource(R.string.action_startReceivingTips), - buttonState = ButtonState.Filled, - onClick = { - flowNavigator.navigate( - AppRoute.UpdateUserProfile( - origin = AppRoute.Sheets.Tips(), - // A card only needs a name, and only if the account hasn't got one. - steps = if (userManager?.profile?.displayName.isNullOrEmpty()) { - listOf(UpdateProfileStep.Name(DisplayNameSource.TipCardSetup)) - } else { - emptyList() - }, - target = AppRoute.Sheets.Tips(resumed = true), - ) - ) - }, - ) - } - ) { padding -> - Box( - modifier = Modifier - .fillMaxSize() - .padding(padding), - ) { - Column( - modifier = Modifier.align(Alignment.Center), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Image( - painter = painterResource(R.drawable.ic_tipping_hand_large), - contentDescription = null, - ) - - Text( - text = stringResource(R.string.title_tipIntro), - style = CodeTheme.typography.displaySmall, - color = CodeTheme.colors.textMain, - ) - Text( - modifier = Modifier - .padding(top = CodeTheme.dimens.grid.x2) - .padding(horizontal = CodeTheme.dimens.inset), - text = stringResource(R.string.subtitle_tipIntro), - style = CodeTheme.typography.textSmall, - color = CodeTheme.colors.textSecondary, - textAlign = TextAlign.Center, - ) - } - } - } -} - -@Preview -@PreviewWrapper(FlipcashThemeWrapper::class) -@Composable -private fun Preview_TipIntro() { - CompositionLocalProvider( - LocalFlowNavigator provides PreviewFlowNavigator(), - ) { - TipInfoScreen() - } -} diff --git a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt index 3db5373635..9cbfdf396e 100644 --- a/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt +++ b/apps/flipcash/features/withdrawal/src/main/kotlin/com/flipcash/app/withdrawal/WithdrawalFlowScreen.kt @@ -71,7 +71,7 @@ fun WithdrawalFlowScreen( ) when (result) { WithdrawalResult.Success -> { - outerNavigator.popUntil { it == AppRoute.Sheets.Menu } + outerNavigator.popUntil { it == AppRoute.Tabs.Menu } } WithdrawalResult.Canceled -> { outerNavigator.pop() diff --git a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/decor/PayableDecorator.kt b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/decor/PayableDecorator.kt index 90a5f168b3..85f0971772 100644 --- a/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/decor/PayableDecorator.kt +++ b/apps/flipcash/shared/bills/src/main/kotlin/com/flipcash/app/bills/decor/PayableDecorator.kt @@ -100,7 +100,7 @@ internal data class PayableDecorator(private val bill: Scannable.Payable) : Scan onClaim = { if (session == null) context.onDismiss(PutInWallet) else if (session.claimReceivedFunds()) { - navigator.navigateAll(listOf(AppRoute.Sheets.Wallet)) + navigator.navigateAll(listOf(AppRoute.Tabs.Wallet)) } } ) diff --git a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/ChatTapTargetPlanner.kt b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/ChatTapTargetPlanner.kt index 5605aae7e2..5c7c879b64 100644 --- a/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/ChatTapTargetPlanner.kt +++ b/apps/flipcash/shared/notifications/src/main/kotlin/com/flipcash/app/notifications/ChatTapTargetPlanner.kt @@ -26,7 +26,7 @@ sealed interface ChatTapTarget { /** * Whether a chat push can open its conversation, and by which link. * - * The Chats tab lists tip DMs and groups (`TipFlowViewModel`: `feed(TIP_DM, GROUP)`), and both open + * The Chats tab lists tip DMs and groups (`ChatsViewModel`: `feed(TIP_DM, GROUP)`), and both open * by chat id from there. A `CONTACT_DM` is not: the Send tab and direct-send flow that used to open * one were removed, and `AppRouter` deliberately routes nothing for them. * diff --git a/apps/flipcash/shared/permissions/src/main/kotlin/com/flipcash/app/permissions/NotificationPermissionScreen.kt b/apps/flipcash/shared/permissions/src/main/kotlin/com/flipcash/app/permissions/NotificationPermissionScreen.kt index 55516fd63f..ef36f92c59 100644 --- a/apps/flipcash/shared/permissions/src/main/kotlin/com/flipcash/app/permissions/NotificationPermissionScreen.kt +++ b/apps/flipcash/shared/permissions/src/main/kotlin/com/flipcash/app/permissions/NotificationPermissionScreen.kt @@ -25,7 +25,7 @@ fun NotificationPermissionScreen(fromOnboarding: Boolean = false) { analytics.action(Button.AllowPush) if (fromOnboarding) analytics.action(Action.CompletedOnboarding) navigator.navigate( - route = AppRoute.Main.Scanner, + route = AppRoute.Tabs.Scanner, options = NavOptions(popUpTo = NavOptions.PopUpTo.ClearAll) ) } @@ -42,7 +42,7 @@ fun NotificationPermissionScreen(fromOnboarding: Boolean = false) { LaunchedEffect(permissionState.status) { when (permissionState.status) { PermissionResult.Granted -> navigator.navigate( - route = AppRoute.Main.Scanner, + route = AppRoute.Tabs.Scanner, options = NavOptions(popUpTo = NavOptions.PopUpTo.ClearAll) ) PermissionResult.PermanentlyDenied -> navigator.push( @@ -61,7 +61,7 @@ fun NotificationPermissionScreen(fromOnboarding: Boolean = false) { onSkip = { analytics.action(Button.SkipPush) navigator.navigate( - route = AppRoute.Main.Scanner, + route = AppRoute.Tabs.Scanner, options = NavOptions(popUpTo = NavOptions.PopUpTo.ClearAll) ) } @@ -77,7 +77,7 @@ fun NotificationPermissionRationaleScreen(permanentlyDenied: Boolean = false) { permanentlyDenied = permanentlyDenied, onComplete = { navigator.navigate( - route = AppRoute.Main.Scanner, + route = AppRoute.Tabs.Scanner, options = NavOptions(popUpTo = NavOptions.PopUpTo.ClearAll) ) }, diff --git a/apps/flipcash/shared/router/src/main/kotlin/com/flipcash/app/router/internal/AppRouter.kt b/apps/flipcash/shared/router/src/main/kotlin/com/flipcash/app/router/internal/AppRouter.kt index 9411ffd1bc..4621f02c30 100644 --- a/apps/flipcash/shared/router/src/main/kotlin/com/flipcash/app/router/internal/AppRouter.kt +++ b/apps/flipcash/shared/router/src/main/kotlin/com/flipcash/app/router/internal/AppRouter.kt @@ -119,7 +119,7 @@ internal class AppRouter( is DeeplinkType.TokenInfo -> DeeplinkAction.OpenToken( mint = type.mint, routes = listOf( - AppRoute.Sheets.Wallet, + AppRoute.Tabs.Wallet, AppRoute.Token.Info(type.mint, TokenInfoEntry.Deeplink), ), ) @@ -127,7 +127,7 @@ internal class AppRouter( is DeeplinkType.EmailVerification -> resolveEmailVerification(type) is DeeplinkType.TipChat -> DeeplinkAction.Navigate( - listOf(AppRoute.Sheets.Tips(), AppRoute.Messaging.Chat(type.identifier)) + listOf(AppRoute.Tabs.Chats, AppRoute.Messaging.Chat(type.identifier)) ) // The same destination a group push tap resolves to (see ChatTapTargetPlanner): the @@ -135,7 +135,7 @@ internal class AppRouter( // a push land on one screen rather than on two that would have to agree. is DeeplinkType.GroupChatInvite -> DeeplinkAction.Navigate( listOf( - AppRoute.Sheets.Tips(), + AppRoute.Tabs.Chats, AppRoute.Messaging.Chat(ChatIdentifier.ByChatId(type.chatId)), ) ) @@ -149,7 +149,7 @@ internal class AppRouter( /** * Where a tip card link goes. Your own leads nowhere payable, so instead of presenting a card * that can't be acted on it lands on the You tab — the surface that owns your tip card (see - * NavBarRoutes: NavBarButton.TipCard -> Sheets.Menu). + * NavBarRoutes: NavBarButton.TipCard -> Tabs.Menu). * * The self-check belongs here and not after resolution: the session announces a self-tip * through a replay-less event that only the scanner collects, so a link opened onto any other @@ -158,7 +158,7 @@ internal class AppRouter( private fun tipCard(owner: TipCardOwner): DeeplinkAction { val self = currentUserProvider() return if (owner.isSelf(self.id, self.username)) { - DeeplinkAction.Navigate(listOf(AppRoute.Sheets.Menu)) + DeeplinkAction.Navigate(listOf(AppRoute.Tabs.Menu)) } else { DeeplinkAction.PresentTipCard(owner) } @@ -259,7 +259,7 @@ internal class AppRouter( EmailDeeplinkOrigin.MyAccount -> listOf( - AppRoute.Sheets.Menu, + AppRoute.Tabs.Menu, AppRoute.Menu.MyAccount ) + AppRoute.Verification( origin = AppRoute.Menu.MyAccount, diff --git a/apps/flipcash/shared/router/src/test/kotlin/com/flipcash/app/router/internal/AppRouterTest.kt b/apps/flipcash/shared/router/src/test/kotlin/com/flipcash/app/router/internal/AppRouterTest.kt index 6914edec2e..ad75cb0dcc 100644 --- a/apps/flipcash/shared/router/src/test/kotlin/com/flipcash/app/router/internal/AppRouterTest.kt +++ b/apps/flipcash/shared/router/src/test/kotlin/com/flipcash/app/router/internal/AppRouterTest.kt @@ -237,7 +237,7 @@ class AppRouterTest { loggedIn() val action = router.dispatch(DeepLink(verifyUrl("myaccount"))) assertIs(action) - assertEquals(AppRoute.Sheets.Menu, action.routes[0]) + assertEquals(AppRoute.Tabs.Menu, action.routes[0]) assertIs(action.routes[1]) assertIs(action.routes[2]) } @@ -338,7 +338,7 @@ class AppRouterTest { val action = router.dispatch(DeepLink("https://app.flipcash.com/token/$mint")) assertIs(action) assertEquals(2, action.routes.size) - assertIs(action.routes[0]) + assertIs(action.routes[0]) val tokenInfo = action.routes[1] assertIs(tokenInfo) assertEquals(Mint(mint), tokenInfo.mint) @@ -379,7 +379,7 @@ class AppRouterTest { val action = router.dispatch(DeepLink(Linkify.tipChatById(sampleChatId))) assertIs(action) assertEquals(2, action.routes.size) - assertIs(action.routes[0]) + assertIs(action.routes[0]) val chat = action.routes[1] assertIs(chat) val identifier = chat.identifier @@ -409,7 +409,7 @@ class AppRouterTest { // Tipping yourself is a payment no-op, so the link lands on the You tab instead. assertIs(action) - assertEquals(listOf(AppRoute.Sheets.Menu), action.routes) + assertEquals(listOf(AppRoute.Tabs.Menu), action.routes) } @Test @@ -477,7 +477,7 @@ class AppRouterTest { assertIs(action) assertEquals(2, action.routes.size) - assertIs(action.routes[0]) + assertIs(action.routes[0]) val chat = action.routes[1] assertIs(chat) val identifier = chat.identifier @@ -602,7 +602,7 @@ class AppRouterTest { val action = router.dispatch(DeepLink("https://flipcash.com/$userId")) assertIs(action) - assertEquals(AppRoute.Sheets.Menu, action.routes.single()) + assertEquals(AppRoute.Tabs.Menu, action.routes.single()) } // UUID.fromString would take these; the regex doesn't. A shape the app doesn't claim goes back @@ -649,7 +649,7 @@ class AppRouterTest { currentUsername = "sally_streamer" val action = router.dispatch(DeepLink(Linkify.tipcard(TipCardOwner.ByUsername("sally_streamer")))) assertIs(action) - assertEquals(AppRoute.Sheets.Menu, action.routes.single()) + assertEquals(AppRoute.Tabs.Menu, action.routes.single()) } // A link can be typed or pasted in any case; handles are lowercase on the wire. @@ -659,7 +659,7 @@ class AppRouterTest { currentUsername = "sally_streamer" val action = router.dispatch(DeepLink("https://flipcash.com/Sally_Streamer")) assertIs(action) - assertEquals(AppRoute.Sheets.Menu, action.routes.single()) + assertEquals(AppRoute.Tabs.Menu, action.routes.single()) } @Test @@ -710,7 +710,7 @@ class AppRouterTest { val action = router.dispatch(DeepLink(url)) assertIs(action) assertEquals(3, action.routes.size) - assertIs(action.routes[0]) + assertIs(action.routes[0]) assertIs(action.routes[1]) val verification = action.routes[2] assertIs(verification) diff --git a/apps/flipcash/shared/router/src/test/kotlin/com/flipcash/app/router/internal/NavigateToTest.kt b/apps/flipcash/shared/router/src/test/kotlin/com/flipcash/app/router/internal/NavigateToTest.kt index 9ae91d888b..d79877ae90 100644 --- a/apps/flipcash/shared/router/src/test/kotlin/com/flipcash/app/router/internal/NavigateToTest.kt +++ b/apps/flipcash/shared/router/src/test/kotlin/com/flipcash/app/router/internal/NavigateToTest.kt @@ -40,7 +40,7 @@ class NavigateToTest { @Test fun `navigateTo without existing sheet navigates directly`() { - val navigator = createNavigator(AppRoute.Main.Scanner) + val navigator = createNavigator(AppRoute.Tabs.Scanner) val mint = Mint("So11111111111111111111111111111111111111112") navigator.navigateAll( @@ -56,7 +56,7 @@ class NavigateToTest { @Test fun `navigateTo non-sheet routes navigates directly even with existing sheet`() { val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet(AppRoute.Sheets.ActivityHistory), ) @@ -72,7 +72,7 @@ class NavigateToTest { @Test fun `navigateTo with existing sheet sets pendingSheetDismiss`() { val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet(AppRoute.Sheets.ActivityHistory), ) val mint = Mint("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") @@ -90,7 +90,7 @@ class NavigateToTest { @Test fun `pendingSheetDismiss callback increments sheetGeneration`() { val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet(AppRoute.Sheets.ActivityHistory), ) val initialGeneration = navigator.sheetGeneration @@ -107,7 +107,7 @@ class NavigateToTest { @Test fun `pendingSheetDismiss callback navigates to new routes`() { val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet(AppRoute.Sheets.ActivityHistory), ) val mint = Mint("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v") @@ -130,7 +130,7 @@ class NavigateToTest { @Test fun `repeated dismiss-then-replace increments generation each time`() { val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet(AppRoute.Sheets.ActivityHistory), ) @@ -153,7 +153,7 @@ class NavigateToTest { @Test fun `empty routes is a no-op`() { - val navigator = createNavigator(AppRoute.Main.Scanner) + val navigator = createNavigator(AppRoute.Tabs.Scanner) navigator.navigateAll(emptyList(), options = quietOptions) @@ -165,7 +165,7 @@ class NavigateToTest { fun `same token dismiss-then-replace works`() { val mint = Mint("So11111111111111111111111111111111111111112") val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet( AppRoute.Sheets.ActivityHistory, listOf(AppRoute.Token.Info(mint, TokenInfoEntry.Deeplink)), @@ -188,7 +188,7 @@ class NavigateToTest { // backstack. The callback's navigate() must not produce a duplicate Sheet key // (which would crash SaveableStateProvider). val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet(AppRoute.Sheets.ActivityHistory), ) @@ -211,7 +211,7 @@ class NavigateToTest { @Test fun `dismiss-replace increments sheetGeneration for composition scoping`() { val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet(AppRoute.Sheets.ActivityHistory), ) @@ -234,7 +234,7 @@ class NavigateToTest { // backstack after onBack removed the wrong entry during a dismiss animation. // A subsequent navigate for the same Sheet must not produce a duplicate. val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet(AppRoute.Sheets.ActivityHistory), ) @@ -253,16 +253,16 @@ class NavigateToTest { @Test fun `double navigateTo with pending dismiss does not produce duplicate sheets`() { val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet(AppRoute.Sheets.ActivityHistory), ) // First navigate sets pendingSheetDismiss - navigator.navigateAll(listOf(AppRoute.Sheets.Menu), options = quietOptions) + navigator.navigateAll(listOf(AppRoute.Tabs.Menu), options = quietOptions) assertNotNull(navigator.pendingSheetDismiss) // Second navigate overwrites pendingSheetDismiss - navigator.navigateAll(listOf(AppRoute.Sheets.Menu), options = quietOptions) + navigator.navigateAll(listOf(AppRoute.Tabs.Menu), options = quietOptions) // Simulate: onBack removes old sheet, then callback fires navigator.backStack.removeAt(navigator.backStack.lastIndex) @@ -270,7 +270,7 @@ class NavigateToTest { // Simulate a stale callback also firing navigate for the same sheet navigator.navigate( - AppRoute.Main.Sheet(AppRoute.Sheets.Menu), + AppRoute.Main.Sheet(AppRoute.Tabs.Menu), NavOptions(debugRouting = false), ) @@ -285,7 +285,7 @@ class NavigateToTest { @Test fun `openAsSheet with existing sheet sets pendingSheetDismiss`() { val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet(AppRoute.Sheets.ActivityHistory), ) @@ -299,7 +299,7 @@ class NavigateToTest { @Test fun `openAsSheet callback navigates to new sheet after dismiss`() { val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet(AppRoute.Sheets.ActivityHistory), ) @@ -317,7 +317,7 @@ class NavigateToTest { @Test fun `openAsSheet increments sheetGeneration on dismiss-replace`() { val navigator = createNavigator( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Main.Sheet(AppRoute.Sheets.ActivityHistory), ) val initialGeneration = navigator.sheetGeneration @@ -333,7 +333,7 @@ class NavigateToTest { @Test fun `openAsSheet without existing sheet navigates directly`() { - val navigator = createNavigator(AppRoute.Main.Scanner) + val navigator = createNavigator(AppRoute.Tabs.Scanner) navigator.openAsSheet(AppRoute.Sheets.ShareApp) diff --git a/apps/flipcash/shared/router/src/test/kotlin/com/flipcash/app/router/internal/ResolveRoutesTest.kt b/apps/flipcash/shared/router/src/test/kotlin/com/flipcash/app/router/internal/ResolveRoutesTest.kt index 93e7cfd542..340f1422e0 100644 --- a/apps/flipcash/shared/router/src/test/kotlin/com/flipcash/app/router/internal/ResolveRoutesTest.kt +++ b/apps/flipcash/shared/router/src/test/kotlin/com/flipcash/app/router/internal/ResolveRoutesTest.kt @@ -32,7 +32,7 @@ class ResolveRoutesTest { @Test fun `non-sheet routes pass through unchanged`() { - val routes = listOf(AppRoute.Main.Scanner, AppRoute.Menu.MyAccount) + val routes = listOf(AppRoute.Tabs.Scanner, AppRoute.Menu.MyAccount) assertEquals(routes, resolveRoutes(routes)) } @@ -92,14 +92,14 @@ class ResolveRoutesTest { @Test fun `routes before sheet stay on root backstack`() { val routes = listOf( - AppRoute.Main.Scanner, + AppRoute.Tabs.Scanner, AppRoute.Sheets.ActivityHistory, AppRoute.Token.Info(mint), ) val resolved = resolveRoutes(routes) assertEquals(2, resolved.size) - assertIs(resolved[0]) + assertIs(resolved[0]) val sheet = resolved[1] assertIs(sheet) assertEquals(AppRoute.Sheets.ActivityHistory, sheet.initialRoute) @@ -141,7 +141,7 @@ class ResolveRoutesTest { @Test fun `wallet tab stays flat and token info pushes on top`() { val routes = listOf( - AppRoute.Sheets.Wallet, + AppRoute.Tabs.Wallet, AppRoute.Token.Info(mint, TokenInfoEntry.Deeplink), ) @@ -153,7 +153,7 @@ class ResolveRoutesTest { @Test fun `chats tab stays flat and the chat pushes on top`() { val routes = listOf( - AppRoute.Sheets.Tips(), + AppRoute.Tabs.Chats, AppRoute.Messaging.Chat(ChatIdentifier.ByChatId(ChatId(listOf(1, 2, 3, 4)))), ) @@ -165,7 +165,7 @@ class ResolveRoutesTest { @Test fun `menu tab stays flat with my account and verification pushed on top`() { val routes = listOf( - AppRoute.Sheets.Menu, + AppRoute.Tabs.Menu, AppRoute.Menu.MyAccount, AppRoute.Verification( origin = AppRoute.Menu.MyAccount, @@ -181,14 +181,14 @@ class ResolveRoutesTest { @Test fun `a genuine sheet that follows a tab home still wraps`() { val routes = listOf( - AppRoute.Sheets.Wallet, + AppRoute.Tabs.Wallet, AppRoute.Token.Info(mint), AppRoute.Sheets.ActivityHistory, ) val resolved = resolveRoutes(routes) assertEquals(3, resolved.size) - assertEquals(AppRoute.Sheets.Wallet, resolved[0]) + assertEquals(AppRoute.Tabs.Wallet, resolved[0]) assertIs(resolved[1]) val sheet = resolved[2] assertIs(sheet) @@ -201,32 +201,32 @@ class ResolveRoutesTest { @Test fun `a deeplink to a tab replaces the launch home rather than stacking on it`() { - val base = listOf(AppRoute.Sheets.Wallet) + val base = listOf(AppRoute.Tabs.Wallet) val deeplink = listOf( - AppRoute.Sheets.Tips(), + AppRoute.Tabs.Chats, AppRoute.Messaging.Chat(ChatIdentifier.ByChatId(ChatId(listOf(9)))), ) val stack = resolveBackStack(base, deeplink) assertEquals(deeplink, stack) // The Wallet home the app launched on must not linger beneath the Chats tab. - assertTrue(stack.none { it == AppRoute.Sheets.Wallet }) + assertTrue(stack.none { it == AppRoute.Tabs.Wallet }) } @Test fun `a token deeplink lands on the wallet tab exactly once`() { - val base = listOf(AppRoute.Sheets.Wallet) - val deeplink = listOf(AppRoute.Sheets.Wallet, AppRoute.Token.Info(mint, TokenInfoEntry.Deeplink)) + val base = listOf(AppRoute.Tabs.Wallet) + val deeplink = listOf(AppRoute.Tabs.Wallet, AppRoute.Token.Info(mint, TokenInfoEntry.Deeplink)) val stack = resolveBackStack(base, deeplink) assertEquals(2, stack.size) - assertEquals(1, stack.count { it == AppRoute.Sheets.Wallet }) + assertEquals(1, stack.count { it == AppRoute.Tabs.Wallet }) assertIs(stack[1]) } @Test fun `a deeplink without a tab home stacks on the launch home`() { - val base = listOf(AppRoute.Sheets.Wallet) + val base = listOf(AppRoute.Tabs.Wallet) val deeplink = listOf(AppRoute.Token.Info(mint), AppRoute.Token.Swap(SwapPurpose.Buy(mint))) assertEquals(base + deeplink, resolveBackStack(base, deeplink)) diff --git a/docs/architecture/features/README.md b/docs/architecture/features/README.md index b310b78041..35532f0936 100644 --- a/docs/architecture/features/README.md +++ b/docs/architecture/features/README.md @@ -36,7 +36,7 @@ Two recurring shapes show up in the **Pattern** column: | **scanner** | Camera capture of a peer's Kik Code (e.g. on a cash bill) | `scanner/ScannerScreen.kt` | Screen-local; uses `:ui:scanner`, routes scans through `Router.classify` | | **withdrawal** | Withdraw funds on-chain | `withdrawal/WithdrawalFlowScreen.kt` · `withdrawal/WithdrawalViewModel` | **Flow** (`AppRoute.Transfers.Withdrawal`); entry → destination → confirmation | | **deposit** | Add funds via on-ramp / deposit address | `deposit/DepositFlowScreen.kt` · `…/internal/DepositViewModel` | **Flow** (`FlowRouteWithResult`); USDC deposit info, Coinbase on-ramp | -| **tipping** | Tip card, tip chats, and tip amount entry | `tipping/TippingFlowScreen.kt`, `tipping/TipCardScreen.kt` · `…/internal/TipFlowViewModel` | **Flow** (`AppRoute.Sheets.Tips`); intro → tip card → tips steps | +| **tipping** | The Chats tab (tip DMs and groups), new chat, and new group | `tipping/ChatsScreen.kt` · `…/internal/ChatsViewModel` | **VM-driven** tab home (`AppRoute.Tabs.Chats`); combines the chat feed with tokens | | **transactions** | Activity / transaction history | `transactions/TransactionHistoryScreen.kt` · `…/internal/TransactionHistoryViewModel` | **VM-driven**; Paging-backed list from persistence ([05](../05-persistence.md)) | ## C. Currencies & tokens diff --git a/maestro/README.md b/maestro/README.md index 8ebe32d1cc..cccaa909b7 100644 --- a/maestro/README.md +++ b/maestro/README.md @@ -73,7 +73,7 @@ Every routed screen is addressable by a stable `_screen` resource-id. Thes - The tag defaults to one **derived from the route type name** (`screenRootTag` in `NavMetadata.kt`): `AppRoute.Menu.MyAccount` → `my_account_screen`, - `AppRoute.Main.Scanner` → `scanner_screen`. + `AppRoute.Tabs.Scanner` → `scanner_screen`. - Pass an explicit `testTag` only when a route needs a different id than its type name, e.g. `annotatedEntry(testTag = "cash_screen") { ... }`. diff --git a/maestro/blocking.yaml b/maestro/blocking.yaml index 1d9ebdc008..ee62e345dd 100644 --- a/maestro/blocking.yaml +++ b/maestro/blocking.yaml @@ -11,7 +11,7 @@ tags: # Open the tip conversation (the chats tab). - tapOn: { id: nav_chats } -- extendedWaitUntil: { visible: { id: tips_screen }, timeout: 8000 } +- extendedWaitUntil: { visible: { id: chats_screen }, timeout: 8000 } # Pick the chat by participant rather than by position: the list is ordered by recency and # carries the account's own self-tip chat, so index 0 is not stably the contact. - scrollUntilVisible: @@ -26,7 +26,7 @@ tags: - tapOn: "Block" - assertVisible: "Block ${CONTACT_NAME}?" - tapOn: { text: "^Block$" } -- extendedWaitUntil: { visible: { id: tips_screen }, timeout: 8000 } +- extendedWaitUntil: { visible: { id: chats_screen }, timeout: 8000 } # Verify they appear in My Account -> Blocked. Chats is a root tab, so the You tab is one tap # away rather than behind a sheet dismiss. diff --git a/maestro/subflows/create_account.yaml b/maestro/subflows/create_account.yaml index a2aff5774b..ae1dd8c4e4 100644 --- a/maestro/subflows/create_account.yaml +++ b/maestro/subflows/create_account.yaml @@ -29,5 +29,5 @@ appId: com.flipcash.app.android text: "Allow" optional: true -# Onboarding releases to the Wallet tab, not the camera (homeRoute = AppRoute.Sheets.Wallet). +# Onboarding releases to the Wallet tab, not the camera (homeRoute = AppRoute.Tabs.Wallet). - extendedWaitUntil: { visible: { id: wallet_screen }, timeout: 20000 } diff --git a/maestro/subflows/login.yaml b/maestro/subflows/login.yaml index a5a5e05529..b2223fa314 100644 --- a/maestro/subflows/login.yaml +++ b/maestro/subflows/login.yaml @@ -9,7 +9,7 @@ appId: com.flipcash.app.android id: "seed_input_field" - inputText: ${SEED_PHRASE} - tapOn: Log in -# Launch lands on the Wallet tab, not the camera (homeRoute = AppRoute.Sheets.Wallet). +# Launch lands on the Wallet tab, not the camera (homeRoute = AppRoute.Tabs.Wallet). - extendedWaitUntil: visible: id: "wallet_screen" diff --git a/maestro/subflows/login_usdf_only.yaml b/maestro/subflows/login_usdf_only.yaml index 9ea1dfd316..aa42328f31 100644 --- a/maestro/subflows/login_usdf_only.yaml +++ b/maestro/subflows/login_usdf_only.yaml @@ -9,7 +9,7 @@ appId: com.flipcash.app.android clearAppState: "true" deeplink: ${USDF_ONLY_DEEPLINK} -# Launch lands on the Wallet tab, not the camera (homeRoute = AppRoute.Sheets.Wallet). +# Launch lands on the Wallet tab, not the camera (homeRoute = AppRoute.Tabs.Wallet). - extendedWaitUntil: visible: id: "wallet_screen" diff --git a/maestro/subflows/login_with_deeplink.yaml b/maestro/subflows/login_with_deeplink.yaml index 4d0cf5e9b7..9b5e8d112c 100644 --- a/maestro/subflows/login_with_deeplink.yaml +++ b/maestro/subflows/login_with_deeplink.yaml @@ -7,7 +7,7 @@ appId: com.flipcash.app.android clearAppState: "true" deeplink: ${LOGIN_DEEPLINK} -# Launch lands on the Wallet tab, not the camera (homeRoute = AppRoute.Sheets.Wallet). +# Launch lands on the Wallet tab, not the camera (homeRoute = AppRoute.Tabs.Wallet). - extendedWaitUntil: visible: id: "wallet_screen" diff --git a/maestro/subflows/open_tip_chat.yaml b/maestro/subflows/open_tip_chat.yaml index f29a8bfa40..fc7554438b 100644 --- a/maestro/subflows/open_tip_chat.yaml +++ b/maestro/subflows/open_tip_chat.yaml @@ -4,7 +4,7 @@ appId: com.flipcash.app.android # The list is ordered by recency and carries the account's own self-tip chat, so index 0 is not # stably the contact — open it by name. - tapOn: { id: nav_chats } -- extendedWaitUntil: { visible: { id: tips_screen }, timeout: 8000 } +- extendedWaitUntil: { visible: { id: chats_screen }, timeout: 8000 } - assertVisible: { id: send_contact_row } - scrollUntilVisible: element: "${CONTACT_NAME}" diff --git a/maestro/tipping_setup.yaml b/maestro/tipping_setup.yaml index ac5df7fb4f..c56cabbabc 100644 --- a/maestro/tipping_setup.yaml +++ b/maestro/tipping_setup.yaml @@ -13,7 +13,7 @@ tags: # Tipping is on by default -> the chats tab is present. - assertVisible: { id: nav_chats } - tapOn: { id: nav_chats } -- extendedWaitUntil: { visible: { id: tips_screen }, timeout: 8000 } +- extendedWaitUntil: { visible: { id: chats_screen }, timeout: 8000 } # The display name is captured during onboarding, so tip setup lands directly on the # Tips list — no name step here. Open the tip card from the list. From b6c3194a02fb6e112b2e5bc7166659c3b37ddb68 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 23 Sep 2026 14:31:11 -0400 Subject: [PATCH 2/2] test(maestro): wait for the Chats tab after a seed login Since #1529 a signed-in launch with no deeplink opens on Chats, but subflows/login.yaml still waited for wallet_screen. Every flow that logs in by seed timed out on that check before reaching its own steps. Flows that need the wallet after login already go through navigate_to_wallet. --- maestro/subflows/login.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/maestro/subflows/login.yaml b/maestro/subflows/login.yaml index b2223fa314..f75bfedd2a 100644 --- a/maestro/subflows/login.yaml +++ b/maestro/subflows/login.yaml @@ -9,7 +9,7 @@ appId: com.flipcash.app.android id: "seed_input_field" - inputText: ${SEED_PHRASE} - tapOn: Log in -# Launch lands on the Wallet tab, not the camera (homeRoute = AppRoute.Tabs.Wallet). +# A seed login with no deeplink lands on the Chats tab (AppRoute.Tabs.Chats). - extendedWaitUntil: visible: - id: "wallet_screen" + id: "chats_screen"