[GPS] Update user location on GPS screen when there is no active GPS trip#91418
Conversation
|
@Expensify/design @JmillsExpensify so I added user's location updates every 5 seconds even if there is no active GPS trip, but in this case I feel like it would be better to not move the map to the updated location every time and staying on the recorded route is better than this IMO: Screen.Recording.2026-05-22.at.12.50.29.movWhat do you think? And is 5 seconds ok? |
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.
|
|
Can you rephrase, I'm not quite following... |
|
Hmm, yes I think I agree with Gabriel. Basically if we move to the updated location it's a bit jarring with the zoom. Then additionally, the blue dot sticks at the last recorded location. Is that what you mean? |
|
Sorry, what I mean is currently because we do not update current location when GPS tracking is paused/not started the map shows the whole recorded route and user can freely move the map. But now when I added current location updates every 5 seconds when the tracking is not active, the map keeps moving to show latest location on every update, so user can't freely move the map or see the recorded trip without the map flying to latest location after a few seconds as you can see on the video. IMO it would be better to:
|
|
Nice, yeah I think that makes sense then and I agree. The first bullet makes the most sense to try first, right? |
Yep, just wanted to confirm that you want it like this |
|
Nice, I agree as well. Let's try the first one and we can always re-assess later. |
|
I'll push changes in a moment, but now it will work like this: Screen.Recording.2026-05-25.at.11.16.51.movSee it follows users location when there is no recorded trip and while recording it, but afterwards it does not follow the user, it just keeps showing the recorded route. When user moves the map and then taps map controls it moves it back to the recorded trip |
|
I think that looks good? Happy to help test as well. |
|
@codex review |
|
@shawnborton feel free to run build here to test and see if it works good for you |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 94488137b1
ℹ️ 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".
|
🚧 @shawnborton has triggered a test Expensify/App build. You can view the workflow run here. |
This comment has been minimized.
This comment has been minimized.
|
I found a bug in one edge case where it throws an error and I just fixed it, so it shouldn't appear in the next build, sorry for that in case you encounter it |
|
🚧 @shawnborton has triggered a test Expensify/App build. You can view the workflow run here. |
This comment has been minimized.
This comment has been minimized.
|
I will test this tomorrow at some point I hope, sorry for the delay! |
No problem! |
The only perceivable change on this PR is that now when the tracking is not active the current location marker is still being updated every 5 seconds. The screenshot you attached has recorded route and there were no changes according to how often we do location updates while recording the GPS trip. As Jason mentioned on related issue, we will probably experiment with location updates frequency once we will work on user's activity updates. But I can change the frequency of user location updates while not recording a GPS trip if you want something else than 5 seconds |
|
Ah okay, apologies for the confusion! All good on my end then, looking forward to the route improvements :) |
|
on it now |
| if (!lastSegment) { | ||
| return directionCoordinates; | ||
| } | ||
|
|
||
| if (lastSegment.length === 0) { | ||
| return directionCoordinates; | ||
| } |
There was a problem hiding this comment.
| if (!lastSegment) { | |
| return directionCoordinates; | |
| } | |
| if (lastSegment.length === 0) { | |
| return directionCoordinates; | |
| } | |
| if (!lastSegment?.length) { | |
| return directionCoordinates; | |
| } |
|
|
||
| const centerMap = () => { | ||
| const waypointCoordinates = waypoints?.map((waypoint) => waypoint.coordinate) ?? []; | ||
| if (!isTrackingGPS && (waypointCoordinates.length > 1 || (directionCoordinates ?? []).length > 1)) { |
There was a problem hiding this comment.
| if (!isTrackingGPS && (waypointCoordinates.length > 1 || (directionCoordinates ?? []).length > 1)) { | |
| if (!isTrackingGPS && (waypointCoordinates.length > 1 || directionCoordinates?.length > 1)) { |
Screen.Recording.2026-06-18.at.12.16.25.AM.mov |
|
@dukenv0307 should be fixed now, layering is very tricky with this LocationPuck component because its layer is not ready to be used as Screen.Recording.2026-06-18.at.09.54.45.movI also added additional foreground location permissions check in Untitled.mov |
This comment was marked as resolved.
This comment was marked as resolved.
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppScreen.Recording.2026-06-18.at.10.08.13.PM.movAndroid: mWeb ChromeiOS: HybridAppScreen.Recording.2026-06-18.at.9.23.04.PM.mp4iOS: mWeb SafariMacOS: Chrome / Safari |
|
Looks like we're back to Neil for final review. |
neil-marcellini
left a comment
There was a problem hiding this comment.
Here is a very small review from Friday. I'll review more today.
neil-marcellini
left a comment
There was a problem hiding this comment.
The code looks good. Great work! I have a few non blocking items that could be improved in a quick follow up.
| - iOS — `ios/RNMBX/RNMBXCamera.swift`, `ios/RNMBX/RNMBXCameraComponentView.mm`, `ios/RNMBX/RNMBXCameraViewManager.m` | ||
| - Android — `android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt` and `RNMBXCameraManager.kt` | ||
|
|
||
| - Upstream PR/issue: 🛑 |
There was a problem hiding this comment.
NAB: Why no upstream PR or issue?
|
🚧 @neil-marcellini has triggered a test Expensify/App build. You can view the workflow run here. |
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/neil-marcellini in version: 9.4.17-0 🚀
Bundle Size Analysis (Sentry): |
|
🤖 I reviewed the changes in this PR against the help-site files under Why: This PR is an internal/native rendering and UX improvement to the GPS distance map — it does not change any documented user-facing workflow. What the PR changes vs. what the docs coverThe PR's user-visible effects are all about how the map behaves, not what the user does:
The existing GPS documentation only describes the steps and outcomes, which are unchanged:
None of these steps, platform notes, permission descriptions, or receipt descriptions change as a result of this PR. The map's camera/puck/animation behavior and the recenter button are not (and have not been) documented at that level of detail, so there's nothing to update or correct. Since no changes are required, I did not create a draft help-site PR. @GCyganek, please confirm you agree no help-site updates are needed. If you'd like a doc to call out the new map/recenter behavior, reply with |
|
🚀 Deployed to production by https://github.com/blimpich in version: 9.4.17-3 🚀
|
|
🚀 Deployed to production by https://github.com/blimpich in version: 9.4.17-3 🚀
|
|
Deploy Blocker #94406 was identified to be related to this PR. |
|
Noting that my NAB comments were resolved here. Thanks. |

Explanation of Change
Requested here
Summary: New
GPSMapViewthat uses Mapbox internal location engine viafollowUserLocationCamera prop to smoothly follow user location instead of jumping from one spot to another during GPS location updates for the GPS trip.List of changes:
New GPSMapView component
What: New native-only map component used on the GPS distance screen instead of DistanceMapView.
Why: GPS tracking needs different camera behavior (follow user, permissions, custom puck, route animation) than the manual distance map.
How: Handles foreground location permission, followUserLocation, center/recenter, fallback marker when permission is denied, and wires up waypoints, route, and location puck.
@rnmapbox/maps patch — immediate follow transition
What: Adds opt-in followUserLocationUseImmediateTransition to Mapbox Camera (iOS + Android).
Why: On first follow, Mapbox animates from world view (“fly-in from space”); there is no JS-only fix when followUserLocation is true.
How: Uses makeImmediateViewportTransition() on initial follow (no waypoints); later re-centers use the default animated transition.
GPSDirection + useAnimatedTrailingDirectionCoordinate
What: Smoothly animates the trailing end of the GPS route line as the user moves.
Why: Raw GPS updates make the line jump; animation keeps movement fluid during an active trip.
How: Interpolates the last segment endpoint over GPS_ROUTE_ANIMATION_DURATION_MS (1s) using requestAnimationFrame.
GPSWaypointLayer
What: Renders start/stop/waypoint markers as a Mapbox SymbolLayer instead of MarkerView.
Why: Markers must sit below the native location puck so the puck stays on top.
How: Uses belowLayerID with platform-specific puck layer IDs; remounts on Android when the puck layer becomes available.
Custom location puck (MapCurrentLocationPuck)
What: Custom SVG puck for Mapbox LocationPuck, plus shared blue fill color in CONST.
Why: Default Mapbox puck does not match Expensify design; puck geometry is tuned for Mapbox’s anchor.
How: Registers puck image via Mapbox.Images; hides default UserLocation marker and listens for updates for route animation.
Platform-specific locationPuckLayerId
What: Exports the native puck layer ID per platform (puck on iOS, mapbox-location-indicator-layer on Android).
Why: Layer IDs differ by platform and are required for correct z-ordering.
Extract useAccessToken hook
What: Shared hook for setting the Mapbox access token.
Why: Deduplicates logic between MapView and GPSMapView.
How: MapView now uses the hook instead of an inline useEffect.
GPS screen integration (IOURequestStepDistanceGPS)
What: Swaps DistanceMapView for GPSMapView; removes manual flyTo on each GPS update and the map ref.
Why: Follow mode and animated route line replace imperative camera moves; tracking state is passed via isTrackingGPS.
GPS draft segment handling
What:
Why: Avoids dropping the last segment entirely when stopping a trip with a single segment.
Types & constants
What: GPSMapViewProps, GPSDirectionProps, optional markerType on WayPoint; new WAYPOINTS layer IDs, GPS_ROUTE_ANIMATION_DURATION_MS, MAP_CURRENT_LOCATION_FILL_COLOR.
Why: Supports the new GPS map API and shared styling/animation values.
Map utils
What: areCoordinatesEqual, interpolateCoordinate; isSingleSegmentRoute no longer depends on is2dArray.
Why: Supports route animation and multi-segment GPS coordinates.
ESLint seatbelt
What: Adds GPSMapView.tsx to the seatbelt for react-hooks/set-state-in-effect.
Why: Keeps lint passing for intentional setState-in-effect usage.
isSingleSegmentRouteis fixed as it was returningtruefor[[]]which is a multi segment empty routeFixed Issues
$ #85802
PROPOSAL: N/A
Tests
Camera behaviour:
Foreground location permissions not granted yet (granted):
Prerequisites: foreground location permissions not granted, user can ask, precise location is on, last visited Track distance screen is GPS:
Allow while using app(iOS) orWhile using the appwithPreciselocation (Android)Foreground location permissions not granted yet (denied):
Prerequisites: foreground location permissions not granted, user can ask, precise location is on, last visited Track distance screen is GPS:
Don't allowForeground location permissions not granted, GPS trip saved:
Never(iOS) orDon't allow(Android) in app's settingsOffline tests
Map pendingscreen in offline modeQA 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
Camera behaviour:
Screen.Recording.2026-06-16.at.18.36.09.mov
Offline
Foregrund location permissions:
Screen.Recording.2026-06-16.at.18.38.40.mov
Screen.Recording.2026-06-16.at.18.40.12.mov
Screen.Recording.2026-06-16.at.18.37.58.mov
iOS: Native
Camera behaviour:
Screen.Recording.2026-06-16.at.18.20.18.mov
Offline:
Foreground permissions
Screen.Recording.2026-06-16.at.18.26.41.mov
Screen.Recording.2026-06-16.at.18.27.04.mov
Screen.Recording.2026-06-16.at.18.27.04.mov
Screen.Recording.2026-06-16.at.18.27.42.mov
Screen.Recording.2026-06-16.at.18.28.20.mov