From 21ece9bb522b8ba142ea901d4df7e56494a43144 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Thu, 20 Aug 2026 13:24:23 -0400 Subject: [PATCH] fix(onboarding): land on the wallet tab, not the camera MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The end of onboarding hardcoded AppRoute.Main.Scanner on both exit paths of PermissionsPhaseFlowHost, so a freshly onboarded v2 user was dropped on the camera and only saw the wallet on the next cold launch. Launch already picked the right home (Sheets.Wallet under FeatureFlag.NewUi, Main.Scanner under v1) — the two just drifted apart. Extract that choice into a shared homeRoute(isNewUi) helper and use it in both places. The v2 wallet is a flat tab home, so navigating to it with ClearAll is exactly what a nav bar tab tap does. --- .../app/internal/ui/navigation/MainRoot.kt | 3 +- .../flipcash/app/core/navigation/HomeRoute.kt | 14 +++++++++ .../app/core/navigation/HomeRouteTest.kt | 29 +++++++++++++++++++ .../app/login/OnboardingFlowScreen.kt | 22 +++++++++----- 4 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/HomeRoute.kt create mode 100644 apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/HomeRouteTest.kt diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt index 991feab8cd..c1894a8527 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/MainRoot.kt @@ -25,6 +25,7 @@ import com.flipcash.app.android.R import com.flipcash.app.core.LocalUserManager import com.flipcash.app.core.AppRoute import com.flipcash.app.core.navigation.DeeplinkAction +import com.flipcash.app.core.navigation.homeRoute import com.flipcash.app.core.extensions.navigateAll import com.flipcash.app.core.extensions.resolveBackStack import com.flipcash.app.featureflags.LocalFeatureFlags @@ -219,7 +220,7 @@ internal fun buildNavGraphForLaunch( AuthState.Ready -> { // New UI opens on the Wallet tab; v1 opens on the Scanner. - val home = if (isNewUi) AppRoute.Sheets.Wallet else AppRoute.Main.Scanner + val home = homeRoute(isNewUi) val link = deepLink() if (link != null) { when (val action = router.dispatch(link)) { 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 new file mode 100644 index 0000000000..fe605365b5 --- /dev/null +++ b/apps/flipcash/core/src/main/kotlin/com/flipcash/app/core/navigation/HomeRoute.kt @@ -0,0 +1,14 @@ +package com.flipcash.app.core.navigation + +import com.flipcash.app.core.AppRoute + +/** + * The top-level route the app releases to once there's a session — on launch and when onboarding + * finishes. v2 opens on the Wallet tab; v1 has no tab bar and opens on the Scanner. + * + * Under [isNewUi] the wallet is a flat tab home (not a sheet), so this route is applied with + * `replaceAll` / [com.getcode.navigation.core.NavOptions.PopUpTo.ClearAll], exactly like a tab + * switch from the nav bar. + */ +fun homeRoute(isNewUi: Boolean): AppRoute = + if (isNewUi) AppRoute.Sheets.Wallet else AppRoute.Main.Scanner 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 new file mode 100644 index 0000000000..96787b5d6c --- /dev/null +++ b/apps/flipcash/core/src/test/kotlin/com/flipcash/app/core/navigation/HomeRouteTest.kt @@ -0,0 +1,29 @@ +package com.flipcash.app.core.navigation + +import com.flipcash.app.core.AppRoute +import kotlin.test.Test +import kotlin.test.assertEquals + +/** + * Home is where both launch and the end of onboarding release the user to. Under v2 that's the + * Wallet tab — not the camera — so a freshly onboarded user lands on their balance. + */ +class HomeRouteTest { + + @Test + fun `v2 home is the wallet tab`() { + assertEquals(AppRoute.Sheets.Wallet, homeRoute(isNewUi = true)) + } + + @Test + fun `v1 home is the scanner`() { + assertEquals(AppRoute.Main.Scanner, homeRoute(isNewUi = false)) + } + + @Test + fun `v2 home is a nav bar tab home`() { + // Guards the release path: it's applied with ClearAll, so it must map to a tab or the + // hoisted nav bar would render with no selection. + assertEquals(NavBarButton.Wallet, homeRoute(isNewUi = true).asNavBarTab()) + } +} diff --git a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/OnboardingFlowScreen.kt b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/OnboardingFlowScreen.kt index b5c3e6567c..343aaaadc4 100644 --- a/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/OnboardingFlowScreen.kt +++ b/apps/flipcash/features/login/src/main/kotlin/com/flipcash/app/login/OnboardingFlowScreen.kt @@ -27,6 +27,7 @@ import com.flipcash.app.analytics.Button import com.flipcash.app.core.AppRoute import com.flipcash.app.core.LocalUserManager import com.flipcash.app.core.extensions.openAsSheet +import com.flipcash.app.core.navigation.homeRoute import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.LocalFeatureFlags import com.flipcash.app.core.onboarding.OnboardingResult @@ -80,17 +81,17 @@ import kotlin.time.Duration.Companion.milliseconds * ``` * 1. New account (ResumePoint.Login → ProceedToVerification) * - * Start → AccessKey ──┬────────────→ Name² → Contacts¹ → Notifications → Scanner + * Start → AccessKey ──┬────────────→ Name² → Contacts¹ → Notifications → Home³ * └→ Purchase ─┘ * * 2. Seed restore (ResumePoint.Login → LoggedIn via SeedInput) * - * Start → SeedInput ──┬────────────→ Name² → Contacts¹ → Notifications → Scanner + * Start → SeedInput ──┬────────────→ Name² → Contacts¹ → Notifications → Home³ * └→ Purchase ─┘ * * 3. App resume (ResumePoint.PostAccessKey) * - * → Notifications → Scanner + * → Notifications → Home³ * (contacts and name entry skipped — existing users encounter these in-app) * * 4. Mid-flow resume (ResumePoint.AccessKey / AccessKeyThenPurchase / DisplayName) @@ -104,6 +105,8 @@ import kotlin.time.Duration.Companion.milliseconds * [PermissionsPhaseFlowHost]. * ² Display-name entry is shown only when no display name is set. It reuses the * UpdateUserProfile subflow, whose `target` replaces the stack with the permissions phase. + * ³ Home is the same route the app launches on — the Wallet tab under [FeatureFlag.NewUi], the + * Scanner under v1. See [homeRoute]. */ @Composable fun OnboardingFlowScreen( @@ -149,6 +152,11 @@ private fun PermissionsPhaseFlowHost( val userManager = LocalUserManager.current val contactPickerMode by featureFlags.observe(FeatureFlag.ContactPickerMode).collectAsStateWithLifecycle() + // Onboarding releases to the same home the app launches on: the Wallet tab under v2, the + // Scanner under v1. + val isNewUi by featureFlags.observe(FeatureFlag.NewUi).collectAsStateWithLifecycle() + val home = homeRoute(isNewUi) + val permissionsSteps = buildList { if (!route.skipContacts && !contactPickerMode) add(OnboardingStep.ContactPermission) add(OnboardingStep.NotificationPermission) @@ -179,20 +187,20 @@ private fun PermissionsPhaseFlowHost( when (reason) { is FlowExitReason.Completed -> { analytics.action(Action.CompletedOnboarding) - trace(tag = "Onboarding", message = "Onboarding complete — releasing to scanner", type = TraceType.Process) + trace(tag = "Onboarding", message = "Onboarding complete — releasing to $home", type = TraceType.Process) userManager?.set(AuthState.Ready) outerNavigator.navigate( - route = AppRoute.Main.Scanner, + route = home, options = NavOptions(popUpTo = NavOptions.PopUpTo.ClearAll), ) } FlowExitReason.BackedOutOfRoot -> { // All permissions already granted - trace(tag = "Onboarding", message = "Onboarding complete (permissions already granted) — releasing to scanner", type = TraceType.Process) + trace(tag = "Onboarding", message = "Onboarding complete (permissions already granted) — releasing to $home", type = TraceType.Process) userManager?.set(AuthState.Ready) outerNavigator.navigate( - route = AppRoute.Main.Scanner, + route = home, options = NavOptions(popUpTo = NavOptions.PopUpTo.ClearAll), ) }