Skip to Content
Onboarding

Onboarding

NativeExpress shows a three-step onboarding flow the first time someone opens your app.

The steps

StepWhat it does
1. Value propositionShows the logo, headline and Continue button.
2. PersonalizeAsks “What brings you here?” with four options.
3. NotificationsExplains notifications and offers to request push permission.

Each screen uses OnboardingHeader for progress dots and the Skip link. Skipping step 1 or 2 goes to step 3. Skipping step 3 completes onboarding.

Onboarding step 1, the value proposition screen, with progress dots and a Skip link
Onboarding step 2, the What brings you here? question with four options
Onboarding step 3, the notifications primer
Each screen shows progress dots and a Skip link.

The screens are in src/app/(public)/onboarding/: index.tsx, personalize.tsx and notifications.tsx.

Changing the copy

Edit the onboarding.* keys in src/i18n/en.json and de.json. For example, change value_headline and value_subtitle to describe your app:

src/i18n/en.json
{ "onboarding": { "value_headline": "Plan your next meal", "value_subtitle": "Save recipes and build a weekly meal plan." } }

Update these keys in the existing JSON object. Keep the remaining onboarding keys.

Changing the personalize options

Edit ONBOARDING_INTENTS in src/lib/onboarding.ts. The personalize screen and the saved-answer schema both use this list.

For each new ID, add these keys to both translation files:

  • onboarding.personalize_option_<id> for the option label.
  • home.intent_caption_<id> for the caption on the Home screen.

Run yarn check:i18n, then complete onboarding with the new option. After signing in, confirm that Home shows the matching caption.

Saved answers

Onboarding saves the selected intent and notification choice to AsyncStorage under app.onboarding-pending when the final step completes. The next signed-in session attaches that record to user.user_metadata.onboarding in Supabase.

The record contains completedAt and answers. Read it with onboardingOf(user) from @/lib/onboarding. The helper returns null when the user has no valid saved record.

An existing account keeps its own onboarding record. If the Supabase update fails, the pending record remains on the device for another attempt after the app restarts.

The saved notification value records which button the user chose. enabled does not mean the operating system granted permission. The permission request also requires OneSignal setup.

When onboarding shows

The entry route, src/app/index.tsx, reads the app.onboarding-done flag:

StateDestination
No session, onboarding not done/(public)/onboarding
No session, onboarding done/(public)/welcome
Signed in/(protected)/(tabs)/home

The completion flag is stored on the device. Signing out does not reset it.

When onboarding finishes

The final step saves the answers and completion flag, records onboarding_completed, requests the onboarding_end paywall placement, then opens the welcome screen.

RevenueCat presents the paywall when configured. If you use Superwall, its campaign decides whether to show one.

Adding a step

Create the screen

Add src/app/(public)/onboarding/<name>.tsx. Start with an existing screen and keep its shared layout and translation pattern.

Register it in the layout

Add the screen inside the existing Stack:

src/app/(public)/onboarding/_layout.tsx
<Stack.Screen name="<name>" />

Keep OnboardingFlowProvider around the Stack so answers remain available when moving between screens.

Update the step counters

Set total on every OnboardingHeader and give the new screen its step number.

Update navigation and saved answers

Point the previous screen’s router.push(...) at the new screen. Adjust skipToEnd if the new step changes the skip behavior.

If the step collects an answer, add it to the schema in src/lib/onboarding.ts and update OnboardingFlowProvider. The final screen saves the answers from that provider.

Verify

Run yarn typecheck and yarn check:i18n. Complete the flow and repeat it using Skip. Confirm the progress dots, saved answers and final destination.

Removing onboarding entirely

Simplify the entry route

In src/app/index.tsx, remove the onboarding state, effect and imports. Keep the session-based redirect:

src/app/index.tsx
import { Redirect } from 'expo-router'; import { useSession } from '@/hooks/useSession'; export default function Index() { const { session } = useSession(); return <Redirect href={session ? '/(protected)/(tabs)/home' : '/(public)/welcome'} />; }

Remove the onboarding screens

Delete src/app/(public)/onboarding/. Remove the unused OnboardingFlowProvider and useOnboardingFlow.

If you no longer need saved answers, remove useOnboardingSync from the root layout and remove the onboarding caption from Home. Update the related tests and translation keys.

Choose when to show the first paywall

Move presentPaywall('onboarding_end') to the action where you want it to appear, or remove that placement from your product flow.

Verify

Run yarn ci. With no saved session, the app opens on Welcome; with a session, it opens on Home.

Last updated on