-
Notifications
You must be signed in to change notification settings - Fork 4k
[GPS] Update user location on GPS screen when there is no active GPS trip #91418
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
neil-marcellini
merged 31 commits into
Expensify:main
from
software-mansion-labs:@GCyganek/gps/user-location-updates-gps-screen
Jun 22, 2026
Merged
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
8bd5684
[GPS] Update user location on GPS screen when there is no active GPS …
GCyganek 7ac11d9
Merge branch 'main' into @GCyganek/gps/user-location-updates-gps-screen
GCyganek 9448813
Do not follow current user location when there is a recorded trip
GCyganek 555483a
Do not follow user location on GPS map when there is a recorded trip
GCyganek e1cc363
Check directionCoordinates length
GCyganek eee9fe8
Fix isSingleSegmentRoute when [[]]
GCyganek 4a09a9f
Move cancelled check to .then
GCyganek ee529e0
Merge branch 'main' into @GCyganek/gps/user-location-updates-gps-screen
GCyganek 0d76dd9
Re-run tests
GCyganek 91c6fd0
Merge branch 'main' into @GCyganek/gps/user-location-updates-gps-screen
GCyganek 3dc8e68
Drop shouldFollowCurrentLocationWhenRoutePresent
GCyganek b2d5cdf
Use Mapbox's own location engine, animate route
GCyganek 18a4705
Fix knip
GCyganek bf2bf06
Fix long fly-in animation from space
GCyganek ed66b6e
Code clean-up
GCyganek f10c77c
fix ts error
GCyganek 557cfd7
Fix animation
GCyganek 75de587
Fix TS error
GCyganek 91a860d
Revert changes in MapView.tsx
GCyganek e1d80b8
Merge branch 'main' into @GCyganek/gps/user-location-updates-gps-screen
GCyganek 18cce67
Code clean-up
GCyganek bacc2c4
Seatbelt update
GCyganek 5c9ae29
Refactor code
GCyganek ec5c202
Handle foreground location permissions
GCyganek d46a1db
PendingMapView
GCyganek dc31cfd
Fix AI linter suggestions
GCyganek f8bdf43
Do not request locatio permissions while offline
GCyganek 8526c5c
Merge branch 'main' into @GCyganek/gps/user-location-updates-gps-screen
GCyganek 84fd6a6
Apply review feedback
GCyganek 5342a09
Fix GPSMapView layers
GCyganek 5a75416
Add empty line above comment
GCyganek File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
160 changes: 160 additions & 0 deletions
160
patches/@rnmapbox/maps/@rnmapbox+maps+10.1.44+001+immediate-follow-camera-transition.patch
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt | ||
| index 2bd4079..91ad32e 100644 | ||
| --- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt | ||
| +++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCamera.kt | ||
| @@ -62,6 +62,7 @@ class RNMBXCamera(private val mContext: Context, private val mManager: RNMBXCame | ||
| private val mHeading = 0.0 | ||
|
|
||
| private var mFollowUserLocation = defaultFollowUserLocation | ||
| + private var mFollowUserLocationUseImmediateTransition = false | ||
| private var mFollowUserMode: String? = null | ||
| private var mFollowZoomLevel : Double? = null | ||
| private var mFollowPitch : Double? = null | ||
| @@ -143,6 +144,11 @@ class RNMBXCamera(private val mContext: Context, private val mManager: RNMBXCame | ||
| _updateViewportState() | ||
| } | ||
|
|
||
| + fun setFollowUserLocationUseImmediateTransition(value: Boolean?) { | ||
| + mFollowUserLocationUseImmediateTransition = value ?: false | ||
| + _updateViewportState() | ||
| + } | ||
| + | ||
| fun setFollowZoomLevel(zoomLevel: Double?) { | ||
| mFollowZoomLevel = zoomLevel | ||
| _updateViewportState(); | ||
| @@ -517,7 +523,11 @@ class RNMBXCamera(private val mContext: Context, private val mManager: RNMBXCame | ||
|
|
||
|
|
||
| val followState = viewport.makeFollowPuckViewportState(followOptions.build()) | ||
| - viewport.transitionTo(followState) | ||
| + if (mFollowUserLocationUseImmediateTransition) { | ||
| + viewport.transitionTo(followState, viewport.makeImmediateViewportTransition()) | ||
| + } else { | ||
| + viewport.transitionTo(followState) | ||
| + } | ||
| } | ||
| mapboxMap?.let { | ||
| it.getStyle()?.let { | ||
| diff --git a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraManager.kt b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraManager.kt | ||
| index ac2fd10..c625050 100644 | ||
| --- a/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraManager.kt | ||
| +++ b/node_modules/@rnmapbox/maps/android/src/main/java/com/rnmapbox/rnmbx/components/camera/RNMBXCameraManager.kt | ||
| @@ -84,6 +84,11 @@ class RNMBXCameraManager(private val mContext: ReactApplicationContext, val view | ||
| camera.setFollowUserLocation(value.asBooleanOrNull()) | ||
| } | ||
|
|
||
| + @ReactProp(name = "followUserLocationUseImmediateTransition") | ||
| + override fun setFollowUserLocationUseImmediateTransition(camera: RNMBXCamera, value: Dynamic) { | ||
| + camera.setFollowUserLocationUseImmediateTransition(value.asBooleanOrNull()) | ||
| + } | ||
| + | ||
| @ReactProp(name = "followUserMode") | ||
| override fun setFollowUserMode(camera: RNMBXCamera, value: Dynamic) { | ||
| camera.setFollowUserMode(value.asStringOrNull()) | ||
| diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCamera.swift b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCamera.swift | ||
| index 69c7e27..ca13fbd 100644 | ||
| --- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCamera.swift | ||
| +++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCamera.swift | ||
| @@ -142,6 +142,8 @@ open class RNMBXCamera : RNMBXMapComponentBase { | ||
| } | ||
| } | ||
|
|
||
| + @objc public var followUserLocationUseImmediateTransition: Bool = false | ||
| + | ||
| @objc public var followUserMode: String? { | ||
| didSet { | ||
| _updateCameraFromTrackingMode() | ||
| @@ -365,7 +367,11 @@ open class RNMBXCamera : RNMBXMapComponentBase { | ||
|
|
||
| let followState = map.viewport.makeFollowPuckViewportState(options: followOptions) | ||
|
|
||
| - map.viewport.transition(to: followState) | ||
| + if self.followUserLocationUseImmediateTransition { | ||
| + map.viewport.transition(to: followState, transition: map.viewport.makeImmediateViewportTransition()) | ||
| + } else { | ||
| + map.viewport.transition(to: followState) | ||
| + } | ||
| map.viewport.addStatusObserver(self) | ||
| map.mapboxMap.setCamera(to: _camera) | ||
| } | ||
| diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraComponentView.mm b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraComponentView.mm | ||
| index abc8b4a..93d9940 100644 | ||
| --- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraComponentView.mm | ||
| +++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraComponentView.mm | ||
| @@ -97,6 +97,10 @@ - (void)updateProps:(const Props::Shared &)props oldProps:(const Props::Shared & | ||
| if (followUserLocationDynamic != nil) { | ||
| _view.followUserLocation = [followUserLocationDynamic boolValue]; | ||
| } | ||
| + id followUserLocationUseImmediateTransitionDynamic = RNMBXConvertFollyDynamicToId(newViewProps.followUserLocationUseImmediateTransition); | ||
| + if (followUserLocationUseImmediateTransitionDynamic != nil) { | ||
| + _view.followUserLocationUseImmediateTransition = [followUserLocationUseImmediateTransitionDynamic boolValue]; | ||
| + } | ||
| id followUserMode = RNMBXConvertFollyDynamicToId(newViewProps.followUserMode); | ||
| if (followUserMode != nil) { | ||
| _view.followUserMode = followUserMode; | ||
| diff --git a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m | ||
| index 7002793..473ebc3 100644 | ||
| --- a/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m | ||
| +++ b/node_modules/@rnmapbox/maps/ios/RNMBX/RNMBXCameraViewManager.m | ||
| @@ -12,6 +12,7 @@ @interface RCT_EXTERN_REMAP_MODULE(RNMBXCamera, RNMBXCameraViewManager, RCTViewM | ||
| RCT_EXPORT_VIEW_PROPERTY(defaultStop, NSDictionary) | ||
|
|
||
| RCT_EXPORT_VIEW_PROPERTY(followUserLocation, BOOL) | ||
| +RCT_EXPORT_VIEW_PROPERTY(followUserLocationUseImmediateTransition, BOOL) | ||
| RCT_EXPORT_VIEW_PROPERTY(followUserMode, NSString) | ||
| RCT_EXPORT_VIEW_PROPERTY(followZoomLevel, NSNumber) | ||
| RCT_EXPORT_VIEW_PROPERTY(followPitch, NSNumber) | ||
| diff --git a/node_modules/@rnmapbox/maps/lib/typescript/src/components/Camera.d.ts b/node_modules/@rnmapbox/maps/lib/typescript/src/components/Camera.d.ts | ||
| index 9355007..2493343 100644 | ||
| --- a/node_modules/@rnmapbox/maps/lib/typescript/src/components/Camera.d.ts | ||
| +++ b/node_modules/@rnmapbox/maps/lib/typescript/src/components/Camera.d.ts | ||
| @@ -61,6 +61,8 @@ export type CameraStop = { | ||
| export type CameraFollowConfig = { | ||
| /** Whether the map orientation follows the user location. */ | ||
| followUserLocation?: boolean; | ||
| + /** Whether to snap immediately when entering follow mode instead of using the default viewport transition. */ | ||
| + followUserLocationUseImmediateTransition?: boolean; | ||
| /** The mode used to track the user location on the map. */ | ||
| followUserMode?: UserTrackingMode; | ||
| /** The zoom level used when following the user location. */ | ||
| diff --git a/node_modules/@rnmapbox/maps/src/components/Camera.tsx b/node_modules/@rnmapbox/maps/src/components/Camera.tsx | ||
| index ee6dbc2..f991849 100644 | ||
| --- a/node_modules/@rnmapbox/maps/src/components/Camera.tsx | ||
| +++ b/node_modules/@rnmapbox/maps/src/components/Camera.tsx | ||
| @@ -158,6 +158,8 @@ export type CameraStop = { | ||
| export type CameraFollowConfig = { | ||
| /** Whether the map orientation follows the user location. */ | ||
| followUserLocation?: boolean; | ||
| + /** Whether to snap immediately when entering follow mode instead of using the default viewport transition. */ | ||
| + followUserLocationUseImmediateTransition?: boolean; | ||
| /** The mode used to track the user location on the map. */ | ||
| followUserMode?: UserTrackingMode; | ||
| /** The zoom level used when following the user location. */ | ||
| @@ -270,6 +272,7 @@ export const Camera = memo( | ||
| maxZoomLevel, | ||
| maxBounds, | ||
| followUserLocation, | ||
| + followUserLocationUseImmediateTransition, | ||
| followUserMode, | ||
| followZoomLevel, | ||
| followPitch, | ||
| @@ -674,6 +677,7 @@ export const Camera = memo( | ||
| animationMode={animationMode} | ||
| defaultStop={nativeDefaultStop} | ||
| followUserLocation={followUserLocation} | ||
| + followUserLocationUseImmediateTransition={followUserLocationUseImmediateTransition} | ||
| followUserMode={followUserMode} | ||
| followZoomLevel={followZoomLevel} | ||
| followPitch={followPitch} | ||
| diff --git a/node_modules/@rnmapbox/maps/src/specs/RNMBXCameraNativeComponent.ts b/node_modules/@rnmapbox/maps/src/specs/RNMBXCameraNativeComponent.ts | ||
| index 43a17e8..10849c1 100644 | ||
| --- a/node_modules/@rnmapbox/maps/src/specs/RNMBXCameraNativeComponent.ts | ||
| +++ b/node_modules/@rnmapbox/maps/src/specs/RNMBXCameraNativeComponent.ts | ||
| @@ -27,6 +27,7 @@ export interface NativeProps extends ViewProps { | ||
| userTrackingMode?: OptionalProp<Int32>; | ||
|
|
||
| followUserLocation?: OptionalProp<boolean>; | ||
| + followUserLocationUseImmediateTransition?: OptionalProp<boolean>; | ||
| followUserMode?: OptionalProp<string>; | ||
| followZoomLevel?: OptionalProp<Double>; | ||
| followPitch?: OptionalProp<Double>; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # `@rnmapbox/maps` patches | ||
|
|
||
| ### [@rnmapbox+maps+10.1.44+001+immediate-follow-camera-transition.patch](@rnmapbox+maps+10.1.44+001+immediate-follow-camera-transition.patch) | ||
|
|
||
| - Reason: Removes the long "fly-in from space" animation that plays the first time a `Camera` with `followUserLocation={true}` engages follow mode (used by `GPSMapView` on the GPS distance screen). When entering follow mode, `@rnmapbox/maps` calls the Mapbox viewport transition with no explicit transition argument, so the SDK uses its `DefaultViewportTransition`, which animates from the current camera (world view / zoom ~0) all the way in to the user. Because the JS layer discards `defaultStop`/`stop` while `followUserLocation` is true (`src/components/Camera.tsx`), there is no JS-only way to seed the start position or pick an animation mode — the only lever is the native transition itself. | ||
| - Solution: Adds an opt-in `followUserLocationUseImmediateTransition` prop to `Camera`. When `true`, entering follow mode uses `makeImmediateViewportTransition()` so the camera snaps straight to the follow state. When `false` (default), upstream behavior is preserved and the default viewport transition animates. `GPSMapView` passes the prop only on initial map load when follow is active with no route; later re-engagements (center button, follow after clearing a route) pass `false` so the camera animates back to the user. | ||
| - Files changed: | ||
| - `src/specs/RNMBXCameraNativeComponent.ts`, `src/components/Camera.tsx`, and `lib/typescript/src/components/Camera.d.ts` — new prop | ||
| - 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: 🛑 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NAB: Why no upstream PR or issue? |
||
| - E/App issue: https://github.com/Expensify/App/pull/90169#issuecomment-4476930634 | ||
| - PR introducing patch: https://github.com/Expensify/App/pull/91418 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| import Direction from './Direction'; | ||
| import type {Coordinate, GPSDirectionProps} from './MapViewTypes'; | ||
| import useAnimatedTrailingDirectionCoordinate from './useAnimatedTrailingDirectionCoordinate'; | ||
|
|
||
| function GPSDirection({directionCoordinates, isTrackingGPS, lastLocation, belowLayerID}: GPSDirectionProps) { | ||
| const animatedTrailingCoordinate = useAnimatedTrailingDirectionCoordinate({ | ||
| isEnabled: isTrackingGPS && !!lastLocation && directionCoordinates.flat().length > 0, | ||
| targetCoordinate: lastLocation ? [lastLocation.longitude, lastLocation.latitude] : undefined, | ||
| directionCoordinates, | ||
| }); | ||
|
|
||
| const getCoordinates = () => { | ||
| if (!isTrackingGPS || !lastLocation || !directionCoordinates || directionCoordinates.length === 0) { | ||
| return directionCoordinates; | ||
| } | ||
|
|
||
| const lastSegment = directionCoordinates.at(-1); | ||
| if (!lastSegment?.length) { | ||
| return directionCoordinates; | ||
| } | ||
|
|
||
| const lastLocationCoordinate: Coordinate = animatedTrailingCoordinate ?? [lastLocation.longitude, lastLocation.latitude]; | ||
|
|
||
| const newLastSegment = [...lastSegment.slice(0, lastSegment.length === 1 ? undefined : -1), lastLocationCoordinate]; | ||
|
|
||
| const newDirectionCoordinates = [...directionCoordinates.slice(0, directionCoordinates.length - 1), newLastSegment]; | ||
| return newDirectionCoordinates; | ||
| }; | ||
|
|
||
| return ( | ||
| <Direction | ||
| coordinates={getCoordinates()} | ||
| belowLayerID={belowLayerID} | ||
| /> | ||
| ); | ||
| } | ||
|
|
||
| export default GPSDirection; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.