From 7243560872fb812754e80e23ce99da3ddf0db461 Mon Sep 17 00:00:00 2001
From: "Eric Han (via MelvinBot)" <infra+melvinbot@expensify.com>
Date: Tue, 7 Apr 2026 13:50:24 +0000
Subject: [PATCH 1/2] Navigate to HOME after openApp to trigger OnboardingGuard
 for anonymous sign-in

When an anonymous user signs in via SignInModal, dismissModal() fires while
IS_LOADING_APP is still true, causing OnboardingGuard to skip the onboarding
redirect. After openApp completes (loading NVP_ONBOARDING and setting
IS_LOADING_APP=false), we navigate to HOME to trigger the guard with accurate
state, allowing it to properly redirect new users to onboarding.

This avoids the regression from the previous approach (PR 85660) which blocked
dismissModal until openApp completed, causing the sign-in modal to stay open.

Co-authored-by: Eric Han <eh2077@users.noreply.github.com>
---
 src/pages/signin/SignInModal.tsx | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/pages/signin/SignInModal.tsx b/src/pages/signin/SignInModal.tsx
index 44cb7dd93d75..450bea3c4189 100644
--- a/src/pages/signin/SignInModal.tsx
+++ b/src/pages/signin/SignInModal.tsx
@@ -10,6 +10,7 @@ import {isMobileSafari} from '@libs/Browser';
 import Navigation from '@libs/Navigation/Navigation';
 import {waitForIdle} from '@libs/Network/SequentialQueue';
 import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
 import SCREENS from '@src/SCREENS';
 import SignInPageWrapped, {SignInPage} from './SignInPage';
 import type {SignInPageRef} from './SignInPage';
@@ -42,9 +43,15 @@ function SignInModal() {
             // To prevent deadlock when OpenReport and OpenApp overlap, wait for the queue to be idle before calling openApp.
             // This ensures that any communication gaps between the client and server during OpenReport processing do not cause the queue to pause,
             // which would prevent us from processing or clearing the queue.
-            waitForIdle().then(() => {
-                openApp(true);
-            });
+            // After openApp completes, check if onboarding is needed. The earlier dismissModal fires while
+            // IS_LOADING_APP is still true, which causes OnboardingGuard to skip the onboarding redirect.
+            // By navigating to HOME after openApp loads the data, we trigger the guard with accurate state,
+            // allowing it to properly redirect new users to onboarding.
+            waitForIdle()
+                .then(() => openApp(true))
+                .then(() => {
+                    Navigation.navigate(ROUTES.HOME);
+                });
         }
     }, [session?.authTokenType]);
 

From 4aa31313b8fed9e14cc0225f5ff638a162f95459 Mon Sep 17 00:00:00 2001
From: "Eric Han (via MelvinBot)" <infra+melvinbot@expensify.com>
Date: Fri, 10 Apr 2026 14:47:13 +0000
Subject: [PATCH 2/2] =?UTF-8?q?Listen=20for=20IS=5FLOADING=5FAPP=20?=
 =?UTF-8?q?=E2=86=92=20false=20before=20dismissing=20modal?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

openApp() queues the request and returns immediately, so the previous
.then() chain fired before the response was processed. Instead, subscribe
to IS_LOADING_APP via useOnyx and only dismiss the modal + navigate to
HOME once IS_LOADING_APP transitions to false, ensuring NVP_ONBOARDING
is loaded and OnboardingGuard evaluates with accurate data.

Co-authored-by: Eric Han <eh2077@users.noreply.github.com>
---
 src/pages/signin/SignInModal.tsx | 32 +++++++++++++++++++-------------
 1 file changed, 19 insertions(+), 13 deletions(-)

diff --git a/src/pages/signin/SignInModal.tsx b/src/pages/signin/SignInModal.tsx
index 450bea3c4189..f520a689be76 100644
--- a/src/pages/signin/SignInModal.tsx
+++ b/src/pages/signin/SignInModal.tsx
@@ -3,6 +3,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import {useSession} from '@components/OnyxListItemProvider';
 import ScreenWrapper from '@components/ScreenWrapper';
 import useAndroidBackButtonHandler from '@hooks/useAndroidBackButtonHandler';
+import useOnyx from '@hooks/useOnyx';
 import useStyleUtils from '@hooks/useStyleUtils';
 import useTheme from '@hooks/useTheme';
 import {openApp} from '@libs/actions/App';
@@ -10,6 +11,7 @@ import {isMobileSafari} from '@libs/Browser';
 import Navigation from '@libs/Navigation/Navigation';
 import {waitForIdle} from '@libs/Network/SequentialQueue';
 import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
 import SCREENS from '@src/SCREENS';
 import SignInPageWrapped, {SignInPage} from './SignInPage';
@@ -20,6 +22,8 @@ function SignInModal() {
     const StyleUtils = useStyleUtils();
     const signinPageRef = useRef<SignInPageRef | null>(null);
     const session = useSession();
+    const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
+    const hasSignedInRef = useRef(false);
     // Use of SignInPageWrapped (with shouldEnableMaxHeight prop in SignInPageWrapper) is a workaround for Safari not supporting interactive-widget=resizes-content.
     // This allows better scrolling experience after keyboard shows for modals with input, that are larger than remaining screen height.
     // More info https://github.com/Expensify/App/pull/62799#issuecomment-2943136220.
@@ -35,26 +39,28 @@ function SignInModal() {
     useEffect(() => {
         const isAnonymousUser = session?.authTokenType === CONST.AUTH_TOKEN_TYPES.ANONYMOUS;
         if (!isAnonymousUser) {
-            // Signing in RHP is only for anonymous users
-            Navigation.isNavigationReady().then(() => {
-                Navigation.dismissModal();
-            });
+            hasSignedInRef.current = true;
 
             // To prevent deadlock when OpenReport and OpenApp overlap, wait for the queue to be idle before calling openApp.
             // This ensures that any communication gaps between the client and server during OpenReport processing do not cause the queue to pause,
             // which would prevent us from processing or clearing the queue.
-            // After openApp completes, check if onboarding is needed. The earlier dismissModal fires while
-            // IS_LOADING_APP is still true, which causes OnboardingGuard to skip the onboarding redirect.
-            // By navigating to HOME after openApp loads the data, we trigger the guard with accurate state,
-            // allowing it to properly redirect new users to onboarding.
-            waitForIdle()
-                .then(() => openApp(true))
-                .then(() => {
-                    Navigation.navigate(ROUTES.HOME);
-                });
+            waitForIdle().then(() => openApp(true));
         }
     }, [session?.authTokenType]);
 
+    // Wait for IS_LOADING_APP to become false after sign-in before dismissing the modal.
+    // openApp queues a request and IS_LOADING_APP only transitions to false once the response
+    // is processed and NVP_ONBOARDING is loaded. Dismissing at that point ensures OnboardingGuard
+    // evaluates with accurate data and properly redirects new users to onboarding.
+    useEffect(() => {
+        if (!hasSignedInRef.current || isLoadingApp !== false) {
+            return;
+        }
+
+        Navigation.dismissModal();
+        Navigation.navigate(ROUTES.HOME);
+    }, [isLoadingApp]);
+
     return (
         <ScreenWrapper
             style={[StyleUtils.getBackgroundColorStyle(theme.PAGE_THEMES[SCREENS.RIGHT_MODAL.SIGN_IN].backgroundColor)]}
