From 43fa95469836af52610255b1b8511ec463857351 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Tue, 17 Feb 2026 17:20:11 +0100
Subject: [PATCH 01/13] Freeze non-top screens within SplitNavigator on web to
 prevent unnecessary re-renders

---
 .../createSplitNavigator/index.tsx            |  1 +
 .../index.tsx                                 | 25 +++++++++++++++++--
 .../types/NavigatorComponent.ts               |  1 +
 3 files changed, 25 insertions(+), 2 deletions(-)

diff --git a/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx b/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx
index 4483297f6a1b..96607dc9b1d5 100644
--- a/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx
+++ b/src/libs/Navigation/AppNavigator/createSplitNavigator/index.tsx
@@ -41,6 +41,7 @@ const SplitNavigatorComponent = createPlatformStackNavigatorComponent('SplitNavi
     defaultScreenOptions: defaultPlatformStackScreenOptions,
     useCustomState: useCustomSplitNavigatorState,
     NavigationContentWrapper: SidebarSpacerWrapper,
+    freezeNonTopScreens: true,
 });
 
 function createSplitNavigator<
diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx
index 730e269d507a..b848c0ffc776 100644
--- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx
+++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx
@@ -13,6 +13,7 @@ import type {
     PlatformStackNavigatorProps,
     PlatformStackRouterOptions,
 } from '@libs/Navigation/PlatformStackNavigation/types';
+import ScreenFreezeWrapper from './ScreenFreezeWrapper';
 
 function createPlatformStackNavigatorComponent<RouterOptions extends PlatformStackRouterOptions = PlatformStackRouterOptions>(
     displayName: string,
@@ -24,6 +25,7 @@ function createPlatformStackNavigatorComponent<RouterOptions extends PlatformSta
     const ExtraContent = options?.ExtraContent;
     const NavigationContentWrapper = options?.NavigationContentWrapper;
     const useCustomEffects = options?.useCustomEffects ?? (() => undefined);
+    const freezeNonTopScreens = options?.freezeNonTopScreens;
 
     function PlatformNavigator({
         id,
@@ -100,6 +102,25 @@ function createPlatformStackNavigatorComponent<RouterOptions extends PlatformSta
             };
         }, [persistentScreens, state]);
 
+        const wrappedDescriptors = useMemo(() => {
+            if (!freezeNonTopScreens) {
+                return descriptors;
+            }
+
+            const topRouteKey = state.routes[state.index]?.key;
+            const result: typeof descriptors = {};
+            for (const [key, descriptor] of Object.entries(descriptors)) {
+                const isOnTop = key === topRouteKey;
+                const isPersistent = persistentScreens?.includes(descriptor.route.name);
+                const isScreenBlurred = !isOnTop && !isPersistent;
+                result[key] = {
+                    ...descriptor,
+                    render: () => <ScreenFreezeWrapper isScreenBlurred={isScreenBlurred}>{descriptor.render()}</ScreenFreezeWrapper>,
+                };
+            }
+            return result;
+        }, [descriptors, persistentScreens, state]);
+
         const Content = useMemo(
             () => (
                 <NavigationContent>
@@ -108,7 +129,7 @@ function createPlatformStackNavigatorComponent<RouterOptions extends PlatformSta
                         {...props}
                         direction="ltr"
                         state={mappedState}
-                        descriptors={descriptors}
+                        descriptors={wrappedDescriptors}
                         navigation={navigation}
                         describe={describe}
                     />
@@ -119,7 +140,7 @@ function createPlatformStackNavigatorComponent<RouterOptions extends PlatformSta
                     )}
                 </NavigationContent>
             ),
-            [NavigationContent, customCodePropsWithCustomState, describe, descriptors, mappedState, navigation, props],
+            [NavigationContent, customCodePropsWithCustomState, describe, wrappedDescriptors, mappedState, navigation, props],
         );
 
         // eslint-disable-next-line react/jsx-props-no-spreading
diff --git a/src/libs/Navigation/PlatformStackNavigation/types/NavigatorComponent.ts b/src/libs/Navigation/PlatformStackNavigation/types/NavigatorComponent.ts
index 87d8141437a5..eb0c95880c86 100644
--- a/src/libs/Navigation/PlatformStackNavigation/types/NavigatorComponent.ts
+++ b/src/libs/Navigation/PlatformStackNavigation/types/NavigatorComponent.ts
@@ -56,6 +56,7 @@ type CreatePlatformStackNavigatorComponentOptions<RouterOptions extends Platform
     useCustomEffects?: CustomEffectsHook<ParamList>;
     ExtraContent?: ExtraContent;
     NavigationContentWrapper?: NavigationContentWrapper;
+    freezeNonTopScreens?: boolean;
 };
 
 export type {CustomCodeProps, CustomStateHookProps, CustomEffectsHookProps, CreatePlatformStackNavigatorComponentOptions, ExtraContentProps};

From 273e9c1f7fc87d530090975ae6618bd4e4ed70c2 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 18 Feb 2026 10:05:29 +0100
Subject: [PATCH 02/13] Add ScreenFreezeWrapper

---
 .../ScreenFreezeWrapper.tsx                   | 22 +++++++++++++++++++
 1 file changed, 22 insertions(+)
 create mode 100644 src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx

diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx
new file mode 100644
index 000000000000..4697a3f41f3c
--- /dev/null
+++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx
@@ -0,0 +1,22 @@
+import React, {useLayoutEffect, useState} from 'react';
+import {Freeze} from 'react-freeze';
+
+type ScreenFreezeWrapperProps = {
+    isScreenBlurred: boolean;
+    children: React.ReactNode;
+};
+
+function ScreenFreezeWrapper({isScreenBlurred, children}: ScreenFreezeWrapperProps) {
+    const [frozen, setFrozen] = useState(false);
+
+    // Decouple the Suspense render task so it won't be interrupted by React's concurrent mode
+    // and stuck in an infinite loop
+    useLayoutEffect(() => {
+        // eslint-disable-next-line react-hooks/set-state-in-effect
+        setFrozen(isScreenBlurred);
+    }, [isScreenBlurred]);
+
+    return <Freeze freeze={frozen}>{children}</Freeze>;
+}
+
+export default ScreenFreezeWrapper;

From 13e4629dd1621c3398c56a7616397250883c98d3 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Fri, 20 Feb 2026 11:01:02 +0100
Subject: [PATCH 03/13] Freeze screen below focused on mobile

---
 ...+7.3.14+003+freeze-screen-below-focused.patch | 16 ++++++++++++++++
 patches/react-navigation/details.md              | 10 +++++++++-
 src/setup/index.ts                               |  5 +++++
 3 files changed, 30 insertions(+), 1 deletion(-)
 create mode 100644 patches/react-navigation/@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch

diff --git a/patches/react-navigation/@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch b/patches/react-navigation/@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch
new file mode 100644
index 000000000000..21c056fe388e
--- /dev/null
+++ b/patches/react-navigation/@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch
@@ -0,0 +1,16 @@
+diff --git a/node_modules/@react-navigation/native-stack/lib/module/views/NativeStackView.native.js b/node_modules/@react-navigation/native-stack/lib/module/views/NativeStackView.native.js
+--- a/node_modules/@react-navigation/native-stack/lib/module/views/NativeStackView.native.js
++++ b/node_modules/@react-navigation/native-stack/lib/module/views/NativeStackView.native.js
+@@ -376,9 +376,9 @@
+         const isModal = modalRouteKeys.includes(route.key);
+         const isPreloaded = preloadedDescriptors[route.key] !== undefined && descriptors[route.key] === undefined;
+
+-        // On Fabric, when screen is frozen, animated and reanimated values are not updated
+-        // due to component being unmounted. To avoid this, we don't freeze the previous screen there
+-        const shouldFreeze = isFabric() ? !isPreloaded && !isFocused && !isBelowFocused : !isPreloaded && !isFocused;
++        // Freezing the screen below the focused one is safe on Fabric because
++        // DelayedFreeze defers it to the next macrotask, and transition animations run on the UI thread.
++        const shouldFreeze = !isPreloaded && !isFocused;
+         return /*#__PURE__*/_jsx(SceneView, {
+           index: index,
+           focused: isFocused,
\ No newline at end of file
diff --git a/patches/react-navigation/details.md b/patches/react-navigation/details.md
index 353fac20ab40..5d7ef976c7b8 100644
--- a/patches/react-navigation/details.md
+++ b/patches/react-navigation/details.md
@@ -39,6 +39,14 @@
 - PR Introducing Patch: [#37891](https://github.com/Expensify/App/pull/37891)
 - PR Updating Patch: [#64155](https://github.com/Expensify/App/pull/64155) 
 
+### [@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch](@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch)
+
+- Reason: Removes the `isBelowFocused` exception on Fabric that prevented freezing the screen directly below the focused one. This is now safe because `DelayedFreeze` in `react-native-screens` defers freezing to the next macrotask, and native-stack transition animations run on the UI thread independently of the React tree.
+- Upstream PR/issue: N/A
+- E/App issue: N/A
+- PR Introducing Patch: https://github.com/Expensify/App/pull/82764
+- PR Updating Patch: N/A
+
 ### [@react-navigation+native+7.1.10+001+initial.patch](@react-navigation+native+7.1.10+001+initial.patch)
 
 - Reason: Allows us to use some more advanced navigation actions without messing up the browser history
@@ -75,4 +83,4 @@
 - Upstream PR/issue: N/A
 - E/App issue: [#65709](https://github.com/Expensify/App/issues/65211)
 - PR Introducing Patch: [#65836](https://github.com/Expensify/App/pull/66890)
-- PR Updating Patch: N/A
\ No newline at end of file
+- PR Updating Patch: N/A
diff --git a/src/setup/index.ts b/src/setup/index.ts
index 1c8f5655ff0d..87c461a69442 100644
--- a/src/setup/index.ts
+++ b/src/setup/index.ts
@@ -2,6 +2,7 @@ import toSortedPolyfill from 'array.prototype.tosorted';
 import {I18nManager} from 'react-native';
 import Config from 'react-native-config';
 import Onyx from 'react-native-onyx';
+import {enableFreeze} from 'react-native-screens';
 import intlPolyfill from '@libs/IntlPolyfill';
 import {setDeviceID} from '@userActions/Device';
 import initOnyxDerivedValues from '@userActions/OnyxDerived';
@@ -14,6 +15,10 @@ import telemetry from './telemetry';
 const enableDevTools = Config?.USE_REDUX_DEVTOOLS ? Config.USE_REDUX_DEVTOOLS === 'true' : true;
 
 export default function () {
+    // Enable screen freezing on mobile to prevent unnecessary re-renders on screens that are not visible to the user.
+    // This is a no-op on web — for web, we use ScreenFreezeWrapper in SplitNavigator instead.
+    enableFreeze(true);
+
     telemetry();
 
     toSortedPolyfill.shim();

From 7f9b9e789f67e9d180beef6d1bd6d2a69be3ae02 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Fri, 27 Feb 2026 09:28:34 +0100
Subject: [PATCH 04/13] Update the patch to change the
 NativeStackView.native.tsx too for consistency

---
 ...3.14+003+freeze-screen-below-focused.patch | 26 ++++++++++++++++---
 1 file changed, 23 insertions(+), 3 deletions(-)

diff --git a/patches/react-navigation/@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch b/patches/react-navigation/@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch
index 21c056fe388e..726fe73f8ecc 100644
--- a/patches/react-navigation/@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch
+++ b/patches/react-navigation/@react-navigation+native-stack+7.3.14+003+freeze-screen-below-focused.patch
@@ -1,10 +1,11 @@
 diff --git a/node_modules/@react-navigation/native-stack/lib/module/views/NativeStackView.native.js b/node_modules/@react-navigation/native-stack/lib/module/views/NativeStackView.native.js
+index 35dfc05..1758a24 100644
 --- a/node_modules/@react-navigation/native-stack/lib/module/views/NativeStackView.native.js
 +++ b/node_modules/@react-navigation/native-stack/lib/module/views/NativeStackView.native.js
-@@ -376,9 +376,9 @@
+@@ -376,9 +376,9 @@ export function NativeStackView({
          const isModal = modalRouteKeys.includes(route.key);
          const isPreloaded = preloadedDescriptors[route.key] !== undefined && descriptors[route.key] === undefined;
-
+ 
 -        // On Fabric, when screen is frozen, animated and reanimated values are not updated
 -        // due to component being unmounted. To avoid this, we don't freeze the previous screen there
 -        const shouldFreeze = isFabric() ? !isPreloaded && !isFocused && !isBelowFocused : !isPreloaded && !isFocused;
@@ -13,4 +14,23 @@ diff --git a/node_modules/@react-navigation/native-stack/lib/module/views/Native
 +        const shouldFreeze = !isPreloaded && !isFocused;
          return /*#__PURE__*/_jsx(SceneView, {
            index: index,
-           focused: isFocused,
\ No newline at end of file
+           focused: isFocused,
+diff --git a/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx b/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx
+index ca15b1d..3191475 100644
+--- a/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx
++++ b/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx
+@@ -542,11 +542,9 @@ export function NativeStackView({
+             preloadedDescriptors[route.key] !== undefined &&
+             descriptors[route.key] === undefined;
+ 
+-          // On Fabric, when screen is frozen, animated and reanimated values are not updated
+-          // due to component being unmounted. To avoid this, we don't freeze the previous screen there
+-          const shouldFreeze = isFabric()
+-            ? !isPreloaded && !isFocused && !isBelowFocused
+-            : !isPreloaded && !isFocused;
++          // Freezing the screen below the focused one is safe on Fabric because
++          // DelayedFreeze defers it to the next macrotask, and transition animations run on the UI thread.
++          const shouldFreeze = !isPreloaded && !isFocused;
+ 
+           return (
+             <SceneView

From ab31e39136b81fae345f322d8e1f7a0407214008 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Fri, 27 Feb 2026 09:47:10 +0100
Subject: [PATCH 05/13] Add comments

---
 .../ScreenFreezeWrapper.tsx                                    | 3 +++
 .../createPlatformStackNavigatorComponent/index.tsx            | 3 +++
 2 files changed, 6 insertions(+)

diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx
index 4697a3f41f3c..405fa5e71673 100644
--- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx
+++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx
@@ -2,7 +2,10 @@ import React, {useLayoutEffect, useState} from 'react';
 import {Freeze} from 'react-freeze';
 
 type ScreenFreezeWrapperProps = {
+    /** Whether the screen is not currently visible to the user */
     isScreenBlurred: boolean;
+
+    /** The screen content to freeze when blurred */
     children: React.ReactNode;
 };
 
diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx
index b848c0ffc776..ae3de2578cad 100644
--- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx
+++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx
@@ -102,6 +102,9 @@ function createPlatformStackNavigatorComponent<RouterOptions extends PlatformSta
             };
         }, [persistentScreens, state]);
 
+        // Wrap each screen's render function with ScreenFreezeWrapper to freeze non-top screens.
+        // This prevents off-screen components from re-rendering.
+        // Persistent screens (e.g. sidebar) are excluded from freezing so they stay interactive.
         const wrappedDescriptors = useMemo(() => {
             if (!freezeNonTopScreens) {
                 return descriptors;

From 9df11aa50b3018576a19b806bbb66b503398b330 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Tue, 3 Mar 2026 17:13:52 +0100
Subject: [PATCH 06/13] Fix crash on attachment sending

---
 .../AttachmentModalBaseContent/index.tsx                      | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx b/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx
index 72c272305de0..97d870f7986c 100644
--- a/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx
+++ b/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx
@@ -166,11 +166,11 @@ function AttachmentModalBaseContent({
             return;
         }
 
+        onClose?.();
+
         if (onConfirm) {
             onConfirm(Object.assign(files ?? {}, {source} as FileObject));
         }
-
-        onClose?.();
     }, [isConfirmButtonDisabled, onConfirm, onClose, files, source]);
 
     // Close the modal when the escape key is pressed

From 76d2a9b18f65e6b0a66b764ce89f374c84385e98 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Tue, 3 Mar 2026 18:31:24 +0100
Subject: [PATCH 07/13] Fix text briefly greyed out when swiping back

---
 patches/react-native-screens/details.md       |  8 +++++++
 ...+4.15.4+001+delay-freeze-until-paint.patch | 23 +++++++++++++++++++
 2 files changed, 31 insertions(+)
 create mode 100644 patches/react-native-screens/details.md
 create mode 100644 patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch

diff --git a/patches/react-native-screens/details.md b/patches/react-native-screens/details.md
new file mode 100644
index 000000000000..e07de39d8108
--- /dev/null
+++ b/patches/react-native-screens/details.md
@@ -0,0 +1,8 @@
+# `react-native-screens` patches
+
+### [react-native-screens+4.15.4+001+delay-freeze-until-paint.patch](react-native-screens+4.15.4+001+delay-freeze-until-paint.patch)
+
+- Reason: Adds `requestAnimationFrame` to `DelayedFreeze` so the screen freeze is deferred until after the current frame is painted. Without this, screens can be frozen while transitional UI states are still visible, causing a visual flicker.
+- Upstream PR/issue: N/A
+- E/App issue: https://github.com/Expensify/App/issues/33725
+- PR introducing patch: https://github.com/Expensify/App/pull/82764
diff --git a/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch b/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch
new file mode 100644
index 000000000000..34b81d29b89f
--- /dev/null
+++ b/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch
@@ -0,0 +1,23 @@
+diff --git a/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx b/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx
+index 443da63..72517fd 100644
+--- a/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx
++++ b/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx
+@@ -12,12 +12,17 @@ function DelayedFreeze({ freeze, children }: FreezeWrapperProps) {
+   // flag used for determining whether freeze should be enabled
+   const [freezeState, setFreezeState] = React.useState(false);
+ 
++  // We use requestAnimationFrame to ensure the current frame is fully painted
++  // before scheduling the freeze. This prevents freezing the screen while
++  // transitional UI states (e.g. opacity from useSingleExecution) are still visible.
+   React.useEffect(() => {
++    let rafID: number;
+     const id = setTimeout(() => {
+-      setFreezeState(freeze);
++      rafID = requestAnimationFrame(() => setFreezeState(freeze));
+     }, 0);
+     return () => {
+       clearTimeout(id);
++      cancelAnimationFrame(rafID);
+     };
+   }, [freeze]);
+ 

From ad30958aed8eb042929810bfa9139f8519450ef2 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 4 Mar 2026 11:25:14 +0100
Subject: [PATCH 08/13] Fix tooltip remains displayed when navigate to thread
 page

---
 .../ScreenFreezeWrapper.tsx                        | 14 ++++++++++++--
 1 file changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx
index 405fa5e71673..ca6e1e2ba641 100644
--- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx
+++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/ScreenFreezeWrapper.tsx
@@ -1,5 +1,6 @@
 import React, {useLayoutEffect, useState} from 'react';
 import {Freeze} from 'react-freeze';
+import TooltipSense from '@components/Tooltip/TooltipSense';
 
 type ScreenFreezeWrapperProps = {
     /** Whether the screen is not currently visible to the user */
@@ -15,8 +16,17 @@ function ScreenFreezeWrapper({isScreenBlurred, children}: ScreenFreezeWrapperPro
     // Decouple the Suspense render task so it won't be interrupted by React's concurrent mode
     // and stuck in an infinite loop
     useLayoutEffect(() => {
-        // eslint-disable-next-line react-hooks/set-state-in-effect
-        setFrozen(isScreenBlurred);
+        if (!TooltipSense.isActive() || !isScreenBlurred) {
+            // eslint-disable-next-line react-hooks/set-state-in-effect
+            setFrozen(isScreenBlurred);
+        }
+
+        // When a tooltip is active, defer freezing by one frame so it can dismiss first.
+        // Otherwise the frozen tree can't hide the tooltip portal rendered on <body>.
+        const id = requestAnimationFrame(() => setFrozen(isScreenBlurred));
+        return () => {
+            cancelAnimationFrame(id);
+        };
     }, [isScreenBlurred]);
 
     return <Freeze freeze={frozen}>{children}</Freeze>;

From 971bd23474c62acfe688ba3e6cd7aafabb168b00 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 4 Mar 2026 16:00:45 +0100
Subject: [PATCH 09/13] Use startTransition in DelayedFreeze to avoid blocking
 queued callbacks

---
 patches/react-native-screens/details.md       |  4 ++--
 ...eens+4.15.4+001+low-priority-freeze.patch} | 23 ++++++++-----------
 2 files changed, 12 insertions(+), 15 deletions(-)
 rename patches/react-native-screens/{react-native-screens+4.15.4+001+delay-freeze-until-paint.patch => react-native-screens+4.15.4+001+low-priority-freeze.patch} (53%)

diff --git a/patches/react-native-screens/details.md b/patches/react-native-screens/details.md
index e07de39d8108..dab9b6c4c87b 100644
--- a/patches/react-native-screens/details.md
+++ b/patches/react-native-screens/details.md
@@ -1,8 +1,8 @@
 # `react-native-screens` patches
 
-### [react-native-screens+4.15.4+001+delay-freeze-until-paint.patch](react-native-screens+4.15.4+001+delay-freeze-until-paint.patch)
+### [react-native-screens+4.15.4+001+low-priority-freeze.patch](react-native-screens+4.15.4+001+low-priority-freeze.patch)
 
-- Reason: Adds `requestAnimationFrame` to `DelayedFreeze` so the screen freeze is deferred until after the current frame is painted. Without this, screens can be frozen while transitional UI states are still visible, causing a visual flicker.
+- Reason: Wraps the freeze state update in `startTransition` so it is treated as a low-priority update. Without this, the Suspense boundary used by react-freeze can starve pending macrotasks (setTimeout, rAF), blocking queued callbacks from firing.
 - Upstream PR/issue: N/A
 - E/App issue: https://github.com/Expensify/App/issues/33725
 - PR introducing patch: https://github.com/Expensify/App/pull/82764
diff --git a/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch b/patches/react-native-screens/react-native-screens+4.15.4+001+low-priority-freeze.patch
similarity index 53%
rename from patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch
rename to patches/react-native-screens/react-native-screens+4.15.4+001+low-priority-freeze.patch
index 34b81d29b89f..6984709d81cb 100644
--- a/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch
+++ b/patches/react-native-screens/react-native-screens+4.15.4+001+low-priority-freeze.patch
@@ -1,23 +1,20 @@
 diff --git a/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx b/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx
-index 443da63..72517fd 100644
+index 443da63..a1b2c3d 100644
 --- a/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx
 +++ b/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx
-@@ -12,12 +12,17 @@ function DelayedFreeze({ freeze, children }: FreezeWrapperProps) {
+@@ -12,11 +12,16 @@ function DelayedFreeze({ freeze, children }: FreezeWrapperProps) {
    // flag used for determining whether freeze should be enabled
    const [freezeState, setFreezeState] = React.useState(false);
- 
-+  // We use requestAnimationFrame to ensure the current frame is fully painted
-+  // before scheduling the freeze. This prevents freezing the screen while
-+  // transitional UI states (e.g. opacity from useSingleExecution) are still visible.
+
++  // We use startTransition to mark the freeze as a low-priority update so it
++  // doesn't starve pending macrotasks (setTimeout, rAF). Without this, the
++  // Suspense boundary used by react-freeze can block queued callbacks from firing.
    React.useEffect(() => {
-+    let rafID: number;
      const id = setTimeout(() => {
 -      setFreezeState(freeze);
-+      rafID = requestAnimationFrame(() => setFreezeState(freeze));
++      React.startTransition(() => {
++        setFreezeState(freeze);
++      });
      }, 0);
      return () => {
-       clearTimeout(id);
-+      cancelAnimationFrame(rafID);
-     };
-   }, [freeze]);
- 
+       clearTimeout(id);
\ No newline at end of file

From cfe0d141cb1e0ce50d0602552cebcd5eefdd7f46 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 4 Mar 2026 16:30:40 +0100
Subject: [PATCH 10/13] Revert "Use startTransition in DelayedFreeze to avoid
 blocking queued callbacks"

This reverts commit 971bd23474c62acfe688ba3e6cd7aafabb168b00.
---
 patches/react-native-screens/details.md       |  4 ++--
 ...4.15.4+001+delay-freeze-until-paint.patch} | 23 +++++++++++--------
 2 files changed, 15 insertions(+), 12 deletions(-)
 rename patches/react-native-screens/{react-native-screens+4.15.4+001+low-priority-freeze.patch => react-native-screens+4.15.4+001+delay-freeze-until-paint.patch} (53%)

diff --git a/patches/react-native-screens/details.md b/patches/react-native-screens/details.md
index dab9b6c4c87b..e07de39d8108 100644
--- a/patches/react-native-screens/details.md
+++ b/patches/react-native-screens/details.md
@@ -1,8 +1,8 @@
 # `react-native-screens` patches
 
-### [react-native-screens+4.15.4+001+low-priority-freeze.patch](react-native-screens+4.15.4+001+low-priority-freeze.patch)
+### [react-native-screens+4.15.4+001+delay-freeze-until-paint.patch](react-native-screens+4.15.4+001+delay-freeze-until-paint.patch)
 
-- Reason: Wraps the freeze state update in `startTransition` so it is treated as a low-priority update. Without this, the Suspense boundary used by react-freeze can starve pending macrotasks (setTimeout, rAF), blocking queued callbacks from firing.
+- Reason: Adds `requestAnimationFrame` to `DelayedFreeze` so the screen freeze is deferred until after the current frame is painted. Without this, screens can be frozen while transitional UI states are still visible, causing a visual flicker.
 - Upstream PR/issue: N/A
 - E/App issue: https://github.com/Expensify/App/issues/33725
 - PR introducing patch: https://github.com/Expensify/App/pull/82764
diff --git a/patches/react-native-screens/react-native-screens+4.15.4+001+low-priority-freeze.patch b/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch
similarity index 53%
rename from patches/react-native-screens/react-native-screens+4.15.4+001+low-priority-freeze.patch
rename to patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch
index 6984709d81cb..34b81d29b89f 100644
--- a/patches/react-native-screens/react-native-screens+4.15.4+001+low-priority-freeze.patch
+++ b/patches/react-native-screens/react-native-screens+4.15.4+001+delay-freeze-until-paint.patch
@@ -1,20 +1,23 @@
 diff --git a/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx b/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx
-index 443da63..a1b2c3d 100644
+index 443da63..72517fd 100644
 --- a/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx
 +++ b/node_modules/react-native-screens/src/components/helpers/DelayedFreeze.tsx
-@@ -12,11 +12,16 @@ function DelayedFreeze({ freeze, children }: FreezeWrapperProps) {
+@@ -12,12 +12,17 @@ function DelayedFreeze({ freeze, children }: FreezeWrapperProps) {
    // flag used for determining whether freeze should be enabled
    const [freezeState, setFreezeState] = React.useState(false);
-
-+  // We use startTransition to mark the freeze as a low-priority update so it
-+  // doesn't starve pending macrotasks (setTimeout, rAF). Without this, the
-+  // Suspense boundary used by react-freeze can block queued callbacks from firing.
+ 
++  // We use requestAnimationFrame to ensure the current frame is fully painted
++  // before scheduling the freeze. This prevents freezing the screen while
++  // transitional UI states (e.g. opacity from useSingleExecution) are still visible.
    React.useEffect(() => {
++    let rafID: number;
      const id = setTimeout(() => {
 -      setFreezeState(freeze);
-+      React.startTransition(() => {
-+        setFreezeState(freeze);
-+      });
++      rafID = requestAnimationFrame(() => setFreezeState(freeze));
      }, 0);
      return () => {
-       clearTimeout(id);
\ No newline at end of file
+       clearTimeout(id);
++      cancelAnimationFrame(rafID);
+     };
+   }, [freeze]);
+ 

From 181b06078f56f4e15d0280b119a22a06ca9007b6 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 4 Mar 2026 17:29:29 +0100
Subject: [PATCH 11/13] Fix navigation after inviting a member

---
 src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx
index 4e95f50ca45d..267cfc26ae48 100644
--- a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx
+++ b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx
@@ -142,7 +142,7 @@ function WorkspaceInviteMessageComponent({
         }
 
         if ((backTo as string)?.endsWith('members')) {
-            Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.dismissModal());
+            Navigation.dismissModal();
             return;
         }
 

From 16ecb005ac1f21ff0fdc6e210676bba3f219f71f Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Thu, 5 Mar 2026 09:47:25 +0100
Subject: [PATCH 12/13] Fix crash on attachment sending

---
 .../AttachmentModalBaseContent/index.tsx                   | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx b/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx
index 97d870f7986c..779bfbd03da7 100644
--- a/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx
+++ b/src/pages/media/AttachmentModalScreen/AttachmentModalBaseContent/index.tsx
@@ -168,9 +168,10 @@ function AttachmentModalBaseContent({
 
         onClose?.();
 
-        if (onConfirm) {
-            onConfirm(Object.assign(files ?? {}, {source} as FileObject));
-        }
+        // Defer onConfirm to the next frame so the target screen has time to unfreeze and re-mount its refs (e.g. composerRef.clearWorklet)
+        requestAnimationFrame(() => {
+            onConfirm?.(Object.assign(files ?? {}, {source} as FileObject));
+        });
     }, [isConfirmButtonDisabled, onConfirm, onClose, files, source]);
 
     // Close the modal when the escape key is pressed

From b16e8f914663a1631d214429cb432de193e00303 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 11 Mar 2026 08:05:25 +0100
Subject: [PATCH 13/13] Remove redundant memoization

---
 .../createPlatformStackNavigatorComponent/index.tsx   | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx
index ae3de2578cad..495ca5c25d39 100644
--- a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx
+++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigatorComponent/index.tsx
@@ -105,11 +105,8 @@ function createPlatformStackNavigatorComponent<RouterOptions extends PlatformSta
         // Wrap each screen's render function with ScreenFreezeWrapper to freeze non-top screens.
         // This prevents off-screen components from re-rendering.
         // Persistent screens (e.g. sidebar) are excluded from freezing so they stay interactive.
-        const wrappedDescriptors = useMemo(() => {
-            if (!freezeNonTopScreens) {
-                return descriptors;
-            }
-
+        let wrappedDescriptors = descriptors;
+        if (freezeNonTopScreens) {
             const topRouteKey = state.routes[state.index]?.key;
             const result: typeof descriptors = {};
             for (const [key, descriptor] of Object.entries(descriptors)) {
@@ -121,8 +118,8 @@ function createPlatformStackNavigatorComponent<RouterOptions extends PlatformSta
                     render: () => <ScreenFreezeWrapper isScreenBlurred={isScreenBlurred}>{descriptor.render()}</ScreenFreezeWrapper>,
                 };
             }
-            return result;
-        }, [descriptors, persistentScreens, state]);
+            wrappedDescriptors = result;
+        }
 
         const Content = useMemo(
             () => (
