From 6660263498abdff327eccf0c6e6f741c2db8f3fa Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Tue, 16 Jun 2026 16:18:33 +0200 Subject: [PATCH 1/3] fix: inbox pre-insert and search dismiss --- .../SubmitExpenseOrchestrator.tsx | 161 +++++++++++------- tests/ui/TimeExpenseConfirmationTest.tsx | 14 +- .../IOURequestStepConfirmationPageTest.tsx | 14 +- 3 files changed, 119 insertions(+), 70 deletions(-) diff --git a/src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx b/src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx index db84fcb8bb4f..6eb60b9f2874 100644 --- a/src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx +++ b/src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx @@ -5,6 +5,7 @@ import DateUtils from '@libs/DateUtils'; import {cancelDeferredWrite, flushDeferredWrite, reserveDeferredWriteChannel} from '@libs/deferredLayoutWrite'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import Log from '@libs/Log'; +import getTopmostReportParams from '@libs/Navigation/helpers/getTopmostReportParams'; import isReportOpenInRHP from '@libs/Navigation/helpers/isReportOpenInRHP'; import isReportOpenInSuperWideRHP from '@libs/Navigation/helpers/isReportOpenInSuperWideRHP'; import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; @@ -140,11 +141,15 @@ function SubmitExpenseOrchestrator({ return () => clearTimeout(confirmingSafetyTimeout.current); }, [isConfirming]); + // Unified from both prop (isFromGlobalCreate) and transaction flags because + // the transaction flags are the source of truth — the prop is derived from + // the same transaction at mount time. Either source being true is sufficient + // for correct handler selection (e.g. SEARCH_DISMISS) and telemetry. + const isFromGlobalCreateFromTransaction = !!(isFromGlobalCreateOnTransaction || isFromFloatingActionButtonOnTransaction); + const isFromGlobalCreateForNavigation = !!(isFromGlobalCreate || isFromGlobalCreateFromTransaction); + const startSubmitSpans = () => { const hasReceiptFiles = Object.values(receiptFiles).some((receipt) => !!receipt); - // Re-derive from transaction inside the callback so telemetry captures the value - // at submission time, not at render time (transaction is mutable Onyx state). - const isFromGlobalCreateForTelemetry = !!(isFromGlobalCreateOnTransaction || isFromFloatingActionButtonOnTransaction); const scenario = getSubmitExpenseScenario({ iouType, isDistanceRequest, @@ -153,7 +158,7 @@ function SubmitExpenseOrchestrator({ isCategorizingTrackExpense, isSharingTrackExpense, isPerDiemRequest, - isFromGlobalCreate: isFromGlobalCreateForTelemetry, + isFromGlobalCreate: isFromGlobalCreateForNavigation, hasReceiptFiles, }); @@ -161,7 +166,7 @@ function SubmitExpenseOrchestrator({ scenario, iouType, requestType: requestType ?? 'unknown', - isFromGlobalCreate: isFromGlobalCreateForTelemetry, + isFromGlobalCreate: isFromGlobalCreateForNavigation, hasReceipt: hasReceiptFiles, }); }; @@ -174,7 +179,7 @@ function SubmitExpenseOrchestrator({ return { isPreInserted, isReportPreInserted: isPreInserted && Navigation.getPreInsertedFullscreenRouteName() === NAVIGATORS.REPORTS_SPLIT_NAVIGATOR, - isFromGlobalCreate, + isFromGlobalCreate: isFromGlobalCreateForNavigation, canDismissFromSearch, navigatesToDestinationReport: iouType === CONST.IOU.TYPE.SPLIT || iouType === CONST.IOU.TYPE.TRACK, destinationReportID, @@ -188,7 +193,7 @@ function SubmitExpenseOrchestrator({ // Fast-path handlers defer createTransaction until after the dismiss animation completes // via dismissModal's afterTransition callback (backed by TransitionTracker). This prevents // heavy optimistic Onyx writes from blocking the JS thread during the RHP slide-out animation. - const handleSearchPreInsert = () => { + const handleSearchPreInsert = (locationPermissionGranted = false) => { setFastPath(CONST.TELEMETRY.FAST_PATH_HANDLER.SEARCH_PRE_INSERT, CONST.TELEMETRY.SUBMIT_OPTIMIZATION.PRE_INSERT, CONST.TELEMETRY.SUBMIT_OPTIMIZATION.DISMISS_FIRST); setPendingSubmitFollowUpAction(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.NAVIGATE_TO_SEARCH); Navigation.clearFullscreenPreInsertedFlag(); @@ -198,32 +203,51 @@ function SubmitExpenseOrchestrator({ // shouldHandleNavigation defaults to true here (other fast paths pass false). The Search screen was // pre-inserted before the modal opened, so the nav stack is already correct and createTransaction's // post-create cleanup (navigateAfterExpenseCreate) finishes the flow. - createTransaction(); + createTransaction(locationPermissionGranted); setIsConfirming(false); }, }); }; - const handleReportPreInsert = () => { + const dismissAfterEnsuringDestinationReportIsPreInserted = (reportID: string | undefined, afterTransition: () => void) => { + if (!reportID) { + Navigation.dismissModal({afterTransition}); + return; + } + + // Only trust the pre-inserted report if the Reports tab is actually focused. + // getTopmostReportParams can find a matching report in an unfocused/stale + // Reports tab (e.g. user opened the report earlier, returned to Inbox, then + // submitted). Dismissing in that case would reveal Inbox, not the report. + if (isReportTopmostSplitNavigator()) { + const preInsertedReportID = getTopmostReportParams(navigationRef.getRootState())?.reportID; + if (preInsertedReportID === reportID) { + Navigation.dismissModal({afterTransition}); + return; + } + } + + Navigation.revealRouteBeforeDismissingModal(ROUTES.REPORT_WITH_ID.getRoute(reportID), {afterTransition}); + }; + + const handleReportPreInsert = (locationPermissionGranted = false) => { setFastPath(CONST.TELEMETRY.FAST_PATH_HANDLER.REPORT_PRE_INSERT, CONST.TELEMETRY.SUBMIT_OPTIMIZATION.PRE_INSERT, CONST.TELEMETRY.SUBMIT_OPTIMIZATION.DISMISS_FIRST); Navigation.clearFullscreenPreInsertedFlag(); setPendingSubmitFollowUpAction(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_AND_OPEN_REPORT, destinationReportID); reserveDeferredWriteChannel(CONST.DEFERRED_LAYOUT_WRITE_KEYS.DISMISS_MODAL, {destinationReportID}); - Navigation.dismissModal({ - afterTransition: () => { - createTransaction(false, false); - setIsConfirming(false); - }, + dismissAfterEnsuringDestinationReportIsPreInserted(destinationReportID, () => { + createTransaction(locationPermissionGranted, false); + setIsConfirming(false); }); }; - const handleDismissModalFastPath = () => { + const handleDismissModalFastPath = (locationPermissionGranted = false) => { setFastPath(CONST.TELEMETRY.FAST_PATH_HANDLER.DISMISS_MODAL, CONST.TELEMETRY.SUBMIT_OPTIMIZATION.DISMISS_FIRST); const shouldPreserveSearchWithPlaceholder = (iouType === CONST.IOU.TYPE.SPLIT || iouType === CONST.IOU.TYPE.TRACK) && isSearchTopmostFullScreenRoute(); reserveDeferredWriteChannel(shouldPreserveSearchWithPlaceholder ? CONST.DEFERRED_LAYOUT_WRITE_KEYS.SEARCH : CONST.DEFERRED_LAYOUT_WRITE_KEYS.DISMISS_MODAL, {destinationReportID}); const runAfterDismiss = () => { - createTransaction(false, false); + createTransaction(locationPermissionGranted, false); setIsConfirming(false); }; @@ -241,35 +265,56 @@ function SubmitExpenseOrchestrator({ // Wide: always the handler // Narrow: only runs if the user submitted before the pre-insert timer (300ms) // elapsed - SEARCH_PRE_INSERT is the primary narrow handler. - const handleSearchDismiss = () => { + const handleSearchDismiss = (locationPermissionGranted = false) => { setFastPath(CONST.TELEMETRY.FAST_PATH_HANDLER.SEARCH_DISMISS, CONST.TELEMETRY.SUBMIT_OPTIMIZATION.DISMISS_FIRST); const searchType = iouType === CONST.IOU.TYPE.INVOICE ? CONST.SEARCH.DATA_TYPES.INVOICE : CONST.SEARCH.DATA_TYPES.EXPENSE; const isSameType = getCurrentSearchQueryJSON()?.type === searchType; + const isNarrow = getIsNarrowLayout(); setPendingSubmitFollowUpAction(isSameType ? CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY : CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.NAVIGATE_TO_SEARCH); reserveDeferredWriteChannel(CONST.DEFERRED_LAYOUT_WRITE_KEYS.SEARCH); const runAfterDismiss = () => { - createTransaction(false, false); + createTransaction(locationPermissionGranted, false); setIsConfirming(false); }; - if (!isSameType && !getIsNarrowLayout()) { + const runAfterSearchDismissRecovery = (afterRecovery?: () => void) => { + const finish = () => { + runAfterDismiss(); + afterRecovery?.(); + }; + + if (navigationRef.getRootState()?.routes?.at(-1)?.name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + finish(); + return; + } + + Log.info('[SubmitExpenseOrchestrator] Search dismiss recovery: RHP still on top after first dismiss, dismissing again'); + Navigation.dismissModal({ + afterTransition: finish, + }); + }; + + if (!isSameType && !isNarrow) { dismissWideToNewSearchType(searchType, runAfterDismiss); return; } Navigation.dismissModal({ afterTransition: () => { - runAfterDismiss(); - // Narrow fallback: pre-insert timer didn't fire, navigate after dismiss. - if (!isSameType) { + runAfterSearchDismissRecovery(() => { + // Narrow fallback: pre-insert timer didn't fire, navigate after dismiss. + if (isSameType) { + return; + } + Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: buildCannedSearchQuery({type: searchType})}), {forceReplace: true}); - } + }); }, }); }; - const handleDismissToReport = () => { + const handleDismissToReport = (locationPermissionGranted = false) => { if (!destinationReportID) { // Tracking already started in onSubmit; just override the fast path label. Log.warn('[SubmitExpenseOrchestrator] handleDismissToReport reached without destinationReportID - falling back to default submit'); @@ -281,7 +326,7 @@ function SubmitExpenseOrchestrator({ // is intentionally the same approach used in handleDefaultSubmit so // this fallback behaves identically to the standard submit path. requestAnimationFrame(() => { - createTransaction(); + createTransaction(locationPermissionGranted); requestAnimationFrame(() => { setIsConfirming(false); }); @@ -294,7 +339,7 @@ function SubmitExpenseOrchestrator({ Navigation.revealRouteBeforeDismissingModal(ROUTES.REPORT_WITH_ID.getRoute(destinationReportID), { afterTransition: () => { - createTransaction(false, false); + createTransaction(locationPermissionGranted, false); setIsConfirming(false); }, }); @@ -302,17 +347,17 @@ function SubmitExpenseOrchestrator({ // A global-create submit off the inbox lands on Search — reserve the channel so the optimistic write defers behind the skeleton. const reserveSearchChannelIfGlobalCreate = () => { - if (!isFromGlobalCreate || isReportTopmostSplitNavigator()) { + if (!isFromGlobalCreateForNavigation || isReportTopmostSplitNavigator()) { return; } reserveDeferredWriteChannel(CONST.DEFERRED_LAYOUT_WRITE_KEYS.SEARCH); }; - const handleDefaultSubmit = () => { + const handleDefaultSubmit = (locationPermissionGranted = false) => { setFastPath(CONST.TELEMETRY.FAST_PATH_HANDLER.DEFAULT); reserveSearchChannelIfGlobalCreate(); requestAnimationFrame(() => { - createTransaction(); + createTransaction(locationPermissionGranted); requestAnimationFrame(() => { setIsConfirming(false); }); @@ -323,7 +368,7 @@ function SubmitExpenseOrchestrator({ // When the destination report is empty we reserve a DISMISS_MODAL deferred-write channel // so that MoneyRequestReportActionsList can show a loading skeleton instead of the // "no expenses" empty state while the dismiss animation plays. - const handleReportInRHPDismiss = () => { + const handleReportInRHPDismiss = (locationPermissionGranted = false) => { setFastPath(CONST.TELEMETRY.FAST_PATH_HANDLER.REPORT_IN_RHP_DISMISS, CONST.TELEMETRY.SUBMIT_OPTIMIZATION.DISMISS_FIRST); const rootState = navigationRef.getRootState(); @@ -341,7 +386,7 @@ function SubmitExpenseOrchestrator({ if (isDestinationEmpty) { flushDeferredWrite(CONST.DEFERRED_LAYOUT_WRITE_KEYS.DISMISS_MODAL); } - createTransaction(false, false); + createTransaction(locationPermissionGranted, false); setIsConfirming(false); }; @@ -359,7 +404,26 @@ function SubmitExpenseOrchestrator({ if (isDestinationEmpty) { cancelDeferredWrite(CONST.DEFERRED_LAYOUT_WRITE_KEYS.DISMISS_MODAL); } - handleDefaultSubmit(); + handleDefaultSubmit(locationPermissionGranted); + }; + + const dispatchSubmitHandler = (locationPermissionGranted = false) => { + startSubmitSpans(); + const rootState = navigationRef.getRootState(); + const snapshot = buildNavigationSnapshot(rootState); + const handler = getSubmitHandler(snapshot); + + const handlers: Record void> = { + [SUBMIT_HANDLER.SEARCH_PRE_INSERT]: () => handleSearchPreInsert(locationPermissionGranted), + [SUBMIT_HANDLER.REPORT_PRE_INSERT]: () => handleReportPreInsert(locationPermissionGranted), + [SUBMIT_HANDLER.DISMISS_MODAL]: () => handleDismissModalFastPath(locationPermissionGranted), + [SUBMIT_HANDLER.DISMISS_TO_REPORT]: () => handleDismissToReport(locationPermissionGranted), + [SUBMIT_HANDLER.REPORT_IN_RHP_DISMISS]: () => handleReportInRHPDismiss(locationPermissionGranted), + [SUBMIT_HANDLER.SEARCH_DISMISS]: () => handleSearchDismiss(locationPermissionGranted), + [SUBMIT_HANDLER.DEFAULT]: () => handleDefaultSubmit(locationPermissionGranted), + }; + + handlers[handler](); }; // Not wrapped in useCallback: MoneyRequestConfirmationList is React.memo-wrapped, but this @@ -381,22 +445,7 @@ function SubmitExpenseOrchestrator({ } } - startSubmitSpans(); - - const rootState = navigationRef.getRootState(); - const handler = getSubmitHandler(buildNavigationSnapshot(rootState)); - - const handlers: Record void> = { - [SUBMIT_HANDLER.SEARCH_PRE_INSERT]: handleSearchPreInsert, - [SUBMIT_HANDLER.REPORT_PRE_INSERT]: handleReportPreInsert, - [SUBMIT_HANDLER.DISMISS_MODAL]: handleDismissModalFastPath, - [SUBMIT_HANDLER.DISMISS_TO_REPORT]: handleDismissToReport, - [SUBMIT_HANDLER.REPORT_IN_RHP_DISMISS]: handleReportInRHPDismiss, - [SUBMIT_HANDLER.SEARCH_DISMISS]: handleSearchDismiss, - [SUBMIT_HANDLER.DEFAULT]: handleDefaultSubmit, - }; - - handlers[handler](); + dispatchSubmitHandler(); }; return ( @@ -407,24 +456,16 @@ function SubmitExpenseOrchestrator({ resetPermissionFlow={() => { setStartLocationPermissionFlow(false); }} + // onGrant/onDeny fire before the permission modal finishes closing. + // On iOS, navigating immediately would break the modal close animation. onGrant={() => { - startSubmitSpans(); - setFastPath(CONST.TELEMETRY.FAST_PATH_HANDLER.DEFAULT); - reserveSearchChannelIfGlobalCreate(); - navigateAfterInteraction(() => { - createTransaction(true); - }); + navigateAfterInteraction(() => dispatchSubmitHandler(true)); }} onDeny={(wasUserInitiated) => { - startSubmitSpans(); - setFastPath(CONST.TELEMETRY.FAST_PATH_HANDLER.DEFAULT); if (wasUserInitiated) { updateLastLocationPermissionPrompt(); } - reserveSearchChannelIfGlobalCreate(); - navigateAfterInteraction(() => { - createTransaction(false); - }); + navigateAfterInteraction(() => dispatchSubmitHandler(false)); }} onInitialGetLocationCompleted={() => { setIsConfirming(false); diff --git a/tests/ui/TimeExpenseConfirmationTest.tsx b/tests/ui/TimeExpenseConfirmationTest.tsx index 7a2bc8376319..faebe1c0500e 100644 --- a/tests/ui/TimeExpenseConfirmationTest.tsx +++ b/tests/ui/TimeExpenseConfirmationTest.tsx @@ -63,11 +63,15 @@ jest.mock('@components/ProductTrainingContext', () => ({ jest.mock('@src/hooks/useResponsiveLayout'); jest.mock('@libs/Navigation/navigationRef', () => ({ - getCurrentRoute: jest.fn(() => ({ - name: 'Money_Request_Step_Confirmation', - params: {}, - })), - getState: jest.fn(() => ({})), + __esModule: true, + default: { + getCurrentRoute: jest.fn(() => ({ + name: 'Money_Request_Step_Confirmation', + params: {}, + })), + getState: jest.fn(() => ({})), + getRootState: jest.fn(() => ({routes: []})), + }, })); jest.mock('@libs/Navigation/Navigation', () => { diff --git a/tests/ui/components/IOURequestStepConfirmationPageTest.tsx b/tests/ui/components/IOURequestStepConfirmationPageTest.tsx index 5664e457666a..c1c0f3b8b1d6 100644 --- a/tests/ui/components/IOURequestStepConfirmationPageTest.tsx +++ b/tests/ui/components/IOURequestStepConfirmationPageTest.tsx @@ -89,11 +89,15 @@ jest.mock('@libs/getCurrentPosition'); jest.mock('@libs/getIsNarrowLayout', () => jest.fn(() => false)); jest.mock('@libs/Navigation/navigationRef', () => ({ - getCurrentRoute: jest.fn(() => ({ - name: 'Money_Request_Step_Confirmation', - params: {}, - })), - getState: jest.fn(() => ({})), + __esModule: true, + default: { + getCurrentRoute: jest.fn(() => ({ + name: 'Money_Request_Step_Confirmation', + params: {}, + })), + getState: jest.fn(() => ({})), + getRootState: jest.fn(() => ({routes: []})), + }, })); jest.mock('@libs/Navigation/Navigation', () => { From c8cc1776e4bc950fc7a0781814730e080bb3f126 Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Wed, 17 Jun 2026 16:39:14 +0200 Subject: [PATCH 2/3] remove added Navigation.runAfterUpcomingTransition --- .../request/step/confirmation/SubmitExpenseOrchestrator.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx b/src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx index 9368289f07c1..5bf6ec5c42a3 100644 --- a/src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx +++ b/src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx @@ -455,16 +455,14 @@ function SubmitExpenseOrchestrator({ resetPermissionFlow={() => { setStartLocationPermissionFlow(false); }} - // onGrant/onDeny fire before the permission modal finishes closing. - // On iOS, navigating immediately would break the modal close animation. onGrant={() => { - Navigation.runAfterUpcomingTransition(() => dispatchSubmitHandler(true)); + dispatchSubmitHandler(true); }} onDeny={(wasUserInitiated) => { if (wasUserInitiated) { updateLastLocationPermissionPrompt(); } - Navigation.runAfterUpcomingTransition(() => dispatchSubmitHandler(false)); + dispatchSubmitHandler(false); }} onInitialGetLocationCompleted={() => { setIsConfirming(false); From 48079b9a8685e4649ef6157e085672dff7b377fd Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Wed, 17 Jun 2026 16:58:25 +0200 Subject: [PATCH 3/3] fix: GPS submit report reveal check --- .../SubmitExpenseOrchestrator.tsx | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx b/src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx index 5bf6ec5c42a3..e7ba9127bcc3 100644 --- a/src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx +++ b/src/pages/iou/request/step/confirmation/SubmitExpenseOrchestrator.tsx @@ -5,7 +5,6 @@ import DateUtils from '@libs/DateUtils'; import {cancelDeferredWrite, flushDeferredWrite, reserveDeferredWriteChannel} from '@libs/deferredLayoutWrite'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import Log from '@libs/Log'; -import getTopmostReportParams from '@libs/Navigation/helpers/getTopmostReportParams'; import isReportOpenInRHP from '@libs/Navigation/helpers/isReportOpenInRHP'; import isReportOpenInSuperWideRHP from '@libs/Navigation/helpers/isReportOpenInSuperWideRHP'; import isReportTopmostSplitNavigator from '@libs/Navigation/helpers/isReportTopmostSplitNavigator'; @@ -21,6 +20,7 @@ import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import type {Receipt} from '@src/types/onyx/Transaction'; import {getSubmitHandler, SUBMIT_HANDLER} from './getSubmitHandler'; import type {SubmitHandler, SubmitNavigationSnapshot} from './getSubmitHandler'; @@ -87,6 +87,19 @@ type SubmitExpenseOrchestratorProps = { children: (props: SubmitExpenseOrchestratorRenderProps) => React.ReactNode; }; +function getFocusedReportsSplitReportID(rootState: ReturnType): string | undefined { + const topmostTabNavigatorRoute = rootState?.routes.findLast((route) => route.name === NAVIGATORS.TAB_NAVIGATOR); + const tabState = topmostTabNavigatorRoute?.state; + const activeTabRoute = tabState?.routes.at(tabState.index ?? 0); + if (activeTabRoute?.name !== NAVIGATORS.REPORTS_SPLIT_NAVIGATOR || !activeTabRoute.state) { + return undefined; + } + + const focusedRoute = activeTabRoute.state.routes.at(activeTabRoute.state.index ?? 0); + const reportID = focusedRoute?.name === SCREENS.REPORT && focusedRoute.params && 'reportID' in focusedRoute.params ? focusedRoute.params.reportID : undefined; + return typeof reportID === 'string' ? reportID : undefined; +} + /** * Encapsulates the submit-expense navigation orchestration: telemetry lifecycle, * dismiss animation coordination, deferred writes, and the GPS permission flow. @@ -171,7 +184,7 @@ function SubmitExpenseOrchestrator({ }; // Captures navigation state at decision time for getSubmitHandler. Some handlers - // re-read live state (e.g. getIsNarrowLayout, getTopmostReportParams) for execution + // re-read live state (e.g. getIsNarrowLayout, focused Reports state) for execution // details - this is safe because snapshot + handler run in the same synchronous block. const buildNavigationSnapshot = (rootState: ReturnType): SubmitNavigationSnapshot => { const isPreInserted = Navigation.getIsFullscreenPreInsertedUnderRHP(); @@ -214,16 +227,11 @@ function SubmitExpenseOrchestrator({ return; } - // Only trust the pre-inserted report if the Reports tab is actually focused. - // getTopmostReportParams can find a matching report in an unfocused/stale - // Reports tab (e.g. user opened the report earlier, returned to Inbox, then - // submitted). Dismissing in that case would reveal Inbox, not the report. - if (isReportTopmostSplitNavigator()) { - const preInsertedReportID = getTopmostReportParams(navigationRef.getRootState())?.reportID; - if (preInsertedReportID === reportID) { - Navigation.dismissModal({afterTransition}); - return; - } + // Only trust the pre-inserted report if it is the focused child of the Reports tab. + // A stale report route can still exist behind Inbox in the Reports stack. + if (getFocusedReportsSplitReportID(navigationRef.getRootState()) === reportID) { + Navigation.dismissModal({afterTransition}); + return; } Navigation.revealRouteBeforeDismissingModal(ROUTES.REPORT_WITH_ID.getRoute(reportID), {afterTransition});