Fix: Show discard changes modal on browser back without URL flicker#84250
Fix: Show discard changes modal on browser back without URL flicker#84250
Conversation
…gesConfirmation When DiscardChangesConfirmation is rendered inside a MaterialTopTabNavigator (e.g., the odometer tab in DistanceRequestStartPage), the nearest navigator doesn't emit transitionStart events. This adds a useParentStackForWebBack prop that makes the component listen on the parent stack navigator instead, fixing the missing discard changes modal on browser back. Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
|
@MelvinBot Mark it as ready for review |
|
@ShridharGoel Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
|
PR has been marked as ready for review! ✅ |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppiOS: mWeb SafariMacOS: Chrome / Safari |
|
@MelvinBot Now, pressing browser back shows the discard modal as expected, but it changes the link from something like |
…gator Use pushState/popstate approach instead of transitionStart on parent navigator. The transitionStart event fires after the URL has already changed, causing the URL to briefly show the previous route. The pushState approach pushes a guard history entry that gets popped by browser back, keeping the URL stable while the discard modal is displayed. Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
|
The root cause is that listening for I've replaced the What changed:
|
|
@srikarparsi Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button] |
|
@MelvinBot We still have the same issue. The difference is that we have to click back button twice now, and the link gets changed even though the modal is showing and the odometer page is open in the background. |
…guard The pushState/popstate guard approach conflicted with React Navigation's own popstate listener in createMemoryHistory, causing the URL to change when the discard modal appeared and requiring a double back press. Instead, listen for transitionStart on the parent stack navigator (via navigation.getParent()) which correctly emits these events when the screen containing the tab is being removed, matching the existing pattern used for non-tab screens. Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
|
The root cause of the issue was that the What changed: Replaced the This is a net reduction of ~33 lines — the two separate |
Cast navigation.getParent() to match the navigation type so TypeScript can resolve the addListener call on the union type. Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
|
Fixed the failing Fix: Cast |
The type assertion on navigation.getParent() is necessary to prevent no-unsafe-assignment and no-unsafe-return errors downstream, even though no-unnecessary-type-assertion considers it redundant. Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
|
Fixed the failing Root cause: The Fix: Added an |
|
All checks are now passing on the latest commit. The ESLint failures that occurred on the previous commit ( |
|
Hey @ShridharGoel, please re-assign me when this is ready for review |
|
@MelvinBot Implement the changes from your refined proposal: #84246 (comment) |
…rd entry for browser back Instead of reacting to transitionStart after the browser has already navigated (which causes URL flicker), push a guard entry onto the browser history when there are unsaved changes. Browser back pops the guard entry instead of the real navigation entry, keeping the URL stable. This also removes the need for the useParentStackForWebBack prop since the new approach works regardless of navigator type (stack or tab). Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
|
Implemented the refined proposal. Here's what changed: Approach: Replaced the How it works:
Why this prevents URL flicker: The guard entry has no associated URL change. When browser back pops it, the browser stays on the same URL. The modal is shown without any navigation occurring. Removed Files changed:
|
Explanation of Change
When
DiscardChangesConfirmationhandles browser back on web, the previous approach used atransitionStartlistener +window.history.go(1)to reverse navigation after it already happened. This caused URL flicker (the URL briefly changed to the previous page and back) and failed entirely when the component was inside aMaterialTopTabNavigator(which doesn't emittransitionStartevents).This PR replaces that approach with a proactive browser history guard entry — the same pattern used by the
Modalcomponent (src/components/Modal/index.tsx):hasUnsavedChangesbecomestrue, a guard entry is pushed viapushState({...currentState, isDiscardGuard: true}, '', null)— URL doesn't changepopstatelistener detects the guard was popped and shows the discard modalhasUnsavedChangesbecomesfalseor the component unmountsThis removes the need for the
useParentStackForWebBackprop since the approach works regardless of navigator type.Fixed Issues
$ #84246
PROPOSAL: #84246 (comment)
Tests
DiscardChangesConfirmation(e.g., Merchant, Description) still work correctly with browser backOffline tests
N/A — This is a web-only browser back navigation fix. The discard changes modal is UI-only and doesn't involve network requests.
QA Steps
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
N/A — Web-only fix (browser back button)
Android: mWeb Chrome
N/A — Web-only fix (browser back button)
iOS: Native
N/A — Web-only fix (browser back button)
iOS: mWeb Safari
N/A — Web-only fix (browser back button)
MacOS: Chrome / Safari