Skip to content

[$250] Android/iOS-Offline-Navigation arrows disappear after deleted expense syncs online #103403

Open

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: 9.5.5-0
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: #103109
Email or phone of affected tester (no customers): [email protected]
Issue reported by: Applause Internal Team
Bug source: Pull Request QA execution
Device used: Samsung Galaxy S21/Android 15, iPhone 13/18.6.2
App Component: Other

Action Performed:

  1. Launch Expensify app
  2. Log in with valid credentials
  3. Open Home and make sure the Recently added section shows at least 3 expenses.
  4. Open dev tools and turn on Force offline.
  5. Open the first (newest) expense, then select More > Delete.
  6. Go back to Home and open the expense right under the deleted one.
  7. Turn off Force offline and wait for the delete to sync, keeping the expense open.

Expected Result:

The deleted expense is removed from Recently added, and the navigation arrows remain visible and allow navigation between expenses that were not deleted.

Actual Result:

After the delete syncs while the expense is open, the navigation arrows disappear.

Workaround:

Unknown

Platforms:

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

Screenshots/Videos

Bug7280546_1791407123287.6-and.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Mita Rachim
Bug ID: 7280546

Upwork Automation - Do Not Edit

Activity

  1. applause-bot commented on Oct 8, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/991fcc884615f7f34162ec2bb59523f8cd4800b9fe29cc80ee67921d351a60e3

  2. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 8, 2026
  3. MelvinBot commented on Oct 8, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    The header remounts while the Test Tools modal covers the expense, and its unmount cleanup wipes the arrow list. The delete sync only coincides with this. Keep pending-delete expenses out of the Recently added carousel did not cause it, and the issue already reproduces in production.

    1. On native, deleting expense A leaves nvp_deleteTransactionNavigateBackURL set. The native stack unmounts A's screen right away, which cancels the pending clear in DeleteTransactionNavigateBackHandler.tsx:22-37.
    2. You open expense B, then open Test Tools to turn off Force offline. B loses focus, so B's own handler clears the URL.
    3. ReportNotFoundGuard.tsx:127-131 switches from bare children to <ReportNotFoundInnerGuard>. React then remounts the whole header.
    4. The old navigation component's cleanup in MoneyRequestReportTransactionsNavigation.tsx:209-217 sees TEST_TOOLS_MODAL.ROOT focused. That screen is not in CAROUSEL_PRESERVING_SCREENS, so the cleanup calls clearActiveTransactionIDs().
    5. The new component reads an empty list and renders no arrows. Nothing seeds the list again.

    Web is not affected because the JS stack keeps the closing screen mounted through the animation, so step 1 clears the URL normally.

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

    In the unmount cleanup of MoneyRequestReportTransactionsNavigation, skip clearActiveTransactionIDs() while the component's own route key (useRoute().key) is still in the navigation state. If the route is still there, the header remounted inside an open screen and the user did not leave. When the user really leaves, the route is gone from the state, so the list still clears. Use extractNavigationKeys from extractNavigationKeys.ts for the check.

    Add a unit test in MoneyRequestReportTransactionsNavigation.test.tsx with a useRoute mock. The test should expect no clear when the route is still in the state while Test Tools is focused.

    What alternative solutions did you explore? (Optional)

    • Stop ReportNotFoundGuard from changing its root element type, for example by always rendering the inner guard with an enabled flag. This prevents the remount, but the inner guard was split out for performance.
    • Clear the URL when the deleted screen leaves the stack on native. This fixes the leak, which also disables the deleted-parent "not found" check on the next expense you open. On its own it does not protect the arrows from other remounts.
    Confidence and verification

    Confidence is medium. This comes from reading the code. It has not been tested on a device, and the web tester can't reproduce a native-only bug. Two predictions would confirm it on Android/iOS:

    1. After steps 1–6, opening and closing Test Tools without going online also hides the arrows.
    2. Going online with airplane mode instead of Test Tools keeps the arrows.

    The hook itself still returns the right siblings after the sync (useCarouselTransactionIDs.ts:26-47). The list goes empty only because the stored list was cleared.


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


    view run

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 8, 2026
  5. melvin-bot commented on Oct 8, 2026

    @melvin-bot

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

  6. changed the title [-]Android/iOS-Offline-Navigation arrows disappear after deleted expense syncs online[/-] [+][$175] Android/iOS-Offline-Navigation arrows disappear after deleted expense syncs online[/+] on Oct 8, 2026
  7. melvin-bot commented on Oct 8, 2026

    @melvin-bot
  8. jloa-dev commented on Oct 8, 2026

    @jloa-dev

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~0163955a2ba22190aa

    Proposal

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

    In Android/iOS, after an expense is deleted while offline, opening an adjacent expense from the carousel and waiting for the offline delete to sync causes the navigation arrows in the header (MoneyRequestReportTransactionsNavigation) to disappear.

    What is the root cause of that problem?

    1. Unnecessary Component Remounting in ReportNotFoundGuard.tsx:
      In src/pages/inbox/ReportNotFoundGuard.tsx:127-131:

      if (!deleteTransactionNavigateBackUrl && isReportTransactionThread(report)) {
          return <ReportNotFoundInnerGuard reportIDFromPath={routeParams?.reportID}>{children}</ReportNotFoundInnerGuard>;
      }
      
      return children;

      When an expense is deleted offline, deleteTransactionNavigateBackUrl is set in Onyx (ONYXKEYS.NVP_DELETE_TRANSACTION_NAVIGATE_BACK_URL). While it is set, ReportNotFoundGuard renders {children} directly. When the delete syncs and DeleteTransactionNavigateBackHandler clears deleteTransactionNavigateBackUrl, ReportNotFoundGuard switches from bare {children} to <ReportNotFoundInnerGuard>{children}</ReportNotFoundInnerGuard>. Because the element type changes at the guard root, React unmounts the entire children tree (including MoneyRequestHeader and MoneyRequestReportTransactionsNavigation) and mounts a new one.

    2. Premature Clearing in MoneyRequestReportTransactionsNavigation.tsx:
      In src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx:209-217:

      useEffect(() => {
          return () => {
              const focusedRoute = findFocusedRoute(navigationRef.getRootState());
              if (focusedRoute?.name && (CAROUSEL_PRESERVING_SCREENS as readonly string[]).includes(focusedRoute.name)) {
                  return;
              }
              clearActiveTransactionIDs();
          };
      }, []);

      When the unmount cleanup fires:

      • If the user has opened the Test Tools modal to toggle offline mode, findFocusedRoute returns TEST_TOOLS_MODAL.ROOT (or SCREENS.RIGHT_MODAL.TEST_TOOLS), which is not in CAROUSEL_PRESERVING_SCREENS.
      • Checking only focusedRoute.name fails to distinguish between the user actually navigating away from the screen versus a modal being focused or an internal remount where the screen route is still intact in the navigation state.
      • Consequently, clearActiveTransactionIDs() is called, wiping ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS. When the re-mounted MoneyRequestReportTransactionsNavigation evaluates useCarouselTransactionIDs(), transactionIDsList is empty, causing lines 426-429 to return early and hide the arrows.

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

    1. Stabilize ReportNotFoundGuard.tsx to prevent tree remounts:
      Instead of conditionally swapping between {children} and <ReportNotFoundInnerGuard>{children}</ReportNotFoundInnerGuard>, always render <ReportNotFoundInnerGuard> for transaction threads and pass a condition (or check !deleteTransactionNavigateBackUrl internally in ReportNotFoundInnerGuard before determining shouldShowNotFoundPage):

      diff --git a/src/pages/inbox/ReportNotFoundGuard.tsx b/src/pages/inbox/ReportNotFoundGuard.tsx
      --- a/src/pages/inbox/ReportNotFoundGuard.tsx
      +++ b/src/pages/inbox/ReportNotFoundGuard.tsx
      @@ -127,5 +127,9 @@ function ReportNotFoundGuard({children}: ReportNotFoundGuardProps) {
      -    if (!deleteTransactionNavigateBackUrl && isReportTransactionThread(report)) {
      -        return <ReportNotFoundInnerGuard reportIDFromPath={routeParams?.reportID}>{children}</ReportNotFoundInnerGuard>;
      +    if (isReportTransactionThread(report)) {
      +        return (
      +            <ReportNotFoundInnerGuard
      +                reportIDFromPath={routeParams?.reportID}
      +                shouldCheckParent={!deleteTransactionNavigateBackUrl}
      +            >
      +                {children}
      +            </ReportNotFoundInnerGuard>
      +        );
           }

      In ReportNotFoundInnerGuard, if shouldCheckParent is false, set shouldShowNotFoundPage = false. This guarantees the component tree never unmounts due to deleteTransactionNavigateBackUrl updates.

    2. Harden MoneyRequestReportTransactionsNavigation.tsx unmount cleanup:
      In src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx, check whether the component's route key (useRoute().key) is still in the navigation state before clearing:

      const route = useRoute();
      useEffect(() => {
          return () => {
              const rootState = navigationRef.getRootState();
              const activeKeys = extractNavigationKeys(rootState);
              if (activeKeys.includes(route.key)) {
                  return;
              }
              const focusedRoute = findFocusedRoute(rootState);
              if (focusedRoute?.name && (CAROUSEL_PRESERVING_SCREENS as readonly string[]).includes(focusedRoute.name)) {
                  return;
              }
              clearActiveTransactionIDs();
          };
      }, [route.key]);

      Additionally, include SCREENS.RIGHT_MODAL.TEST_TOOLS in CAROUSEL_PRESERVING_SCREENS.

    What alternative solutions did you explore? (Optional)

    • Only updating CAROUSEL_PRESERVING_SCREENS: Does not solve cases where other overlays or transient state transitions cause a remount while the screen route remains alive. Combining route key verification with ReportNotFoundGuard stabilization provides complete stability across both native and web platforms.

    What specific scenarios should we cover in automated tests?

    1. In tests/unit/components/MoneyRequestReportTransactionsNavigation.test.tsx:
      • Verify that when MoneyRequestReportTransactionsNavigation unmounts while its route key is still in navigation state (or while a modal is focused), clearActiveTransactionIDs is not called.
    2. In tests/unit/ReportNotFoundGuardTest.tsx:
      • Verify that toggling deleteTransactionNavigateBackUrl does not cause children to unmount and remount.
  9. melvin-bot commented on Oct 8, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  10. ashusatyarthi-wq commented on Oct 8, 2026

    @ashusatyarthi-wq

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~010e53044b5828fe60

    Proposal

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

    In Android/iOS, after an expense is deleted while offline, opening an adjacent expense from the carousel and waiting for the offline delete to sync causes the navigation arrows in the header (MoneyRequestReportTransactionsNavigation) to disappear.

    What is the root cause of that problem?

    The bug is caused by a race condition between a component tree remount in ReportNotFoundGuard and premature cache clearing in MoneyRequestReportTransactionsNavigation:

    1. Tree Remount in ReportNotFoundGuard.tsx:
      In src/pages/inbox/ReportNotFoundGuard.tsx:127-131:

      if (!deleteTransactionNavigateBackUrl && isReportTransactionThread(report)) {
          return <ReportNotFoundInnerGuard reportIDFromPath={routeParams?.reportID}>{children}</ReportNotFoundInnerGuard>;
      }
      
      return children;

      On native platforms, deleting an expense leaves deleteTransactionNavigateBackUrl set in Onyx because the native stack immediately unmounts the deleted expense screen, aborting the pending clear in DeleteTransactionNavigateBackHandler.
      When the user opens a sibling expense from the carousel, deleteTransactionNavigateBackUrl is initially truthy, so ReportNotFoundGuard renders {children} directly. When the user opens Test Tools or the offline action syncs, DeleteTransactionNavigateBackHandler clears deleteTransactionNavigateBackUrl. This causes ReportNotFoundGuard to switch from bare {children} to <ReportNotFoundInnerGuard>{children}</ReportNotFoundInnerGuard>. Because the root wrapper component type changes, React completely unmounts the existing {children} tree (including MoneyRequestHeader and MoneyRequestReportTransactionsNavigation) and mounts a new one.

    2. Premature Cache Clearing in MoneyRequestReportTransactionsNavigation.tsx:
      In src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx:209-217:

      useEffect(() => {
          return () => {
              const focusedRoute = findFocusedRoute(navigationRef.getRootState());
              if (focusedRoute?.name && (CAROUSEL_PRESERVING_SCREENS as readonly string[]).includes(focusedRoute.name)) {
                  return;
              }
              clearActiveTransactionIDs();
          };
      }, []);

      When the unmount cleanup fires during the tree remount:

      • findFocusedRoute inspects the top of the stack. If Test Tools or another modal is open, its name is not in CAROUSEL_PRESERVING_SCREENS.
      • The cleanup assumes the user navigated away from the screen entirely and calls clearActiveTransactionIDs(), wiping ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS.
      • When the re-mounted MoneyRequestReportTransactionsNavigation evaluates useCarouselTransactionIDs(), the stored IDs list is empty, causing the component to return early and hide the arrows.

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

    We should apply a two-layer fix to eliminate both the spurious tree remount and the unsafe unmount cleanup:

    1. In src/pages/inbox/ReportNotFoundGuard.tsx:
      Keep the component structure stable so deleteTransactionNavigateBackUrl changes do not unmount and remount {children}:

      if (isReportTransactionThread(report)) {
          return (
              <ReportNotFoundInnerGuard
                  reportIDFromPath={routeParams?.reportID}
                  shouldCheck={!deleteTransactionNavigateBackUrl}
              >
                  {children}
              </ReportNotFoundInnerGuard>
          );
      }
      
      return children;

      Inside ReportNotFoundInnerGuard, if shouldCheck is false, bypass the parent report action check (setting shouldShowNotFoundPage = false). This guarantees {children} remains mounted without unmounting when deleteTransactionNavigateBackUrl updates.

    2. In src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx:
      Harden the unmount cleanup so it verifies whether the screen's route key is still in the active navigation state using extractNavigationKeys:

      const route = useRoute();
      useEffect(() => {
          return () => {
              const rootState = navigationRef.getRootState();
              const activeKeys = extractNavigationKeys(rootState?.routes);
              // If the route key is still present in the navigation hierarchy, the screen is still active (remount or modal overlay)
              if (activeKeys.has(route.key)) {
                  return;
              }
      
              const focusedRoute = findFocusedRoute(rootState);
              if (focusedRoute?.name && (CAROUSEL_PRESERVING_SCREENS as readonly string[]).includes(focusedRoute.name)) {
                  return;
              }
              clearActiveTransactionIDs();
          };
      }, [route.key]);

    What specific scenarios should we cover in automated tests?

    1. In tests/unit/ReportNotFoundGuardTest.tsx:
      • Verify that updating deleteTransactionNavigateBackUrl from a URL to null does not trigger an unmount/remount of children.
    2. In tests/unit/components/MoneyRequestReportTransactionsNavigation.test.tsx:
      • Verify that when MoneyRequestReportTransactionsNavigation unmounts while its route key exists in extractNavigationKeys, clearActiveTransactionIDs is not invoked.
      • Verify that when the route key is removed from navigation state, clearActiveTransactionIDs is called as expected.
  11. melvin-bot commented on Oct 8, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  12. bonnhatnguyen commented on Oct 8, 2026

    @bonnhatnguyen

    Proposal

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

    On Android/iOS, when an expense is deleted while offline and the user opens an adjacent expense from the carousel, waiting for the delete to sync online causes the header navigation arrows to disappear.

    What is the root cause of that problem?

    The root cause consists of two intertwined issues in ReportNotFoundGuard and MoneyRequestReportTransactionsNavigation:

    1. Unnecessary Component Remounting in ReportNotFoundGuard.tsx:
      ReportNotFoundGuard conditionally wraps {children} in <ReportNotFoundInnerGuard> only when !deleteTransactionNavigateBackUrl && isReportTransactionThread(report). When deleteTransactionNavigateBackUrl is present, it renders bare {children}.
      When an offline delete finishes syncing and clears deleteTransactionNavigateBackUrl, the JSX tree switches component types from {children} to <ReportNotFoundInnerGuard>{children}</ReportNotFoundInnerGuard>. In React's reconciliation engine, changing the wrapper component type destroys and remounts the entire subtree, unmounting MoneyRequestReportTransactionsNavigation.

    2. Premature Cache Eviction in MoneyRequestReportTransactionsNavigation.tsx:
      During unmount, the cleanup effect in MoneyRequestReportTransactionsNavigation checks findFocusedRoute. If a modal (such as Test Tools or offline sync overlay) is active, or during this transient remount, it concludes navigation has exited the screen and unconditionally invokes clearActiveTransactionIDs(), wiping ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS. When the component remounts, the carousel transaction ID list in Onyx is empty, causing the component to render nothing and hiding the navigation arrows.

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

    1. In src/pages/inbox/ReportNotFoundGuard.tsx:
      Stabilize the component hierarchy so that updating deleteTransactionNavigateBackUrl does not alter the wrapper component structure. Always render ReportNotFoundInnerGuard for transaction threads and pass a boolean flag (e.g. shouldCheckParent={!deleteTransactionNavigateBackUrl}) into ReportNotFoundInnerGuard. When shouldCheckParent is false, bypass the parent report action check (setting shouldShowNotFoundPage = false). This prevents React from unmounting {children} when deleteTransactionNavigateBackUrl updates.

    2. In src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx:
      Harden the unmount cleanup hook so it does not clear transaction IDs when the screen remains active in navigation state. Check whether the current screen's route key (useRoute().key) still exists within navigation state (navigationRef.getRootState()) using extractNavigationKeys. Only execute clearActiveTransactionIDs() when the route has truly been popped from navigation.

    What alternative solutions did you explore? (Optional)

    • Only updating CAROUSEL_PRESERVING_SCREENS: Adding individual modal screens does not prevent the full unmount/remount cycle in ReportNotFoundGuard, leaving other overlays vulnerable to arrow loss. Stabilizing the component hierarchy at the source is the cleaner and more robust fix.

    What specific scenarios should we cover in automated tests?

    1. In tests/unit/ReportNotFoundGuardTest.tsx:
    • Verify that transitioning deleteTransactionNavigateBackUrl from a string to undefined/null maintains component stability without unmounting children.
    1. In tests/unit/components/MoneyRequestReportTransactionsNavigation.test.tsx:
    • Verify that unmounting while the route key is still in navigation state does not call clearActiveTransactionIDs.
    • Verify that navigating away (route key removed from navigation state) correctly calls clearActiveTransactionIDs.

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~0139ed4098bdc50182

  13. 24 remaining items

  14. trasnake87 commented on Oct 19, 2026

    @trasnake87
    Contributor

    Proposal

    What is the root cause of that problem?

    Reading this as a Test Tools problem - and fixing it by adding the Test Tools screens to CAROUSEL_PRESERVING_SCREENS - leaves the bug live. Nothing in the chain depends on which screen gets pushed, only that something is pushed over the open expense while the delete-back NVP is still set. Two checkable consequences: with the NVP leaked, opening expense B's Description row (Dynamic_Money_Request_Step_Description) or tapping its receipt (ReportAttachments) wipes the arrows the same way, and neither is - or should become - an allowlist entry; and no later carousel overwrites the wiped list, because nothing reseeds it while the user stays on B (step 4).

    1. Deleting expense A offline sets the global nvp_deleteTransactionNavigateBackURL. The handler that clears it only schedules the clear while its own screen is blurred, and cancels the pending clear on unmount - so A's screen being destroyed on the way back to Home leaves the NVP set:

    useEffect(() => {
    if (isFocused || !deleteTransactionNavigateBackUrl) {
    return;
    }
    if (doesDeleteNavigateBackUrlIncludeDuplicatesReview(deleteTransactionNavigateBackUrl)) {
    return;
    }
    // Clear the URL only after we navigate away to avoid a brief Not Found flash.
    const handle = TransitionTracker.runAfterTransitions({
    callback: () => {
    requestAnimationFrame(clearDeleteTransactionNavigateBackUrl);
    },
    waitForUpcomingTransition: true,
    });
    return () => handle.cancel();
    }, [isFocused, deleteTransactionNavigateBackUrl]);

    1. Expense B is then opened from Home with that NVP still set, so ReportNotFoundGuard returns bare children. When B blurs under the pushed route, B's own handler clears the NVP and the guard swaps to <ReportNotFoundInnerGuard>{children}</ReportNotFoundInnerGuard>. The element type at that position changes, so React discards the entire report subtree - MoneyRequestHeader and its carousel with it - and mounts a fresh one:

    if (!deleteTransactionNavigateBackUrl && isReportTransactionThread(report)) {
    return <ReportNotFoundInnerGuard reportIDFromPath={routeParams?.reportID}>{children}</ReportNotFoundInnerGuard>;
    }
    return children;

    1. The old carousel's unmount cleanup then asks the wrong question - it reads the globally focused route name:

    useEffect(() => {
    return () => {
    const focusedRoute = findFocusedRoute(navigationRef.getRootState());
    if (focusedRoute?.name && (CAROUSEL_PRESERVING_SCREENS as readonly string[]).includes(focusedRoute.name)) {
    return;
    }
    clearActiveTransactionIDs();
    };
    }, []);

    CAROUSEL_PRESERVING_SCREENS was built for the pop case the unit test documents (#90366): this route is gone and a screen below it in the RHP stack is now focused. The remount is a different event - B's route is still in the navigation state and still the screen the user is looking at, only its React subtree was replaced. A focused-route name cannot separate the two, so the cleanup runs clearActiveTransactionIDs() and wipes the list Home seeded.

    1. Nothing puts it back. RecentlyAddedSection writes the carousel only inside its row-press handler, so there is no effect to re-run when the delete later syncs:

    setActiveTransactionIDs(siblingTransactionIDs, {source: CAROUSEL_SOURCE.homeRecentlyAdded, descriptors: siblingDescriptorsByTransactionID}).then(() => {
    markReportRHPWidth(reportID, 'wide');
    // The anchor is what lets the header show the carousel on a cold open: getReportIDToOpenForExpense
    // resolves the snapshot's childReportID without materializing the thread, so the header has no
    // transaction of its own until OpenReport round-trips, and the counter would pop in only after that.
    Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, backTo: ROUTES.HOME, anchorTransactionID: expense.transactionID}));

    To reproduce with no sync and no Test Tools: offline, delete the newest Home expense, open the one under it, then open its Description row.

    Step 1 is a teardown race - the clear is queued behind runAfterTransitions and the unmount cancels it - so whether the NVP leaks at all depends on how fast the closing screen is torn down. That is a stack-dependent outcome, matching the reporter ticking only the two native rows.

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

    Make the cleanup confirm the carousel's own route is actually gone before concluding the user left it. extractNavigationKeys already does the recursive key walk, so this is a short-circuit in front of the existing check, in MoneyRequestReportTransactionsNavigation.tsx:

        const routeKey = useRoute().key;
        ...
        useEffect(() => {
            return () => {
                const rootState = navigationRef.getRootState();
                // An in-place teardown (ReportNotFoundGuard swapping its wrapper when the delete-back URL clears)
                // leaves this route mounted, so the list still belongs to the expense on screen - keep it.
                if (extractNavigationKeys(rootState?.routes).has(routeKey)) {
                    return;
                }
                const focusedRoute = findFocusedRoute(rootState);
                if (focusedRoute?.name && (CAROUSEL_PRESERVING_SCREENS as readonly string[]).includes(focusedRoute.name)) {
                    return;
                }
                clearActiveTransactionIDs();
            };
        }, [routeKey]);

    This widens rather than removes: the pop case drops this route from the state first, so CAROUSEL_PRESERVING_SCREENS stays and keeps covering #90366 - and no new entry is needed per screen that can be pushed over an expense. Keep the clear too - a leaked descriptor-backed carousel would permanently stop a report's own transaction list from seeding its arrows:

    const {ids: activeIDs, descriptors: activeDescriptors, source: activeSource} = getActiveTransactionIDs();
    if (activeDescriptors) {
    return;
    }

    It also matches the writer side of the same module, which already refuses to clear on unmount unless it still owns what it is clearing (clearActiveTransactionIDsForSource) instead of reading the focused route.

    What specific scenarios should we cover in automated tests?

    In the existing clearing the carousel on unmount block of tests/unit/components/MoneyRequestReportTransactionsNavigation.test.tsx: with an unrelated screen focused, unmounting must not call clearActiveTransactionIDs while the component's route key is still present in the mocked root state, and must still call it when that key is absent - the pop case the existing tests cover.

    What alternative solutions did you explore?

    Always mounting ReportNotFoundInnerGuard and gating it with a prop. That removes this one remount trigger, but it needs a new prop just to keep the not-found page suppressed while the delete-back URL is set, pulls the parent-action subscriptions the guard was split to defer into that window, and leaves the cleanup's focused-route heuristic wrong for any other in-place teardown. Less coverage for more surface.

    Test branch: main...trasnake87:App:fix/103403

    Regression test

    tests/unit/components/MoneyRequestReportTransactionsNavigation.test.tsx, run with npx jest tests/unit/components/MoneyRequestReportTransactionsNavigation.test.tsx:

    • without the change: Tests: 1 failed, 30 passed, 31 total
    • with the change: Tests: 31 passed, 31 total
  15. trasnake87 commented on Oct 19, 2026

    @trasnake87
    Contributor

    Proposal updated: added a test branch.

  16. neerajbachani commented on Oct 19, 2026

    @neerajbachani
    Contributor

    Proposal

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

    On Android and iOS, after you delete one expense from Home's Recently added section and open the expense under it, the prev/next arrows disappear the first time anything opens on top of that expense. In the reported steps that something is Test Tools, used to turn Force offline off. The arrows should stay, and they should still move between the expenses that were not deleted. They stay gone until the expense is opened from Home again.

    What is the root cause of that problem?

    The delete sync does not hide the arrows. Opening Test Tools does, and it does it while the app is still offline. The arrows are already gone in the frame the modal starts to open. Going back online only happens to be the next step in the report.

    Home seeds one global sibling list when the row is pressed, then opens the expense:

    setActiveTransactionIDs(siblingTransactionIDs, {source: CAROUSEL_SOURCE.homeRecentlyAdded, descriptors: siblingDescriptorsByTransactionID}).then(() => {
    markReportRHPWidth(reportID, 'wide');
    // The anchor is what lets the header show the carousel on a cold open: getReportIDToOpenForExpense
    // resolves the snapshot's childReportID without materializing the thread, so the header has no
    // transaction of its own until OpenReport round-trips, and the counter would pop in only after that.
    Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, backTo: ROUTES.HOME, anchorTransactionID: expense.transactionID}));

    Nothing else writes that list while you stay on the expense. Filtering out the deleted expense still leaves the other siblings, so two or more entries remain and the arrows should keep rendering:

    const validTransactionIDsSelector = (allTransactions: OnyxCollection<OnyxTypes.Transaction>) =>
    seededTransactionIDs.filter((transactionID) => {
    const key = `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}` as const;
    // A live `null` means the expense was deleted, so don't let the stale snapshot copy bring it back.
    const liveTransaction = allTransactions?.[key];
    if (liveTransaction === null) {
    return false;
    }
    // A sibling that hasn't landed in the live collection yet (snapshot-backed flows) is read from the snapshot instead.
    const transaction = liveTransaction ?? snapshot?.data?.[key];
    if (!transaction) {
    // Nothing to inspect, so keep the ID only while a descriptor still vouches for the sibling.
    return !!siblingDescriptors?.[transactionID];
    }
    return !isTransactionPendingDelete(transaction) && !isDeletedTransaction(transaction);

    The list goes empty because the header remounts and its unmount cleanup wipes it. Four steps:

    1. Deleting an expense sets nvp_deleteTransactionNavigateBackURL, then pops the screen. The only clear waits for that screen to render unfocused, then runs after the transition:

    useEffect(() => {
    if (isFocused || !deleteTransactionNavigateBackUrl) {
    return;
    }
    if (doesDeleteNavigateBackUrlIncludeDuplicatesReview(deleteTransactionNavigateBackUrl)) {
    return;
    }
    // Clear the URL only after we navigate away to avoid a brief Not Found flash.
    const handle = TransitionTracker.runAfterTransitions({
    callback: () => {
    requestAnimationFrame(clearDeleteTransactionNavigateBackUrl);
    },
    waitForUpcomingTransition: true,
    });
    return () => handle.cancel();
    }, [isFocused, deleteTransactionNavigateBackUrl]);

    On native that unfocused render never happens. native-stack 7.19.2 only mounts state.routes (plus preloaded routes), so a popped screen leaves the tree in the same commit that removes it from state. It never renders with isFocused === false, the effect returns on its last focused render, and the URL stays set:

            {state.routes.concat(state.preloadedRoutes).map((route, index) => {
    

    https://github.com/react-navigation/react-navigation/blob/%40react-navigation/native-stack%407.19.2/packages/native-stack/src/views/NativeStackView.native.tsx#L533

    Web is unaffected because the JS stack keeps the closing screen mounted through the animation, so it does render unfocused and the clear runs. Home is not a report screen, so nothing clears the URL there either. The next expense therefore mounts with the URL still set. The same blur effect is copied in SearchMoneyRequestReportPage.tsx (lines 105 to 120), so a delete from the super-wide expense report leaks the same way.

    1. While the URL is set, the open expense's ReportNotFoundGuard returns its children directly. The first time that expense blurs, its own handler clears the URL, and the guard wraps the same children in ReportNotFoundInnerGuard. The element type at that spot changes, so React throws away the whole report subtree, header included, and mounts a new one:

    if (!deleteTransactionNavigateBackUrl && isReportTransactionThread(report)) {
    return <ReportNotFoundInnerGuard reportIDFromPath={routeParams?.reportID}>{children}</ReportNotFoundInnerGuard>;
    }
    return children;

    That condition used to live inside the inner guard, so toggling the URL only changed a boolean. PR #87559's review follow-up (2e33a14422f) moved it onto the outer if so the inner guard would not subscribe while the URL was set. That is what turns a URL change into a remount. Test Tools is just the first blur in this repro. Opening Description, the receipt, or any other screen on top does the same thing.

    1. The old carousel's cleanup treats "I unmounted" as "the user left". It only looks at the focused route name:

    const CAROUSEL_PRESERVING_SCREENS = [
    SCREENS.RIGHT_MODAL.SEARCH_REPORT,
    SCREENS.RIGHT_MODAL.SEARCH_MONEY_REQUEST_REPORT,
    SCREENS.RIGHT_MODAL.EXPENSE_REPORT,
    SCREENS.TRANSACTION_DUPLICATE.DYNAMIC_REVIEW,
    ] as const;

    useEffect(() => {
    return () => {
    const focusedRoute = findFocusedRoute(navigationRef.getRootState());
    if (focusedRoute?.name && (CAROUSEL_PRESERVING_SCREENS as readonly string[]).includes(focusedRoute.name)) {
    return;
    }
    clearActiveTransactionIDs();
    };
    }, []);

    CAROUSEL_PRESERVING_SCREENS was added for the pop case in #90366: this route is gone, and a screen under it in the RHP is now focused, so the list must survive. A remount is a different event. The expense route is still in the stack. With Test Tools open, findFocusedRoute returns TEST_TOOLS_MODAL.ROOT, which is not in the list, so the cleanup calls clearActiveTransactionIDs().

    1. The new header reads the empty list and renders nothing. Only a Home row press seeds the list again, which is why reopening the expense brings the arrows back:

    // Two entries are the minimum for there to be anything to page between, and an anchor that isn't in the list
    // means this expense doesn't belong to the active carousel at all. The list belongs to a screen the user has
    // since left. Showing arrows then would step to an unrelated expense.
    if (transactionIDsList.length < 2 || currentTransactionIndex === -1) {
    return;
    }

    The same component is what MoneyReportHeader renders for a report-level carousel, so a cleared list drops those arrows too.

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

    Make the cleanup check that this screen has actually left before it clears the list. The carousel already sits inside the report screen, and useRouteKey already reads that screen's route key (it returns null outside a navigator, which is what the existing unit tests render):

    /** The current route's key from React Navigation, or null when the consumer isn't inside a navigator. */
    function useRouteKey(): string | null {
    return useContext(NavigationRouteContext)?.key ?? null;
    }

    extractNavigationKeys already walks nested navigators. That matters because the report route sits under RightModalNavigator, with Test Tools as a sibling navigator on top:

    // If the route has a nested state, add its routes to the processing queue
    if (route.state && 'routes' in route.state && Array.isArray(route.state.routes)) {
    routesToProcess.push(...route.state.routes);
    }
    }

    In MoneyRequestReportTransactionsNavigation.tsx, keep the list when that key is still anywhere in the root state. Keep the existing focused-screen check behind it, for #90366, where the route really is gone:

    const routeKey = useRouteKey();
    
    useEffect(() => {
        return () => {
            const rootState = navigationRef.getRootState();
            // A remount in place leaves this route in the stack, so the list still belongs to the expense on screen.
            if (routeKey && extractNavigationKeys(rootState?.routes).has(routeKey)) {
                return;
            }
            const focusedRoute = findFocusedRoute(rootState);
            if (focusedRoute?.name && (CAROUSEL_PRESERVING_SCREENS as readonly string[]).includes(focusedRoute.name)) {
                return;
            }
            clearActiveTransactionIDs();
        };
    }, [routeKey]);

    This covers every screen that can cover the expense, not just Test Tools, and it covers any later remount of this component while the expense is still open. ReportHeader swaps between MoneyRequestHeader and MoneyReportHeader when isTransactionThreadView flips (lines 75 to 89), which unmounts this same component. A focused-route allowlist cannot name all of those.

    A real exit still clears. On both native and web the route is already removed from navigationRef state before this cleanup runs, so the key is absent and the clear still happens. Going back to Home, closing the RHP, and paging to a sibling all behave as they do today. The only new case is an unmount whose own route is still mounted, which today can only be a remount.

    I ran tests/unit/components/MoneyRequestReportTransactionsNavigation.test.tsx against this change. With the new tests and without the change: 1 failed, 31 passed, 32 total. The failing test is the remount under Test Tools, and it fails because clearActiveTransactionIDs is called once. With the change: 32 passed. The existing allowlist tests and the unrelated-screen test still pass, and a new case where Test Tools is focused but the expense route is already gone still clears.

    What alternative solutions did you explore? (Optional)

    • Add TEST_TOOLS_MODAL.ROOT and TEST_TOOLS_MODAL.SERVER to CAROUSEL_PRESERVING_SCREENS. This is the smallest change for the recorded steps, and it is what the updated contributor-plus note asks to focus on. It leaves the bug in place for every other screen pushed over the expense (Description, the receipt, a confirm modal), and for a header remount that lands while any of those is in front. It also keeps the list whenever Test Tools is focused, even if the expense route has really been removed. The route-key check only skips the clear when the screen that owns the carousel is still there.

    • Always render ReportNotFoundInnerGuard for a transaction thread and pass the URL in as a flag, which is how PR [No QA] add two-level gate pattern to ReportNotFoundGuard #87559 gated this before 2e33a14422f. That removes this one remount, and the children stay mounted, so this repro goes away. It does not fix the cleanup, so the next in-place remount under a covering screen wipes the list again. It also mounts the inner guard's parent-report subscriptions for the whole time the URL is set, which is the cost that review change was avoiding. I would rather leave the guard alone.

    • Clear the URL when the deleted screen unmounts on native, reading the key at fire time because the pop can commit before Onyx.set reaches the component. That is a real bug on its own: after any delete on native, no report shows "not found" until some report screen blurs. It removes this repro's trigger. It does not help the next remount of the header under a covering screen, so it is a follow-up rather than this fix.

    • Keep the list whenever any CAROUSEL_PRESERVING_SCREENS name is still somewhere in the navigation tree. A preserving screen left in a background navigator would then block a clear that should happen. Checking this component's own route key does not have that problem.

    What specific scenarios should we cover in automated tests?

    In the existing clearing the carousel on unmount block of tests/unit/components/MoneyRequestReportTransactionsNavigation.test.tsx, with useRouteKey mocked and the root state shaped as RightModalNavigator > SEARCH_REPORT under a focused TestToolsModalNavigator > TEST_TOOLS_MODAL.ROOT:

    • Unmounting while the component's route key is still in that state does not call clearActiveTransactionIDs. This fails on main.
    • Unmounting after that route key has been removed, with Test Tools still focused, does call clearActiveTransactionIDs.
    • The four existing allowlist cases still keep the list, and unmounting onto an unrelated screen still clears it.

    Video demo

    Screen.Recording.2026-10-19.at.11.46.14.AM.online-video-cutter.com.mp4
  17. mukhrr commented on Oct 19, 2026

    @mukhrr
    Contributor

    added before/after videos in proposal

  18. yusufdeveloper2903 commented on Oct 19, 2026

    @yusufdeveloper2903
    Contributor

    @shubham1206agra
    Adding the Android videos for my proposal here: #103403 (comment)

    I reproduced it on Android with the issue's steps. The arrows disappear the moment Test Tools opens over the next expense, while still offline, before the delete syncs.

    Before:

    revert-android-2x.mp4

    After (with the ReportNotFoundGuard change from the proposal), the arrows stay after Test Tools and after opening Description:

    after-android-3x.mp4
  19. lopezpope commented on Oct 19, 2026

    @lopezpope

    Proposal

    Please re-state the problem that we are trying to solve in this issue.
    On Android/iOS offline, when user deletes an expense and then comes online, the navigation arrows (prev/next) in the expense RHP disappear after sync. User cannot navigate between expenses anymore, even though other expenses exist in the report.

    What is the root cause of that problem?
    The navigation arrows are controlled by Onyx key TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS which stores sibling transaction IDs. When expense is deleted offline, the optimistic delete removes ID from list. When it syncs online, the sync re-validates the list but does not restore the navigation context. The shouldUpdateLastSearchParams flag is not persisted, so background calls overwrite navigation context and arrows hide.

    What changes do you think we should make in order to solve the problem?
    Persist lastSearchParams explicitly when user selects an expense report row (onSelectRow) and when arrows prefetch more results (goToNextReport). Ensure offline delete sync calls dismiss and re-set TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS instead of clearing it. In MoneyRequestReportTransactionsNavigation.tsx, add check to keep navigation context if deleted ID was the current one, and re-calculate sibling IDs from reportTransactions after sync.

    What alternative solutions did you explore? (Optional)
    Considered forcing full report re-fetch on delete sync — too heavy. Considered hiding arrows permanently — wrong UX. Persisting params at select and prefetch points is same fix used in PR #88118 for similar arrows disappearing bug, low risk.

  20. open5588 commented on Oct 19, 2026

    @open5588

    Proposal

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

    On Android and iOS, after deleting an expense from Home while offline and opening another expense from the same section, opening any overlay screen (such as Test Tools or an edit step) causes the prev/next navigation arrows in the expense header to disappear.

    What is the root cause of that problem?

    1. When the first expense is deleted offline, nvp_deleteTransactionNavigateBackURL is set. DeleteTransactionNavigateBackHandler (src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx) only schedules clearing that NVP when its screen blurs and cancels the pending clear if the screen unmounts before the callback runs. Because the deleted expense screen unmounts on navigation back to Home, nvp_deleteTransactionNavigateBackURL remains set in Onyx.
    2. When the user opens the next expense from Home (src/pages/home/RecentlyAddedSection/index.tsx), TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS is seeded with the sibling IDs. However, as soon as another screen is pushed on top of that expense while the stale nvp_deleteTransactionNavigateBackURL is still present, the navigation context cleanup triggers and clears TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS, hiding the prev/next arrows.

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

    1. In src/pages/inbox/DeleteTransactionNavigateBackHandler.tsx, ensure clearDeleteTransactionNavigateBackURL() is executed immediately when consuming the back-navigation target or on unmount after the navigation action is dispatched, rather than canceling the cleanup when the deleted report screen unmounts.
    2. In src/components/MoneyRequestReportView/MoneyRequestReportTransactionsNavigation.tsx, keep the remaining valid sibling IDs in TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS when a single deleted transaction ID is removed instead of wiping the entire list.

    What alternative solutions did you explore? (Optional)

    Adding Test Tools routes to CAROUSEL_PRESERVING_SCREENS would only mask the issue for Test Tools while leaving the same bug reproducible when opening Description or attachment modals.


    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~0159c00273dd3bd644

  21. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  22. Mannerless commented on Oct 19, 2026

    @Mannerless

    Proposal

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

    On Android/iOS, after an expense is deleted offline, the prev/next arrows on the next expense disappear as soon as a screen opens over it (Test Tools in the repro), and they don't come back.

    What is the root cause of that problem?

    When the stale delete-back URL clears, ReportNotFoundGuard changes its wrapper, and the header remounts in place. The old MoneyRequestReportTransactionsNavigation unmount effect sees the covering screen focused and calls clearActiveTransactionIDs() right away. Nothing reseeds the list afterward, so an in-place remount is treated as "the user left".

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

    Make the unmount clear deferred and cancellable:

    • On unmount, record the current getActiveTransactionIDs().ids reference and schedule the clear (TransitionTracker.runAfterTransitions / requestAnimationFrame) in a module-level pending handle.
    • A new mount of the component cancels the pending clear.
    • When the clear runs, it only clears if the list is still the same reference (nobody re-seeded it in the meantime), following the identity contract setActiveTransactionIDs already documents. This mirrors the pattern already used in TransactionThreadNavigation.ts.

    Any remount from any trigger keeps the arrows, a real exit still clears, and ReportNotFoundGuard and the allowlist stay as they are.

    What alternative solutions did you explore? (Optional)

    • Route-key check on unmount (depends on nav state at unmount time).
    • Stable guard tree (covers this trigger only).
    • Adding Test Tools to the preserving allowlist (narrow).

    Tests

    In MoneyRequestReportTransactionsNavigation.test.tsx: unmount then remount keeps the IDs; unmount with no remount clears them; a re-seed between unmount and flush is not wiped.

    I'll open a clean PR with these unit tests and before/after videos for Android and iOS. Could I be assigned?


    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~0124daa6abc73d9d18

  23. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  24. nsunilkumarnani-sudo commented on Oct 20, 2026

    @nsunilkumarnani-sudo

    Proposal

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

    After deleting an expense offline from Home, opening the next expense and then toggling offline mode (or opening another overlay like Test Tools) causes the next/prev navigation arrows in the header to disappear.

    What is the root cause of that problem?

    When an expense is deleted offline from Home, nvp_deleteTransactionNavigateBackURL is set to manage back navigation. In DeleteTransactionNavigateBackHandler.tsx (lines 22–37), clearing this key is scheduled inside runAfterTransitions when the screen blurs. But navigating back to Home unmounts that screen, which cancels the transition task and leaves the NVP uncleared in Onyx.

    When the user opens the next expense from Home, ReportNotFoundGuard.tsx (lines 127–131) sees that NVP still present, so it renders bare {children} without the <ReportNotFoundInnerGuard> wrapper.

    Later, when the user opens Test Tools (or any other modal/step) over that expense, the expense screen blurs. That blur finally allows DeleteTransactionNavigateBackHandler to run and clear the leaked NVP. But once the NVP clears, ReportNotFoundGuard switches its render to <ReportNotFoundInnerGuard>{children}</ReportNotFoundInnerGuard>. Because the element wrapper type changes, React unmounts and remounts the whole report subtree, including MoneyRequestReportTransactionsNavigation.

    During that unmount, line 211 of MoneyRequestReportTransactionsNavigation.tsx checks findFocusedRoute(navigationRef.getRootState()). At that moment, the focused route is the modal (SCREENS.TEST_TOOLS_MODAL.ROOT). Because Test Tools isn't in CAROUSEL_PRESERVING_SCREENS, the cleanup assumes the user navigated away from the expense and calls clearActiveTransactionIDs(). That wipes ONYXKEYS.TRANSACTION_THREAD_NAVIGATION_TRANSACTION_IDS, so when the modal closes, useCarouselTransactionIDs() gets an empty list and the arrows disappear.

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

    Instead of adding individual screens to CAROUSEL_PRESERVING_SCREENS (which breaks again whenever someone opens Description, receipt attachments, etc.), we should check if the expense screen itself is still in the navigation stack before clearing:

    1. In MoneyRequestReportTransactionsNavigation.tsx:
      Grab the current route key with const routeKey = useRoute().key.

    In the unmount cleanup effect, check if this route is still present in the navigation state:

    const rootState = navigationRef.getRootState();
    if (extractNavigationKeys(rootState?.routes).has(routeKey)) {
        return;
    }

    If the route key is still in rootState, it was just an in-place remount (like the guard swapping its wrapper), so we keep the transaction IDs. If the key is gone (user actually popped the screen), we fall through to the existing CAROUSEL_PRESERVING_SCREENS check for pop navigation (#90366) and clear as normal.

    What specific scenarios should we cover in automated tests?

    In tests/unit/components/MoneyRequestReportTransactionsNavigation.test.tsx under the unmount cleanup tests:

    • When an unrelated screen is focused but the component's routeKey is still present in navigationRef.getRootState(), unmounting should NOT call clearActiveTransactionIDs.
    • When the route key is absent from the navigation state and the focused screen is unrelated, unmounting SHOULD call clearActiveTransactionIDs.

    What alternative solutions did you explore? (Optional)

    • Adding SCREENS.TEST_TOOLS_MODAL.ROOT to CAROUSEL_PRESERVING_SCREENS: This only patches the symptom when using Test Tools. Opening any other overlay (like editing description or opening attachments) triggers the exact same bug. Checking route presence is cleaner and covers all overlays without maintaining an allowlist.
    • Unconditionally mounting ReportNotFoundInnerGuard: Removes the remount trigger here, but pulling parent-action subscriptions into that early phase defeats why the guard was split in the first place, and leaves the cleanup's focused-route heuristic vulnerable to any future remounts.
  25. melvin-bot commented on Oct 20, 2026

    @melvin-bot

    📣 @nsunilkumarnani-sudo! 📣
    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>
    
  26. nsunilkumarnani-sudo commented on Oct 20, 2026

    @nsunilkumarnani-sudo
  27. melvin-bot commented on Oct 20, 2026

    @melvin-bot

    ⚠️ Invalid email. Please make sure to create an Expensify account with this email first here.

  28. github-actions commented on Oct 20, 2026

    @github-actions
    Contributor

    ⚠️ @nsunilkumarnani-sudo Thanks for your interest. To be considered for this job, please post a proposal following the proposal template (note the mandatory sections). Comments claiming the job without one are not reviewed.

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

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorHelp WantedApply this label when an issue is open to proposals by contributors

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions