Coming from this proposal
Background
When users sign up for Expensify or migrate from OldDot to NewDot, they go through an onboarding flow that consists of several screens (personal details, purpose selection, company size, etc.). The app needs to ensure users complete onboarding before accessing the main application. Currently, this redirect logic exists in multiple places across the codebase: useOnboardingFlow hook (with ~20 dependencies in its useEffect), openReportFromDeepLink action (triggered from Expensify.tsx with minimal dependencies), and various other places that check onboarding status.
Each location independently determines whether to redirect to onboarding and when to do so. The redirect from openReportFromDeepLink typically happens early since it only depends on session data being loaded, while the redirect from useOnboardingFlow happens later due to its many dependencies. Additionally, certain paths like onboarding/migrated-user-welcome are excluded from being saved as the last visited onboarding path, meaning users can be redirected back to different onboarding paths when they reopen the app.
Problem
When navigation redirects are scattered across multiple locations with different dependency arrays, if different redirect logic executes at different times, then users experience race conditions that cause onboarding loops and incorrect navigation states.
Solution
Implement a navigation guard system that centralizes all redirect logic at the router level, eliminating race conditions by evaluating navigation synchronously before any state changes occur.
The guard system eliminates race conditions by moving all redirect decisions to a single evaluation point at the router level. A guard infrastructure with a registry evaluates guards in explicit order, integrated directly into RootStackRouter.getStateForAction() before any routing logic runs. When any navigation action occurs, guards evaluate synchronously with complete, up-to-date Onyx data and can return ALLOW, BLOCK, or REDIRECT results. The OnboardingGuard subscribes to relevant Onyx keys and implements logic to redirect users to onboarding screens when needed, or block navigation away from incomplete onboarding. The scattered redirect logic is removed from useOnboardingFlow, openReportFromDeepLink, and Expensify.tsx, replaced with simple navigation calls that trigger guard evaluation.
Issue Owner
Current Issue Owner: @CortneyOfstad
Coming from this proposal
Background
When users sign up for Expensify or migrate from OldDot to NewDot, they go through an onboarding flow that consists of several screens (personal details, purpose selection, company size, etc.). The app needs to ensure users complete onboarding before accessing the main application. Currently, this redirect logic exists in multiple places across the codebase: useOnboardingFlow hook (with ~20 dependencies in its useEffect), openReportFromDeepLink action (triggered from Expensify.tsx with minimal dependencies), and various other places that check onboarding status.
Each location independently determines whether to redirect to onboarding and when to do so. The redirect from openReportFromDeepLink typically happens early since it only depends on session data being loaded, while the redirect from useOnboardingFlow happens later due to its many dependencies. Additionally, certain paths like onboarding/migrated-user-welcome are excluded from being saved as the last visited onboarding path, meaning users can be redirected back to different onboarding paths when they reopen the app.
Problem
When navigation redirects are scattered across multiple locations with different dependency arrays, if different redirect logic executes at different times, then users experience race conditions that cause onboarding loops and incorrect navigation states.
Solution
Implement a navigation guard system that centralizes all redirect logic at the router level, eliminating race conditions by evaluating navigation synchronously before any state changes occur.
The guard system eliminates race conditions by moving all redirect decisions to a single evaluation point at the router level. A guard infrastructure with a registry evaluates guards in explicit order, integrated directly into RootStackRouter.getStateForAction() before any routing logic runs. When any navigation action occurs, guards evaluate synchronously with complete, up-to-date Onyx data and can return ALLOW, BLOCK, or REDIRECT results. The OnboardingGuard subscribes to relevant Onyx keys and implements logic to redirect users to onboarding screens when needed, or block navigation away from incomplete onboarding. The scattered redirect logic is removed from useOnboardingFlow, openReportFromDeepLink, and Expensify.tsx, replaced with simple navigation calls that trigger guard evaluation.
Issue Owner
Current Issue Owner: @CortneyOfstad