Skip to content

Commit 5d094c0

Browse files
authored
Merge pull request Expensify#88609 from software-mansion-labs/@GCyganek/gps/pause-resume-gps-trip
[GPS] Pause/Resume functionality
2 parents 97419f9 + 3593310 commit 5d094c0

38 files changed

Lines changed: 793 additions & 343 deletions

src/components/AccountSwitcher.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
1515
import {clearDelegatorErrors, connect, disconnect} from '@libs/actions/Delegate';
1616
import {close} from '@libs/actions/Modal';
1717
import {getLatestError} from '@libs/ErrorUtils';
18-
import {stopGpsTrip} from '@libs/GPSDraftDetailsUtils';
18+
import {getGpsPoints, stopGpsTrip} from '@libs/GPSDraftDetailsUtils';
1919
import {sortAlphabetically} from '@libs/OptionsListUtils';
2020
import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
2121
import TextWithEmojiFragment from '@pages/inbox/report/comment/TextWithEmojiFragment';
@@ -58,6 +58,7 @@ function AccountSwitcher({isScreenFocused}: AccountSwitcherProps) {
5858
const [session] = useOnyx(ONYXKEYS.SESSION);
5959
const [stashedSession] = useOnyx(ONYXKEYS.STASHED_SESSION);
6060
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
61+
const [gpsDraftDetails] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS);
6162

6263
const buttonRef = useRef<HTMLDivElement>(null);
6364
const {windowHeight} = useWindowDimensions();
@@ -98,7 +99,7 @@ function AccountSwitcher({isScreenFocused}: AccountSwitcherProps) {
9899
return;
99100
}
100101

101-
await stopGpsTrip(false, true);
102+
await stopGpsTrip(false, getGpsPoints(gpsDraftDetails), true);
102103

103104
switchAccount();
104105
};

src/components/ConfirmedRoute.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import useOnyx from '@hooks/useOnyx';
77
import useStyleUtils from '@hooks/useStyleUtils';
88
import useTheme from '@hooks/useTheme';
99
import useThemeStyles from '@hooks/useThemeStyles';
10+
import getArrayDepth from '@libs/getArrayDepth';
1011
import {getWaypointIndex} from '@libs/TransactionUtils';
1112
import {init as initMapboxToken, stop as stopMapboxToken} from '@userActions/MapboxToken';
1213
import CONST from '@src/CONST';
@@ -86,7 +87,8 @@ function ConfirmedRoute({transaction, isSmallerIcon, shouldHaveBorderRadius = tr
8687
return stopMapboxToken;
8788
}, []);
8889

89-
const shouldDisplayMap = !requireRouteToDisplayMap || !!coordinates.length;
90+
const hasCoordinates = getArrayDepth(coordinates) === 3 ? !!coordinates.flat().length : !!coordinates.length;
91+
const shouldDisplayMap = !requireRouteToDisplayMap || hasCoordinates;
9092

9193
return !isOffline && !!mapboxAccessToken?.token && shouldDisplayMap ? (
9294
<DistanceMapView

src/components/GPSInProgressModal/index.native.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ import useNetwork from '@hooks/useNetwork';
44
import useOnyx from '@hooks/useOnyx';
55
import {closeReactNativeApp} from '@libs/actions/HybridApp';
66
import {setIsGPSInProgressModalOpen} from '@libs/actions/isGPSInProgressModalOpen';
7-
import {stopGpsTrip} from '@libs/GPSDraftDetailsUtils';
7+
import {getGpsPoints, stopGpsTrip} from '@libs/GPSDraftDetailsUtils';
88
import ONYXKEYS from '@src/ONYXKEYS';
99

1010
function GPSInProgressModal() {
1111
const [isGPSInProgressModalOpen] = useOnyx(ONYXKEYS.IS_GPS_IN_PROGRESS_MODAL_OPEN);
12+
const [gpsDraftDetails] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS);
1213
const {translate} = useLocalize();
1314
const {isOffline} = useNetwork();
1415

1516
const stopGpsAndSwitchToOD = async () => {
1617
setIsGPSInProgressModalOpen(false);
17-
await stopGpsTrip(isOffline);
18+
await stopGpsTrip(isOffline, getGpsPoints(gpsDraftDetails));
1819
closeReactNativeApp({shouldSetNVP: true, isTrackingGPS: false, shouldIgnoreTryNewDotLoading: true});
1920
};
2021

src/components/GPSTripStateChecker/index.native.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import ConfirmModal from '@components/ConfirmModal';
55
import useLocalize from '@hooks/useLocalize';
66
import useNetwork from '@hooks/useNetwork';
77
import useOnyx from '@hooks/useOnyx';
8-
import {stopGpsTrip} from '@libs/GPSDraftDetailsUtils';
8+
import {getGpsPoints, stopGpsTrip} from '@libs/GPSDraftDetailsUtils';
99
import Navigation from '@libs/Navigation/Navigation';
1010
import {generateReportID} from '@libs/ReportUtils';
1111
import {BACKGROUND_LOCATION_TASK_OPTIONS, BACKGROUND_LOCATION_TRACKING_TASK_NAME} from '@pages/iou/request/step/IOURequestStepDistanceGPS/const';
@@ -27,7 +27,7 @@ function GPSTripStateChecker() {
2727

2828
const reportID = gpsDraftDetails?.reportID ?? generateReportID();
2929

30-
useUpdateGpsTripOnReconnect();
30+
useUpdateGpsTripOnReconnect({gpsPoints: getGpsPoints(gpsDraftDetails)});
3131
useUpdateGpsNotification();
3232

3333
useEffect(() => {
@@ -94,7 +94,7 @@ function GPSTripStateChecker() {
9494

9595
const onViewTrip = () => {
9696
setShowContinueTripModal(false);
97-
stopGpsTrip(isOffline);
97+
stopGpsTrip(isOffline, getGpsPoints(gpsDraftDetails));
9898
navigateToGpsScreen();
9999
};
100100

src/components/GPSTripStateChecker/useUpdateGpsTripOnReconnect.ts

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,70 @@
1+
import OnyxUtils from 'react-native-onyx/dist/OnyxUtils';
12
import useNetwork from '@hooks/useNetwork';
2-
import useOnyx from '@hooks/useOnyx';
3-
import {setEndAddress, setStartAddress} from '@libs/actions/GPSDraftDetails';
4-
import {addressFromGpsPoint} from '@libs/GPSDraftDetailsUtils';
3+
import {updateGpsPoints} from '@libs/actions/GPSDraftDetails';
4+
import {addressFromGpsPoint, getGpsPoints} from '@libs/GPSDraftDetailsUtils';
55
import ONYXKEYS from '@src/ONYXKEYS';
6-
import type {GpsDraftDetails} from '@src/types/onyx';
6+
import type {GPSPoint} from '@src/types/onyx/GpsDraftDetails';
77

8-
function useUpdateGpsTripOnReconnect() {
9-
const [gpsDraftDetails] = useOnyx(ONYXKEYS.GPS_DRAFT_DETAILS);
8+
function useUpdateGpsTripOnReconnect({gpsPoints}: {gpsPoints: GPSPoint[][]}) {
9+
const updateAddressesToHumanReadable = async () => {
10+
const waypointUpdates: Array<Promise<{point: GPSPoint; segmentIndex: number; type: 'start' | 'end'}>> = [];
1011

11-
const updateAddressToHumanReadable = async (gpsPoint: GpsDraftDetails['gpsPoints'][number] | undefined, setAddress: typeof setStartAddress) => {
12-
if (!gpsPoint) {
13-
return;
12+
for (const [segmentIndex, tripSegment] of gpsPoints.entries()) {
13+
for (const [pointIndex, point] of tripSegment.entries()) {
14+
// If the address is not a coordinates (already human readable), we don't need to update it
15+
if (point.address?.type === 'address') {
16+
continue;
17+
}
18+
19+
if (pointIndex === 0) {
20+
waypointUpdates.push(
21+
addressFromGpsPoint(point).then((address) => ({
22+
point: {...point, address: address ? {value: address, type: 'address'} : point.address},
23+
segmentIndex,
24+
type: 'start',
25+
})),
26+
);
27+
} else if (pointIndex === tripSegment.length - 1) {
28+
waypointUpdates.push(
29+
addressFromGpsPoint(point).then((address) => ({
30+
point: {...point, address: address ? {value: address, type: 'address'} : point.address},
31+
segmentIndex,
32+
type: 'end',
33+
})),
34+
);
35+
}
36+
}
1437
}
1538

16-
const address = await addressFromGpsPoint(gpsPoint);
39+
const waypointAddresses = (await Promise.all(waypointUpdates)).filter((waypoints) => !!waypoints.point.address);
1740

18-
if (address !== null) {
19-
setAddress({value: address, type: 'address'});
20-
}
21-
};
41+
// To avoid race conditions, we need to get the latest gpsDraftDetails, because reverse geocoding may even take a few seconds
42+
const gpsDraftDetailsPromiseResult = await OnyxUtils.get(ONYXKEYS.GPS_DRAFT_DETAILS).catch(() => undefined);
43+
const latestGpsDraftDetails = gpsDraftDetailsPromiseResult;
2244

23-
const updateAddressesToHumanReadable = () => {
24-
if (!gpsDraftDetails) {
25-
return;
26-
}
45+
const latestGpsPoints = getGpsPoints(latestGpsDraftDetails) ?? gpsPoints;
46+
const newGpsPoints = [...latestGpsPoints];
2747

28-
const {gpsPoints, startAddress, endAddress} = gpsDraftDetails;
48+
for (const {point, segmentIndex, type} of waypointAddresses) {
49+
const segment = newGpsPoints.at(segmentIndex);
50+
if (!segment) {
51+
continue;
52+
}
2953

30-
if (startAddress.type === 'coordinates') {
31-
updateAddressToHumanReadable(gpsPoints.at(0), setStartAddress);
54+
if (type === 'start') {
55+
const newSegment = [point, ...segment.slice(1)];
56+
newGpsPoints.splice(segmentIndex, 1, newSegment);
57+
} else if (type === 'end') {
58+
const newSegment = [...segment.slice(0, -1), point];
59+
newGpsPoints.splice(segmentIndex, 1, newSegment);
60+
}
3261
}
3362

34-
if (endAddress.type === 'coordinates') {
35-
updateAddressToHumanReadable(gpsPoints.at(-1), setEndAddress);
36-
}
63+
updateGpsPoints(newGpsPoints);
3764
};
3865

3966
// This is intentional to use async/await pattern for better readability
40-
67+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
4168
useNetwork({onReconnect: updateAddressesToHumanReadable});
4269
}
4370

src/components/MapView/Direction.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,46 @@ import Mapbox from '@rnmapbox/maps';
22
import useThemeStyles from '@hooks/useThemeStyles';
33
import CONST from '@src/CONST';
44
import type {DirectionProps} from './MapViewTypes';
5+
import utils from './utils';
56

67
function Direction({coordinates, belowLayerID}: DirectionProps) {
78
const styles = useThemeStyles();
9+
10+
// If not a single segment route, we need to handle route split into multiple separate segments
11+
if (!utils.isSingleSegmentRoute(coordinates)) {
12+
const validSegments = coordinates.filter((segment) => segment.length >= 2);
13+
if (validSegments.length === 0) {
14+
return null;
15+
}
16+
17+
return (
18+
<>
19+
{validSegments.map((segmentCoordinates, index) => (
20+
<Mapbox.ShapeSource
21+
// Using index as key is safe because we are not reordering the routes
22+
// eslint-disable-next-line react/no-array-index-key
23+
key={`${CONST.MAP_VIEW_LAYERS.ROUTE_SOURCE}-segment-${index}`}
24+
id={`${CONST.MAP_VIEW_LAYERS.ROUTE_SOURCE}-segment-${index}`}
25+
shape={{
26+
type: 'Feature',
27+
properties: {},
28+
geometry: {
29+
type: 'LineString',
30+
coordinates: segmentCoordinates,
31+
},
32+
}}
33+
>
34+
<Mapbox.LineLayer
35+
belowLayerID={belowLayerID}
36+
id={`${CONST.MAP_VIEW_LAYERS.ROUTE_FILL}-segment-${index}`}
37+
style={styles.mapDirection}
38+
/>
39+
</Mapbox.ShapeSource>
40+
))}
41+
</>
42+
);
43+
}
44+
845
if (coordinates.length < 2) {
946
return null;
1047
}

src/components/MapView/Direction.website.tsx

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,54 @@ import {View} from 'react-native';
88
import useThemeStyles from '@hooks/useThemeStyles';
99
import CONST from '@src/CONST';
1010
import type {DirectionProps} from './MapViewTypes';
11+
import utils from './utils';
1112

1213
function Direction({coordinates}: DirectionProps) {
1314
const styles = useThemeStyles();
1415
const layerLayoutStyle: Record<string, string> = styles.mapDirectionLayer.layout;
1516
const layerPointStyle: Record<string, string | number> = styles.mapDirectionLayer.paint;
1617

17-
if (coordinates.length < 1) {
18+
if (!utils.isSingleSegmentRoute(coordinates)) {
19+
const validSegments = coordinates.filter((segment) => segment.length >= 2);
20+
if (validSegments.length === 0) {
21+
return null;
22+
}
23+
24+
return (
25+
<View>
26+
{validSegments.map((segmentCoordinates, index) => (
27+
<Source
28+
// Using index as key is safe because we are not reordering the routes
29+
// eslint-disable-next-line react/no-array-index-key
30+
key={`${CONST.MAP_VIEW_LAYERS.ROUTE_SOURCE}-segment-${index}`}
31+
id={`${CONST.MAP_VIEW_LAYERS.ROUTE_SOURCE}-segment-${index}`}
32+
type="geojson"
33+
data={{
34+
type: 'Feature',
35+
properties: {},
36+
geometry: {
37+
type: 'LineString',
38+
coordinates: segmentCoordinates,
39+
},
40+
}}
41+
>
42+
<Layer
43+
id={`${CONST.MAP_VIEW_LAYERS.ROUTE_FILL}-segment-${index}`}
44+
type="line"
45+
source={`${CONST.MAP_VIEW_LAYERS.ROUTE_SOURCE}-segment-${index}`}
46+
paint={layerPointStyle}
47+
layout={layerLayoutStyle}
48+
/>
49+
</Source>
50+
))}
51+
</View>
52+
);
53+
}
54+
55+
if (coordinates.length < 2) {
1856
return null;
1957
}
58+
2059
return (
2160
<View>
2261
{!!coordinates && (

src/components/MapView/MapView.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,15 @@ function MapView({
3535
pitchEnabled,
3636
initialState,
3737
waypoints,
38-
directionCoordinates,
38+
directionCoordinates: directionCoordinatesProp,
3939
onMapReady,
4040
interactive = true,
4141
distanceInMeters,
4242
unit,
4343
ref,
4444
}: MapViewProps) {
45+
const directionCoordinates = !directionCoordinatesProp || utils.isSingleSegmentRoute(directionCoordinatesProp) ? directionCoordinatesProp : directionCoordinatesProp.flat();
46+
4547
const [userLocation] = useOnyx(ONYXKEYS.USER_LOCATION);
4648
const navigation = useNavigation();
4749
const {isOffline} = useNetwork();
@@ -319,9 +321,9 @@ function MapView({
319321
);
320322
})}
321323

322-
{!!directionCoordinates && (
324+
{!!directionCoordinatesProp && (
323325
<Direction
324-
coordinates={directionCoordinates}
326+
coordinates={directionCoordinatesProp}
325327
belowLayerID={interactive ? CONST.MAP_VIEW_LAYERS.USER_LOCATION : undefined}
326328
/>
327329
)}

src/components/MapView/MapViewImpl.website.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,15 @@ function MapViewImpl({
4141
waypoints,
4242
mapPadding,
4343
accessToken,
44-
directionCoordinates,
44+
directionCoordinates: directionCoordinatesProp,
4545
initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM},
4646
interactive = true,
4747
distanceInMeters,
4848
unit,
4949
ref,
5050
}: MapViewProps) {
51+
const directionCoordinates = !directionCoordinatesProp || utils.isSingleSegmentRoute(directionCoordinatesProp) ? directionCoordinatesProp : directionCoordinatesProp.flat();
52+
5153
const [userLocation] = useOnyx(ONYXKEYS.USER_LOCATION);
5254

5355
const {isOffline} = useNetwork();
@@ -308,7 +310,7 @@ function MapViewImpl({
308310
</Marker>
309311
);
310312
})}
311-
{!!directionCoordinates && <Direction coordinates={directionCoordinates} />}
313+
{!!directionCoordinatesProp && <Direction coordinates={directionCoordinatesProp} />}
312314
</Map>
313315
{interactive && (
314316
<View style={[styles.pAbsolute, styles.p5, styles.t0, styles.r0, {zIndex: 1}]}>

src/components/MapView/MapViewTypes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type MapViewProps = {
2020
// Locations on which to put markers
2121
waypoints?: WayPoint[];
2222
// List of coordinates which together forms a direction.
23-
directionCoordinates?: Coordinate[];
23+
directionCoordinates?: Coordinate[] | Coordinate[][];
2424
// Callback to call when the map is idle / ready.
2525
onMapReady?: () => void;
2626
// Whether the map is interactive or not
@@ -38,7 +38,7 @@ type MapViewProps = {
3838

3939
type DirectionProps = {
4040
// Coordinates of points that constitute the direction
41-
coordinates: Coordinate[];
41+
coordinates: Coordinate[] | Coordinate[][];
4242

4343
// ID of the layer to place the line layer below
4444
belowLayerID?: string;

0 commit comments

Comments
 (0)