Repository: Expensify/App
Pull request #57934: 🍒 Cherry pick PR #56698 to staging 🍒
Author: login: os-botify[bot]
Labels: name: Engineering
name: Hourly
description: KSv2
name: Ready To Build
Created: 2025-03-06T14:49:20Z | Merged: 2025-03-06T17:57:56Z

🍒 Cherry pick https://github.com/Expensify/App/pull/56382 to staging 🍒

This PR had conflicts when we tried to cherry-pick it to staging. You'll need to manually perform the cherry-pick, using the following steps:

```bash
git fetch
git checkout roryabraham-cherry-pick-staging-56382-1
git cherry-pick -S -x --mainline 1 6486b1d1961d4c6de85154905d57af5118b33ff9
```

Then manually resolve conflicts, and commit the change with `git cherry-pick --continue`. Lastly, please run:

```bash
git commit --amend -m "$(git log -1 --pretty=%B)" -m "(CP triggered by roryabraham)"
```

That will help us keep track of who triggered this CP. Once all that's done, push your changes with `git push origin roryabraham-cherry-pick-staging-6486b1d1961d4c6de85154905d57af5118b33ff9-1`, and then open this PR for review.

Note that you **must** test this PR, and both the author and reviewer checklist should be completed, just as if you were merging the PR to main.

_Pro-tip:_ If this PR appears to have conflicts against the _staging_ base, it means that the version on staging has been updated. The easiest thing to do if you see this is to close the PR and re-run the CP.

### PR Author Checklist
<!--
This is a checklist for PR authors. Please make sure to complete all tasks and check them off once you do, or else your PR will not be merged!
-->

- [x] I linked the correct issue in the `### Fixed Issues` section above
- [x] I wrote clear testing steps that cover the changes made in this PR
    - [x] I added steps for local testing in the `Tests` section
    - [x] I added steps for the expected offline behavior in the `Offline steps` section
    - [x] I added steps for Staging and/or Production testing in the `QA steps` section
    - [x] I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    - [x] I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    - [x] I tested this PR with a [High Traffic account](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#high-traffic-accounts) against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
- [x] I included screenshots or videos for tests on [all platforms](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#make-sure-you-can-test-on-all-platforms)
- [x] I ran the tests on **all platforms** & verified they passed on:
    - [x] Android: Native
    - [x] Android: mWeb Chrome
    - [x] iOS: Native
    - [x] iOS: mWeb Safari
    - [x] MacOS: Chrome / Safari
    - [x] MacOS: Desktop
- [x] I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
- [x] I followed proper code patterns (see [Reviewing the code](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md#reviewing-the-code))
    - [x] I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. `toggleReport` and not `onIconClick`)
    - [x] I verified that comments were added to code that is not self explanatory
    - [x] I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    - [x] I verified any copy / text shown in the product is localized by adding it to `src/languages/*` files and using the [translation method](https://github.com/Expensify/App/blob/4bd99402cebdf4d7394e0d1f260879ea238197eb/src/components/withLocalize.js#L60)
      - [x] If any non-english text was added/modified, I used [JaimeGPT](https://chatgpt.com/g/g-2dgOQl5VM-english-to-spanish-translator-aka-jaimegpt) to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    - [x] I verified all numbers, amounts, dates and phone numbers shown in the product are using the [localization methods](https://github.com/Expensify/App/blob/4bd99402cebdf4d7394e0d1f260879ea238197eb/src/components/withLocalize.js#L60-L68)
    - [x] I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    - [x] I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    - [x] I verified the JSDocs style guidelines (in [`STYLE.md`](https://github.com/Expensify/App/blob/main/contributingGuides/STYLE.md#jsdocs)) were followed
- [x] If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
- [x] I followed the guidelines as stated in the [Review Guidelines](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md)
- [x] I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like `Avatar`, I verified the components using `Avatar` are working as expected)
- [x] I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
- [x] I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
- [x] I verified that if a function's arguments changed that all usages have also been updated correctly
- [x] If any new file was added I verified that:
    - [x] The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
- [x] If a new CSS style is added I verified that:
    - [x] A similar style doesn't already exist
    - [x] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/styles/utils/index.ts) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)`)
- [x] If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
- [x] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases)
- [x] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
- [x] If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
- [x] If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    - [x] I verified that all the inputs inside a form are aligned with each other.
    - [x] I added `Design` label and/or tagged `@Expensify/design` so the design team can review the changes.
- [x] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page.
- [x] I added [unit tests](https://github.com/Expensify/App/blob/main/tests/README.md) for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
- [x] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps.

### Screenshots/Videos
<details>
<summary>Android: Native</summary>

<!-- add screenshots or videos here -->

</details>

<details>
<summary>Android: mWeb Chrome</summary>

<!-- add screenshots or videos here -->

</details>

<details>
<summary>iOS: Native</summary>

<!-- add screenshots or videos here -->

</details>

<details>
<summary>iOS: mWeb Safari</summary>

<!-- add screenshots or videos here -->

</details>

<details>
<summary>MacOS: Chrome / Safari</summary>

<!-- add screenshots or videos here -->

</details>

<details>
<summary>MacOS: Desktop</summary>

<!-- add screenshots or videos here -->

</details>


Diff:
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 50f7d935cc7b..2ba4e2c4fa39 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -114,8 +114,8 @@ android {
         minSdkVersion rootProject.ext.minSdkVersion
         targetSdkVersion rootProject.ext.targetSdkVersion
         multiDexEnabled rootProject.ext.multiDexEnabled
-        versionCode 1009010906
-        versionName "9.1.9-6"
+        versionCode 1009010907
+        versionName "9.1.9-7"
         // Supported language variants must be declared here to avoid from being removed during the compilation.
         // This also helps us to not include unnecessary language variants in the APK.
         resConfigs "en", "es"
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 8a63ead6c69e..79a7e4701fc0 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -44,7 +44,7 @@
 		</dict>
 	</array>
 	<key>CFBundleVersion</key>
-	<string>9.1.9.6</string>
+	<string>9.1.9.7</string>
 	<key>FullStory</key>
 	<dict>
 		<key>OrgId</key>
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index ed5e0ecd5508..616d8c3fca30 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
 	<key>CFBundleSignature</key>
 	<string>????</string>
 	<key>CFBundleVersion</key>
-	<string>9.1.9.6</string>
+	<string>9.1.9.7</string>
 </dict>
 </plist>
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 8ee2634c68f2..d7c686d3bdc1 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
 	<key>CFBundleShortVersionString</key>
 	<string>9.1.9</string>
 	<key>CFBundleVersion</key>
-	<string>9.1.9.6</string>
+	<string>9.1.9.7</string>
 	<key>NSExtension</key>
 	<dict>
 		<key>NSExtensionPointIdentifier</key>
diff --git a/package-lock.json b/package-lock.json
index 8d50253a07e9..922d4e3a1e2e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "new.expensify",
-  "version": "9.1.9-6",
+  "version": "9.1.9-7",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "new.expensify",
-      "version": "9.1.9-6",
+      "version": "9.1.9-7",
       "hasInstallScript": true,
       "license": "MIT",
       "dependencies": {
diff --git a/package.json b/package.json
index 2c49a32f6883..21daed7d67da 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "new.expensify",
-  "version": "9.1.9-6",
+  "version": "9.1.9-7",
   "author": "Expensify, Inc.",
   "homepage": "https://new.expensify.com",
   "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
diff --git a/src/App.tsx b/src/App.tsx
index f3d37fe87c0b..8dd2631a6b7d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -42,10 +42,17 @@ import type {Route} from './ROUTES';
 import './setup/backgroundTask';
 import {SplashScreenStateContextProvider} from './SplashScreenStateContext';
 
-/** Values passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */
+/**
+ * Properties passed to the top-level React Native component by HybridApp.
+ * These will always be `undefined` in "pure" NewDot builds.
+ */
 type AppProps = {
-    /** URL containing all necessary data to run React Native app (e.g. login data) */
+    /** The URL specifying the initial navigation destination when the app opens */
     url?: Route;
+    /** Serialized configuration data required to initialize the React Native app (e.g. authentication details) */
+    hybridAppSettings?: string;
+    /** A timestamp indicating when the initial properties were last updated, used to detect changes */
+    timestamp?: string;
 };
 
 LogBox.ignoreLogs([
@@ -61,14 +68,18 @@ const fill = {flex: 1};
 
 const StrictModeWrapper = CONFIG.USE_REACT_STRICT_MODE_IN_DEV ? React.StrictMode : ({children}: {children: React.ReactElement}) => children;
 
-function App({url}: AppProps) {
+function App({url, hybridAppSettings, timestamp}: AppProps) {
     useDefaultDragAndDrop();
     OnyxUpdateManager();
 
     return (
         <StrictModeWrapper>
             <SplashScreenStateContextProvider>
-                <InitialURLContextProvider url={url}>
+                <InitialURLContextProvider
+                    url={url}
+                    hybridAppSettings={hybridAppSettings}
+                    timestamp={timestamp}
+                >
                     <GestureHandlerRootView style={fill}>
                         <ComposeProviders
                             components={[
@@ -118,3 +129,5 @@ function App({url}: AppProps) {
 App.displayName = 'App';
 
 export default App;
+
+export type {AppProps};
diff --git a/src/CONST.ts b/src/CONST.ts
index bb52a09504b5..52b8a7f159b6 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -6696,11 +6696,6 @@ const CONST = {
         },
     },
 
-    HYBRID_APP: {
-        REORDERING_REACT_NATIVE_ACTIVITY_TO_FRONT: 'reorderingReactNativeActivityToFront',
-        SINGLE_NEW_DOT_ENTRY: 'singleNewDotEntry',
-    },
-
     MIGRATED_USER_WELCOME_MODAL: 'migratedUserWelcomeModal',
 
     BASE_LIST_ITEM_TEST_ID: 'base-list-item-',
diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx
index eb4e04fefb80..fd3aa68df3ec 100644
--- a/src/components/BookTravelButton.tsx
+++ b/src/components/BookTravelButton.tsx
@@ -75,7 +75,7 @@ function BookTravelButton({text}: BookTravelButtonProps) {
 
                     // Close NewDot if it was opened only for Travel, as its purpose is now fulfilled.
                     Log.info('[HybridApp] Returning to OldDot after opening TravelDot');
-                    NativeModules.HybridAppModule.closeReactNativeApp(false, false);
+                    NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false});
                     setRootStatusBarEnabled(false);
                 })
                 ?.catch(() => {
diff --git a/src/components/InitialURLContextProvider.tsx b/src/components/InitialURLContextProvider.tsx
index c84379af36d8..5276e861f140 100644
--- a/src/components/InitialURLContextProvider.tsx
+++ b/src/components/InitialURLContextProvider.tsx
@@ -1,12 +1,9 @@
 import React, {createContext, useEffect, useMemo, useState} from 'react';
 import type {ReactNode} from 'react';
 import {Linking} from 'react-native';
-import {useOnyx} from 'react-native-onyx';
-import type {ValueOf} from 'type-fest';
 import {signInAfterTransitionFromOldDot} from '@libs/actions/Session';
-import Navigation from '@navigation/Navigation';
+import type {AppProps} from '@src/App';
 import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
 import type {Route} from '@src/ROUTES';
 import {useSplashScreenStateContext} from '@src/SplashScreenStateContext';
 
@@ -21,38 +18,22 @@ const InitialURLContext = createContext<InitialUrlContextType>({
     setInitialURL: () => {},
 });
 
-type InitialURLContextProviderProps = {
-    /** URL passed to our top-level React Native component by HybridApp. Will always be undefined in "pure" NewDot builds. */
-    url?: Route | ValueOf<typeof CONST.HYBRID_APP>;
-
+type InitialURLContextProviderProps = AppProps & {
     /** Children passed to the context provider */
     children: ReactNode;
 };
 
-function InitialURLContextProvider({children, url}: InitialURLContextProviderProps) {
+function InitialURLContextProvider({children, url, hybridAppSettings, timestamp}: InitialURLContextProviderProps) {
     const [initialURL, setInitialURL] = useState<Route | undefined>();
-    const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH);
     const {splashScreenState, setSplashScreenState} = useSplashScreenStateContext();
 
     useEffect(() => {
-        if (url !== CONST.HYBRID_APP.REORDERING_REACT_NATIVE_ACTIVITY_TO_FRONT) {
-            return;
-        }
-
-        if (splashScreenState !== CONST.BOOT_SPLASH_STATE.HIDDEN) {
-            setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN);
-            Navigation.navigate(lastVisitedPath as Route);
-        }
-    }, [lastVisitedPath, setSplashScreenState, splashScreenState, url]);
-
-    useEffect(() => {
-        if (url === CONST.HYBRID_APP.REORDERING_REACT_NATIVE_ACTIVITY_TO_FRONT) {
-            return;
-        }
-
-        if (url) {
-            signInAfterTransitionFromOldDot(url).then((route) => {
-                setInitialURL(route);
+        if (url && hybridAppSettings) {
+            signInAfterTransitionFromOldDot(hybridAppSettings).then(() => {
+                setInitialURL(url);
+                if (splashScreenState === CONST.BOOT_SPLASH_STATE.HIDDEN) {
+                    return;
+                }
                 setSplashScreenState(CONST.BOOT_SPLASH_STATE.READY_TO_BE_HIDDEN);
             });
             return;
@@ -60,7 +41,8 @@ function InitialURLContextProvider({children, url}: InitialURLContextProviderPro
         Linking.getInitialURL().then((initURL) => {
             setInitialURL(initURL as Route);
         });
-    }, [setSplashScreenState, url]);
+        // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+    }, [url, hybridAppSettings, timestamp]);
 
     const initialUrlContext = useMemo(
         () => ({
diff --git a/src/components/ScreenWrapper.tsx b/src/components/ScreenWrapper.tsx
index 34062802c1a0..557d44048cc4 100644
--- a/src/components/ScreenWrapper.tsx
+++ b/src/components/ScreenWrapper.tsx
@@ -1,8 +1,9 @@
-import {UNSTABLE_usePreventRemove, useIsFocused, useNavigation, useRoute} from '@react-navigation/native';
+import {UNSTABLE_usePreventRemove, useIsFocused, useNavigation} from '@react-navigation/native';
 import type {ForwardedRef, ReactNode} from 'react';
 import React, {createContext, forwardRef, useContext, useEffect, useMemo, useRef, useState} from 'react';
 import type {StyleProp, ViewStyle} from 'react-native';
 import {Keyboard, NativeModules, PanResponder, View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
 import {PickerAvoidingView} from 'react-native-picker-select';
 import type {EdgeInsets} from 'react-native-safe-area-context';
 import useEnvironment from '@hooks/useEnvironment';
@@ -18,6 +19,7 @@ import type {ReportsSplitNavigatorParamList, RootNavigatorParamList} from '@libs
 import addViewportResizeListener from '@libs/VisualViewport';
 import toggleTestToolsModal from '@userActions/TestTool';
 import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
 import CustomDevMenu from './CustomDevMenu';
 import CustomStatusBarAndBackgroundContext from './CustomStatusBarAndBackground/CustomStatusBarAndBackgroundContext';
 import FocusTrapForScreens from './FocusTrap/FocusTrapForScreen';
@@ -158,6 +160,7 @@ function ScreenWrapper(
     // since Modals are drawn in separate native view hierarchy we should always add paddings
     const ignoreInsetsConsumption = !useContext(ModalContext).default;
     const {setRootStatusBarEnabled} = useContext(CustomStatusBarAndBackgroundContext);
+    const [isSingleNewDotEntry] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY);
 
     // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout for a case where we want to show the offline indicator only on small screens
     // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth
@@ -169,14 +172,10 @@ function ScreenWrapper(
     const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined;
     const minHeight = shouldEnableMinHeight && !isSafari() ? initialHeight : undefined;
 
-    const route = useRoute();
-    const shouldReturnToOldDot = useMemo(() => {
-        return !!route?.params && CONST.HYBRID_APP.SINGLE_NEW_DOT_ENTRY in route.params && route.params[CONST.HYBRID_APP.SINGLE_NEW_DOT_ENTRY] === 'true';
-    }, [route?.params]);
     const {isBlurred, setIsBlurred} = useInputBlurContext();
 
-    UNSTABLE_usePreventRemove(shouldReturnToOldDot, () => {
-        NativeModules.HybridAppModule?.closeReactNativeApp(false, false);
+    UNSTABLE_usePreventRemove(isSingleNewDotEntry ?? false, () => {
+        NativeModules.HybridAppModule?.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: false});
         setRootStatusBarEnabled(false);
     });
 
diff --git a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts
index e866eebcb792..66c51eb2a807 100644
--- a/src/libs/Notification/PushNotification/subscribePushNotification/index.ts
+++ b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts
@@ -26,6 +26,17 @@ Onyx.connect({
     },
 });
 
+let isSingleNewDotEntry: boolean | undefined;
+Onyx.connect({
+    key: ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY,
+    callback: (value) => {
+        if (!value) {
+            return;
+        }
+        isSingleNewDotEntry = value;
+    },
+});
+
 function getLastUpdateIDAppliedToClient(): Promise<number> {
     return new Promise((resolve) => {
         Onyx.connect({
@@ -106,7 +117,7 @@ function navigateToReport({reportID, reportActionID}: ReportActionPushNotificati
             try {
                 // When transitioning to the new experience via the singleNewDotEntry flow, the navigation
                 // is handled elsewhere. So we cancel here to prevent double navigation.
-                if (Navigation.getActiveRoute().includes(CONST.HYBRID_APP.SINGLE_NEW_DOT_ENTRY)) {
+                if (isSingleNewDotEntry) {
                     Log.info('[PushNotification] Not navigating because this is a singleNewDotEntry flow', false, {reportID, reportActionID});
                     return;
                 }
diff --git a/src/libs/actions/Delegate.ts b/src/libs/actions/Delegate.ts
index 39ada8cf2a10..a4053855d813 100644
--- a/src/libs/actions/Delegate.ts
+++ b/src/libs/actions/Delegate.ts
@@ -157,7 +157,14 @@ function connect(email: string) {
 
                     NetworkStore.setAuthToken(response?.restrictedToken ?? null);
                     confirmReadyToOpenApp();
-                    openApp().then(() => NativeModules.HybridAppModule?.switchAccount(email, restrictedToken, policyID, String(previousAccountID)));
+                    openApp().then(() =>
+                        NativeModules.HybridAppModule?.switchAccount({
+                            newDotCurrentAccountEmail: email,
+                            authToken: restrictedToken,
+                            policyID,
+                            accountID: String(previousAccountID),
+                        }),
+                    );
                 });
         })
         .catch((error) => {
@@ -241,7 +248,14 @@ function disconnect() {
                     NetworkStore.setAuthToken(response?.authToken ?? null);
 
                     confirmReadyToOpenApp();
-                    openApp().then(() => NativeModules.HybridAppModule?.switchAccount(requesterEmail, authToken, '', ''));
+                    openApp().then(() =>
+                        NativeModules.HybridAppModule?.switchAccount({
+                            newDotCurrentAccountEmail: requesterEmail,
+                            authToken,
+                            policyID: '',
+                            accountID: '',
+                        }),
+                    );
                 });
         })
         .catch((error) => {
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index 98f643b385e5..ed8707efce23 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -231,7 +231,7 @@ function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSess
     if (!isAnonymousUser()) {
         // In the HybridApp, we want the Old Dot to handle the sign out process
         if (NativeModules.HybridAppModule && killHybridApp) {
-            NativeModules.HybridAppModule.closeReactNativeApp(true, false);
+            NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false});
             return;
         }
         // We'll only call signOut if we're not stashing the session and this is not a supportal session,
@@ -514,9 +514,22 @@ function getLastUpdateIDAppliedToClient(): Promise<number> {
     });
 }
 
-function signInAfterTransitionFromOldDot(transitionURL: string) {
-    const [route, queryParams] = transitionURL.split('?');
+type HybridAppSettings = {
+    email: string;
+    authToken: string;
+    accountID: number;
+    autoGeneratedLogin: string;
+    autoGeneratedPassword: string;
+    clearOnyxOnStart: boolean;
+    completedHybridAppOnboarding: boolean;
+    isSingleNewDotEntry: boolean;
+    primaryLogin: string;
+    encryptedAuthToken: string;
+    nudgeMigrationTimestamp?: string;
+    oldDotOriginalAccountEmail?: string;
+};
 
+function signInAfterTransitionFromOldDot(hybridAppSettings: string) {
     const {
         email,
         authToken,
@@ -530,75 +543,63 @@ function signInAfterTransitionFromOldDot(transitionURL: string) {
         isSingleNewDotEntry,
         primaryLogin,
         oldDotOriginalAccountEmail,
-    } = Object.fromEntries(
-        queryParams.split('&').map((param) => {
-            const [key, value] = param.split('=');
-            return [key, value];
-        }),
-    );
+    } = JSON.parse(hybridAppSettings) as HybridAppSettings;
 
     const clearOnyxForNewAccount = () => {
-        if (clearOnyxOnStart !== 'true') {
+        if (!clearOnyxOnStart) {
             return Promise.resolve();
         }
 
         return Onyx.clear(KEYS_TO_PRESERVE);
     };
 
-    const setSessionDataAndOpenApp = new Promise<Route>((resolve) => {
-        clearOnyxForNewAccount()
-            .then(() => {
-                // This section controls copilot changes
-                const currentUserEmail = getCurrentUserEmail();
-
-                // If ND and OD account are the same - do nothing
-                if (email === currentUserEmail) {
-                    return;
-                }
-
-                // If account was changed to original one on OD side - clear onyx
-                if (!oldDotOriginalAccountEmail) {
-                    return Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS);
-                }
-
-                // If we're already logged in - do nothing, data will be set in next step
-                if (currentUserEmail) {
-                    return;
-                }
-
-                // If we're not logged in - set stashed data
-                return Onyx.multiSet({
-                    [ONYXKEYS.STASHED_CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword},
-                });
-            })
-            .then(() =>
-                Onyx.multiSet({
-                    [ONYXKEYS.SESSION]: {email, authToken, encryptedAuthToken: decodeURIComponent(encryptedAuthToken), accountID: Number(accountID)},
-                    [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword},
-                    [ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: isSingleNewDotEntry === 'true',
-                    [ONYXKEYS.NVP_TRYNEWDOT]: {
-                        classicRedirect: {completedHybridAppOnboarding: completedHybridAppOnboarding === 'true'},
-                        nudgeMigration: nudgeMigrationTimestamp ? {timestamp: new Date(nudgeMigrationTimestamp)} : undefined,
-                    },
-                }).then(() => Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin})),
-            )
-            .then(() => {
-                if (clearOnyxOnStart === 'true') {
-                    return openApp();
-                }
-                return getLastUpdateIDAppliedToClient().then((lastUpdateId) => {
-                    return reconnectApp(lastUpdateId);
-                });
-            })
-            .catch((error) => {
-                Log.hmmm('[HybridApp] Initialization of HybridApp has failed. Forcing transition', {error});
-            })
-            .finally(() => {
-                resolve(`${route}${isSingleNewDotEntry === 'true' ? '?singleNewDotEntry=true' : ''}` as Route);
-            });
-    });
+    return clearOnyxForNewAccount()
+        .then(() => {
+            // This section controls copilot changes
+            const currentUserEmail = getCurrentUserEmail();
+
+            // If ND and OD account are the same - do nothing
+            if (email === currentUserEmail) {
+                return;
+            }
 
-    return setSessionDataAndOpenApp;
+            // If account was changed to original one on OD side - clear onyx
+            if (!oldDotOriginalAccountEmail) {
+                return Onyx.clear(KEYS_TO_PRESERVE_DELEGATE_ACCESS);
+            }
+
+            // If we're already logged in - do nothing, data will be set in next step
+            if (currentUserEmail) {
+                return;
+            }
+
+            // If we're not logged in - set stashed data
+            return Onyx.multiSet({
+                [ONYXKEYS.STASHED_CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword},
+            });
+        })
+        .then(() =>
+            Onyx.multiSet({
+                [ONYXKEYS.SESSION]: {email, authToken, encryptedAuthToken: decodeURIComponent(encryptedAuthToken), accountID: Number(accountID)},
+                [ONYXKEYS.CREDENTIALS]: {autoGeneratedLogin, autoGeneratedPassword},
+                [ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY]: isSingleNewDotEntry,
+                [ONYXKEYS.NVP_TRYNEWDOT]: {
+                    classicRedirect: {completedHybridAppOnboarding},
+                    nudgeMigration: nudgeMigrationTimestamp ? {timestamp: new Date(nudgeMigrationTimestamp)} : undefined,
+                },
+            }).then(() => Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin})),
+        )
+        .then(() => {
+            if (clearOnyxOnStart) {
+                return openApp();
+            }
+            return getLastUpdateIDAppliedToClient().then((lastUpdateId) => {
+                return reconnectApp(lastUpdateId);
+            });
+        })
+        .catch((error) => {
+            Log.hmmm('[HybridApp] Initialization of HybridApp has failed. Forcing transition', {error});
+        });
 }
 
 /**
diff --git a/src/libs/actions/Welcome/index.ts b/src/libs/actions/Welcome/index.ts
index f1faebe27e75..e64d7690833b 100644
--- a/src/libs/actions/Welcome/index.ts
+++ b/src/libs/actions/Welcome/index.ts
@@ -146,7 +146,7 @@ function completeHybridAppOnboarding() {
 
         // No matter what the response is, we want to mark the onboarding as completed (user saw the explanation modal)
         Log.info(`[HybridApp] Onboarding status has changed. Propagating new value to OldDot`, true);
-        NativeModules.HybridAppModule.completeOnboarding(true);
+        NativeModules.HybridAppModule.completeOnboarding({status: true});
     });
 }
 
diff --git a/src/pages/ErrorPage/SessionExpiredPage.tsx b/src/pages/ErrorPage/SessionExpiredPage.tsx
index 5ccf70c40ab6..2849dcd624ac 100644
--- a/src/pages/ErrorPage/SessionExpiredPage.tsx
+++ b/src/pages/ErrorPage/SessionExpiredPage.tsx
@@ -9,7 +9,7 @@ import useLocalize from '@hooks/useLocalize';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import Navigation from '@libs/Navigation/Navigation';
-import * as Session from '@userActions/Session';
+import {clearSignInData} from '@userActions/Session';
 
 function SessionExpiredPage() {
     const styles = useThemeStyles();
@@ -33,11 +33,11 @@ function SessionExpiredPage() {
                         <TextLink
                             onPress={() => {
                                 if (!NativeModules.HybridAppModule) {
-                                    Session.clearSignInData();
+                                    clearSignInData();
                                     Navigation.goBack();
                                     return;
                                 }
-                                NativeModules.HybridAppModule.closeReactNativeApp(true, false);
+                                NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false});
                             }}
                         >
                             {translate('deeplinkWrapper.signIn')}
diff --git a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx
index 6587909231f6..f5d51101ed51 100644
--- a/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx
+++ b/src/pages/OnboardingEmployees/BaseOnboardingEmployees.tsx
@@ -111,7 +111,7 @@ function BaseOnboardingEmployees({shouldUseNativeStyles, route}: BaseOnboardingE
                     }
 
                     if (NativeModules.HybridAppModule) {
-                        NativeModules.HybridAppModule.closeReactNativeApp(false, true);
+                        NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true});
                         setRootStatusBarEnabled(false);
                     } else {
                         openOldDotLink(CONST.OLDDOT_URLS.INBOX, true);
diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
index d3e0bb5013de..5f3aba4b3a28 100644
--- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
+++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
@@ -571,7 +571,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT
                 onConfirm={() => {
                     setModalVisible(false);
                     if (NativeModules.HybridAppModule) {
-                        NativeModules.HybridAppModule.closeReactNativeApp(false, true);
+                        NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true});
                         setRootStatusBarEnabled(false);
                         return;
                     }
diff --git a/src/pages/settings/InitialSettingsPage.tsx b/src/pages/settings/InitialSettingsPage.tsx
index 821b5b5eb2c2..93a36697084a 100755
--- a/src/pages/settings/InitialSettingsPage.tsx
+++ b/src/pages/settings/InitialSettingsPage.tsx
@@ -251,7 +251,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
                     ...(NativeModules.HybridAppModule
                         ? {
                               action: () => {
-                                  NativeModules.HybridAppModule.closeReactNativeApp(false, true);
+                                  NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: false, shouldSetNVP: true});
                                   setInitialURL(undefined);
                                   setRootStatusBarEnabled(false);
                               },
diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts
index ae63bf77b2a0..8aed217e73ff 100644
--- a/src/types/modules/react-native.d.ts
+++ b/src/types/modules/react-native.d.ts
@@ -6,9 +6,9 @@ import type {ShortcutManagerModule} from '@libs/ShortcutManager';
 import type StartupTimer from '@libs/StartupTimer/types';
 
 type HybridAppModule = {
-    closeReactNativeApp: (shouldSignOut: boolean, shouldSetNVP: boolean) => void;
-    completeOnboarding: (status: boolean) => void;
-    switchAccount: (newDotCurrentAccountEmail: string, authToken: string, policyID: string, accountID: string) => void;
+    closeReactNativeApp: ({shouldSignOut: boolean, shouldSetNVP: boolean}) => void;
+    completeOnboarding: ({status: boolean}) => void;
+    switchAccount: ({newDotCurrentAccountEmail: string, authToken: string, policyID: string, accountID: string}) => void;
 };
 
 type RNTextInputResetModule = {


Reviews:
user: login: war-in
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T15:53:08Z
user: login: war-in
body: Looks good 👍 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:04:47Z
user: login: Julesssss
body: Auth and app switching is working perfectly for me, new and existing accounts on Android
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T16:24:49Z
user: login: hungvu193
body: Test well!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T17:53:05Z

Review comments:
diff_hunk: @@ -228,71 +228,81 @@ function isExpiredSession(sessionCreationDate: number): boolean {
 function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, killHybridApp = true) {
     Log.info('Redirecting to Sign In because signOut() was called');
     hideContextMenu(false);
-    if (!isAnonymousUser()) {
-        // In the HybridApp, we want the Old Dot to handle the sign out process
-        if (NativeModules.HybridAppModule && killHybridApp) {
-            NativeModules.HybridAppModule.closeReactNativeApp(true, false);
-            return;
-        }
-        // We'll only call signOut if we're not stashing the session and this is not a supportal session,
-        // otherwise we'll call the API to invalidate the autogenerated credentials used for infinite
-        // session.
-        const isSupportal = isSupportAuthToken();
-        if (!isSupportal && !shouldStashSession) {
-            signOut();
-        }
 
-        // The function redirectToSignIn will clear the whole storage, so let's create our onyx params
-        // updates for the credentials before we call it
-        let onyxSetParams = {};
-
-        // If we are not currently using a support token, and we received stashSession as true, we need to
-        // store the credentials so the user doesn't need to login again after they finish their supportal
-        // action. This needs to computed before we call `redirectToSignIn`
-        if (!isSupportal && shouldStashSession) {
-            onyxSetParams = {
-                [ONYXKEYS.STASHED_CREDENTIALS]: credentials,
-                [ONYXKEYS.STASHED_SESSION]: session,
-            };
-        }
-        // If this is a supportal token, and we've received the parameters to stashSession as true, and
-        // we already have a stashedSession, that means we are supportaled, currently supportaling
-        // into another account and we want to keep the stashed data from the original account.
-        if (isSupportal && shouldStashSession && hasStashedSession()) {
-            onyxSetParams = {
-                [ONYXKEYS.STASHED_CREDENTIALS]: stashedCredentials,
-                [ONYXKEYS.STASHED_SESSION]: stashedSession,
-            };
-        }
-        // Now if this is a supportal access, we do not want to stash the current session and we have a
-        // stashed session, then we need to restore the stashed session instead of completely logging out
-        if (isSupportal && !shouldStashSession && hasStashedSession()) {
-            onyxSetParams = {
-                [ONYXKEYS.CREDENTIALS]: stashedCredentials,
-                [ONYXKEYS.SESSION]: stashedSession,
-            };
-        }
-        if (isSupportal && !shouldStashSession && !hasStashedSession()) {
-            Log.info('No stashed session found for supportal access, clearing the session');
+    if (isAnonymousUser()) {
+        if (!Navigation.isActiveRoute(ROUTES.SIGN_IN_MODAL)) {
+            if (shouldResetToHome) {
+                Navigation.resetToHome();
+            }
+            Navigation.navigate(ROUTES.SIGN_IN_MODAL);
+            Linking.getInitialURL().then((url) => {
+                const reportID = getReportIDFromLink(url);
+                if (reportID) {
+                    Onyx.merge(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID, reportID);
+                }
+            });
         }
-        redirectToSignIn().then(() => {
+        return;
+    }
path: src/libs/actions/Session/index.ts
user: login: war-in
body: I'd say that those changes are not from my PR 🤔 
created_at: 2025-03-06T15:50:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -228,71 +228,81 @@ function isExpiredSession(sessionCreationDate: number): boolean {
 function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, killHybridApp = true) {
     Log.info('Redirecting to Sign In because signOut() was called');
     hideContextMenu(false);
-    if (!isAnonymousUser()) {
-        // In the HybridApp, we want the Old Dot to handle the sign out process
-        if (NativeModules.HybridAppModule && killHybridApp) {
-            NativeModules.HybridAppModule.closeReactNativeApp(true, false);
-            return;
-        }
-        // We'll only call signOut if we're not stashing the session and this is not a supportal session,
-        // otherwise we'll call the API to invalidate the autogenerated credentials used for infinite
-        // session.
-        const isSupportal = isSupportAuthToken();
-        if (!isSupportal && !shouldStashSession) {
-            signOut();
-        }
 
-        // The function redirectToSignIn will clear the whole storage, so let's create our onyx params
-        // updates for the credentials before we call it
-        let onyxSetParams = {};
-
-        // If we are not currently using a support token, and we received stashSession as true, we need to
-        // store the credentials so the user doesn't need to login again after they finish their supportal
-        // action. This needs to computed before we call `redirectToSignIn`
-        if (!isSupportal && shouldStashSession) {
-            onyxSetParams = {
-                [ONYXKEYS.STASHED_CREDENTIALS]: credentials,
-                [ONYXKEYS.STASHED_SESSION]: session,
-            };
-        }
-        // If this is a supportal token, and we've received the parameters to stashSession as true, and
-        // we already have a stashedSession, that means we are supportaled, currently supportaling
-        // into another account and we want to keep the stashed data from the original account.
-        if (isSupportal && shouldStashSession && hasStashedSession()) {
-            onyxSetParams = {
-                [ONYXKEYS.STASHED_CREDENTIALS]: stashedCredentials,
-                [ONYXKEYS.STASHED_SESSION]: stashedSession,
-            };
-        }
-        // Now if this is a supportal access, we do not want to stash the current session and we have a
-        // stashed session, then we need to restore the stashed session instead of completely logging out
-        if (isSupportal && !shouldStashSession && hasStashedSession()) {
-            onyxSetParams = {
-                [ONYXKEYS.CREDENTIALS]: stashedCredentials,
-                [ONYXKEYS.SESSION]: stashedSession,
-            };
-        }
-        if (isSupportal && !shouldStashSession && !hasStashedSession()) {
-            Log.info('No stashed session found for supportal access, clearing the session');
+    if (isAnonymousUser()) {
+        if (!Navigation.isActiveRoute(ROUTES.SIGN_IN_MODAL)) {
+            if (shouldResetToHome) {
+                Navigation.resetToHome();
+            }
+            Navigation.navigate(ROUTES.SIGN_IN_MODAL);
+            Linking.getInitialURL().then((url) => {
+                const reportID = getReportIDFromLink(url);
+                if (reportID) {
+                    Onyx.merge(ONYXKEYS.LAST_OPENED_PUBLIC_ROOM_ID, reportID);
+                }
+            });
         }
-        redirectToSignIn().then(() => {
+        return;
+    }
+
+    // In the HybridApp, we want the Old Dot to handle the sign out process
+    if (NativeModules.HybridAppModule && killHybridApp) {
+        NativeModules.HybridAppModule.closeReactNativeApp({shouldSignOut: true, shouldSetNVP: false});
path: src/libs/actions/Session/index.ts
user: login: war-in
body: only this is relevant in the entire function
created_at: 2025-03-06T15:51:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: melvin-bot[bot]
created_at: 2025-03-06T14:49:50Z
body: @arosiclair Please copy/paste the Reviewer Checklist from [here](https://raw.githubusercontent.com/Expensify/App/main/contributingGuides/REVIEWER_CHECKLIST.md) into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-06T15:42:23Z
body: 🚧 @Julesssss has triggered a test hybrid app build. You can view the [workflow run here](https://github.com/Expensify/App/actions/runs/13702499412).
author_association: CONTRIBUTOR
user: login: hungvu193
created_at: 2025-03-06T15:54:50Z
body: I'm running the hybrid app, it's taking more time than I expected 👀 
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T16:24:00Z
body: :test_tube::test_tube: Use the links below to test this adhoc build on Android, iOS, Desktop, and Web. Happy testing! :test_tube::test_tube:
| Android :robot:  | iOS :apple: |
| ------------- | ------------- |
| https://ad-hoc-expensify-cash.s3.amazonaws.com/android/57934/index.html  | https://ad-hoc-expensify-cash.s3.amazonaws.com/ios/57934/index.html  |
| ![Android](https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=https://ad-hoc-expensify-cash.s3.amazonaws.com/android/57934/index.html)  | ![iOS](https://api.qrserver.com/v1/create-qr-code/?size=120x120&data=https://ad-hoc-expensify-cash.s3.amazonaws.com/ios/57934/index.html)  |
| Android Hybrid :robot::arrows_counterclockwise:  | iOS Hybrid :apple::arrows_counterclockwise: |
| N/A  | N/A  |
| N/A  | N/A  |
| Desktop :computer: | Web :spider_web: |
| N/A  | N/A  |
| N/A  | N/A  |

---

:eyes: [View the workflow run that generated this build](https://github.com/Expensify/App/actions/runs/13702499412) :eyes:

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T17:35:27Z
body: 🚧 @puneetlath has triggered a test app build. You can view the [workflow run here](https://github.com/Expensify/App/actions/runs/13704696899).
author_association: CONTRIBUTOR
user: login: hungvu193
created_at: 2025-03-06T17:51:44Z
body: ## Reviewer Checklist

- [x] I have verified the author checklist is complete (all boxes are checked off).
- [x] I verified the correct issue is linked in the `### Fixed Issues` section above
- [x] I verified testing steps are clear and they cover the changes made in this PR
    - [x] I verified the steps for local testing are in the `Tests` section
    - [x] I verified the steps for Staging and/or Production testing are in the `QA steps` section
    - [x] I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    - [x] I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
- [x] I checked that screenshots or videos are included for tests on [all platforms](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#make-sure-you-can-test-on-all-platforms)
- [x] I included screenshots or videos for tests on [all platforms](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#make-sure-you-can-test-on-all-platforms)
- [x] I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
- [x] I verified tests pass on **all platforms** & I tested again on:
    - [x] Android: Native
    - [x] Android: mWeb Chrome
    - [x] iOS: Native
    - [x] iOS: mWeb Safari
    - [x] MacOS: Chrome / Safari
    - [x] MacOS: Desktop
- [x] If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
- [x] I verified proper code patterns were followed (see [Reviewing the code](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md#reviewing-the-code))
    - [x] I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. `toggleReport` and not `onIconClick`).
    - [x] I verified that comments were added to code that is not self explanatory
    - [x] I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    - [x] I verified any copy / text shown in the product is localized by adding it to `src/languages/*` files and using the [translation method](https://github.com/Expensify/App/blob/4bd99402cebdf4d7394e0d1f260879ea238197eb/src/components/withLocalize.js#L60)
    - [x] I verified all numbers, amounts, dates and phone numbers shown in the product are using the [localization methods](https://github.com/Expensify/App/blob/4bd99402cebdf4d7394e0d1f260879ea238197eb/src/components/withLocalize.js#L60-L68)
    - [x] I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    - [x] I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    - [x] I verified the JSDocs style guidelines (in [`STYLE.md`](https://github.com/Expensify/App/blob/main/contributingGuides/STYLE.md#jsdocs)) were followed
- [x] If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
- [x] I verified that this PR follows the guidelines as stated in the [Review Guidelines](https://github.com/Expensify/App/blob/main/contributingGuides/PR_REVIEW_GUIDELINES.md)
- [x] I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like `Avatar`, I verified the components using `Avatar` have been tested & I retested again)
- [x] I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
- [x] I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
- [x] If a new component is created I verified that:
    - [x] A similar component doesn't exist in the codebase
    - [x] All props are defined accurately and each prop has a `/** comment above it */`
    - [x] The file is named correctly
    - [x] The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    - [x] The only data being stored in the state is data necessary for rendering and nothing else
    - [x] For Class Components, any internal methods passed to components event handlers are bound to `this` properly so there are no scoping issues (i.e. for `onClick={this.submit}` the method `this.submit` should be bound to `this` in the constructor)
    - [x] Any internal methods bound to `this` are necessary to be bound (i.e. avoid `this.submit = this.submit.bind(this);` if `this.submit` is never passed to a component event handler like `onClick`)
    - [x] All JSX used for rendering exists in the render method
    - [x] The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
- [x] If any new file was added I verified that:
    - [x] The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
- [x] If a new CSS style is added I verified that:
    - [x] A similar style doesn't already exist
    - [x] The style can't be created with an existing [StyleUtils](https://github.com/Expensify/App/blob/main/src/utils/index.ts) function (i.e. `StyleUtils.getBackgroundAndBorderStyle(theme.componentBG`)
- [x] If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
- [x] If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like `Avatar` is modified, I verified that `Avatar` is working as expected in all cases)
- [x] If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
- [x] If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
- [x] If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    - [x] I verified that all the inputs inside a form are aligned with each other.
    - [x] I added `Design` label and/or tagged `@Expensify/design` so the design team can review the changes.
- [x] If a new page is added, I verified it's using the `ScrollView` component to make it scrollable when more elements are added to the page.
- [x] For any bug fix or new feature in this PR, I verified that sufficient [unit tests](https://github.com/Expensify/App/blob/main/tests/README.md) are included to prevent regressions in this flow.
- [x] If the `main` branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the `Test` steps.
- [x] I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

### Screenshots/Videos
<details>
<summary>Android: Native</summary>

<!-- add screenshots or videos here -->


https://github.com/user-attachments/assets/4d2d9c4d-f22f-4bf1-97f8-cf5281f2a8f7




</details>

<details>
<summary>Android: mWeb Chrome</summary>

<!-- add screenshots or videos here -->


https://github.com/user-attachments/assets/2eba494e-ee2c-417d-9197-2dafb969e971




</details>

<details>
<summary>iOS: Native</summary>

<!-- add screenshots or videos here -->


https://github.com/user-attachments/assets/f9a29f62-954f-4142-a3df-32a45f04de83



</details>

<details>
<summary>iOS: mWeb Safari</summary>

<!-- add screenshots or videos here -->


https://github.com/user-attachments/assets/59c02f77-c4a2-4f22-a912-0fed853bb292


</details>

<details>
<summary>MacOS: Chrome / Safari</summary>

<!-- add screenshots or videos here -->

https://github.com/user-attachments/assets/d83759e4-f210-4a0c-a44c-740b009799a3


</details>

<details>
<summary>MacOS: Desktop</summary>

<!-- add screenshots or videos here -->

https://github.com/user-attachments/assets/3c64a46f-c7af-45a0-b8a4-09816bdc42e9


</details>

author_association: CONTRIBUTOR
user: login: melvin-bot[bot]
created_at: 2025-03-06T17:53:11Z
body: 🎯 @hungvu193, thanks for reviewing and testing this PR! 🎉

An E/App issue has been created to issue payment here: https://github.com/Expensify/App/issues/57946.
author_association: NONE