Skip to content

[Batch 6] Migrate GPS trip ConfirmModal usages to useConfirmModal - #102013

Merged
roryabraham merged 8 commits into
mainfrom
claude-migrateGpsTripConfirmModals
Oct 2, 2026
Merged

roryabraham merged 8 commits into
mainfrom
claude-migrateGpsTripConfirmModals

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

Batch 6 of the ConfirmModal → useConfirmModal() migration: the five GPS trip modals. ConfirmModal is deprecated, so each instance moves off the inline-rendered component and onto the global modal stack.

GPSButtons/index.tsx — three user-triggered confirms

Zero-distance acknowledgement, "location required", and "location services disabled" are all straightforward: a press handler calls showConfirmModal() and acts on ModalActions.CONFIRM. The three useState visibility flags are gone, since the stack now owns visibility.

useGPSInProgressModal/index.native.ts (was GPSInProgressModal/index.native.tsx) — null-rendering controller

The prompt is asked for by closeReactNativeApp, a non-React action that cannot call a hook, so the IS_GPS_IN_PROGRESS_MODAL_OPEN Onyx flag stays as the cross-boundary trigger and the component becomes a controller hook that turns the flag into a showConfirmModal() call from a useEffect behind a useRef guard. Nothing other than the user's answer clears the flag, so the entry never has to be taken down by hand.

useGPSTripStateChecker/index.native.ts (was GPSTripStateChecker/index.native.tsx) — the hard one

Its isVisible was a four-term derived expression, and three of those terms can flip while the prompt is open, which used to hide it for free. Three things had to be handled explicitly:

  1. Auto-hide. An effect now takes the entry down when tracking stops, the trip turns out to belong to another user, or the splash screen comes back. It closes by ID rather than with closeModal(), because closeModal() pops whatever sits on top and would take an unrelated modal down if one had been opened over this one.
  2. Cancel here is destructive, not a dismissal. The old onCancel={onViewTrip} stops the trip. The wrapper collapses cancel and backdrop press into a single CLOSE, and shouldHandleNavigationBack silently defaults undefined → true, which would have routed Android hardware-back into "stop the trip". It is now passed explicitly as false. A useRef flag also tells an auto-close apart from a real answer, so the auto-hide above cannot be mistaken for the user choosing to stop the trip.
  3. Stale closures. getGpsPoints(gpsDraftDetails) read the current render's Onyx value, but a .then() continuation captures whatever the value was at show time. GPS points keep accumulating into GPS_DRAFT_DETAILS while tracking, so a trip answered minutes later would have been submitted truncated. Both controllers now read gpsDraftDetails and isOffline through refs inside the continuation.

Both controllers are now called as hooks from AuthScreens instead of being rendered there as components.

The no-deprecated/ConfirmModal seatbelt entries for all three files drop out of config/eslint/eslint.seatbelt.tsv (1 + 1 + 3 = the five instances in this batch).

Tests

tests/unit/hooks/useGPSTripStateChecker.test.tsx replaces tests/unit/components/GPSTripStateCheckerTest.tsx: the prompt no longer renders in the page-local tree, so the suite mocks useConfirmModal with the existing tests/utils/mockUseConfirmModal helper and asserts on what was pushed. Cases cover the traps above — that nothing is pushed behind the splash screen, that shouldHandleNavigationBack is false, that the entry is closed by ID without stopping the trip when tracking stops, and that a trip stopped by the user submits every point recorded up to the moment they answered rather than the show-time set.

Note

This is a native-only surface, and browser verification was unavailable in this environment, so the changes below are verified by the automated checks and unit tests only. They have not been exercised on a device.

Fixed Issues

$ #100156
PROPOSAL:

Tests

GPS tracking is native-only. Run these on Android and iOS native.

Zero-distance trip (GPSButtons)

  1. Go to FAB > Track distance > GPS.
  2. Tap Start, then tap Stop right away without moving.
  3. Tap Save.
  4. Verify a "Can't create expense" modal appears with only a Confirm button.
  5. Tap Confirm and verify the modal closes and you stay on the GPS page.

Location permission denied (GPSButtons)

  1. Remove location permission for the app in device settings.
  2. Go to FAB > Track distance > GPS and tap Start.
  3. Deny the location permission prompt.
  4. Verify a "Location access required" modal appears with the location marker illustration, a Settings button, and a Dismiss button.
  5. Tap Dismiss and verify the modal closes and no trip starts.
  6. Repeat steps 2–4, tap Settings, and verify the device settings for the app open.

Location services disabled (GPSButtons)

  1. Turn off location services for the whole device.
  2. Go to FAB > Track distance > GPS and tap Start.
  3. Verify a "Location access required" modal appears with Open settings and Dismiss buttons.
  4. Tap Dismiss and verify the modal closes and no trip starts.
  5. Tap Start again, tap Open settings, and verify the device location settings open.

Continue trip after the app closes (useGPSTripStateChecker)

  1. Turn location services back on and grant location permission.
  2. Start a GPS trip and move far enough to record some distance.
  3. Force-close the app while the trip is still recording, then reopen it.
  4. Verify a "Continue GPS trip recording?" modal appears only after the splash screen is gone.
  5. On Android, press the hardware back button and verify the modal stays open and the trip is not stopped.
  6. Tap Continue trip and verify the GPS page opens with tracking still running.
  7. Repeat steps 3–4, tap View trip, and verify the GPS page opens with the trip stopped and all the distance recorded before you tapped the button.

Switch to Expensify Classic during a trip (useGPSInProgressModal, HybridApp only)

  1. Start a GPS trip in the HybridApp build.
  2. Go to Account and tap Switch to Expensify Classic.
  3. Verify a "GPS tracking in progress" modal appears with a red Stop and switch button and a Cancel button.
  4. Tap Cancel and verify the modal closes and the trip keeps recording.
  5. Repeat step 2, tap Stop and switch, and verify the trip stops and the app switches to Expensify Classic.
  • Verify that no errors appear in the JS console

Offline tests

  1. Start a GPS trip and move far enough to record some distance.
  2. Turn on airplane mode, keeping location services on.
  3. Force-close the app, reopen it, and tap View trip on the "Continue GPS trip recording?" modal.
  4. Verify the trip stops with all recorded distance and no error appears.
  5. Turn airplane mode off and verify the trip's start and end addresses update to readable street addresses.

QA Steps

Same as tests, on Android and iOS native.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Batch 6: the three user-triggered confirms in GPSButtons plus the two Onyx-driven controllers, GPSInProgressModal and GPSTripStateChecker.

The controllers read gpsDraftDetails and isOffline through refs so a trip answered after points kept accumulating is submitted in full rather than truncated to its show-time state.

GPSTripStateChecker names its entry on the modal stack and closes it by ID when tracking stops, the trip changes owner, or the splash screen comes back, which the old derived isVisible did for free. Its cancel stops the trip rather than dismissing the prompt, so it opts out of shouldHandleNavigationBack and distinguishes an auto-close from a real answer.

Co-authored-by: Yauheni Horbach <ZhenjaHorbach@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team September 23, 2026 09:07
@melvin-bot melvin-bot Bot added Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web labels Sep 23, 2026
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from an upstream branch because I don't have push access to your fork.

To take ownership of this branch and be able to push updates, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-migrateGpsTripConfirmModals
git checkout -b claude-migrateGpsTripConfirmModals upstream/claude-migrateGpsTripConfirmModals
git push -u origin claude-migrateGpsTripConfirmModals

Then you can close this PR and open a new one from your fork.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ A setup step failed before I could start. This is usually a temporary issue — try again in a few minutes, and if it persists post in #melvinbot-ai.

Run details

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ❌ could not run

I couldn't produce screenshots or recordings for this PR on Web, for two independent reasons.

1. There are no documented testing steps yet. The Tests and QA Steps sections of this PR are still the unfilled // TODO: **The human co-author** must fill out... placeholders. The only checkbox in either section is the generic "Verify that no errors appear in the JS console", so there is nothing platform-specific to execute.

2. Nothing this PR changes renders on Web. Even with steps and a working browser, a Web run would exercise none of the migrated code:

Changed file Reachable on Web?
src/components/GPSInProgressModal/index.native.tsx No — Web resolves to index.tsx, which is return null under the comment // GPS is not available on web
src/components/GPSTripStateChecker/index.native.tsx No — Web resolves to index.tsx, also return null
src/pages/iou/request/step/IOURequestStepDistanceGPS/GPSButtons/index.tsx No — GPSButtons has no .native variant, but its only importer is IOURequestStepDistanceGPS/index.native.tsx:50. On Web the page variant renders a "get the mobile app" screen and never mounts it

Both null-rendering Web variants are the files AuthScreens.tsx:406-407 mounts, and neither is touched by this PR — so the migrated useConfirmModal() code paths are unreachable in a browser by construction. This matches the note already in the PR body: this is a native-only surface.

I also could not drive a browser in this run at all — Web prep failed with no dev SSL certificate, so no browser-based verification was possible even for the generic console check.

What would actually verify this: an Android or iOS native run. This PR carries the Melvin-Test-Android label, so an Android run is the one worth triggering — but it still needs real steps in the Tests section first, covering the five migrated modals (zero distance, location required, location services disabled, GPS-in-progress, and the trip state checker's stop-trip confirm).

Unrelated but blocking: the Oxfmt check is failing on a formatting diff in src/components/GPSTripStateChecker/index.native.tsx. Running npm run fmt and committing should clear it. All 8 Jest test jobs pass, including the updated GPSTripStateCheckerTest.tsx.


view run

@codecov

codecov Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
...StateChecker/useUpdateGpsNotification/index.ios.ts 100.00% <ø> (ø)
...TripStateChecker/useUpdateGpsNotification/index.ts 0.00% <ø> (ø)
...GPSTripStateChecker/useUpdateGpsTripOnReconnect.ts 22.22% <ø> (ø)
src/libs/Navigation/AppNavigator/AuthScreens.tsx 83.50% <ø> (-0.34%) ⬇️
src/hooks/useGPSInProgressModal/index.ts 0.00% <0.00%> (ø)
src/hooks/useGPSTripStateChecker/index.ts 0.00% <0.00%> (ø)
src/hooks/useGPSTripStateChecker/index.native.ts 82.85% <88.88%> (ø)
src/hooks/useGPSInProgressModal/index.native.ts 83.33% <83.33%> (ø)
...tep/IOURequestStepDistanceGPS/GPSButtons/index.tsx 0.00% <0.00%> (ø)
... and 10 files with indirect coverage changes

Co-authored-by: Yauheni Horbach <ZhenjaHorbach@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed and pushed — Oxfmt check is green again (commit 67c86ae).

What was failing: oxfmt wanted the Navigation.navigate(...) call in src/components/GPSTripStateChecker/index.native.tsx:132 back on one line. Swapping reportID → reportIDRef.current pushed it to 183 characters, which still fits under the repo's printWidth of 190, so hand-wrapping it across three lines was a formatting diff. The fix is whitespace only — no behavior change.

The one check still red is Check independent approval, which needs a human reviewer, not a code change.

Local verification

Run from the App checkout on the pushed commit:

  • npm run fmt → clean, no diff (idempotent on a second run)
  • npm run lint src/components/GPSTripStateChecker/index.native.tsx → clean
  • npm run typecheck → passed
  • npm run react-compiler-compliance-check check src/components/GPSTripStateChecker/index.native.tsx → clean
  • npm test -- tests/unit/components/GPSTripStateCheckerTest.tsx → 7/7 passed

Note: a bare npx eslint on the file reports two react-hooks/exhaustive-deps errors, but that bypasses the React Compiler processor that CI's npm run lint wrapper applies. Through the wrapper — the same path CI uses — the file is clean, and the ESLint check job is green.

This is a native-only surface and browser control was unavailable in this environment, so nothing here was verified on a device or in a browser. Everything above is static analysis and the automated checks.


view run

buttonVariant={CONST.BUTTON_VARIANT.DANGER}
/>
);
return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot
Let's refactor this to use hooks instead of empty components

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1f9471f. Both null-rendering components are now hooks, called from AuthScreens:

The hook bodies are unchanged. The test moved to tests/unit/hooks/useGPSTripStateChecker.test.tsx and uses renderHook.

One trade-off: AuthScreens now owns the GPS_DRAFT_DETAILS subscription, so it re-renders on each GPS point while a trip is tracking. React Compiler memoizes its JSX, so the navigator tree should bail out, but you may want to confirm that on a device.

Checks run
  • npm test for useGPSTripStateChecker.test.tsx and HybridAppActionsTest.ts: 20/20 passed
  • npm run typecheck: passed
  • npm run lint on the changed files: clean
  • react-compiler-compliance-check on the changed files: clean
  • npm run fmt: no diff

I have not tested this on a device.

});
}, [closeModalByID, onContinueTrip, onViewTrip, shouldShowContinueTripModal, showConfirmModal, translate]);

return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MelvinBot
Let's refactor this to use hooks instead of empty components

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1f9471f. This is now useGPSTripStateChecker, called from AuthScreens.tsx:140. See the other thread for the details and the re-render trade-off.

GPSTripStateChecker and GPSInProgressModal rendered nothing and only
existed to run effects, so they become useGPSTripStateChecker and
useGPSInProgressModal, called from AuthScreens. The web variants stay
no-ops.

Co-authored-by: Yauheni Horbach <ZhenjaHorbach@users.noreply.github.com>
@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

@MelvinBot
Fix conflicts

…ConfirmModals

Co-authored-by: Yauheni Horbach <ZhenjaHorbach@users.noreply.github.com>

# Conflicts:
#	config/eslint/eslint.seatbelt.tsv
#	src/pages/iou/request/step/IOURequestStepDistanceGPS/GPSButtons/index.tsx
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Merged main into this branch and fixed the conflicts in cec12da2c45.

  • GPSButtons/index.tsx: main swapped the BackgroundLocationPermissionsFlow component for the useBackgroundLocationPermissionsFlow hook. I kept that hook and moved this PR's useConfirmModal calls onto it. The hook's onDeny now calls showLocationRequiredModal, and checkSettingsAndPermissions calls showDisabledServicesModal(). Only the buttons are left to render, so I also removed the extra fragment.
  • eslint.seatbelt.tsv: I kept the removals from both sides. None of these files use ConfirmModal anymore.

Lint, typecheck, and the React Compiler check pass on the changed file.

@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

@MelvinBot
Review this PR

@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

@codex
Review this PR

@MelvinBot

Copy link
Copy Markdown
Contributor Author

PR Review

The migration looks correct, but please look at one performance issue before merging. Two smaller items follow.

1. AuthScreens now re-renders on every GPS point (should fix)

Before this PR, the two null-rendering components kept their GPS_DRAFT_DETAILS subscriptions inside their own small subtree. As hooks, those subscriptions now live in AuthScreens, at AuthScreens.tsx:140-141. Both hooks read the whole object, at useGPSTripStateChecker/index.native.ts:38 and useGPSInProgressModal/index.native.ts:26.

While a trip is running, the app writes to that key every 50 m (const.ts:5). So the root authenticated navigator re-renders, and re-runs all of its hooks, many times per minute while someone drives with the app open. AuthScreens had no direct useOnyx calls before this PR.

You can fix this in either of these ways:

  • Keep the hooks, but call them from one tiny null-rendering host component that AuthScreens renders. This keeps re-renders out of the navigator.
  • Narrow the hooks' render-time subscriptions with selectors, such as isTracking and accountID, and read the full draft only when the user answers. This is harder, because the restart check and useUpdateGpsTripOnReconnect also read the full object.

ZhenjaHorbach, you asked for hooks, so I'm raising this for you to decide rather than changing it.

2. The continue-trip prompt doesn't come back after an auto-close (minor)

When the prompt auto-closes, .then still calls setShowContinueTripModal(false) before it checks the auto-close flag (index.native.ts:204-208). The old inline modal would reappear once its conditions were true again. For example, HybridAppHandler can show the splash screen again. Now the prompt is gone for the rest of the session. If that's intended, it's fine. Otherwise, move setShowContinueTripModal(false) below the auto-close check.

3. The PR description is out of date

It still describes GPSInProgressModal/index.native.tsx, GPSTripStateChecker/index.native.tsx, and tests/unit/components/GPSTripStateCheckerTest.tsx. Those are now useGPSInProgressModal, useGPSTripStateChecker, and tests/unit/hooks/useGPSTripStateChecker.test.tsx. The Tests and QA sections are also still empty.

What I checked
  • tests/unit/hooks/useGPSTripStateChecker.test.tsx passes locally: 7 of 7 tests.
  • closeModalByID resolves the promise with CLOSE, and the hasAutoClosedContinueTripModalRef guard stops that from stopping the trip. That path is correct.
  • shouldHandleNavigationBack: false on the continue-trip prompt is correct. The useConfirmModal wrapper defaults it to true, which would send Android back into the stop-trip action.
  • Reading the trip through refs in the .then handlers is correct. Points recorded while the prompt is open are submitted.
  • GPSButtons: onDeny from useBackgroundLocationPermissionsFlow only runs after the previous step's modal has finished hiding. Opening showLocationRequiredModal from it is therefore safe.
  • GPSInProgressModal: back and backdrop both resolve to CLOSE and clear the Onyx flag, just like the old onCancel.

@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

@MelvinBot
Add test steps and complete the checklist in the PR description

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cec12da2c4

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +140 to +141
useGPSTripStateChecker();
useGPSInProgressModal();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Release global GPS prompts when AuthScreens unmounts

When the session changes while either GPS prompt is open (for example, logout or session expiry), AppNavigator.native.tsx unmounts AuthScreens and renders PublicScreens, but the app-level ModalProvider keeps its modal stack alive. These hooks do not remove their entries on unmount, so the old continue-trip or switch-to-OldDot prompt remains over the public flow; answering it can then run stale trip/navigation actions after the authenticated UI has gone away. The previous inline modal components disappeared with AuthScreens.

Useful? React with 👍 / 👎.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

I updated the PR description with test steps and checked the checklist items I can confirm.

  • Tests: Steps for all five modals: zero-distance trip, permission denied, location services off, continue trip after the app closes (including Android back), and switching to Expensify Classic during a trip.
  • Offline tests and QA Steps are filled in too. QA is "Same as tests" on native.
  • Checklist: I checked the items about the issue link, test steps, code patterns, comments, and unit tests. I also checked the "If…" items that don't apply to this PR.

I left the device-testing items unchecked because nobody has run these steps on a device yet: offline, high-traffic account, screenshots, all platforms, console errors, impacted components, deeplink, and retest after merging main. GPS is native-only, so I couldn't run them on web. You need to run the steps on Android and iOS, then check those items.

I also fixed stale file paths in the Explanation, because the controllers moved to src/hooks/ and the test file was renamed.

@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

@MelvinBot
Add these changes

diff --git a/src/hooks/useGPSInProgressModal/index.native.ts b/src/hooks/useGPSInProgressModal/index.native.ts
index 8005df4d33106f..6dc13166d8ae7b 100644
--- a/src/hooks/useGPSInProgressModal/index.native.ts
+++ b/src/hooks/useGPSInProgressModal/index.native.ts
@@ -14,6 +14,8 @@ import ONYXKEYS from '@src/ONYXKEYS';
 
 import {useEffect, useRef} from 'react';
 
+const GPS_IN_PROGRESS_MODAL_ID = 'gpsInProgress';
+
 /**
  * Pushes the "a trip is in progress" prompt onto the global modal stack.
  *
@@ -26,11 +28,31 @@ function useGPSInProgressModal() {
     const [gpsDraftDetails] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS);
     const {translate} = useLocalize();
     const {isOffline} = useNetwork();
-    const {showConfirmModal} = useConfirmModal();
+    const {showConfirmModal, closeModalByID} = useConfirmModal();
 
     // Keeps a re-render from stacking a second prompt on top of the one that is already open.
     const isPromptShownRef = useRef(false);
 
+    // Set when the unmount cleanup below took the prompt down, so the handler knows the user never answered.
+    const hasAutoClosedPromptRef = useRef(false);
+    const closeModalByIDRef = useRef(closeModalByID);
+    useEffect(() => {
+        closeModalByIDRef.current = closeModalByID;
+    });
+
+    // The prompt lives on the app-level modal stack, which outlives AuthScreens. Take it down when the session ends
+    // (logout, expiry) so it does not stay over the public screens and stop the trip / switch apps once answered there.
+    useEffect(
+        () => () => {
+            if (!isPromptShownRef.current) {
+                return;
+            }
+            hasAutoClosedPromptRef.current = true;
+            closeModalByIDRef.current(GPS_IN_PROGRESS_MODAL_ID);
+        },
+        [],
+    );
+
     // The trip keeps recording points while the prompt is open, so what gets submitted has to be read when the user
     // answers rather than when the prompt is shown. Reading the render-time values inside the handler below would
     // submit the trip as it stood at show time, which is a truncated trip.
@@ -47,6 +69,7 @@ function useGPSInProgressModal() {
         isPromptShownRef.current = true;
 
         showConfirmModal({
+            id: GPS_IN_PROGRESS_MODAL_ID,
             title: translate('gps.switchToODWarningTripInProgress.title'),
             prompt: translate('gps.switchToODWarningTripInProgress.prompt'),
             confirmText: translate('gps.switchToODWarningTripInProgress.confirm'),
@@ -56,6 +79,11 @@ function useGPSInProgressModal() {
             isPromptShownRef.current = false;
             setIsGPSInProgressModalOpen(false);
 
+            if (hasAutoClosedPromptRef.current) {
+                hasAutoClosedPromptRef.current = false;
+                return;
+            }
+
             if (result.action !== ModalActions.CONFIRM) {
                 return;
             }
diff --git a/src/hooks/useGPSTripStateChecker/index.native.ts b/src/hooks/useGPSTripStateChecker/index.native.ts
index 0850360a6690de..346a60406b3176 100644
--- a/src/hooks/useGPSTripStateChecker/index.native.ts
+++ b/src/hooks/useGPSTripStateChecker/index.native.ts
@@ -69,12 +69,27 @@ function useGPSTripStateChecker() {
     const gpsDraftDetailsRef = useRef(gpsDraftDetails);
     const isOfflineRef = useRef(isOffline);
     const reportIDRef = useRef(reportID);
+    const closeModalByIDRef = useRef(closeModalByID);
     useEffect(() => {
         gpsDraftDetailsRef.current = gpsDraftDetails;
         isOfflineRef.current = isOffline;
         reportIDRef.current = reportID;
+        closeModalByIDRef.current = closeModalByID;
     });
 
+    // The prompt lives on the app-level modal stack, which outlives AuthScreens. Take it down when the session ends
+    // (logout, expiry) so it does not stay over the public screens and run trip actions once answered there.
+    useEffect(
+        () => () => {
+            if (!isContinueTripModalOpenRef.current) {
+                return;
+            }
+            hasAutoClosedContinueTripModalRef.current = true;
+            closeModalByIDRef.current(CONTINUE_TRIP_MODAL_ID);
+        },
+        [],
+    );
+
     useEffect(() => {
         if (!isTripFromDifferentUser) {
             return;

…unts

Co-authored-by: Yauheni Horbach <ZhenjaHorbach@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Added your changes in eee12aa. The diff applied cleanly to both hooks:

Lint, typecheck, the React Compiler check, and useGPSTripStateChecker.test.tsx (7 of 7) pass.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

MelvinBot has made 5 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR.

Next steps: A human should review the recent commits and resolve the conflicting CI errors manually.

@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

@MelvinBot
Open the PR

@MelvinBot

Copy link
Copy Markdown
Contributor Author

I couldn't mark this PR as ready for review. The command that does it needs approval, and this run doesn't have it. The PR is still a draft.

To open it, click Ready for review at the bottom of the PR page. The branch is up to date with main and has no conflicts.

@roryabraham

Copy link
Copy Markdown
Contributor

@MelvinBot listen to @ZhenjaHorbach please 😂

});
await waitForBatchedUpdatesWithAct();

// Then the trip is submitted with the points as they stand now; reading the show-time value would truncate it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-15 (docs)

This comment uses a semicolon to join two independent sentences, which the plain-comment style avoids. Split it into two sentences.

// Then the trip is submitted with the points as they stand now. Reading the show-time value would truncate it

Reviewed at: 4dc06db | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c9cb70d. I split the comment into two sentences.


view run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4dc06dbb34

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const isTripStopped = isTripStoppedUtil(gpsDraftDetails);

const showDisabledServicesModal = () => {
showConfirmModal({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent duplicate GPS prompts from repeated presses

When location services are disabled, two taps on Resume/Start can both finish the awaited hasServicesEnabledAsync() check and call this function before any modal is dismissed. Because this global-modal call has neither an ID nor an open guard, ModalProvider creates a separate stack entry for each invocation, whereas the previous boolean state could only render one modal; dismissing the first Location services prompt then immediately exposes another. The same unguarded pattern also affects the zero-distance acknowledgement. Track whether each prompt is already open (or otherwise suppress repeat calls) before showing it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c9cb70d. All three GPSButtons prompts now go through one showPrompt helper. It skips the call while a prompt is already open, then clears the flag when the prompt resolves (GPSButtons/index.tsx:64-83).

This matches the "is open" ref guard already used in useGPSInProgressModal and useGPSTripStateChecker. I didn't use a stable modal id alone. A repeat call with the same id returns the same promise, so the confirm action (opening Settings) would still run once per press.


view run

@MelvinBot

MelvinBot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Everything ZhenjaHorbach asked me to do is now on this PR. You already marked it ready for review, which was the one step I couldn't do.

  • The GPS components are refactored into hooks.
  • The requested diff that closes prompts when AuthScreens unmounts is on the branch.
  • The Tests and Offline tests sections have step-by-step instructions.
  • The branch has no conflicts with main.

The PR Author Checklist still has unchecked items that need a person, such as screenshots and test runs on each platform.

@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

@MelvinBot
Fix last comments

…ment

Co-authored-by: Yauheni Horbach <ZhenjaHorbach@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team as a code owner September 30, 2026 13:52
@melvin-bot
melvin-bot Bot requested review from roryabraham and removed request for a team September 30, 2026 13:52
@melvin-bot

melvin-bot Bot commented Sep 30, 2026

Copy link
Copy Markdown

@roryabraham 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

Copy link
Copy Markdown
Contributor Author

I fixed the last two review comments in c9cb70d.

  • Duplicate GPS prompts: GPSButtons now shows its three prompts through one guarded helper. A second quick press no longer stacks another prompt (GPSButtons/index.tsx:64-83).
  • CONSISTENCY-15: I split the semicolon comment in useGPSTripStateChecker.test.tsx into two sentences.

Lint, typecheck, the React Compiler check, and useGPSTripStateChecker tests pass. I couldn't run oxfmt or cspell in this environment, so CI will check formatting and spelling.


view run

@joekaufmanexpensify

Copy link
Copy Markdown
Contributor

No product review needed.

@joekaufmanexpensify
joekaufmanexpensify removed their request for review September 30, 2026 17:53
@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

@MelvinBot
Fix conflicts

…ConfirmModals

Co-authored-by: Yauheni Horbach <ZhenjaHorbach@users.noreply.github.com>

# Conflicts:
#	config/eslint/eslint.seatbelt.tsv
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I fixed the merge conflicts by merging main into this branch (8102a0b).

The only conflict was in config/eslint/eslint.seatbelt.tsv:

  • Kept main's new no-restricted-imports entries for HeaderPageLayout.tsx and IOURequestStepCurrencyModal.tsx.
  • Dropped the three no-deprecated/ConfirmModal entries for GPSInProgressModal, GPSTripStateChecker, and GPSButtons, because this PR removes those ConfirmModal usages.

Main made no changes to the GPS files this PR moves or edits. I couldn't run ESLint locally because it ran out of memory, so CI will confirm lint.

@roryabraham
roryabraham merged commit 289a7a8 into main Oct 2, 2026
42 of 45 checks passed
@roryabraham
roryabraham deleted the claude-migrateGpsTripConfirmModals branch October 2, 2026 07:06
@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

🚧 roryabraham has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

OSBotify commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify OSBotify mentioned this pull request Oct 4, 2026
59 tasks done
@OSBotify

OSBotify commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/roryabraham in version: 9.5.2-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor Author

No help site update needed. This PR moves the GPS trip modals to useConfirmModal() as an internal refactor, so the modals, buttons, and GPS tracking flow look and behave the same for users.


view run

@jponikarchuk

Copy link
Copy Markdown

Deploy Blocker #102961 was identified to be related to this PR.

@OSBotify

OSBotify commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/Julesssss in version: 9.5.2-4 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

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

Labels

Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants