Skip to content

[Due for payment 2026-09-02] [$250] iOS - Tapping the back button navigates the user to the Inbox after moving the expense #98106

Closed

Description

@nlemma

If you haven’t already, check out our contributing guidelines for onboarding and email contributors@expensify.com to request to join our Slack channel!


Version Number: v9.4.51-1
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: https://github.com/Expensify/Expensify/issues/504535
Email or phone of affected tester (no customers): -
Issue reported by: Applause Internal Team
Bug source: Regression TC Execution
Device used: iPhone 13 - iOS 26.5

Action Performed:

Precondition:
Create a new workspace if your account doesn't have any workspace

  1. Open the app
  2. Go to Self DM chat
  3. Submit any track expense
  4. Tap the "Submit it to someone" button of the whisper action
  5. Search for workspaces
  6. Select any workspace and Create expense
  7. After redirected to Workspace chat, tap back button

Expected Result:

User is redirected to Self Chat

Actual Result:

Tapping the Back button navigates the user to the Inbox after moving the expense to the workspace chat.

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

Screenshots/Videos

Bug7211428_1784381536161.ScreenRecording_07-18-2026_08-14-36_1.mp4

View all open jobs on GitHub

Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Aug 9, 2026
  2. MelvinBot commented on Aug 9, 2026

    @MelvinBot
    Contributor

    Proposal

    Root Cause (high confidence): When the track expense is moved to a workspace, the confirmation step "pre-mounts"/reveals the destination workspace report using the REPLACE_FULLSCREEN_UNDER_RHP navigation action. Because a report's outermost fullscreen route is the TAB_NAVIGATOR wrapper, this action takes the tab-switch branch, which rebuilds the Reports split-navigator stack as [Sidebar, WorkspaceReport] and prepends only the sidebar as the back target — dropping the previously-focused Self DM report from the stack. After the RHP dismisses, pressing back on the workspace report pops to the sidebar (Inbox) instead of the Self DM.

    What is the root cause of that problem?

    The tab-switch branch of handleReplaceFullscreenUnderRHP rebuilds the Reports split navigator and keeps only the sidebar beneath the incoming report, so any report that was open before (the Self DM) is discarded from the back stack.

    Evidence & data flow

    Flow:

    1. Viewing Self DM → root stack is TAB_NAVIGATOR[REPORTS_SPLIT_NAVIGATOR[Sidebar, SelfDM]].
    2. Tap "Submit it to someone" → ChatActionableButtons.tsx (prepareTrackExpenseButton('submit')) → ReportUtils.ts:12001 navigates to the participant selector RHP.
    3. Select workspace → confirmation RHP pre-inserts ROUTES.REPORT_WITH_ID(workspace) via getSubmitExpensePreMountDestinationRoute.ts:66-92.
    4. Tap Create expense → SubmitExpenseOrchestrator.tsx reveal/pre-insert handler → Navigation.ts revealRouteBeforeDismissingModal dispatches REPLACE_FULLSCREEN_UNDER_RHP.
    5. Root cause: GetStateForActionHandlers.ts tab-switch branch (handleReplaceFullscreenUnderRHP) + getTargetTabRoute rebuild the split navigator as [Sidebar, WorkspaceReport], prepending only the sidebar and dropping Self DM.
    6. RHP dismisses → TAB_NAVIGATOR[REPORTS_SPLIT_NAVIGATOR[Sidebar, WorkspaceReport]] → back pops to Sidebar = Inbox. 🐛

    Why iOS/narrow-layout only: The bug is on the fast pre-insert/reveal path. The non-pre-inserted path (submitDismissStrategies.ts → dismissModalWithReport) PUSHes the report onto the existing split navigator, producing [Sidebar, SelfDM, WorkspaceReport] — where back correctly returns to Self DM.

    What changes do you think we should make in order to solve the problem?

    In the tab-switch branch of handleReplaceFullscreenUnderRHP / getTargetTabRoute (GetStateForActionHandlers.ts:207-254), when the focused tab is the Reports split navigator and a different report is already open in the central pane, preserve that previously-focused report beneath the incoming destination report (prepend the existing central-pane report, not just the sidebar). Alternatively, gate the report pre-insert/reveal so that when a different report is topmost in the Reports split navigator, the destination is PUSHed onto the existing split navigator (matching the dismiss/navigate path) instead of doing a full tab rebuild.

    What alternative solutions did you explore? (Optional)

    Since the correct behavior already exists on the non-pre-insert dismiss path, an alternative is to skip the pre-insert/reveal optimization when the topmost split navigator already holds a different report, falling back to dismissModalWithReport.

    Related issues

    Note on verification: This is a code-based analysis. Because the defect is specific to the narrow-layout pre-insert/reveal fast path, the highest-signal confirmation is an on-device iOS repro dumping navigationRef.getRootState() immediately before and after "Create expense" to confirm the REPORT_PRE_INSERT handler fires (vs DISMISS_MODAL).


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/98106) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Aug 9, 2026
  4. j3ffffff commented on Aug 9, 2026

    @j3ffffff

    VERDICT: POST
    ROOT_CAUSE_ONELINER: After moving a track expense to a workspace, dismissModalWithReport calls navigate(workspaceChatRoute) after dismissing the RHP — but since the Inbox tab's stack already contains SCREENS.REPORT (Self DM), React Navigation's navigate action updates that entry's params instead of pushing a new route, so the iOS back stack becomes [INBOX → workspace chat] with Self DM erased from history.
    PROPOSAL_TYPE: FIRST
    AUTOPOST_SAFE: YES

  5. melvin-bot commented on Aug 9, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @ikevin127 (External)

  6. melvin-bot commented on Aug 9, 2026

    @melvin-bot
  7. Uzaifm127 commented on Aug 10, 2026

    @Uzaifm127
    Contributor

    @ikevin127 I have investigated this issue locally and I found:

    Note

    I have a better and complete proposal than Melvin with two possible solutions.

    1. I'm also able to reproduce this issue on Android native so it isn't iOS specific:

      android-repro.mov

      The pre-insert navigation flow is used on narrow layouts and doesn't have separate iOS or Android logic. The pre-insert flow checks getIsNarrowLayout(), not the platform. So the same behavior can occur on Android native, iOS native, and any other narrow layout using this navigation path.

    2. I have an accurate root cause than Melvin. Basically this is the problematic part in the code:

      const existingFirstRoute = existingNestedRoutes?.at(0);

      Here, when the existing routes are Inbox > Self DM, the code takes only the first route, which is Inbox, and combines it with the incoming Workspace chat route. Therefore, it creates Inbox > Workspace chat and drops the Self DM before the user presses Back.

      I also found these two commits related to this:

      • commit 1: This commit added getTargetTabRoute().
        Basically it was to rebuild a tab’s navigation state when a new screen is opened lazily. It tried to keep a simple back destination, usually Inbox. It is intentionally discarding the Self DM and keeping the Inbox.
      • commit 2: This commit connected the expense submission to usePreMountDestination. That means while the user is still filling the expense details, the app starts preparing the destination report behind the confirmation modal

      After reviewing the commits, I found that getTargetTabRoute intentionally remove the Self DM and keeps the Inbox as main page as a better fallback for enable the lazy navigation.

    3. I have two potential solutions for this:

      • Ignore the pre-insertion for moving track expenses (but this could prevent getting the advantage of lazy navigation or preparing the next screen earlier before modal closes in expense confirmation). So this solution comes with a tradeoff which also could be seen clearly in the following solution video:

        solution.mov
      • Keep pre-insertion, but preserve the current report when pre-inserting a different report in the Reports tab.

        better-solution.mov
  8. ikevin127 commented on Aug 11, 2026

    @ikevin127
    Contributor

    Thanks both. I dug into this against latest main and I'm opening it up for proposals. Reasoning below.

    On Melvin's proposal: the root cause is correct and I verified it. getTargetTabRoute in GetStateForActionHandlers.ts takes existingNestedRoutes?.at(0) (line 216) and prepends only that beneath the incoming route, so [Sidebar, SelfDM] gets rebuilt as [Sidebar, WorkspaceReport] and Self DM is dropped before Back is ever pressed.

    Where it falls short is the solution section: it offers two opposite directions and picks neither. Preserving the previous report and skipping the pre-insert entirely are not variants of one fix, and the second one is a regression rather than a fix, since getSubmitExpensePreMountDestinationRoute exists specifically to render the destination behind the RHP for an instant reveal.

    It also frames the bug as iOS-only, but the gate is getIsNarrowLayout() with no platform branch anywhere on this path, so Android native and narrow mWeb are affected too.

    On Uzaifm127's comment: this is the strongest investigation on the thread, and the Android repro plus the commit archaeology are both correct and useful. But it isn't a proposal, it's missing the template sections, and the two solutions listed are the same two Melvin already listed with the same tradeoff called out. The added value is the evidence and the platform correction, not a different solution.

    Why I'm opening it up: neither writeup addresses the part that actually makes this tricky. The fix lands inside a path that deliberately strips the focused tab's key in markFocusedTabRouteForRemount to force a remount and avoid the react-native-screens push flash, and the WORKSPACES_LIST branch right above it already seeds a fresh keyless route for the same reason (#90985).

    Prepending the previous report means that screen now mounts as part of the reveal. Uzaifm127's own commit research points out the discard was intentional, then moves past it.

    A proposal I can accept needs to commit to the preserve approach and answer:

    • preserve only the focused central-pane route, or the whole existing nested stack ?
    • scope it to REPORTS_SPLIT_NAVIGATOR, or all tabs? getTargetTabRoute is shared and WORKSPACE_NAVIGATOR already has bespoke handling
    • does the cancel path still restore correctly? preInsertedOriginalTabRoute feeds handleRemoveFullscreenUnderRHP
    • does the preserved screen need to mount born-non-top to avoid the reorder flash ?

    @Uzaifm127, since you've already done the work and have the videos, please post this as a formal proposal covering the above and I'll review it first.

    @MelvinBot Add Help Wanted label on this issue to open it for proposals.

  9. added
    Help WantedApply this label when an issue is open to proposals by contributors
    on Aug 11, 2026
  10. yusufdeveloper2903 commented on Aug 11, 2026

    @yusufdeveloper2903
    Contributor

    Proposal

    What is the root cause of that problem?

    In the OP's video the tracked expense is created in the Self DM, so when the whisper flow starts the report under the money‑request RHP is the Self DM. Selecting a workspace routes the flow to the confirmation step keyed to the destination report (the policy expense chat), e.g. ReportUtils.ts#L12001. Because navigating to that RHP route also aligns the full‑screen route under the overlay (linkTo/index.ts#L222-L238), the Reports split's central report is swapped from the Self DM to the destination workspace chat — verifiable in the confirmation step, where the yusfu's expenses header already renders under the confirmation dialog.

    On confirm, navigateAfterExpenseCreate takes the non‑global‑create branch and calls dismissModalAndOpenReportInInboxTab(destinationReportID) (navigateAfterExpenseCreate.ts#L57-L60) → dismissModalWithReport({reportID}) (dismissModalAndOpenReportInInboxTab.ts#L84). Since the topmost report is now the destination (topmostReportID === reportID) and the Reports split is topmost, this hits the early branch that only dismisses the modal, without re‑navigating (Navigation.ts#L919-L925). The RHP peels off to reveal [Sidebar, workspaceChat] — the Self DM is no longer in the stack — so tapping back lands on the Inbox instead of the Self Chat.

    What changes do you think we should make in order to solve the problem?

    Open the destination workspace chat with backTo set to the originating Self DM, so its header back button returns there regardless of what the swap left in the stack — report headers already resolve back via goBack(backTo), so this is the deterministic mechanism the flow is missing.

    Thread the source report through the terminal navigation. The move‑from‑track flow already knows the origin as the draft's linkedTrackedExpenseReportID. Pass it as backTo from navigateAfterExpenseCreate's dismiss branch into dismissModalAndOpenReportInInboxTab → dismissModalWithReport, building it once with ROUTES.REPORT_WITH_ID.getRoute(linkedTrackedExpenseReportID).

    Honor it in the collapsed branch. In dismissModalWithReport at Navigation.ts#L919-L925, when the topmost report already equals reportID and a backTo was supplied, re‑open it with the param instead of a bare dismiss:

    if (topmostReportID === reportID && areReportsIDsDefined && isReportsSplitTopmostFullScreen) {
        options?.onBeforeNavigate?.(false);
        dismissModal({
            afterTransition: backTo ? () => navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID, reportActionID, referrer, backTo), {forceReplace: true}) : options?.afterTransition,
        });
        return;
    }

    This is scoped to the move‑from‑track paths (CATEGORIZE/SHARE/SUBMIT), since only they launch from a report the user expects to return to; the global‑create case passes no backTo and keeps today's "open on the inbox tab" behavior, so no other consumer of dismissModalAndOpenReportInInboxTab changes.

    What alternative solutions did you explore? (Optional)

    • Keep the Self DM under the overlay (suppress the full‑screen swap in linkTo for these RHP steps) so the terminal dismissModalWithReport pushes the destination on top of the Self DM. Fixes back navigation but touches the shared overlay logic in linkTo, which many RHP flows rely on — higher blast radius than attaching a backTo.
    • Land back on the Self DM and push the workspace chat on top. Rejected: the video's expected result is to land on the workspace chat first and only return to Self Chat on back, so the destination must remain the visible screen.
  11. David-2002065 commented on Aug 11, 2026

    @David-2002065
    Contributor

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On narrow layout (iOS app), after submitting a tracked expense from the self-DM to a workspace via the "Submit it to someone" whisper, the app opens the workspace chat — but pressing the back button lands on the Inbox instead of returning to the self-DM the user came from.

    What is the root cause of that problem?

    While the expense confirmation RHP is open on narrow layout, the destination workspace chat is pre-mounted underneath the RHP so it can be revealed instantly on dismiss: getSubmitExpensePreMountDestinationRoute returns REPORT_WITH_ID(workspaceChat), and usePreMountDestination dispatches it via Navigation.preInsertFullscreenUnderRHP, which fires a REPLACE_FULLSCREEN_UNDER_RHP action.

    Because every report route resolves to the TAB_NAVIGATOR fullscreen, handleReplaceFullscreenUnderRHP takes the tab branch and rebuilds the Reports split navigator through getTargetTabRoute. That's where the history is lost: it keeps only the FIRST existing nested route (existingFirstRoute — the Inbox sidebar) as the back target and throws away every route above it — including the self-DM report the flow started from:

    before pre-insert:  REPORTS_SPLIT: [Inbox, Report(selfDM)]        <- RHP on top
    after pre-insert:   REPORTS_SPLIT: [Inbox, Report(workspaceChat)] <- selfDM dropped
    

    So when the RHP dismisses and the user presses back on the workspace chat, the stack pops to the Inbox sidebar.

    I verified this against the real handler with a unit test (same harness as the existing handleReplaceFullscreenUnderRHP.test.ts): given [Inbox, Report(100)] under the RHP and a pre-insert of Report(200), the handler returns [Inbox, Report(200)] — Report(100) is gone.

    What changes do you think we should make in order to solve the problem?

    In getTargetTabRoute, preserve the tab's entire existing nested stack beneath the incoming screen instead of only its first route. The WORKSPACE_NAVIGATOR branch (which deliberately seeds a fresh WORKSPACES_LIST to avoid the reveal flash from #90985) stays exactly as it is:

    let backTargetRoutes: NavigationPartialRoute[] | undefined;
    if (focusedTargetTab.name === NAVIGATORS.WORKSPACE_NAVIGATOR) {
        // ... existing fresh WORKSPACES_LIST seeding, unchanged ...
        backTargetRoutes = [{name: SCREENS.WORKSPACES_LIST, ...(existingListParams ? {params: existingListParams} : {})}];
    } else if (existingNestedRoutes?.length) {
        // Preserve the tab's entire existing stack (sidebar + any open screens) beneath the incoming
        // screen so back navigation returns to the screen the user was on, not the sidebar.
        // Drop a trailing route that duplicates the incoming screen (same name and reportID) so a
        // repeated pre-insert of the same destination stays idempotent.
        const lastExistingRoute = existingNestedRoutes.at(-1);
        const isTrailingDuplicateOfIncoming =
            lastExistingRoute?.name === newFirstRoute?.name &&
            (lastExistingRoute?.params as {reportID?: string} | undefined)?.reportID === (newFirstRoute?.params as {reportID?: string} | undefined)?.reportID;
        backTargetRoutes = isTrailingDuplicateOfIncoming ? existingNestedRoutes.slice(0, -1) : [...existingNestedRoutes];
    } else {
        backTargetRoutes = defaultSidebarRouteName ? [{name: defaultSidebarRouteName}] : undefined;
    }
    if (backTargetRoutes?.length && newFirstRoute && backTargetRoutes.at(0)?.name !== newFirstRoute.name) {
        const prependedRoutes = [...backTargetRoutes, ...(newNestedRoutes ?? [])];
        mergedNestedState = {...focusedTargetTab.state, routes: prependedRoutes, index: prependedRoutes.length - 1};
    }

    With this change my repro test produces [Inbox, Report(selfDM), Report(workspaceChat)] — back returns to the self-DM, matching what the non-pre-insert dismiss path already does. All 8 existing handleReplaceFullscreenUnderRHP tests (the workspace-navigator seeding suite) still pass, and the cancel path is unaffected because preInsertedOriginalTabRoute already saves and restores the full original tab route.

    This also fixes the same history loss for any other flow that pre-inserts a fullscreen while a report is open, not just the track-expense whisper.

    What specific scenarios should we cover in automated tests to prevent reintroducing this issue?

    Unit tests in tests/unit/Navigation/createRootStackNavigator/handleReplaceFullscreenUnderRHP.test.ts:

    1. Pre-inserting a workspace chat while another report (self-DM) is open in the Reports split keeps the previous report in the stack: [Inbox, Report(A)] + pre-insert Report(B) results in [Inbox, Report(A), Report(B)] with the index on Report(B).
    2. Re-dispatching the same pre-insert (the hook can recompute while the RHP is open) is idempotent — no duplicate Report(B) entries.
    3. The existing WORKSPACE_NAVIGATOR seeding scenarios ([Due for payment 2026-07-13] [$250] Workspace - When new workspace is created on narrow screen, previous page is briefly visible #90985, [Due for payment 2026-07-13] iOS - Workspace - Unable to swipe back to return to previous page after creating a workspace #93003) are unchanged.
    4. Cancelling the confirmation restores the original tab route (self-DM back on top).

    What alternative solutions did you explore? (Optional)

    • Gate the report pre-insert in getSubmitExpensePreMountDestinationRoute when a different report is already topmost, falling back to the plain dismissModalWithReport path (which pushes and preserves history). This fixes the bug but disables the flash-free reveal optimization for exactly the flow it was built for, and leaves the underlying history loss in the handler for other callers.
    • Route same-tab report pre-inserts through the handler's non-tab push branch instead of the tab rebuild. This preserves history too, but bypasses the remount machinery (markFocusedTabRouteForRemount) that prevents the push-transition flash during the reveal, so the rebuild-with-full-history above is the safer shape.
  12. oqildev commented on Aug 11, 2026

    @oqildev
    Contributor

    🚨 Edited by proposal-police: This proposal was edited at 2026-08-12 06:49:27 UTC.

    Proposal

    On the earlier withdrawal: my first proposal was auto-withdrawn against #issuecomment-5259758616. That one puts the cause in linkTo's fullscreen alignment and fixes it by threading backTo into dismissModalWithReport. It can't work on this flow: once a route is pre-inserted, submitWithDismissFirst takes its pre-inserted branch — clearFullscreenPreInsertedFlag(), a bare dismissModal(), executeWrite({shouldHandleNavigation: false}) (submitWithDismissFirst.ts#L69-L76) — so dismissModalWithReport never runs and no backTo is read. Different file, different layer. Reposting with the four questions answered and the change implemented.

    What is the root cause of that problem?

    getTargetTabRoute keeps exactly one route from the tab it is rebuilding — existingNestedRoutes.at(0), the sidebar — and throws away everything above it. The report the user is standing on is dropped while the RHP is still open, before Back is ever pressed.

    Chain, all narrow layout:

    1. On the confirmation step the destination is pre-mounted behind the RHP. Every term of the gate is true for this repro — narrow, isReportTopmostSplitNavigator(), destination ≠ topmost report, destination loaded — so it returns the workspace chat route: getSubmitExpensePreMountDestinationRoute.ts#L44, #L68-L92.
    2. The hook dispatches REPLACE_FULLSCREEN_UNDER_RHP (usePreMountDestination#L63 → Navigation.ts#L1116). A report's outermost fullscreen is the TAB_NAVIGATOR, so the handler takes its tab branch and re-derives the reports tab.
    3. The defect is getTargetTabRoute#L212-L237. The only live route carried over is existingNestedRoutes?.at(0)#L216, used as the single backTargetRoute and prepended to the incoming routes at #L234-L237. Live stack [Inbox, Report(selfDM)] becomes [Inbox, Report(workspace)].

    The prepend at L234 does fire here, and I want to correct a claim on the thread that it doesn't: the reports split declares no initialRouteName in the linking config, so getStateFromPath('/r/<id>') resolves the split's nested routes to [Report] alone — no sidebar. I ran it: the split state comes back as {routes: [{name: 'Report', params: {reportID}}]}. So newFirstRoute is Report, backTargetRoute is Inbox, the names differ, the branch runs, and the result is [Inbox, Report(workspace)]. Both readings lose the self DM; only the mechanism differs, and getting it right matters because a fix that dedupes by route name will silently collapse two SCREENS.REPORT entries into one.

    The assumption baked into getTargetTabRoute is that a tab's stack is one screen deep. True for LHN and global-create entry points, false when the RHP flow was launched from a report.

    Scope is wider than the OP:

    • Not iOS-only. The gate is getIsNarrowLayout() with no platform branch, so Android app and narrow mWeb reproduce; wide does not, because preInsertFullscreenUnderRHP returns early there.
    • Not only Submit. Categorize and Share set isMovingTransactionFromTrackExpense and share this confirmation step.
    • Skip-confirmation too. getSkipConfirmationPreMountDestinationRoute pre-inserts a report with no topmost-report gate at all, from scan/distance/amount. Same collapse, same line — and it can pre-insert the report already on top, which the fix has to tolerate.

    What changes do you think we should make in order to solve the problem?

    Fix layer: the navigation state reducer. The stack is already malformed while the RHP is open, so nothing downstream can repair it — and both entry points, the eager preInsertFullscreenUnderRHP#L1116 and the submit-time revealRouteBeswitchDismissingModal#L1077, dispatch the same action, so one change covers every caller.

    In getTargetTabRoute, when the incoming report lands in a reports split the user is currently standing in, keep that split's stack and put the destination on top of it instead of collapsing to the sidebar. Roughly:

    preserved = existingRoutes.slice(0, focusedIndex + 1)   // keyless on the focused route
    routes    = [...preserved, ...incomingRoutes]           // deduped by name + reportID
    

    Answering the four questions in order:

    1. Preserve the whole stack up to the focused route, not the focused route alone. The sidebar has to stay at index 0 or the split navigator has nothing in its left pane, so "focused route only" isn't available. Anything above the focused index is forward history the user already popped past — resurrecting it would be a second bug. slice(0, focusedIndex + 1) is the correct window: [Inbox, Report(selfDM)] here, and [Inbox, Report(A)] (not [Inbox, A, B]) when the user had opened B and gone back to A.

    2. Scope it to REPORTS_SPLIT_NAVIGATOR — and additionally to the case where that tab is the focused one. This second condition is the part I think the other proposals miss, and it's load-bearing. getTabStateWithExistingFocusedTarget#L256-L269 locates the target tab by name at #L257, so existingTabRoute is the reports tab's own route whether or not the user is looking at it — which means a guard like existingTabRoute.name === focusedTargetTab.name is always true and buys nothing. Without a real check, revealing a report while the user sits on Search would resurrect whatever report the reports tab happened to hold from earlier in the session, and Back would land there instead of the Inbox. The gate is targetTabIndex === existingTabState.index; pass it down as a parameter. WORKSPACE_NAVIGATOR keeps its keyless WORKSPACES_LIST seeding untouched, and HOME, SEARCH_FULLSCREEN_NAVIGATOR and SETTINGS_SPLIT_NAVIGATOR fall through to today's code unchanged. Depth-1 stacks also fall through, since at(0) and the preserved slice produce the same thing there.

    3. The cancel path needs nothing. preInsertedOriginalTabRoute is captured from the pre-change route at #L456-L459, before the splice, and handleRemoveFullscreenUnderRHP#L519-L530 restores it wholesale. The change only touches the derived route. Backing out lands on the self DM with its original keys — covered by a test.

    4. Yes, born-non-top — but only for the route that actually moves. The self DM is the mounted, visible top when the pre-insert fires, and it becomes non-top. Carrying it over with its key is exactly the #90985 shape that the WORKSPACES_LIST seeding at #L222-L230 already documents: react-native-screens reorders it top→non-top and flashes it. So it is carried keyless. This costs nothing, because markFocusedTabRouteForRemount#L376-L391 already strips the split's own key and remounts it either way — keeping the report's key would buy no preserved screen state, only the flash risk. I'd stop there rather than stripping the whole preserved prefix: the sidebar is non-top already, nothing reorders it, and today's code prepends it with its key and params intact. Keeping that identical keeps the behavioral delta to exactly one route.

    One more detail that a straightforward implementation gets wrong: the tail must be deduped against the incoming route by name and reportID. Both are SCREENS.REPORT, so a name-only compare treats the self DM and the workspace chat as the same screen and drops one of them. Identity dedupe is also what makes a re-dispatched pre-insert idempotent, and what keeps the skip-confirmation callers — which can pre-insert the report already on top — from stacking it twice.

    Tests. handleReplaceFullscreenUnderRHP.test.ts covers only WORKSPACE_NAVIGATOR today. I've implemented the change and added:

    1. [Inbox, Report(A)] + pre-insert Report(B) → [Inbox, Report(A), Report(B)], index on B. Fails on main.
    2. The preserved Report(A) comes back keyless while the sidebar keeps its key — locks in Q4.
    3. Re-dispatching the same destination is idempotent. Fails on main.
    4. Pre-inserting the report already on top does not stack it twice (skip-confirmation shape).
    5. Forward history above the focused route is dropped: [Inbox, A, B] focused on A + C → [Inbox, A, C]. Fails on main.
    6. Cancel restores [Inbox, Report(A)] with its original keys via preInsertedOriginalTabRoute. Fails on main.
    7. Reports tab not focused → unchanged, Back still goes to the Inbox.
    8. Depth-1 and the settings split are unchanged; the existing WORKSPACE_NAVIGATOR suite still passes.

    Plus one in tests/navigation/handleReplaceFullscreenUnderRHPTests.ts, which runs the real StackRouter rather than a stub, asserting the same stack survives root rehydration with the RHP still on top.

    All 8 existing workspace tests, getTabStateWithFocusedTargetTest, usePreMountDestinationTest and IOURequestStepConfirmationPageTest pass unchanged. Happy to put the branch up with before/after recordings on iOS Simulator and an Android emulator.

    Also worth correcting on the issue itself: the Platforms checklist should include Android app and narrow mWeb.

    What alternative solutions did you explore? (Optional)

    1. Thread the origin report as backTo through dismissModalWithReport. Rejected: that path is bypassed once a route is pre-inserted (submitWithDismissFirst.ts#L69-L76), so nothing would read the param. It also only fixes the header button, not the hardware/system back.
    2. Gate the report pre-insert off when the topmost report differs from the destination. Produces the right stack, but that condition is precisely when getSubmitExpensePreMountDestinationRoute is meant to fire, so it disables the instant reveal for its primary case — and it leaves the three skip-confirmation call sites broken.
    3. Add a PUSH_FULLSCREEN_UNDER_RHP action and opt into it from the submit flow. Same end state, but it adds an action type, a hook option and a call-site branch, and has to re-implement the remount handling the replace handler already owns. The collapse is a bug in the existing builder, not a missing mode.
    4. Add a shouldPreserveReportStack flag threaded from the confirmation screen through the hook and the action payload. Works for this entry point, but it makes correct back behavior opt-in: every current and future pre-insert caller that forgets the flag — starting with the three skip-confirmation ones — keeps the broken stack. The reducer should be right by default.
    5. Preserve the stack for every tab, not just the reports split. Rejected as unnecessary risk: Search pre-inserts always parse to a state that already starts with SEARCH_ROOT, so a blanket rule needs extra dedupe there for no user-visible gain, and the other tabs don't hold a mid-stack screen Back is expected to reach.
  13. 56 remaining items

  14. NEXAITECHAU commented on Aug 30, 2026

    @NEXAITECHAU

    /bounty claim

    Claiming. NEX Agent Co. working on this — drafting a PR now.

    This is an automated claim by an AI agent (NEX Agent Co.). Wallet: 0xf47bdf9add81ff2130509ec564d3387072ba5726 (Base USDC).

  15. melvin-bot commented on Aug 30, 2026

    @melvin-bot

    📣 @NEXAITECHAU! 📣
    Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
    Please follow these steps:

    1. Make sure you've read and understood the contributing guidelines.
    2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
    3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  16. added
    Awaiting PaymentAuto-added when associated PR is deployed to production
    and removed on Sep 1, 2026
  17. melvin-bot commented on Sep 1, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  18. melvin-bot commented on Sep 1, 2026

    @melvin-bot

    Payment Summary

    Resolving PRs:

    Upwork Job

    BugZero Checklist (@mallenexpensify)

    • I have confirmed assignees, roles, and Upwork contracts look correct
    • I have paid out Upwork contracts / manual NewDot requests
    • [BugZero Assignee] I have created a GH issue for creating/updating the regression test once above steps have been agreed upon
  19. mallenexpensify commented on Sep 3, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor: @nabi-ebrahimi paid $250 via Upwork
    Contributor+: @ikevin127 due $250 via NewDot

    @abbasifaizan70 paid, summary updated above. Thx.
    https://www.upwork.com/jobs/~022086544859815465151

  20. nabi-ebrahimi commented on Sep 4, 2026

    @nabi-ebrahimi
    Contributor

    @mallenexpensify, Accepted the offer. thanks.

  21. mallenexpensify commented on Sep 4, 2026

    @mallenexpensify
    Contributor

    @nabi-ebrahimi paid, summary updated above. Thx.

  22. quinthar commented on Sep 12, 2026

    @quinthar
    Contributor

    Approved $250 to @ikevin127

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions