src/features/*/presentation and renders it. Logic belongs in the feature.
Tree
Screens
Outside the signed-in stack
Tabs
Stack routes
meal/add.tsx and meal/result.tsx are the two route files that hold real screen code. Everything else is a wrapper.
Root layout
src/app/_layout.tsx runs once per launch.
applyAppDirection()pins the native layout direction to the stored language before the first render. See RTL and language.SplashScreen.preventAutoHideAsync()holds the native splash until fonts load.useFontsloads Inter, IBM Plex Sans Hebrew and Arabic, Heebo, Assistant, Rubik, Karantina and Varela Round. The layout returnsnulluntil they are ready.- An effect registers the 401 handler with
setUnauthorizedHandlerso a rejected session callslogout, then hydrates the theme and the auth store in that order. - An
AppStatelistener callsrestoreSession()each time the app becomes active. useForegroundHealthSync()runs a step sync on launch and on every return to the foreground.import '@/features/health/lib/backgroundTask'defines the background task at module scope, which TaskManager requires.
GestureHandlerRootView, SafeAreaProvider, QueryClientProvider, ToastProvider, then SplashGate around the router Slot.
SplashGate keeps a JS overlay that matches the native splash until authStatus !== 'unknown' and at least 2100 ms have passed. With reduced motion on, the minimum hold is skipped.
Launch decision
src/app/index.tsx reads three values from the auth store.
Auth layout
(auth)/_layout.tsx is a headerless Stack. When status === 'authed' it redirects to /choose-studio or /home, so a signed-in trainee can never sit on the login screens.
App layout
(app)/_layout.tsx guards the signed-in area and hosts everything that must live above all screens.
Gates, in order:
Mounted next to the
Stack: GlobalWorkoutBar, AppUpdatePrompt and PendingFormPrompt.
The mount effect wires the long-lived pieces:
- Refreshes the nutrition day now, then schedules a timer for the next rollover from
nextNutritionRefreshMs. - Restores the workout Live Activity and subscribes
syncWorkoutActivityto the workout session store. - Resumes the rest timer.
- Syncs the cardio Live Activity and the Android cardio notification, and subscribes both to the cardio session store.
- On every return to
active: refreshes nutrition, resumes the rest timer, re-reads the cardio notification state, callsrefreshSession(), invalidates the home query, and retries push registration without prompting. - On
background: callssyncHomeWidgetsso the home screen widgets hold fresh data.
runStartupPrompts with askForPushPermission, promptForHealthAccessOnce and promptForAppUpdate, in that order. src/lib/startupPrompts.ts runs them one after another so two system sheets never stack, and exposes useStartupPrompts with a settled flag.
Two hooks run for the whole signed-in session: useWorkoutReminders() and useNotificationRouting(). See Notifications.
Tab layouts
There are three platform files for the same five tabs.
All three set
unstable_settings.initialRouteName to home. _layout.tsx also returns AndroidTabsLayout when Platform.OS === 'android', as a fallback for the platform file.
Deep links
The URL scheme isperform, set in app.json. Links used inside the app:
Notification taps do not use URLs. They route by a
target field in the payload. See Notifications.
Typed routes
experiments.typedRoutes generates .expo/types/router.d.ts. That file is ignored by Git and is only rewritten when the Expo dev server starts. After pulling a commit that adds a route, pnpm typecheck can fail with a “not assignable” error on a router.push call even though the route file exists. Start the dev server once to regenerate the types. Do not cast the href.