Skip to Content
Onboarding

Onboarding

NativeExpress shows a three-step onboarding flow the first time someone opens your app. It lives in src/app/(public)/onboarding/.

When it runs

src/app/index.tsx is the entry route. On launch it reads the app.onboarding-done flag from AsyncStorage and decides where to go:

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

The flag is written once, when the funnel finishes. Signed-in users never see onboarding.

The three steps

StepFileWhat it does
1. Value proponboarding/index.tsxLogo, headline, one Continue button
2. Personalizeonboarding/personalize.tsxAsks “What brings you here?” with four options
3. Notificationsonboarding/notifications.tsxExplains the value, then asks for push permission

Every screen shares OnboardingHeader, which renders progress dots and a Skip link in the top right. Skip on step 1 or 2 jumps straight to step 3; skip on step 3 completes the flow.

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
The three steps, in dark mode. Progress dots and the Skip link are in the header of each.

The step 2 answer is not persisted anywhere. It is a local useState only. If you want to act on it, write it to the user’s profile in personalize.tsx before navigating on.

What happens at the end

Step 3 does three things in order, whether the user enables notifications or skips:

  1. Writes app.onboarding-done to AsyncStorage.
  2. Calls presentPaywall('onboarding_end'), which shows RevenueCat’s paywall, or your Superwall paywall if Superwall is configured.
  3. Redirects to /(public)/welcome.

Changing the copy

All strings are translation keys under onboarding.* in src/i18n/en.json (and de.json). Edit those rather than the components.

src/i18n/en.json
"onboarding": { "skip": "Skip", "continue_button": "Continue", "value_headline": "Chat with every model", "value_subtitle": "GPT, Claude, and Gemini in one clean thread — pick the right model for every question." }

Changing the personalize options

Edit the OPTIONS array in src/app/(public)/onboarding/personalize.tsx and add a matching onboarding.personalize_option_<id> key to each translation file.

src/app/(public)/onboarding/personalize.tsx
const OPTIONS = ['work', 'learning', 'creativity', 'exploring'] as const;

Adding a step

Create the screen

Add src/app/(public)/onboarding/<name>.tsx. Copy an existing step; they all follow the same shape.

Register it in the layout

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

Fix the step counters

OnboardingHeader takes step and total. Bump total on every screen and give the new one its position.

Point the previous step at it

Update the previous screen’s router.push(...) target, and the skipToEnd helper if the new step should be skippable.

Removing onboarding entirely

Delete the entry-route branch

In src/app/index.tsx, remove the AsyncStorage read and the redirect:

src/app/index.tsx
if (!session && !onboardingDone) { return <Redirect href="/(public)/onboarding" />; }

Delete the folder

rm -rf src/app/\(public\)/onboarding

Move the paywall trigger

presentPaywall('onboarding_end') lived in step 3. If you still want a paywall on first run, call it from src/app/(public)/welcome.tsx instead — otherwise that placement will never fire.

Verify

A fresh install opens straight on the welcome screen. Delete the app first so AsyncStorage is cleared.

Last updated on