App Gates

Customize onboarding, consent, version, auth, biometric, and rating flows.

The Expo template's launch gates are sibling components around the router <Stack>, not a nested native wrapper chain. That matters when you customize which gates redirect and which gates overlay the current screen.

Real topology

app/_layout.tsx mounts providers first, then renders the gates in this order:

RootErrorBoundary
  -> providers
  -> OnboardingGate
  -> AuthGate
  -> VersionGate
  -> <Stack />
  -> ConsentGate
  -> BiometricGate
  -> PortalHost
GateBehaviorNotes
RootErrorBoundaryError boundaryWraps the full app and reports to Sentry
OnboardingGateRedirectSends first-run users to /onboarding except onboarding, referral, and legal routes
AuthGateRedirect cleanupGuest-first; it only dismisses auth screens after sign-in
VersionGateOverlay modal/dialogBlocks on forced updates and shows dismissible soft updates
ConsentGateOverlay modalRequires the current legal version, except while reading legal screens
BiometricGateFull-screen overlayLocks signed-in users who enabled biometric app lock

Onboarding

lib/onboarding.tsx stores completion in its own cache key:

growth.onboarding.v1

app/onboarding.tsx renders four slides:

launch -> protect -> grow -> permissions

Finishing onboarding tracks onboarding completed, records a rating trigger, persists { completed: true, completedAt }, and replaces the route with Home.

Auth gate

The app is guest-first. AuthGate does not wall the app at launch. Protected actions use SignInGate inside the relevant screen or component.

When a signed-in user is still inside (auth) and not on password recovery, AuthGate closes the auth flow:

canGoBack() -> router.back()
otherwise   -> router.replace("/")

This makes login behave like a modal raised by the current task, while still allowing cold-start login routes.

Version gate

lib/app-update-config.ts reads env config:

EXPO_PUBLIC_MIN_APP_VERSION=
EXPO_PUBLIC_LATEST_APP_VERSION=
EXPO_PUBLIC_APP_UPDATE_URL=
EXPO_PUBLIC_APP_UPDATE_MESSAGE=
EXPO_PUBLIC_APP_UPDATE_CONFIG_URL=

If APP_UPDATE_CONFIG_URL is set, the app fetches remote JSON and merges it over the env values. The TanStack Query cache uses a 10-minute staleTime.

StateRule
Force updateCURRENT_APP_VERSION < minVersion; shows a non-dismissable modal
Soft updateCURRENT_APP_VERSION < latestVersion; shows a dismissible dialog
No gateNo config, equal/newer current version, or failed open URL

To test locally, set EXPO_PUBLIC_MIN_APP_VERSION=2.0.0 while the app version in app.config.ts is 1.0.0, restart Metro, and open the app.

LEGAL_VERSION is defined in lib/legal.ts:

export const LEGAL_VERSION = "2026-04-30";

ConsentProvider persists the accepted legal version in:

legal.consent.v1

When the version changes, existing users are prompted again. Legal documents live under app/legal/; external legal URLs come from EXPO_PUBLIC_TERMS_URL, EXPO_PUBLIC_PRIVACY_URL, and EXPO_PUBLIC_EULA_URL.

This is a legal-consent gate, not an analytics-consent gate. In the current provider order, AnalyticsProvider mounts before ConsentProvider, so PostHog can initialize and screen tracking can run as soon as a PostHog key is configured.

If your policy requires analytics only after consent, change the implementation: read consent before mounting PostHog or make isAnalyticsEnabled() depend on a stored analytics-consent value.

Biometric lock

lib/biometric.tsx uses expo-local-authentication. It checks hardware, enrollment, and supported types, then stores the user's opt-in at:

cache.biometric.enabled.v1

When a signed-in user enables biometric lock, the app locks immediately and locks again after returning from background if it was away for more than 30 seconds. BiometricGate renders outside <Stack />, so it covers every active screen.

This is Native-only and requires hardware or enrolled device authentication.

Rating prompt

Rating is not a gate. lib/rating.ts increments an action counter when onboarding completes or a referral invite is shared. A review prompt is allowed only when:

actionCount >= 3
not prompted for this app version
at least 90 days since the last prompt
expo-store-review has an available action

lib/store-review.web.ts always returns false, so web never prompts.

Customize

  • Remove a gate by removing its provider state and component from app/_layout.tsx.
  • Reorder carefully: redirect gates belong before <Stack />; overlays that must cover the current route belong after <Stack />.
  • Reset persisted flags during QA by clearing the app's local storage/cache or by calling the provider reset helpers from a temporary debug screen.
  • Keep App Review notes aligned with the code: account deletion is in Account & Security, legal consent is versioned, and ATT is separate from this consent gate.

On this page