Skip to main content
Mobile onboarding is implemented inside apps/mobile; the workspace does not contain an onboarding package. The route files live in apps/mobile/app/(onboarding), and per-user completion state lives in apps/mobile/src/features/security/onboardingFlow.ts.

Prerequisites enforced by the access gate

An authenticated user reaches the onboarding group only after completing these app-level gates:
  1. Verify the authenticated email at /(public)/verify-email.
  2. Create and confirm a six-digit PIN at /(public)/setup-pin.
  3. Complete /(public)/setup-biometrics by enabling biometrics or choosing not to enable them.
PIN and biometric setup use per-user exports from @amisi-mobile-starter/security. savePinForUser(userId, pin) marks PIN setup. The biometrics screen always calls setHasBiometricsSetupForUser(userId, true) and records whether biometrics are enabled with setBiometricsEnabledForUser(userId, isEnabled). After biometric setup, the screen replaces the route with /(onboarding)/steps/welcome.

Completion state

The app-local state has exactly three booleans:
getOnboardingFlowState(userId) reads them synchronously from @amisi-mobile-starter/core key-value storage. Missing values default to false. State is scoped by authenticated user ID. There is no reset helper in the current app-local module.

Actual route flow

1. Welcome

/(onboarding) immediately redirects to /(onboarding)/steps/welcome. The welcome step marks hasCompletedWelcome, tracks onboarding_welcome_continue, and pushes /(onboarding)/steps/permissions.

2. Push explanation

The permissions step explains notifications without requesting system permission. Continuing marks hasCompletedPushExplanation, tracks onboarding_push_explanation_continue, and replaces the route with ROUTES.HOME. Because hasCompletedPushPermissionPrompt is still false, AccessGate then replaces home with ROUTES.NOTIFICATION_PERMISSION_MODAL.

3. System permission modal

The actual prompt is /(modal)/notification-permission, not a route inside (onboarding).
  • Allow calls useNotifications().requestPermission() and tracks the returned granted value.
  • Skip does not call the provider.
  • A permission-request error is logged and displayed briefly.
  • Allow, skip, and error paths all mark hasCompletedPushPermissionPrompt and replace the route with ROUTES.HOME.
The completion boolean records that the prompt was handled, not that permission was granted.
Notification initialization occurs after authentication in AuthenticatedServices. It initializes the selected adapter and immediately calls registerForPushNotifications(userId). The permission modal only requests permission; it does not register again after a grant.

Paywall route

/(onboarding)/steps/paywall exists, and ROUTES.ONBOARDING_PAYWALL points to it. Both actions on that screen replace the route with /(app)/(tabs). The current AccessGate does not navigate to the paywall, and onboarding state has no paywall-completion field. The normal enforced flow ends after the notification permission modal. flags.enableSubscriptions controls whether SubscriptionsProvider is mounted; it does not add the onboarding paywall to this flow.

Extending the flow

When you add an enforced step:
  1. Add its route under apps/mobile/app/(onboarding) or document why it is a modal.
  2. Add user-scoped storage in apps/mobile/src/features/security/onboardingFlow.ts.
  3. Mark completion before navigating away from the step.
  4. Add the gate in AccessGate before its completed-group redirect.
  5. Add or update an exact constant in apps/mobile/src/constants/routes.ts.
Keep app-local onboarding paths distinct from public security setup and from package APIs.