Onboarding
NativeExpress shows a three-step onboarding flow the first time someone opens your app.
The steps
| Step | What it does |
|---|---|
| 1. Value proposition | Shows the logo, headline and Continue button. |
| 2. Personalize | Asks “What brings you here?” with four options. |
| 3. Notifications | Explains 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.



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:
{
"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:
| State | Destination |
|---|---|
| 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:
<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:
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.