From 496b702b1286a40bdc9a982b5931cb3fdb193d45 Mon Sep 17 00:00:00 2001
From: Julian Kobrynski <julian.kobrynski@callstack.com>
Date: Mon, 30 Mar 2026 17:13:47 +0200
Subject: [PATCH 01/11] convert ONYXKEYS.IS_CHECKING_PUBLIC_ROOM to a regular
 key with no initWithStoredValues, add initial state

---
 src/Expensify.tsx  | 2 +-
 src/setup/index.ts | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 82900c0ef0a6..825f6c78fbde 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -58,7 +58,7 @@ function Expensify() {
     const {preferredLocale} = useLocalize();
     const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector});
     const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE);
-    const [isCheckingPublicRoom = true] = useOnyx(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, {initWithStoredValues: false});
+    const [isCheckingPublicRoom = true] = useOnyx(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM);
     const [updateRequired] = useOnyx(ONYXKEYS.UPDATE_REQUIRED, {initWithStoredValues: false});
     const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH);
 
diff --git a/src/setup/index.ts b/src/setup/index.ts
index af67594d1cdc..3026dfb4bd1b 100644
--- a/src/setup/index.ts
+++ b/src/setup/index.ts
@@ -57,6 +57,7 @@ export default function () {
             // Ensure the Supportal permission modal doesn't persist across reloads
             [ONYXKEYS.SUPPORTAL_PERMISSION_DENIED]: null,
             [ONYXKEYS.IS_OPEN_APP_FAILURE_MODAL_OPEN]: false,
+            [ONYXKEYS.IS_CHECKING_PUBLIC_ROOM]: true,
         },
         skippableCollectionMemberIDs: CONST.SKIPPABLE_COLLECTION_MEMBER_IDS,
         snapshotMergeKeys: ['pendingAction', 'pendingFields'],

From cdfedb158f1ab03ea27c3595f4ba15fdff4cdf3c Mon Sep 17 00:00:00 2001
From: Julian Kobrynski <julian.kobrynski@callstack.com>
Date: Fri, 3 Apr 2026 16:40:42 +0200
Subject: [PATCH 02/11] implement a hook that emulates initWithStoredValues:
 false

---
 src/Expensify.tsx                  |  3 ++-
 src/hooks/useOnyxWithMountReset.ts | 34 ++++++++++++++++++++++++++++++
 src/setup/index.ts                 |  1 -
 3 files changed, 36 insertions(+), 2 deletions(-)
 create mode 100644 src/hooks/useOnyxWithMountReset.ts

diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 27c3abf72630..0d00af7b9cdc 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -17,6 +17,7 @@ import useDebugShortcut from './hooks/useDebugShortcut';
 import useIsAuthenticated from './hooks/useIsAuthenticated';
 import useLocalize from './hooks/useLocalize';
 import useOnyx from './hooks/useOnyx';
+import useOnyxWithMountReset from './hooks/useOnyxWithMountReset';
 import {updateLastRoute} from './libs/actions/App';
 import * as ActiveClientManager from './libs/ActiveClientManager';
 import {isSafari} from './libs/Browser';
@@ -58,7 +59,7 @@ function Expensify() {
     const {preferredLocale} = useLocalize();
     const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector});
     const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE);
-    const [isCheckingPublicRoom = true] = useOnyx(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM);
+    const [isCheckingPublicRoom = true] = useOnyxWithMountReset(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, true);
     const [updateRequired] = useOnyx(ONYXKEYS.RAM_ONLY_UPDATE_REQUIRED);
     const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH);
 
diff --git a/src/hooks/useOnyxWithMountReset.ts b/src/hooks/useOnyxWithMountReset.ts
new file mode 100644
index 000000000000..32595a8bd850
--- /dev/null
+++ b/src/hooks/useOnyxWithMountReset.ts
@@ -0,0 +1,34 @@
+import {useEffect, useRef} from 'react';
+import Onyx from 'react-native-onyx';
+import type {OnyxKey, OnyxSetInput, OnyxValue, UseOnyxResult} from 'react-native-onyx';
+import useOnyx from './useOnyx';
+
+/**
+ * Wraps `useOnyx` to guarantee a fresh start on every component mount.
+ *
+ * On the first render after mount, the hook returns `resetValue` instead of
+ * whatever is in the Onyx cache. A mount effect then calls `Onyx.set` to
+ * align the cache with `resetValue`, so subsequent renders read the correct
+ * value from `useOnyx`.
+ *
+ * This replaces `initWithStoredValues: false` for keys that must reset to a
+ * known value on every mount (e.g. IS_CHECKING_PUBLIC_ROOM must start as
+ * `true` so NavigationRoot doesn't render before the initial URL is resolved).
+ */
+function useOnyxWithMountReset<TKey extends OnyxKey>(key: TKey, resetValue: NonNullable<OnyxValue<TKey>>): UseOnyxResult<OnyxValue<TKey>> {
+    const hasResetRef = useRef(false);
+    const result = useOnyx(key);
+
+    useEffect(() => {
+        hasResetRef.current = true;
+        Onyx.set(key, resetValue as OnyxSetInput<TKey>);
+    }, [key, resetValue]);
+
+    if (!hasResetRef.current) {
+        return [resetValue, {status: 'loaded'}];
+    }
+
+    return result;
+}
+
+export default useOnyxWithMountReset;
diff --git a/src/setup/index.ts b/src/setup/index.ts
index ef26dd327906..0daed09b2bcb 100644
--- a/src/setup/index.ts
+++ b/src/setup/index.ts
@@ -57,7 +57,6 @@ export default function () {
             // Ensure the Supportal permission modal doesn't persist across reloads
             [ONYXKEYS.SUPPORTAL_PERMISSION_DENIED]: null,
             [ONYXKEYS.IS_OPEN_APP_FAILURE_MODAL_OPEN]: false,
-            [ONYXKEYS.IS_CHECKING_PUBLIC_ROOM]: true,
         },
         skippableCollectionMemberIDs: CONST.SKIPPABLE_COLLECTION_MEMBER_IDS,
         snapshotMergeKeys: ['pendingAction', 'pendingFields'],

From a83bd563b41efdcb82b8ea2883311ed523475c6c Mon Sep 17 00:00:00 2001
From: Julian Kobrynski <julian.kobrynski@callstack.com>
Date: Fri, 10 Apr 2026 15:02:19 +0200
Subject: [PATCH 03/11] replace custom hook solution with initialUrl gate

---
 src/CONST/index.ts                    |  1 +
 src/DeepLinkHandler.tsx               | 78 ++++++++++++++++++++++-----
 src/Expensify.tsx                     | 12 +++--
 src/ONYXKEYS.ts                       |  4 +-
 src/hooks/useOnyxWithMountReset.ts    | 34 ------------
 src/libs/actions/App.ts               |  2 +-
 src/libs/actions/QueuedOnyxUpdates.ts |  2 +-
 src/libs/actions/Report/index.ts      |  6 +--
 src/setup/index.ts                    |  1 +
 tests/unit/APITest.ts                 |  6 +--
 10 files changed, 83 insertions(+), 63 deletions(-)
 delete mode 100644 src/hooks/useOnyxWithMountReset.ts

diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index ea9c5658533b..a9da9d55ef9b 100644
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -1936,6 +1936,7 @@ const CONST = {
         SHOW_HOVER_PREVIEW_DELAY: 270,
         SHOW_HOVER_PREVIEW_ANIMATION_DURATION: 250,
         ACTIVITY_INDICATOR_TIMEOUT: 10000,
+        GET_INITIAL_URL_TIMEOUT: 10000,
         MIN_SMOOTH_SCROLL_EVENT_THROTTLE: 16,
     },
     DEFERRED_LAYOUT_WRITE_KEYS: {
diff --git a/src/DeepLinkHandler.tsx b/src/DeepLinkHandler.tsx
index 81b35023ba35..1745e2c8ede4 100644
--- a/src/DeepLinkHandler.tsx
+++ b/src/DeepLinkHandler.tsx
@@ -26,6 +26,7 @@ type DeepLinkHandlerProps = {
  */
 function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) {
     const linkingChangeListener = useRef<NativeEventSubscription | null>(null);
+    const initialUrlProcessed = useRef(false);
 
     const [allReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT);
     const [, sessionMetadata] = useOnyx(ONYXKEYS.SESSION);
@@ -39,24 +40,58 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) {
         if (isLoadingOnyxValue(sessionMetadata, isSelfTourViewedMetadata)) {
             return;
         }
-        // If the app is opened from a deep link, get the reportID (if exists) from the deep link and navigate to the chat report
-        Linking.getInitialURL().then((url) => {
-            onInitialUrl(url as Route);
 
-            if (url) {
-                if (conciergeReportID === undefined) {
-                    Log.info('[Deep link] conciergeReportID is undefined when processing initial URL', false, {url});
+        // Guard against stale closures: when deps change and the effect re-runs, the previous
+        // getInitialURL() promise may still be in-flight. Without this guard, its .then() would
+        // fire with stale conciergeReportID/introSelected values, causing a duplicate
+        // openReportFromDeepLink() call.
+        let cancelled = false;
+        let timeoutId: ReturnType<typeof setTimeout>;
+
+        // If the app is opened from a deep link, get the reportID (if exists) from the deep link and navigate to the chat report.
+        // We race against a timeout to prevent permanently blocking NavigationRoot if getInitialURL() never resolves
+        // (e.g. in HybridApp when OldDot fails to send the URL via native bridge).
+        Promise.race([
+            Linking.getInitialURL(),
+            new Promise<null>((resolve) => {
+                timeoutId = setTimeout(() => resolve(null), CONST.TIMING.GET_INITIAL_URL_TIMEOUT);
+            }),
+        ])
+            .then((url) => {
+                if (cancelled) {
+                    return;
                 }
-                if (introSelected === undefined) {
-                    Log.info('[Deep link] introSelected is undefined when processing initial URL', false, {url});
+
+                initialUrlProcessed.current = true;
+                onInitialUrl(url as Route);
+
+                if (url) {
+                    if (conciergeReportID === undefined) {
+                        Log.info('[Deep link] conciergeReportID is undefined when processing initial URL', false, {url});
+                    }
+                    if (introSelected === undefined) {
+                        Log.info('[Deep link] introSelected is undefined when processing initial URL', false, {url});
+                    }
+                    // Use hasAuthToken() for the latest auth state at call time, since the isAuthenticated
+                    // closure value may be stale on cold start (useOnyx reports 'loaded' before storage completes).
+                    const isCurrentlyAuthenticated = hasAuthToken();
+                    openReportFromDeepLink(url, allReports, isCurrentlyAuthenticated, conciergeReportID, introSelected, isSelfTourViewed, betas);
+                } else {
+                    Report.doneCheckingPublicRoom();
                 }
-                openReportFromDeepLink(url, allReports, isAuthenticated, conciergeReportID, introSelected, isSelfTourViewed, betas);
-            } else {
-                Report.doneCheckingPublicRoom();
-            }
 
-            endSpan(CONST.TELEMETRY.SPAN_BOOTSPLASH.DEEP_LINK);
-        });
+                endSpan(CONST.TELEMETRY.SPAN_BOOTSPLASH.DEEP_LINK);
+            })
+            .catch(() => {
+                if (cancelled) {
+                    return;
+                }
+
+                initialUrlProcessed.current = true;
+                onInitialUrl(null);
+                Report.doneCheckingPublicRoom();
+                endSpan(CONST.TELEMETRY.SPAN_BOOTSPLASH.DEEP_LINK);
+            });
 
         // Open chat report from a deep link (only mobile native)
         linkingChangeListener.current = Linking.addEventListener('url', (state) => {
@@ -71,11 +106,26 @@ function DeepLinkHandler({onInitialUrl}: DeepLinkHandlerProps) {
         });
 
         return () => {
+            cancelled = true;
+            clearTimeout(timeoutId);
             linkingChangeListener.current?.remove();
         };
         // eslint-disable-next-line react-hooks/exhaustive-deps -- intentionally excluding allReports, isAuthenticated, and onInitialUrl to avoid re-triggering deep link handling on every report update
     }, [sessionMetadata?.status, conciergeReportID, introSelected, isSelfTourViewedMetadata, betas]);
 
+    // Safety net: if getInitialURL() resolves before the session loads, hasAuthToken() may return false
+    // for an authenticated user, causing openReportFromDeepLink to take the wrong path. Once isAuthenticated
+    // settles to true, unblock the UI. The initialUrlProcessed guard ensures this doesn't fire before URL
+    // resolution. In the common case (isAuthenticated settles first), this is a no-op because
+    // openReportFromDeepLink's own doneCheckingPublicRoom() call handles it.
+    useEffect(() => {
+        if (!isAuthenticated || !initialUrlProcessed.current) {
+            return;
+        }
+
+        Report.doneCheckingPublicRoom();
+    }, [isAuthenticated]);
+
     return null;
 }
 
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index c8051af234a5..e9fa50f3091c 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -17,7 +17,6 @@ import useDebugShortcut from './hooks/useDebugShortcut';
 import useIsAuthenticated from './hooks/useIsAuthenticated';
 import useLocalize from './hooks/useLocalize';
 import useOnyx from './hooks/useOnyx';
-import useOnyxWithMountReset from './hooks/useOnyxWithMountReset';
 import {updateLastRoute} from './libs/actions/App';
 import * as ActiveClientManager from './libs/ActiveClientManager';
 import {isSafari} from './libs/Browser';
@@ -59,7 +58,7 @@ function Expensify() {
     const {preferredLocale} = useLocalize();
     const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector});
     const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE);
-    const [isCheckingPublicRoom = true] = useOnyxWithMountReset(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, true);
+    const [isCheckingPublicRoom = true] = useOnyx(ONYXKEYS.RAM_ONLY_IS_CHECKING_PUBLIC_ROOM);
     const [updateRequired] = useOnyx(ONYXKEYS.RAM_ONLY_UPDATE_REQUIRED);
     const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH);
 
@@ -74,7 +73,7 @@ function Expensify() {
 
     const bootsplashSpan = useRef<Sentry.Span>(null);
 
-    const [initialUrl, setInitialUrl] = useState<Route | null>(null);
+    const [initialUrl, setInitialUrl] = useState<Route | null | undefined>(undefined);
     const {setIsAuthenticatedAtStartup} = useInitialURLActions();
 
     useEffect(() => {
@@ -285,12 +284,15 @@ function Expensify() {
             <FullstoryInitHandler />
             <DeepLinkHandler onInitialUrl={setInitialUrl} />
             <AppleAuthWrapper />
-            {hasAttemptedToOpenPublicRoom && (
+            {/* Wait for the initial URL to resolve before mounting NavigationRoot, because its initialState
+                is computed once on mount. In HybridApp, getInitialURL() may never resolve (OldDot native
+                bridge), so we skip this guard to avoid blocking the app. */}
+            {hasAttemptedToOpenPublicRoom && (CONFIG.IS_HYBRID_APP || initialUrl !== undefined) && (
                 <NavigationRoot
                     onReady={setNavigationReady}
                     authenticated={isAuthenticated}
                     lastVisitedPath={lastVisitedPath as Route}
-                    initialUrl={initialUrl}
+                    initialUrl={initialUrl ?? null}
                 />
             )}
             {(isSplashVisible || isSplashReadyToBeHidden) && (
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 0fa0a003371b..9aedb0878431 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -421,7 +421,7 @@ const ONYXKEYS = {
     IS_BETA: 'isBeta',
 
     /** Whether we're checking if the room is public or not */
-    IS_CHECKING_PUBLIC_ROOM: 'isCheckingPublicRoom',
+    RAM_ONLY_IS_CHECKING_PUBLIC_ROOM: 'isCheckingPublicRoom',
 
     /** A map of the user's security group IDs they belong to in specific domains */
     MY_DOMAIN_SECURITY_GROUPS: 'myDomainSecurityGroups',
@@ -1422,7 +1422,7 @@ type OnyxValuesMapping = {
     [ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID]: string;
     [ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: boolean;
     [ONYXKEYS.IS_BETA]: boolean;
-    [ONYXKEYS.IS_CHECKING_PUBLIC_ROOM]: boolean;
+    [ONYXKEYS.RAM_ONLY_IS_CHECKING_PUBLIC_ROOM]: boolean;
     [ONYXKEYS.MY_DOMAIN_SECURITY_GROUPS]: Record<string, string>;
     [ONYXKEYS.VERIFY_3DS_SUBSCRIPTION]: string;
     [ONYXKEYS.PREFERRED_THEME]: ValueOf<typeof CONST.THEME>;
diff --git a/src/hooks/useOnyxWithMountReset.ts b/src/hooks/useOnyxWithMountReset.ts
deleted file mode 100644
index 32595a8bd850..000000000000
--- a/src/hooks/useOnyxWithMountReset.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import {useEffect, useRef} from 'react';
-import Onyx from 'react-native-onyx';
-import type {OnyxKey, OnyxSetInput, OnyxValue, UseOnyxResult} from 'react-native-onyx';
-import useOnyx from './useOnyx';
-
-/**
- * Wraps `useOnyx` to guarantee a fresh start on every component mount.
- *
- * On the first render after mount, the hook returns `resetValue` instead of
- * whatever is in the Onyx cache. A mount effect then calls `Onyx.set` to
- * align the cache with `resetValue`, so subsequent renders read the correct
- * value from `useOnyx`.
- *
- * This replaces `initWithStoredValues: false` for keys that must reset to a
- * known value on every mount (e.g. IS_CHECKING_PUBLIC_ROOM must start as
- * `true` so NavigationRoot doesn't render before the initial URL is resolved).
- */
-function useOnyxWithMountReset<TKey extends OnyxKey>(key: TKey, resetValue: NonNullable<OnyxValue<TKey>>): UseOnyxResult<OnyxValue<TKey>> {
-    const hasResetRef = useRef(false);
-    const result = useOnyx(key);
-
-    useEffect(() => {
-        hasResetRef.current = true;
-        Onyx.set(key, resetValue as OnyxSetInput<TKey>);
-    }, [key, resetValue]);
-
-    if (!hasResetRef.current) {
-        return [resetValue, {status: 'loaded'}];
-    }
-
-    return result;
-}
-
-export default useOnyxWithMountReset;
diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts
index 49a03f553838..cafcc49c607a 100644
--- a/src/libs/actions/App.ts
+++ b/src/libs/actions/App.ts
@@ -122,7 +122,7 @@ Onyx.connectWithoutView({
 
 const KEYS_TO_PRESERVE: OnyxKey[] = [
     ONYXKEYS.ACCOUNT,
-    ONYXKEYS.IS_CHECKING_PUBLIC_ROOM,
+    ONYXKEYS.RAM_ONLY_IS_CHECKING_PUBLIC_ROOM,
     ONYXKEYS.IS_LOADING_APP,
     ONYXKEYS.IS_SIDEBAR_LOADED,
     ONYXKEYS.MODAL,
diff --git a/src/libs/actions/QueuedOnyxUpdates.ts b/src/libs/actions/QueuedOnyxUpdates.ts
index be2a1480ccec..cbb735e4dc30 100644
--- a/src/libs/actions/QueuedOnyxUpdates.ts
+++ b/src/libs/actions/QueuedOnyxUpdates.ts
@@ -45,7 +45,7 @@ function flushQueue(): Promise<void> {
             ONYXKEYS.CREDENTIALS,
             ONYXKEYS.IS_SIDEBAR_LOADED,
             ONYXKEYS.ACCOUNT,
-            ONYXKEYS.IS_CHECKING_PUBLIC_ROOM,
+            ONYXKEYS.RAM_ONLY_IS_CHECKING_PUBLIC_ROOM,
             ONYXKEYS.MODAL,
             ONYXKEYS.NETWORK,
             ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT,
diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts
index a9e5d5f5b452..dd99595ea67c 100644
--- a/src/libs/actions/Report/index.ts
+++ b/src/libs/actions/Report/index.ts
@@ -1370,7 +1370,7 @@ function openReport(params: OpenReportActionParams) {
         });
     }
 
-    const finallyData: Array<OnyxUpdate<typeof ONYXKEYS.IS_CHECKING_PUBLIC_ROOM>> = [];
+    const finallyData: Array<OnyxUpdate<typeof ONYXKEYS.RAM_ONLY_IS_CHECKING_PUBLIC_ROOM>> = [];
 
     const parameters: OpenReportParams = {
         reportID,
@@ -1639,7 +1639,7 @@ function openReport(params: OpenReportActionParams) {
     if (isFromDeepLink) {
         finallyData.push({
             onyxMethod: Onyx.METHOD.SET,
-            key: ONYXKEYS.IS_CHECKING_PUBLIC_ROOM,
+            key: ONYXKEYS.RAM_ONLY_IS_CHECKING_PUBLIC_ROOM,
             value: false,
         });
 
@@ -4281,7 +4281,7 @@ function toggleEmojiReaction(
 }
 
 function doneCheckingPublicRoom() {
-    Onyx.set(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, false);
+    Onyx.set(ONYXKEYS.RAM_ONLY_IS_CHECKING_PUBLIC_ROOM, false);
 }
 
 function navigateToMostRecentReport(
diff --git a/src/setup/index.ts b/src/setup/index.ts
index 0daed09b2bcb..2d5a990e7bd5 100644
--- a/src/setup/index.ts
+++ b/src/setup/index.ts
@@ -62,6 +62,7 @@ export default function () {
         snapshotMergeKeys: ['pendingAction', 'pendingFields'],
         ramOnlyKeys: [
             ONYXKEYS.DERIVED.RAM_ONLY_SORTED_REPORT_ACTIONS,
+            ONYXKEYS.RAM_ONLY_IS_CHECKING_PUBLIC_ROOM,
             ONYXKEYS.RAM_ONLY_UPDATE_AVAILABLE,
             ONYXKEYS.RAM_ONLY_UPDATE_REQUIRED,
             ONYXKEYS.RAM_ONLY_IS_SEARCHING_FOR_REPORTS,
diff --git a/tests/unit/APITest.ts b/tests/unit/APITest.ts
index 8e81e4bfd42a..1b011574482f 100644
--- a/tests/unit/APITest.ts
+++ b/tests/unit/APITest.ts
@@ -874,8 +874,8 @@ describe('API.write() persistence guarantees', () => {
             // persisted-requests queue. This avoids spy-ordering issues that caused
             // false passes on CI.
             const updateMock = jest.spyOn(Onyx, 'update').mockImplementation((data) => {
-                // We use ONYXKEYS.IS_CHECKING_PUBLIC_ROOM as a sample key to identify the marker
-                const hasMarker = data.some((entry) => entry.key === ONYXKEYS.IS_CHECKING_PUBLIC_ROOM);
+                // We use ONYXKEYS.RAM_ONLY_IS_CHECKING_PUBLIC_ROOM as a sample key to identify the marker
+                const hasMarker = data.some((entry) => entry.key === ONYXKEYS.RAM_ONLY_IS_CHECKING_PUBLIC_ROOM);
                 if (hasMarker) {
                     optimisticDataApplied = true;
                     // Note: getAll() checks the in-memory queue, not durable (disk) state.
@@ -892,7 +892,7 @@ describe('API.write() persistence guarantees', () => {
                     optimisticData: [
                         {
                             onyxMethod: Onyx.METHOD.SET,
-                            key: ONYXKEYS.IS_CHECKING_PUBLIC_ROOM,
+                            key: ONYXKEYS.RAM_ONLY_IS_CHECKING_PUBLIC_ROOM,
                             value: true,
                         },
                     ],

From 382b6a290425e1ea0af2ea9bf3e680f2c76592d3 Mon Sep 17 00:00:00 2001
From: Julian Kobrynski <julian.kobrynski@callstack.com>
Date: Fri, 10 Apr 2026 15:51:05 +0200
Subject: [PATCH 04/11] remove initWithStoredValues from IS_LOADING_APP

---
 .../routes/WorkspaceAvatarModalContent.tsx                      | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/media/AttachmentModalScreen/routes/WorkspaceAvatarModalContent.tsx b/src/pages/media/AttachmentModalScreen/routes/WorkspaceAvatarModalContent.tsx
index 70fa08ede752..f1e2083d3cab 100644
--- a/src/pages/media/AttachmentModalScreen/routes/WorkspaceAvatarModalContent.tsx
+++ b/src/pages/media/AttachmentModalScreen/routes/WorkspaceAvatarModalContent.tsx
@@ -16,7 +16,7 @@ function WorkspaceAvatarModalContent({navigation, route}: AttachmentModalScreenP
 
     const defaultAvatars = useDefaultAvatars();
     const policy = usePolicy(policyID);
-    const [isLoadingApp = false] = useOnyx(ONYXKEYS.IS_LOADING_APP, {initWithStoredValues: false});
+    const [isLoadingApp = false] = useOnyx(ONYXKEYS.IS_LOADING_APP);
 
     const avatarURL = policy?.avatarURL ?? getDefaultWorkspaceAvatar(policy?.name ?? fallbackLetter);
     const source = getFullSizeAvatar({avatarSource: avatarURL, defaultAvatars});

From 35012d871ec5ef9aa6f8001d2f5ed5227bfb9d35 Mon Sep 17 00:00:00 2001
From: Julian Kobrynski <julian.kobrynski@callstack.com>
Date: Fri, 10 Apr 2026 15:52:29 +0200
Subject: [PATCH 05/11] remove initWithStoredValues from
 IS_LOADING_REPORT_DATA, prevent regressions

---
 src/libs/actions/Welcome/index.ts | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/src/libs/actions/Welcome/index.ts b/src/libs/actions/Welcome/index.ts
index a10f96e62a8e..252f6a83a04e 100644
--- a/src/libs/actions/Welcome/index.ts
+++ b/src/libs/actions/Welcome/index.ts
@@ -16,6 +16,10 @@ import type Onboarding from '@src/types/onyx/Onboarding';
 import type {OnboardingCompanySize} from './OnboardingFlow';
 
 let isLoadingReportData = true;
+// Tracks whether we've seen loading start (true) in the current session.
+// Without this, a stale persisted `false` from a previous session would
+// resolve the onServerDataReady() promise before OpenApp/ReconnectApp completes.
+let hasStartedLoading = false;
 
 let resolveIsReadyPromise: (value?: Promise<void>) => void | undefined;
 let isServerDataReadyPromise = new Promise<void>((resolve) => {
@@ -116,10 +120,16 @@ function completeHybridAppOnboarding() {
 // and doesn't need to trigger component re-renders.
 Onyx.connectWithoutView({
     key: ONYXKEYS.IS_LOADING_REPORT_DATA,
-    initWithStoredValues: false,
     callback: (value) => {
         isLoadingReportData = value ?? false;
-        checkServerDataReady();
+        if (isLoadingReportData) {
+            hasStartedLoading = true;
+        }
+        // Only resolve once loading has started — this ensures a stale
+        // persisted `false` from a previous session is ignored.
+        if (hasStartedLoading) {
+            checkServerDataReady();
+        }
     },
 });
 
@@ -128,6 +138,7 @@ function resetAllChecks() {
         resolveIsReadyPromise = resolve;
     });
     isLoadingReportData = true;
+    hasStartedLoading = false;
 }
 
 function setSelfTourViewed(shouldUpdateOnyxDataOnlyLocally = false) {

From 934b41bcc73b031122378a040bf1416afcfdd2bc Mon Sep 17 00:00:00 2001
From: Julian Kobrynski <julian.kobrynski@callstack.com>
Date: Fri, 10 Apr 2026 15:52:56 +0200
Subject: [PATCH 06/11] migrate PLAID_LINK_TOKEN to RAM-only

---
 src/ONYXKEYS.ts                               |  4 +--
 src/components/AddPlaidBankAccount.tsx        |  2 +-
 src/libs/ExportOnyxState/common.ts            |  2 +-
 src/libs/actions/BankAccounts.ts              |  2 +-
 src/libs/actions/Plaid.ts                     | 28 ++++++++++++++++---
 .../resetUSDBankAccount.ts                    |  4 +--
 .../USD/BankInfo/BankInfo.tsx                 |  2 +-
 .../steps/PlaidConnectionStep.tsx             |  2 +-
 .../addNew/PlaidConnectionStep.tsx            |  2 +-
 src/setup/index.ts                            |  1 +
 tests/unit/ExportOnyxStateTest.ts             |  4 +--
 11 files changed, 37 insertions(+), 16 deletions(-)

diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 9aedb0878431..86b33d5af433 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -315,7 +315,7 @@ const ONYXKEYS = {
     IS_PLAID_DISABLED: 'isPlaidDisabled',
 
     /** Token needed to initialize Plaid link */
-    PLAID_LINK_TOKEN: 'plaidLinkToken',
+    RAM_ONLY_PLAID_LINK_TOKEN: 'plaidLinkToken',
 
     /** Capture Plaid event  */
     PLAID_CURRENT_EVENT: 'plaidCurrentEvent',
@@ -1372,7 +1372,7 @@ type OnyxValuesMapping = {
     [ONYXKEYS.NVP_SEEN_NEW_USER_MODAL]: boolean;
     [ONYXKEYS.PLAID_DATA]: OnyxTypes.PlaidData;
     [ONYXKEYS.IS_PLAID_DISABLED]: boolean;
-    [ONYXKEYS.PLAID_LINK_TOKEN]: string;
+    [ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN]: string;
     [ONYXKEYS.ONFIDO_TOKEN]: string;
     [ONYXKEYS.ONFIDO_APPLICANT_ID]: string;
     [ONYXKEYS.NVP_PREFERRED_LOCALE]: OnyxTypes.Locale;
diff --git a/src/components/AddPlaidBankAccount.tsx b/src/components/AddPlaidBankAccount.tsx
index c68624505d5a..626e804f4bd0 100644
--- a/src/components/AddPlaidBankAccount.tsx
+++ b/src/components/AddPlaidBankAccount.tsx
@@ -83,7 +83,7 @@ function AddPlaidBankAccount({
     const subscribedKeyboardShortcuts = useRef<Array<() => void>>([]);
     const previousNetworkState = useRef<boolean | undefined>(undefined);
     const [selectedPlaidAccountMask, setSelectedPlaidAccountMask] = useState(defaultSelectedPlaidAccountMask);
-    const [plaidLinkToken] = useOnyx(ONYXKEYS.PLAID_LINK_TOKEN, {initWithStoredValues: false});
+    const [plaidLinkToken] = useOnyx(ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN);
     const [isPlaidDisabled] = useOnyx(ONYXKEYS.IS_PLAID_DISABLED);
     const {translate} = useLocalize();
     const {isOffline} = useNetwork();
diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts
index 872fb7a95099..80198ea446b2 100644
--- a/src/libs/ExportOnyxState/common.ts
+++ b/src/libs/ExportOnyxState/common.ts
@@ -80,7 +80,7 @@ const onyxKeysToRemove = new Set<ValueOf<typeof ONYXKEYS>>([
     ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID,
     ONYXKEYS.NVP_PRIVATE_BILLING_DISPUTE_PENDING,
     ONYXKEYS.NVP_PRIVATE_BILLING_STATUS,
-    ONYXKEYS.PLAID_LINK_TOKEN,
+    ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN,
     ONYXKEYS.ONFIDO_TOKEN,
     ONYXKEYS.ONFIDO_APPLICANT_ID,
 ]);
diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts
index a4b1a8c45e7b..56cfdaa83cad 100644
--- a/src/libs/actions/BankAccounts.ts
+++ b/src/libs/actions/BankAccounts.ts
@@ -87,7 +87,7 @@ type OpenPersonalBankAccountSetupViewProps = {
 };
 
 function clearPlaid(): Promise<void | void[]> {
-    Onyx.set(ONYXKEYS.PLAID_LINK_TOKEN, '');
+    Onyx.set(ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN, '');
     Onyx.set(ONYXKEYS.PLAID_CURRENT_EVENT, null);
     return Onyx.set(ONYXKEYS.PLAID_DATA, CONST.PLAID.DEFAULT_DATA);
 }
diff --git a/src/libs/actions/Plaid.ts b/src/libs/actions/Plaid.ts
index d7f44c40df62..eb0d6f59ff54 100644
--- a/src/libs/actions/Plaid.ts
+++ b/src/libs/actions/Plaid.ts
@@ -31,7 +31,7 @@ function openPlaidBankLogin(allowDebit: boolean, bankAccountID: number) {
         },
         {
             onyxMethod: Onyx.METHOD.SET,
-            key: ONYXKEYS.PLAID_LINK_TOKEN,
+            key: ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN,
             value: '',
         },
         {
@@ -43,7 +43,17 @@ function openPlaidBankLogin(allowDebit: boolean, bankAccountID: number) {
         },
     ];
 
-    API.read(READ_COMMANDS.OPEN_PLAID_BANK_LOGIN, params, {optimisticData});
+    const failureData = [
+        {
+            onyxMethod: Onyx.METHOD.MERGE,
+            key: ONYXKEYS.PLAID_DATA,
+            value: {
+                isLoading: false,
+            },
+        },
+    ];
+
+    API.read(READ_COMMANDS.OPEN_PLAID_BANK_LOGIN, params, {optimisticData, failureData});
 }
 
 /**
@@ -69,12 +79,22 @@ function openPlaidCompanyCardLogin(country: string, domain?: string, feed?: Card
         },
         {
             onyxMethod: Onyx.METHOD.SET,
-            key: ONYXKEYS.PLAID_LINK_TOKEN,
+            key: ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN,
             value: '',
         },
     ];
 
-    API.read(READ_COMMANDS.OPEN_PLAID_CARDS_BANK_LOGIN, params, {optimisticData});
+    const failureData = [
+        {
+            onyxMethod: Onyx.METHOD.MERGE,
+            key: ONYXKEYS.PLAID_DATA,
+            value: {
+                isLoading: false,
+            },
+        },
+    ];
+
+    API.read(READ_COMMANDS.OPEN_PLAID_CARDS_BANK_LOGIN, params, {optimisticData, failureData});
 }
 
 function openPlaidBankAccountSelector(publicToken: string, bankName: string, allowDebit: boolean, bankAccountID: number) {
diff --git a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts
index ece32c9c1f4b..ce33a5b144cc 100644
--- a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts
+++ b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts
@@ -35,7 +35,7 @@ function resetUSDBankAccount(
         | typeof ONYXKEYS.ONFIDO_TOKEN
         | typeof ONYXKEYS.ONFIDO_APPLICANT_ID
         | typeof ONYXKEYS.PLAID_DATA
-        | typeof ONYXKEYS.PLAID_LINK_TOKEN
+        | typeof ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN
         | typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT
         | typeof ONYXKEYS.NVP_LAST_PAYMENT_METHOD
     > = {
@@ -76,7 +76,7 @@ function resetUSDBankAccount(
             },
             {
                 onyxMethod: Onyx.METHOD.SET,
-                key: ONYXKEYS.PLAID_LINK_TOKEN,
+                key: ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN,
                 value: '',
             },
             {
diff --git a/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx b/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx
index 8c372c40c7b8..58562096c77a 100644
--- a/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx
+++ b/src/pages/ReimbursementAccount/USD/BankInfo/BankInfo.tsx
@@ -32,7 +32,7 @@ const receivedRedirectURI = getPlaidOAuthReceivedRedirectURI();
 function BankInfo({onBackButtonPress, onSubmit, policyID}: BankInfoProps) {
     const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
     const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT);
-    const [plaidLinkToken] = useOnyx(ONYXKEYS.PLAID_LINK_TOKEN);
+    const [plaidLinkToken] = useOnyx(ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN);
     const {translate} = useLocalize();
     const markSubmitting = useReimbursementAccountSubmitCallback(onSubmit);
 
diff --git a/src/pages/settings/Wallet/PersonalCards/steps/PlaidConnectionStep.tsx b/src/pages/settings/Wallet/PersonalCards/steps/PlaidConnectionStep.tsx
index b86e05b9abb7..7ccb209cf18a 100644
--- a/src/pages/settings/Wallet/PersonalCards/steps/PlaidConnectionStep.tsx
+++ b/src/pages/settings/Wallet/PersonalCards/steps/PlaidConnectionStep.tsx
@@ -75,7 +75,7 @@ function PlaidConnectionStep({feed, onExit}: {feed?: CompanyCardFeedWithDomainID
     const [addNewPersonalCard] = useOnyx(ONYXKEYS.ADD_NEW_PERSONAL_CARD);
     const isUSCountry = addNewPersonalCard?.data?.selectedCountry === CONST.COUNTRY.US;
     const [isPlaidDisabled] = useOnyx(ONYXKEYS.IS_PLAID_DISABLED);
-    const [plaidLinkToken] = useOnyx(ONYXKEYS.PLAID_LINK_TOKEN);
+    const [plaidLinkToken] = useOnyx(ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN);
     const [plaidData] = useOnyx(ONYXKEYS.PLAID_DATA);
     const plaidErrors = plaidData?.errors;
     const subscribedKeyboardShortcuts = useRef<Array<() => void>>([]);
diff --git a/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx b/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx
index 3af4bce12410..9eba2712e90c 100644
--- a/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx
+++ b/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx
@@ -40,7 +40,7 @@ function PlaidConnectionStep({feed, policyID, onExit, title}: PlaidConnectionSte
     const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD);
     const isUSCountry = addNewCard?.data?.selectedCountry === CONST.COUNTRY.US;
     const [isPlaidDisabled] = useOnyx(ONYXKEYS.IS_PLAID_DISABLED);
-    const [plaidLinkToken] = useOnyx(ONYXKEYS.PLAID_LINK_TOKEN);
+    const [plaidLinkToken] = useOnyx(ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN);
     const [plaidData] = useOnyx(ONYXKEYS.PLAID_DATA);
     const plaidErrors = plaidData?.errors;
     const subscribedKeyboardShortcuts = useRef<Array<() => void>>([]);
diff --git a/src/setup/index.ts b/src/setup/index.ts
index 2d5a990e7bd5..b72addec7bd8 100644
--- a/src/setup/index.ts
+++ b/src/setup/index.ts
@@ -67,6 +67,7 @@ export default function () {
             ONYXKEYS.RAM_ONLY_UPDATE_REQUIRED,
             ONYXKEYS.RAM_ONLY_IS_SEARCHING_FOR_REPORTS,
             ONYXKEYS.RAM_ONLY_WALLET_ONFIDO,
+            ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN,
         ],
     });
 
diff --git a/tests/unit/ExportOnyxStateTest.ts b/tests/unit/ExportOnyxStateTest.ts
index 9d516916ef86..b1037ab04ad1 100644
--- a/tests/unit/ExportOnyxStateTest.ts
+++ b/tests/unit/ExportOnyxStateTest.ts
@@ -123,7 +123,7 @@ describe('maskOnyxState', () => {
                 session: mockSession,
                 [ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: 'sensitive-id',
                 [ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID]: 'stripe-id',
-                [ONYXKEYS.PLAID_LINK_TOKEN]: 'plaid-token',
+                [ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN]: 'plaid-token',
                 [ONYXKEYS.ONFIDO_TOKEN]: 'onfido-token',
             };
             const result = maskOnyxState(input);
@@ -131,7 +131,7 @@ describe('maskOnyxState', () => {
             // Sensitive keys should be removed
             expect(result[ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]).toBeUndefined();
             expect(result[ONYXKEYS.NVP_PRIVATE_STRIPE_CUSTOMER_ID]).toBeUndefined();
-            expect(result[ONYXKEYS.PLAID_LINK_TOKEN]).toBeUndefined();
+            expect(result[ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN]).toBeUndefined();
             expect(result[ONYXKEYS.ONFIDO_TOKEN]).toBeUndefined();
 
             // Session should still be present

From 262a3047f1b986bcb2ac2115c7c4d3d83b869b9c Mon Sep 17 00:00:00 2001
From: Julian Kobrynski <julian.kobrynski@callstack.com>
Date: Fri, 17 Apr 2026 16:26:37 +0200
Subject: [PATCH 07/11] fix: remove USDBankAccountStep from effect deps to
 avoid race with prepareNextStep

---
 src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
index fe6e3f36e7cf..695634d558e6 100644
--- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
+++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx
@@ -271,7 +271,11 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}:
 
         setShouldShowConnectedVerifiedBankAccount(isNonUSDSetup ? achData?.state === CONST.BANK_ACCOUNT.STATE.OPEN : achData?.currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE);
         setShouldShowContinueSetupButton(shouldShowContinueSetupButtonValue);
-    }, [policyIDParam, achData?.currentStep, shouldShowContinueSetupButtonValue, isNonUSDSetup, isPreviousPolicy, achData?.state, policyCurrency, USDBankAccountStep]);
+        // USDBankAccountStep is intentionally omitted from deps. This effect must only react to server-side
+        // achData changes — not to local USDBankAccountStep updates — otherwise it races with prepareNextStep
+        // and briefly pulls USDBankAccountStep back to the server value before the Onyx merge lands.
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [policyIDParam, achData?.currentStep, shouldShowContinueSetupButtonValue, isNonUSDSetup, isPreviousPolicy, achData?.state, policyCurrency]);
 
     useEffect(() => {
         if (!prevPolicyCurrency || policyCurrency === prevPolicyCurrency) {

From d998f91d5affb39c8ee9988cec5105b35751ea75 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=A1bio=20Henriques?= <contact@fabiohenriques.dev>
Date: Tue, 21 Apr 2026 15:37:57 +0100
Subject: [PATCH 08/11] Migrate ISSUE_NEW_EXPENSIFY_CARD to RAM-only

---
 src/ONYXKEYS.ts                                |  4 ++--
 src/libs/actions/Card.ts                       | 18 +++++++++---------
 .../expensifyCard/issueNew/AssigneeStep.tsx    |  2 +-
 .../expensifyCard/issueNew/CardNameStep.tsx    |  2 +-
 .../expensifyCard/issueNew/CardTypeStep.tsx    |  2 +-
 .../issueNew/ConfirmationStep.tsx              |  2 +-
 .../issueNew/InviteNewMemberStep.tsx           |  2 +-
 .../IssueNewCardConfirmMagicCodePage.tsx       |  2 +-
 .../issueNew/IssueNewCardPage.tsx              |  2 +-
 .../expensifyCard/issueNew/LimitTypeStep.tsx   |  2 +-
 .../issueNew/SetExpiryOptionsStep.tsx          |  2 +-
 src/setup/index.ts                             |  1 +
 12 files changed, 21 insertions(+), 20 deletions(-)

diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 740378a30897..0a0db1bed41f 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -808,7 +808,7 @@ const ONYXKEYS = {
         NVP_EXPENSIFY_REPORT_PDF_FILENAME: 'nvp_expensify_report_PDFFilename_',
 
         /** Stores the information about the state of issuing a new card */
-        ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard_',
+        RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard_',
 
         /** Stored the user information with whom bank account is being shared */
         BANK_ACCOUNT_SHARE_DETAILS: 'expensify_bankAccountShare_',
@@ -1294,7 +1294,7 @@ type OnyxCollectionValuesMapping = {
     [ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: OnyxTypes.CompanyCardFeedWithDomainID;
     [ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED]: OnyxTypes.FundID;
     [ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST]: OnyxTypes.CardOnWaitlist;
-    [ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard;
+    [ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard;
     [ONYXKEYS.COLLECTION.SAML_METADATA]: OnyxTypes.SamlMetadata;
     [ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS]: OnyxTypes.DomainPendingActions;
     [ONYXKEYS.COLLECTION.DOMAIN_ERRORS]: OnyxTypes.DomainErrors;
diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts
index 72094dce42b4..6d5c5dd9e5f2 100644
--- a/src/libs/actions/Card.ts
+++ b/src/libs/actions/Card.ts
@@ -748,7 +748,7 @@ function getCardDefaultName(userName?: string) {
 }
 
 function setIssueNewCardStepAndData({data, isEditing, step, policyID, isChangeAssigneeDisabled}: IssueNewCardFlowData) {
-    Onyx.merge(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {
+    Onyx.merge(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {
         data,
         isEditing,
         currentStep: step,
@@ -764,7 +764,7 @@ function setDraftInviteAccountID(assigneeEmail: string | undefined, assigneeAcco
 }
 
 function clearIssueNewCardFlow(policyID: string | undefined) {
-    Onyx.set(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {
+    Onyx.set(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {
         currentStep: null,
         data: {},
         isSuccessful: false,
@@ -777,7 +777,7 @@ function clearIssueNewCardFormData() {
 }
 
 function clearIssueNewCardError(policyID: string | undefined) {
-    Onyx.merge(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {errors: null});
+    Onyx.merge(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {errors: null});
 }
 
 function buildCardListUpdates(workspaceAccountID: number, cardID: number, cardUpdateData: CardListUpdateData, shouldUpdateCardList: boolean): CardOnyxUpdate[] {
@@ -1367,10 +1367,10 @@ function issueExpensifyCard(
 
     const {assigneeEmail, limit, limitType, cardTitle, cardType, validFrom, validThru} = data;
 
-    const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD>> = [
+    const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD>> = [
         {
             onyxMethod: Onyx.METHOD.MERGE,
-            key: `${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`,
+            key: `${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`,
             value: {
                 isLoading: true,
                 errors: null,
@@ -1379,10 +1379,10 @@ function issueExpensifyCard(
         },
     ];
 
-    const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD>> = [
+    const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD>> = [
         {
             onyxMethod: Onyx.METHOD.MERGE,
-            key: `${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`,
+            key: `${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`,
             value: {
                 isLoading: false,
                 isSuccessful: true,
@@ -1390,10 +1390,10 @@ function issueExpensifyCard(
         },
     ];
 
-    const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD>> = [
+    const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD>> = [
         {
             onyxMethod: Onyx.METHOD.MERGE,
-            key: `${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`,
+            key: `${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`,
             value: {
                 isLoading: false,
                 errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
diff --git a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx
index 815e4e82228b..8b7e055d005c 100644
--- a/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/AssigneeStep.tsx
@@ -47,7 +47,7 @@ function AssigneeStep({policy, stepNames, startStepIndex, route}: AssigneeStepPr
     const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar']);
     const {isOffline} = useNetwork();
     const policyID = route.params.policyID;
-    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
+    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
     const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
     const [account] = useOnyx(ONYXKEYS.ACCOUNT);
     const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
diff --git a/src/pages/workspace/expensifyCard/issueNew/CardNameStep.tsx b/src/pages/workspace/expensifyCard/issueNew/CardNameStep.tsx
index 0848e753f410..07ce73efac1b 100644
--- a/src/pages/workspace/expensifyCard/issueNew/CardNameStep.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/CardNameStep.tsx
@@ -34,7 +34,7 @@ function CardNameStep({policyID, stepNames, startStepIndex}: CardNameStepProps)
     const {translate} = useLocalize();
     const styles = useThemeStyles();
     const {inputCallbackRef} = useAutoFocusInput();
-    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
+    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
 
     const isEditing = issueNewCard?.isEditing;
     const data = issueNewCard?.data;
diff --git a/src/pages/workspace/expensifyCard/issueNew/CardTypeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/CardTypeStep.tsx
index c88c6844a3a9..72675705449e 100644
--- a/src/pages/workspace/expensifyCard/issueNew/CardTypeStep.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/CardTypeStep.tsx
@@ -33,7 +33,7 @@ function CardTypeStep({policy, stepNames, startStepIndex}: CardTypeStepProps) {
     const styles = useThemeStyles();
     const illustrations = useMemoizedLazyIllustrations(['HandCard', 'VirtualCard']);
     const policyID = policy?.id;
-    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
+    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
 
     const isEditing = issueNewCard?.isEditing;
 
diff --git a/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx b/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx
index 1a9de688ff4a..5a6c1516abd1 100644
--- a/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/ConfirmationStep.tsx
@@ -45,7 +45,7 @@ function ConfirmationStep({policyID, stepNames, startStepIndex, backTo}: Confirm
     const styles = useThemeStyles();
     const {isOffline} = useNetwork();
     const [account] = useOnyx(ONYXKEYS.ACCOUNT);
-    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
+    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
     const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
     const defaultFundID = useDefaultFundID(policyID);
     const {isBetaEnabled} = usePermissions();
diff --git a/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx b/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx
index 16aaa18f6caa..813fa306e413 100644
--- a/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/InviteNewMemberStep.tsx
@@ -22,7 +22,7 @@ type InviteeNewMemberStepProps = Omit<WithPolicyAndFullscreenLoadingProps, 'rout
 function InviteNewMemberStep({policy, route, currentUserPersonalDetails}: InviteeNewMemberStepProps) {
     const {translate} = useLocalize();
     const policyID = route.params.policyID;
-    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
+    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
 
     const isEditing = issueNewCard?.isEditing;
     const handleBackButtonPress = () => {
diff --git a/src/pages/workspace/expensifyCard/issueNew/IssueNewCardConfirmMagicCodePage.tsx b/src/pages/workspace/expensifyCard/issueNew/IssueNewCardConfirmMagicCodePage.tsx
index b2c2e01fe5d9..242fc4626e02 100644
--- a/src/pages/workspace/expensifyCard/issueNew/IssueNewCardConfirmMagicCodePage.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/IssueNewCardConfirmMagicCodePage.tsx
@@ -26,7 +26,7 @@ function IssueNewCardConfirmMagicCodePage({route}: IssueNewCardConfirmMagicCodeP
     const [account] = useOnyx(ONYXKEYS.ACCOUNT);
     const [session] = useOnyx(ONYXKEYS.SESSION);
     const primaryLogin = account?.primaryLogin ?? session?.email ?? '';
-    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
+    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
     const validateError = getLatestErrorMessageField(issueNewCard);
     const data = issueNewCard?.data;
     const isSuccessful = issueNewCard?.isSuccessful;
diff --git a/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx b/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx
index ac6bd562bfbe..cc39f988df40 100644
--- a/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/IssueNewCardPage.tsx
@@ -46,7 +46,7 @@ function getStartStepIndex(issueNewCard: OnyxEntry<IssueNewCard>): number {
 
 function IssueNewCardPage({policy, route}: IssueNewCardPageProps) {
     const policyID = policy?.id;
-    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {initWithStoredValues: false});
+    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
     const {currentStep} = issueNewCard ?? {};
     const backTo = route?.params?.backTo;
     const {isDelegateAccessRestricted} = useDelegateNoAccessState();
diff --git a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx
index 941339de9921..6e634ee2a96c 100644
--- a/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx
@@ -38,7 +38,7 @@ function LimitTypeStep({policy, stepNames, startStepIndex}: LimitTypeStepProps)
     const {translate} = useLocalize();
     const styles = useThemeStyles();
     const policyID = policy?.id;
-    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
+    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
     const formRef = useRef<FormRef | null>(null);
 
     const areApprovalsConfigured = getApprovalWorkflow(policy) !== CONST.POLICY.APPROVAL_MODE.OPTIONAL;
diff --git a/src/pages/workspace/expensifyCard/issueNew/SetExpiryOptionsStep.tsx b/src/pages/workspace/expensifyCard/issueNew/SetExpiryOptionsStep.tsx
index 78ad0e1bd6f6..1b49efb79427 100644
--- a/src/pages/workspace/expensifyCard/issueNew/SetExpiryOptionsStep.tsx
+++ b/src/pages/workspace/expensifyCard/issueNew/SetExpiryOptionsStep.tsx
@@ -33,7 +33,7 @@ function SetExpiryOptionsStep({policy, stepNames, startStepIndex}: SetExpiryOpti
     const {translate} = useLocalize();
     const styles = useThemeStyles();
     const policyID = policy?.id;
-    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
+    const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`);
     const personalDetails = usePersonalDetails();
 
     const assigneePersonalDetails = Object.values(personalDetails ?? {}).find((detail) => detail?.login === issueNewCard?.data?.assigneeEmail);
diff --git a/src/setup/index.ts b/src/setup/index.ts
index 4d72fc2816b4..d96e8a0d2d66 100644
--- a/src/setup/index.ts
+++ b/src/setup/index.ts
@@ -70,6 +70,7 @@ export default function () {
             ONYXKEYS.RAM_ONLY_IS_SEARCHING_FOR_REPORTS,
             ONYXKEYS.RAM_ONLY_WALLET_ONFIDO,
             ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN,
+            ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD,
         ],
     });
 

From 33d6902a1d31ee2804f285b14bd4d396d299870b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=A1bio=20Henriques?= <contact@fabiohenriques.dev>
Date: Tue, 21 Apr 2026 15:54:02 +0100
Subject: [PATCH 09/11] Implement useHasFreshWalletData hook

---
 src/pages/EnablePayments/EnablePayments.tsx   | 26 ++---------------
 .../EnablePayments/EnablePaymentsPage.tsx     | 20 +++----------
 .../EnablePayments/useHasFreshWalletData.ts   | 28 +++++++++++++++++++
 3 files changed, 35 insertions(+), 39 deletions(-)
 create mode 100644 src/pages/EnablePayments/useHasFreshWalletData.ts

diff --git a/src/pages/EnablePayments/EnablePayments.tsx b/src/pages/EnablePayments/EnablePayments.tsx
index 9b09b1e14e39..282fdf2e33ae 100644
--- a/src/pages/EnablePayments/EnablePayments.tsx
+++ b/src/pages/EnablePayments/EnablePayments.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useRef, useState} from 'react';
+import React, {useEffect} from 'react';
 import {View} from 'react-native';
 import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
 import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -19,6 +19,7 @@ import AddBankAccount from './AddBankAccount/AddBankAccount';
 import FailedKYC from './FailedKYC';
 import FeesAndTerms from './FeesAndTerms/FeesAndTerms';
 import PersonalInfo from './PersonalInfo/PersonalInfo';
+import useHasFreshWalletData from './useHasFreshWalletData';
 import VerifyIdentity from './VerifyIdentity/VerifyIdentity';
 
 function EnablePaymentsPage() {
@@ -30,8 +31,7 @@ function EnablePaymentsPage() {
     const [fundList] = useOnyx(ONYXKEYS.FUND_LIST);
     const paymentCardList = fundList ?? {};
 
-    const wasLoadingRef = useRef(false);
-    const [hasFreshData, setHasFreshData] = useState(false);
+    const hasFreshData = useHasFreshWalletData(isOffline, userWallet?.isLoading);
 
     useEffect(() => {
         if (isOffline) {
@@ -41,26 +41,6 @@ function EnablePaymentsPage() {
         openEnablePaymentsPage();
     }, [isOffline]);
 
-    // Only render step content after the fresh data loading cycle (isLoading: true → false) completes,
-    // to avoid acting on stale cached values from a previous session.
-    useEffect(() => {
-        if (isOffline) {
-            return;
-        }
-
-        if (userWallet?.isLoading) {
-            wasLoadingRef.current = true;
-            return;
-        }
-
-        if (!wasLoadingRef.current) {
-            return;
-        }
-
-        // eslint-disable-next-line react-hooks/set-state-in-effect -- we need to trigger a re-render when fresh data arrives to stop showing the loading indicator
-        setHasFreshData(true);
-    }, [isOffline, userWallet?.isLoading]);
-
     const isUserWalletEmpty = isEmptyObject(userWallet);
     if (isUserWalletEmpty || userWallet?.isLoading || (!hasFreshData && !isOffline)) {
         const reasonAttributes: SkeletonSpanReasonAttributes = {
diff --git a/src/pages/EnablePayments/EnablePaymentsPage.tsx b/src/pages/EnablePayments/EnablePaymentsPage.tsx
index 891c3b21e582..146a40af8087 100644
--- a/src/pages/EnablePayments/EnablePaymentsPage.tsx
+++ b/src/pages/EnablePayments/EnablePaymentsPage.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useRef, useState} from 'react';
+import React, {useEffect} from 'react';
 import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
 import HeaderWithBackButton from '@components/HeaderWithBackButton';
 import ScreenWrapper from '@components/ScreenWrapper';
@@ -18,6 +18,7 @@ import FailedKYC from './FailedKYC';
 // Steps
 import OnfidoStep from './OnfidoStep';
 import TermsStep from './TermsStep';
+import useHasFreshWalletData from './useHasFreshWalletData';
 
 function EnablePaymentsPage() {
     const {translate} = useLocalize();
@@ -25,8 +26,7 @@ function EnablePaymentsPage() {
     const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET);
 
     const {isPendingOnfidoResult, hasFailedOnfido} = userWallet ?? {};
-    const wasLoadingRef = useRef(false);
-    const [hasFreshData, setHasFreshData] = useState(false);
+    const hasFreshData = useHasFreshWalletData(isOffline, userWallet?.isLoading);
 
     // Always fetch fresh wallet data on mount
     useEffect(() => {
@@ -44,23 +44,11 @@ function EnablePaymentsPage() {
             return;
         }
 
-        if (userWallet?.isLoading) {
-            wasLoadingRef.current = true;
-            return;
-        }
-
-        if (!wasLoadingRef.current) {
-            return;
-        }
-
-        // eslint-disable-next-line react-hooks/set-state-in-effect -- we need to trigger a re-render when fresh data arrives to stop showing the loading indicator
-        setHasFreshData(true);
-
         // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
         if (isPendingOnfidoResult || hasFailedOnfido) {
             Navigation.navigate(ROUTES.SETTINGS_WALLET, {forceReplace: true});
         }
-    }, [isOffline, isPendingOnfidoResult, hasFailedOnfido, userWallet?.isLoading]);
+    }, [isOffline, isPendingOnfidoResult, hasFailedOnfido]);
 
     const isUserWalletEmpty = isEmptyObject(userWallet);
     if (isUserWalletEmpty || userWallet?.isLoading || (!hasFreshData && !isOffline)) {
diff --git a/src/pages/EnablePayments/useHasFreshWalletData.ts b/src/pages/EnablePayments/useHasFreshWalletData.ts
new file mode 100644
index 000000000000..a00b490e58c9
--- /dev/null
+++ b/src/pages/EnablePayments/useHasFreshWalletData.ts
@@ -0,0 +1,28 @@
+import {useEffect, useRef, useState} from 'react';
+
+function useHasFreshWalletData(isOffline: boolean, isLoading: boolean | undefined): boolean {
+    const wasLoadingRef = useRef(false);
+    const [hasFreshData, setHasFreshData] = useState(false);
+
+    useEffect(() => {
+        if (isOffline) {
+            return;
+        }
+
+        if (isLoading) {
+            wasLoadingRef.current = true;
+            return;
+        }
+
+        if (!wasLoadingRef.current) {
+            return;
+        }
+
+        // eslint-disable-next-line react-hooks/set-state-in-effect -- we need to trigger a re-render when fresh data arrives to stop showing the loading indicator
+        setHasFreshData(true);
+    }, [isOffline, isLoading]);
+
+    return hasFreshData;
+}
+
+export default useHasFreshWalletData;

From a38329c76b42d0c039556996ff32b948860e56f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?F=C3=A1bio=20Henriques?= <contact@fabiohenriques.dev>
Date: Fri, 24 Apr 2026 10:53:12 +0100
Subject: [PATCH 10/11] fix: gate EnablePaymentsPage redirect on fresh wallet
 data

---
 src/pages/EnablePayments/EnablePaymentsPage.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/pages/EnablePayments/EnablePaymentsPage.tsx b/src/pages/EnablePayments/EnablePaymentsPage.tsx
index 146a40af8087..a54c324ada97 100644
--- a/src/pages/EnablePayments/EnablePaymentsPage.tsx
+++ b/src/pages/EnablePayments/EnablePaymentsPage.tsx
@@ -40,7 +40,7 @@ function EnablePaymentsPage() {
     // Only redirect after the fresh data loading cycle (isLoading: true → false) completes,
     // to avoid acting on stale cached values from a previous session.
     useEffect(() => {
-        if (isOffline) {
+        if (isOffline || !hasFreshData) {
             return;
         }
 
@@ -48,7 +48,7 @@ function EnablePaymentsPage() {
         if (isPendingOnfidoResult || hasFailedOnfido) {
             Navigation.navigate(ROUTES.SETTINGS_WALLET, {forceReplace: true});
         }
-    }, [isOffline, isPendingOnfidoResult, hasFailedOnfido]);
+    }, [isOffline, isPendingOnfidoResult, hasFailedOnfido, hasFreshData]);
 
     const isUserWalletEmpty = isEmptyObject(userWallet);
     if (isUserWalletEmpty || userWallet?.isLoading || (!hasFreshData && !isOffline)) {

From 6c83ac9322c1f9f1f9ec2712f74de788b0fe0d5d Mon Sep 17 00:00:00 2001
From: eliran goshen <elirangoshen@gmail.com>
Date: Mon, 27 Apr 2026 15:00:46 +0200
Subject: [PATCH 11/11] pr fix

---
 .../EnablePayments/useHasFreshWalletData.ts   | 43 +++++++++----------
 1 file changed, 21 insertions(+), 22 deletions(-)

diff --git a/src/pages/EnablePayments/useHasFreshWalletData.ts b/src/pages/EnablePayments/useHasFreshWalletData.ts
index a00b490e58c9..1d98c04a9b67 100644
--- a/src/pages/EnablePayments/useHasFreshWalletData.ts
+++ b/src/pages/EnablePayments/useHasFreshWalletData.ts
@@ -1,28 +1,27 @@
-import {useEffect, useRef, useState} from 'react';
+import {useState} from 'react';
 
-function useHasFreshWalletData(isOffline: boolean, isLoading: boolean | undefined): boolean {
-    const wasLoadingRef = useRef(false);
-    const [hasFreshData, setHasFreshData] = useState(false);
-
-    useEffect(() => {
-        if (isOffline) {
-            return;
-        }
-
-        if (isLoading) {
-            wasLoadingRef.current = true;
-            return;
-        }
+type WalletDataState = 'idle' | 'loading' | 'fresh';
 
-        if (!wasLoadingRef.current) {
-            return;
-        }
-
-        // eslint-disable-next-line react-hooks/set-state-in-effect -- we need to trigger a re-render when fresh data arrives to stop showing the loading indicator
-        setHasFreshData(true);
-    }, [isOffline, isLoading]);
+function computeNextState(current: WalletDataState, isOffline: boolean, isLoading: boolean | undefined): WalletDataState {
+    if (isOffline) {
+        return current;
+    }
+    if (current === 'idle' && isLoading) {
+        return 'loading';
+    }
+    if (current === 'loading' && !isLoading) {
+        return 'fresh';
+    }
+    return current;
+}
 
-    return hasFreshData;
+function useHasFreshWalletData(isOffline: boolean, isLoading: boolean | undefined): boolean {
+    const [state, setState] = useState<WalletDataState>('idle');
+    const next = computeNextState(state, isOffline, isLoading);
+    if (next !== state) {
+        setState(next);
+    }
+    return next === 'fresh';
 }
 
 export default useHasFreshWalletData;
