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:
| State | Destination |
|---|---|
| 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
| Step | File | What it does |
|---|---|---|
| 1. Value prop | onboarding/index.tsx | Logo, headline, one Continue button |
| 2. Personalize | onboarding/personalize.tsx | Asks “What brings you here?” with four options |
| 3. Notifications | onboarding/notifications.tsx | Explains 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.



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:
- Writes
app.onboarding-doneto AsyncStorage. - Calls
presentPaywall('onboarding_end'), which shows RevenueCat’s paywall, or your Superwall paywall if Superwall is configured. - 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.
"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.
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
<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:
if (!session && !onboardingDone) {
return <Redirect href="/(public)/onboarding" />;
}Delete the folder
rm -rf src/app/\(public\)/onboardingMove 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.