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), ) }