Skip to content

feat(onboarding): unify login into a single OnboardingFlow with linear FlowHost API#778

Merged
bmc08gt merged 8 commits into
code/cashfrom
feat/onboarding-flow-internalize-verification
May 28, 2026
Merged

feat(onboarding): unify login into a single OnboardingFlow with linear FlowHost API#778
bmc08gt merged 8 commits into
code/cashfrom
feat/onboarding-flow-internalize-verification

Conversation

@bmc08gt
Copy link
Copy Markdown
Collaborator

@bmc08gt bmc08gt commented May 28, 2026

Replace the fragmented login routing (LoginRouter, standalone screens, and scattered verification logic in MainRoot) with a single OnboardingFlowScreen backed by two FlowHost phases: Account and Permissions.

FlowHost API changes:

  • Add linear flow overload with steps, resumeAt, and completedResult parameters for ordered step-by-step flows
  • Add FlowNavigator.proceed() to advance through the step list, exit with completedResult at the end, or delegate to onProceed for custom behavior
  • Rename the existing overload as the non-linear variant for flows that manage their own navigation via navigateTo/exitWithResult
  • Extract shared logic into FlowHostImpl; support re-seeding when initialStack changes before user navigation (async flag settling)

Onboarding routing:

  • All AuthState.Registered cases now route to AppRoute.OnboardingFlow with a ResumePoint (Login, AccessKey, AccessKeyThenPurchase, or PostAccessKey) — MainRoot no longer routes directly to Verification
  • PostAccessKeyRedirect checks UserProfile.verifiedPhoneNumber to skip verification when phone is already linked
  • Seed restore (LoggedIn) skips verification and goes straight to permissions — existing users encounter phone verification in-app via the send flow
  • Permissions phase uses the linear FlowHost with resumeAt to skip already-granted permissions

Login module restructuring:

  • Delete LoginRouter, AccessKeyScreen, SeedInputScreen (standalone wrappers) — all step content is now composed inline by OnboardingFlowScreen via the entryProvider
  • Move ViewModels to internal package, screen content to internal/screens
  • Add OnboardingStep sealed interface and OnboardingResult for flow step/result modeling

@bmc08gt bmc08gt self-assigned this May 28, 2026
@github-actions github-actions Bot added type: feature New functionality area: auth Login, session, access keys, identity area: ui Compose UI, theme, components, resources area: onramp Deposit, purchase, Coinbase, fiat on-ramp area: deeplinks Deep link handling, URL routing, and link parsing and removed type: feature New functionality labels May 28, 2026
bmc08gt added 2 commits May 27, 2026 21:10
…r FlowHost API

Replace the fragmented login routing (LoginRouter, standalone screens,
and scattered verification logic in MainRoot) with a single
OnboardingFlowScreen backed by two FlowHost phases: Account and
Permissions.

FlowHost API changes:
- Add linear flow overload with `steps`, `resumeAt`, and `completedResult`
  parameters for ordered step-by-step flows
- Add `FlowNavigator.proceed()` to advance through the step list, exit
  with completedResult at the end, or delegate to `onProceed` for custom
  behavior
- Rename the existing overload as the non-linear variant for flows that
  manage their own navigation via navigateTo/exitWithResult
- Extract shared logic into FlowHostImpl; support re-seeding when
  initialStack changes before user navigation (async flag settling)

Onboarding routing:
- All AuthState.Registered cases now route to AppRoute.OnboardingFlow
  with a ResumePoint (Login, AccessKey, AccessKeyThenPurchase, or
  PostAccessKey) — MainRoot no longer routes directly to Verification
- PostAccessKeyRedirect checks UserProfile.verifiedPhoneNumber to skip
  verification when phone is already linked
- Seed restore (LoggedIn) skips verification and goes straight to
  permissions — existing users encounter phone verification in-app via
  the send flow
- Permissions phase uses the linear FlowHost with resumeAt to skip
  already-granted permissions

Login module restructuring:
- Delete LoginRouter, AccessKeyScreen, SeedInputScreen (standalone
  wrappers) — all step content is now composed inline by
  OnboardingFlowScreen via the entryProvider
- Move ViewModels to internal package, screen content to
  internal/screens
- Add OnboardingStep sealed interface and OnboardingResult for flow
  step/result modeling

Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
@bmc08gt bmc08gt force-pushed the feat/onboarding-flow-internalize-verification branch from a595b08 to a01d8b8 Compare May 28, 2026 01:19
@github-actions github-actions Bot added the type: feature New functionality label May 28, 2026
bmc08gt added 3 commits May 28, 2026 08:14
…mber

Ensures UserProfile (including verifiedPhoneNumber) is available before
onboarding routing decisions that depend on phone-linked state.

Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
…act routing logic

- Add skipContacts flag to OnboardingFlow route, passed from PostAccessKey path
- Purchase now exits with ProceedToVerification so IAP paths check phone-linked
  state and route through verification when needed
- Extract resolvePostAccountRoute pure function from composable routing logic
- Add OnboardingRoutingTest covering all flow chart paths
- Replace prose KDoc with ASCII flow charts documenting each onboarding path

Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
…nd loading button states

Make ContactCoordinator.sync() and addPickedContacts() suspend returning
Result<Unit> so callers can await completion. Add ContactAccessHandle using
ContactsPickerSessionContract on API 37+ with phone-only filtering, falling
back to full permission on older APIs. Thread isLoading/isSuccess through
ContactPermissionBottomBar for visual feedback during sync.
@github-actions github-actions Bot added the area: network gRPC, connectivity, API, exchange rates label May 28, 2026
@bmc08gt bmc08gt removed the area: onramp Deposit, purchase, Coinbase, fiat on-ramp label May 28, 2026
@bmc08gt bmc08gt requested a review from jeffyanta as a code owner May 28, 2026 16:35
@github-actions github-actions Bot added area: onramp Deposit, purchase, Coinbase, fiat on-ramp area: session area: onboarding labels May 28, 2026
@bmc08gt bmc08gt removed the request for review from jeffyanta May 28, 2026 16:38
@bmc08gt bmc08gt force-pushed the feat/onboarding-flow-internalize-verification branch 2 times, most recently from 8635a4e to 9186cff Compare May 28, 2026 17:03
bmc08gt added 3 commits May 28, 2026 13:20
…ding resumes at access key

onAccountPurchased was removing seenAccessKeyKey, and login always set
LoggedInWithUser when flags.isRegistered was true, skipping the access key
screen on restart. Now onAccountPurchased preserves the flag value,
hasSeenAccessKey falls back to selectedAccountIdKey, login gates
LoggedInWithUser on seenAccessKey, and RealSessionController respects
incomplete onboarding state.
…e for existing users

When PhoneNumberSend is enabled, new account creation now launches
verification with target=OnboardingFlow(AccessKey) so the user verifies
their phone before seeing the access key. Existing users (PostAccessKey)
skip verification entirely and go straight to permissions. Verification
gating logic moved from composable into LoginViewModel.
Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
@bmc08gt bmc08gt force-pushed the feat/onboarding-flow-internalize-verification branch from 9186cff to 98f7e7b Compare May 28, 2026 17:20
@bmc08gt bmc08gt merged commit cfe2964 into code/cash May 28, 2026
3 checks passed
@bmc08gt bmc08gt deleted the feat/onboarding-flow-internalize-verification branch May 28, 2026 17:49
bmc08gt added a commit that referenced this pull request May 30, 2026
LaunchedEffect(deepLink) in App.kt fired once during cold start, saw
currentRouteKey was Loading, and bailed out. MainRoot intentionally
defers OpenCashLink/Login actions to App.kt (navigates to plain Scanner
without forwarding the entropy). Since deepLink never changed, the
effect never re-fired and the link was permanently lost.

Add currentRoute as a LaunchedEffect key so the effect re-launches
when MainRoot replaces the backstack from Loading to Scanner. The
deep link is then dispatched normally (session.openCashLink or
viewModel.handleLoginEntropy).

The LaunchedEffect(deepLink) single-key pattern was a latent race
introduced in 1219d38 (Nav3 migration). It relied on MainRoot
transitioning away from Loading before Rinku delivered the deep link
(~2-3 frames of async delay). This worked because
PassphraseCredentialManager.login() set AuthState.LoggedInWithUser
immediately on the fast path — before any network calls — so MainRoot
navigated past Loading near-instantly.

cfe2964 (#778) changed that fast path from
updateUserManager(id, LoggedInWithUser) to just
userManager.set(selectedMetadata.id), deferring the auth state
transition until after the getUserFlags() network call. This shifted
the timing so Rinku now consistently delivers the deep link while
still on Loading, the LaunchedEffect bails, and the link is lost.

Latent fragility: 1219d38 (fcash/2026.3.4)
Surfaced by: cfe2964 (fcash/2026.5.6)

Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
bmc08gt added a commit that referenced this pull request May 30, 2026
)

LaunchedEffect(deepLink) in App.kt fired once during cold start, saw
currentRouteKey was Loading, and bailed out. MainRoot intentionally
defers OpenCashLink/Login actions to App.kt (navigates to plain Scanner
without forwarding the entropy). Since deepLink never changed, the
effect never re-fired and the link was permanently lost.

Add currentRoute as a LaunchedEffect key so the effect re-launches
when MainRoot replaces the backstack from Loading to Scanner. The
deep link is then dispatched normally (session.openCashLink or
viewModel.handleLoginEntropy).

The LaunchedEffect(deepLink) single-key pattern was a latent race
introduced in 1219d38 (Nav3 migration). It relied on MainRoot
transitioning away from Loading before Rinku delivered the deep link
(~2-3 frames of async delay). This worked because
PassphraseCredentialManager.login() set AuthState.LoggedInWithUser
immediately on the fast path — before any network calls — so MainRoot
navigated past Loading near-instantly.

cfe2964 (#778) changed that fast path from
updateUserManager(id, LoggedInWithUser) to just
userManager.set(selectedMetadata.id), deferring the auth state
transition until after the getUserFlags() network call. This shifted
the timing so Rinku now consistently delivers the deep link while
still on Loading, the LaunchedEffect bails, and the link is lost.

Latent fragility: 1219d38 (fcash/2026.3.4)
Surfaced by: cfe2964 (fcash/2026.5.6)

Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: auth Login, session, access keys, identity area: deeplinks Deep link handling, URL routing, and link parsing area: network gRPC, connectivity, API, exchange rates area: onboarding area: onramp Deposit, purchase, Coinbase, fiat on-ramp area: session area: ui Compose UI, theme, components, resources type: feature New functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant