[Payment due @DylanDylann] [Odometer] Show discard changes modal when switching tabs - #94899
Conversation
…_modal_when_switching_tabs
…_modal_when_switching_tabs
…_modal_when_switching_tabs
…_modal_when_switching_tabs
…_modal_when_switching_tabs
…_modal_when_switching_tabs
…_modal_when_switching_tabs
Replace per-tab browser history for the distance-create tabs (UNSTABLE_router, web-only, allowlist-scoped) so browser-back closes the RHP instead of cycling tabs, and the existing discard guard fires. Gate the odometer step's discard hook to standalone screens to avoid a double modal.
…eterImageUri for image-change detection Ports two refinements from the earlier RHP-close work that were not carried into the browser-back rewrite: pass restoreLastInputFocus as the tab guard's onCancel so canceling the discard modal during a tab switch re-focuses the input, and compare odometer images via getOdometerImageUri in getHasUnsavedChanges.
- centralize RN tab-switch action types (JUMP_TO/NAVIGATE_DEPRECATED) in CONST.NAVIGATION.ACTION_TYPE and add a test guarding the JUMP_TO coupling - reuse goForwardInBrowserHistory util in useDiscardChangesConfirmation instead of inline window.history.go(1) - document DistanceTabGuardContext as a single-active-guard registry
Move the inline branching of IOURequestStepDistanceOdometer's "resync from transaction" effect into pure isExternalOdometerResync / shouldInitializeOdometerFromTransaction helpers and cover them with unit tests. The effect's side-effects and dependency array are unchanged - this is behavior-preserving, just making the subtle logic named and testable.
…_modal_when_switching_tabs
…causing double-restore
…_modal_when_switching_tabs
…itch Removes the browser-Back-button discard mechanism introduced in 4ab9a73: the tab-history replacement router, goForwardInBrowserHistory helper, and the page-level useActiveTabGuardDiscardConfirmation hook. Restores the per-step useDiscardChangesConfirmation (header/hardware back) and keeps the core tab-switch discard modal and the resync guard untouched.
41b9d7e to
d1934f0
Compare
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
…er_show_discard_changes_modal_when_switching_tabs # Conflicts: # src/hooks/useDiscardChangesConfirmation/index.native.ts # src/hooks/useDiscardChangesConfirmation/index.ts # src/libs/Navigation/OnyxTabNavigator.tsx
d1934f0 to
5c1e679
Compare
This comment was marked as resolved.
This comment was marked as resolved.
|
OKAY so this PR has been in progress for a while, the contributor left SWM but the PR was approved by me and C+ for merge. However a merge conflict arose today and I can't write to that PR so had to clone it. I will handle any issues here but essentially I have just merged main to resolve conflicts. |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppiOS: mWeb SafariMacOS: Chrome / Safari |
|
🚧 Julesssss has triggered a test Expensify/App build. You can view the workflow run here. |
This comment has been minimized.
This comment has been minimized.
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/Julesssss in version: 9.4.25-0 🚀
|
|
Deploy Blocker #94961 was identified to be related to this PR. |
|
🚀 Deployed to production by https://github.com/cristipaval in version: 9.4.25-2 🚀
Bundle Size Analysis (Sentry): |
CLONE OF THIS PR, CONTRIBUTOR LEFT SWM Tested by me and C+.
Explanation of Change
This PR adds a feature we agreed on in this discussion to show a Discard Changes confirmation modal when the user tries to switch away from the Odometer tab to another tab with unsaved changes.
The tab-switch guard is generic, not odometer-specific. A new
TabSwitchGuardContextlets any tab screen registergetHasUnsavedChanges/onDiscard/onCancelcallbacks keyed by tab name, andOnyxTabNavigatorinterceptstabPressto show the shared "Discard changes?" modal - on confirm it runsonDiscardand then jumps to the target tab (it always jumps, even ifonDiscardfails, surfacing a Growl error rather than stranding the user). It's wired through the existinguseDiscardChangesConfirmationhook via a newonTabSwitchDiscardoption that self-disables outside a tab navigator, and both the nav-away and tab-switch paths render an identical modal fromgetDiscardChangesModalConfig. On the Odometer tab, confirming clears the readings/images (or restores the confirmation-edit backup when editing) before the tab switch.The discard diff is hardened against false positives: image changes use a re-mint-invariant identity (
getOdometerImageIdentity,name|size|lastModified) that survives the draft round-trip but still detects a real swap, and the resync / unsaved-changes branching is extracted into unit-tested pure predicates (odometerResync.ts,getOdometerHasUnsavedChanges) that won't re-hydrate readings the user intentionally cleared.Fixed Issues
$ #89940
PROPOSAL: N/A
Tests
OnyxTabNavigator-based flow that does not register a guard (e.g. the Map/Manual distance tabs with no unsaved odometer changes) and verify tab switches happen immediately with no modal.Nav-away discard still works
Save-for-later draft
Offline tests
Same as Tests
QA Steps
Same as Tests
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
untitled.webm
Android: mWeb Chrome
untitled2.webm
iOS: Native
Simulator.Screen.Recording.-.iPhone.16.Pro.Max.-.2026-06-16.at.23.05.25.mov
iOS: mWeb Safari
Simulator.Screen.Recording.-.iPhone.16.Pro.Max.-.2026-06-16.at.23.22.23.mov
MacOS: Chrome / Safari
Screen.Recording.2026-06-16.at.20.56.57.mov