Skip to content

Commit 07809b6

Browse files
authored
Merge pull request #55517 from truph01/feat/53421
feat: Add distance label on map route
2 parents 81bdb03 + 5632ffd commit 07809b6

File tree

7 files changed

+157
-21
lines changed

7 files changed

+157
-21
lines changed

src/components/DistanceRequest/DistanceRequestFooter.tsx

+22-16
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,30 @@
11
import React, {useCallback, useMemo} from 'react';
22
import type {ReactNode} from 'react';
33
import {View} from 'react-native';
4-
import {withOnyx} from 'react-native-onyx';
4+
import {useOnyx} from 'react-native-onyx';
55
import type {OnyxEntry} from 'react-native-onyx';
66
import Button from '@components/Button';
77
import DistanceMapView from '@components/DistanceMapView';
88
import * as Expensicons from '@components/Icon/Expensicons';
99
import ImageSVG from '@components/ImageSVG';
1010
import type {WayPoint} from '@components/MapView/MapViewTypes';
1111
import useLocalize from '@hooks/useLocalize';
12+
import usePolicy from '@hooks/usePolicy';
1213
import useTheme from '@hooks/useTheme';
1314
import useThemeStyles from '@hooks/useThemeStyles';
14-
import * as TransactionUtils from '@libs/TransactionUtils';
15+
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
16+
import {getPersonalPolicy} from '@libs/PolicyUtils';
17+
import {getDistanceInMeters, getWaypointIndex, isCustomUnitRateIDForP2P} from '@libs/TransactionUtils';
1518
import CONST from '@src/CONST';
1619
import ONYXKEYS from '@src/ONYXKEYS';
17-
import type {MapboxAccessToken} from '@src/types/onyx';
20+
import type {Policy} from '@src/types/onyx';
1821
import type {WaypointCollection} from '@src/types/onyx/Transaction';
1922
import type Transaction from '@src/types/onyx/Transaction';
2023
import type IconAsset from '@src/types/utils/IconAsset';
2124

2225
const MAX_WAYPOINTS = 25;
2326

24-
type DistanceRequestFooterOnyxProps = {
25-
/** Data about Mapbox token for calling Mapbox API */
26-
mapboxAccessToken: OnyxEntry<MapboxAccessToken>;
27-
};
28-
29-
type DistanceRequestFooterProps = DistanceRequestFooterOnyxProps & {
27+
type DistanceRequestFooterProps = {
3028
/** The waypoints for the distance expense */
3129
waypoints?: WaypointCollection;
3230

@@ -35,16 +33,26 @@ type DistanceRequestFooterProps = DistanceRequestFooterOnyxProps & {
3533

3634
/** The transaction being interacted with */
3735
transaction: OnyxEntry<Transaction>;
36+
37+
/** The policy */
38+
policy: OnyxEntry<Policy>;
3839
};
3940

40-
function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navigateToWaypointEditPage}: DistanceRequestFooterProps) {
41+
function DistanceRequestFooter({waypoints, transaction, navigateToWaypointEditPage, policy}: DistanceRequestFooterProps) {
4142
const theme = useTheme();
4243
const styles = useThemeStyles();
4344
const {translate} = useLocalize();
45+
const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID);
46+
const activePolicy = usePolicy(activePolicyID);
47+
const [mapboxAccessToken] = useOnyx(ONYXKEYS.MAPBOX_ACCESS_TOKEN);
4448

4549
const numberOfWaypoints = Object.keys(waypoints ?? {}).length;
4650
const numberOfFilledWaypoints = Object.values(waypoints ?? {}).filter((waypoint) => waypoint?.address).length;
4751
const lastWaypointIndex = numberOfWaypoints - 1;
52+
const defaultMileageRate = DistanceRequestUtils.getDefaultMileageRate(policy ?? activePolicy);
53+
const policyCurrency = (policy ?? activePolicy)?.outputCurrency ?? getPersonalPolicy()?.outputCurrency ?? CONST.CURRENCY.USD;
54+
const mileageRate = isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) : defaultMileageRate;
55+
const {unit} = mileageRate ?? {};
4856

4957
const getMarkerComponent = useCallback(
5058
(icon: IconAsset): ReactNode => (
@@ -66,7 +74,7 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig
6674
return;
6775
}
6876

69-
const index = TransactionUtils.getWaypointIndex(key);
77+
const index = getWaypointIndex(key);
7078
let MarkerComponent: IconAsset;
7179
if (index === 0) {
7280
MarkerComponent = Expensicons.DotIndicatorUnfilled;
@@ -114,6 +122,8 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig
114122
waypoints={waypointMarkers}
115123
styleURL={CONST.MAPBOX.STYLE_URL}
116124
overlayStyle={styles.mapEditView}
125+
distanceInMeters={getDistanceInMeters(transaction, undefined)}
126+
unit={unit}
117127
/>
118128
</View>
119129
</>
@@ -122,8 +132,4 @@ function DistanceRequestFooter({waypoints, transaction, mapboxAccessToken, navig
122132

123133
DistanceRequestFooter.displayName = 'DistanceRequestFooter';
124134

125-
export default withOnyx<DistanceRequestFooterProps, DistanceRequestFooterOnyxProps>({
126-
mapboxAccessToken: {
127-
key: ONYXKEYS.MAPBOX_ACCESS_TOKEN,
128-
},
129-
})(DistanceRequestFooter);
135+
export default DistanceRequestFooter;

src/components/MapView/MapView.tsx

+59-5
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@ import {View} from 'react-native';
66
import {useOnyx} from 'react-native-onyx';
77
import Button from '@components/Button';
88
import * as Expensicons from '@components/Icon/Expensicons';
9+
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
10+
import Text from '@components/Text';
911
import useTheme from '@hooks/useTheme';
1012
import useThemeStyles from '@hooks/useThemeStyles';
11-
import * as UserLocation from '@libs/actions/UserLocation';
13+
import {clearUserLocation, setUserLocation} from '@libs/actions/UserLocation';
14+
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
1215
import getCurrentPosition from '@libs/getCurrentPosition';
1316
import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types';
1417
import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types';
@@ -24,7 +27,7 @@ import responder from './responder';
2427
import utils from './utils';
2528

2629
const MapView = forwardRef<MapViewHandle, MapViewProps>(
27-
({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady, interactive = true}, ref) => {
30+
({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates, onMapReady, interactive = true, distanceInMeters, unit}, ref) => {
2831
const [userLocation] = useOnyx(ONYXKEYS.USER_LOCATION);
2932
const navigation = useNavigation();
3033
const {isOffline} = useNetwork();
@@ -39,6 +42,25 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(
3942
const shouldInitializeCurrentPosition = useRef(true);
4043
const [isAccessTokenSet, setIsAccessTokenSet] = useState(false);
4144

45+
const [distanceUnit, setDistanceUnit] = useState(unit);
46+
useEffect(() => {
47+
if (!unit || distanceUnit) {
48+
return;
49+
}
50+
setDistanceUnit(unit);
51+
}, [unit, distanceUnit]);
52+
53+
const toggleDistanceUnit = useCallback(() => {
54+
setDistanceUnit((currentUnit) =>
55+
currentUnit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS ? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES : CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS,
56+
);
57+
}, []);
58+
59+
const distanceLabelText = useMemo(
60+
() => DistanceRequestUtils.getDistanceForDisplayLabel(distanceInMeters ?? 0, distanceUnit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS),
61+
[distanceInMeters, distanceUnit],
62+
);
63+
4264
// Determines if map can be panned to user's detected
4365
// location without bothering the user. It will return
4466
// false if user has already started dragging the map or
@@ -50,7 +72,7 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(
5072
if (error?.code !== GeolocationErrorCode.PERMISSION_DENIED || !initialLocation) {
5173
return;
5274
}
53-
UserLocation.clearUserLocation();
75+
clearUserLocation();
5476
},
5577
[initialLocation],
5678
);
@@ -74,7 +96,7 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(
7496

7597
getCurrentPosition((params) => {
7698
const currentCoords = {longitude: params.coords.longitude, latitude: params.coords.latitude};
77-
UserLocation.setUserLocation(currentCoords);
99+
setUserLocation(currentCoords);
78100
}, setCurrentPositionToInitialState);
79101
}, [isOffline, shouldPanMapToCurrentPosition, setCurrentPositionToInitialState]),
80102
);
@@ -205,6 +227,20 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(
205227
const initCenterCoordinate = useMemo(() => (interactive ? centerCoordinate : undefined), [interactive, centerCoordinate]);
206228
const initBounds = useMemo(() => (interactive ? undefined : waypointsBounds), [interactive, waypointsBounds]);
207229

230+
const distanceSymbolCoorinate = useMemo(() => {
231+
const length = directionCoordinates?.length;
232+
// If the array is empty, return undefined
233+
if (!length) {
234+
return undefined;
235+
}
236+
237+
// Find the index of the middle element
238+
const middleIndex = Math.floor(length / 2);
239+
240+
// Return the middle element
241+
return directionCoordinates.at(middleIndex);
242+
}, [directionCoordinates]);
243+
208244
return !isOffline && isAccessTokenSet && !!defaultSettings ? (
209245
<View style={[style, !interactive ? styles.pointerEventsNone : {}]}>
210246
<Mapbox.MapView
@@ -258,7 +294,6 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(
258294
/>
259295
</Mapbox.ShapeSource>
260296
)}
261-
262297
{waypoints?.map(({coordinate, markerComponent, id}) => {
263298
const MarkerComponent = markerComponent;
264299
if (utils.areSameCoordinate([coordinate[0], coordinate[1]], [currentPosition?.longitude ?? 0, currentPosition?.latitude ?? 0]) && interactive) {
@@ -276,6 +311,25 @@ const MapView = forwardRef<MapViewHandle, MapViewProps>(
276311
})}
277312

278313
{!!directionCoordinates && <Direction coordinates={directionCoordinates} />}
314+
{!!distanceSymbolCoorinate && !!distanceInMeters && !!distanceUnit && (
315+
<MarkerView
316+
coordinate={distanceSymbolCoorinate}
317+
id="distance-label"
318+
key="distance-label"
319+
>
320+
<View style={{zIndex: 1}}>
321+
<PressableWithoutFeedback
322+
accessibilityRole={CONST.ROLE.BUTTON}
323+
accessibilityLabel="distance-label"
324+
onPress={toggleDistanceUnit}
325+
>
326+
<View style={[styles.distanceLabelWrapper]}>
327+
<Text style={styles.distanceLabelText}> {distanceLabelText}</Text>
328+
</View>
329+
</PressableWithoutFeedback>
330+
</View>
331+
</MarkerView>
332+
)}
279333
</Mapbox.MapView>
280334
{interactive && (
281335
<View style={[styles.pAbsolute, styles.p5, styles.t0, styles.r0, {zIndex: 1}]}>

src/components/MapView/MapViewImpl.website.tsx

+49
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,12 @@ import {View} from 'react-native';
1212
import {useOnyx} from 'react-native-onyx';
1313
import Button from '@components/Button';
1414
import * as Expensicons from '@components/Icon/Expensicons';
15+
import {PressableWithoutFeedback} from '@components/Pressable';
1516
import usePrevious from '@hooks/usePrevious';
1617
import useStyleUtils from '@hooks/useStyleUtils';
1718
import useTheme from '@hooks/useTheme';
1819
import useThemeStyles from '@hooks/useThemeStyles';
20+
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
1921
import type {GeolocationErrorCallback} from '@libs/getCurrentPosition/getCurrentPosition.types';
2022
import {GeolocationErrorCode} from '@libs/getCurrentPosition/getCurrentPosition.types';
2123
import {clearUserLocation, setUserLocation} from '@userActions/UserLocation';
@@ -42,13 +44,28 @@ const MapViewImpl = forwardRef<MapViewHandle, MapViewProps>(
4244
directionCoordinates,
4345
initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM},
4446
interactive = true,
47+
distanceInMeters,
48+
unit,
4549
},
4650
ref,
4751
) => {
4852
const [userLocation] = useOnyx(ONYXKEYS.USER_LOCATION);
4953

5054
const {isOffline} = useNetwork();
5155
const {translate} = useLocalize();
56+
const [distanceUnit, setDistanceUnit] = useState(unit);
57+
useEffect(() => {
58+
if (!unit || distanceUnit) {
59+
return;
60+
}
61+
setDistanceUnit(unit);
62+
}, [unit, distanceUnit]);
63+
64+
const toggleDistanceUnit = useCallback(() => {
65+
setDistanceUnit((currentUnit) =>
66+
currentUnit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS ? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES : CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS,
67+
);
68+
}, []);
5269

5370
const theme = useTheme();
5471
const styles = useThemeStyles();
@@ -232,6 +249,20 @@ const MapViewImpl = forwardRef<MapViewHandle, MapViewProps>(
232249
};
233250
}, [waypoints, directionCoordinates, interactive, currentPosition, initialState.zoom]);
234251

252+
const distanceSymbolCoorinate = useMemo(() => {
253+
const length = directionCoordinates?.length;
254+
// If the array is empty, return undefined
255+
if (!length) {
256+
return undefined;
257+
}
258+
259+
// Find the index of the middle element
260+
const middleIndex = Math.floor(length / 2);
261+
262+
// Return the middle element
263+
return directionCoordinates.at(middleIndex);
264+
}, [directionCoordinates]);
265+
235266
return !isOffline && !!accessToken && !!initialViewState ? (
236267
<View
237268
style={style}
@@ -257,6 +288,24 @@ const MapViewImpl = forwardRef<MapViewHandle, MapViewProps>(
257288
<View style={styles.currentPositionDot} />
258289
</Marker>
259290
)}
291+
{!!distanceSymbolCoorinate && !!distanceInMeters && !!distanceUnit && (
292+
<Marker
293+
key="distance"
294+
longitude={distanceSymbolCoorinate.at(0) ?? 0}
295+
latitude={distanceSymbolCoorinate.at(1) ?? 0}
296+
>
297+
<PressableWithoutFeedback
298+
accessibilityLabel={CONST.ROLE.BUTTON}
299+
role={CONST.ROLE.BUTTON}
300+
onPress={toggleDistanceUnit}
301+
style={{marginRight: 100}}
302+
>
303+
<View style={styles.distanceLabelWrapper}>
304+
<View style={styles.distanceLabelText}> {DistanceRequestUtils.getDistanceForDisplayLabel(distanceInMeters, distanceUnit)}</View>
305+
</View>
306+
</PressableWithoutFeedback>
307+
</Marker>
308+
)}
260309
{waypoints?.map(({coordinate, markerComponent, id}) => {
261310
const MarkerComponent = markerComponent;
262311
if (utils.areSameCoordinate([coordinate[0], coordinate[1]], [currentPosition?.longitude ?? 0, currentPosition?.latitude ?? 0]) && interactive) {

src/components/MapView/MapViewTypes.ts

+7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {ReactNode} from 'react';
22
import type {StyleProp, ViewStyle} from 'react-native';
3+
import type {Unit} from '@src/types/onyx/Policy';
34

45
type MapViewProps = {
56
// Public access token to be used to fetch map data from Mapbox.
@@ -22,6 +23,12 @@ type MapViewProps = {
2223
onMapReady?: () => void;
2324
// Whether the map is interactable or not
2425
interactive?: boolean;
26+
27+
// Distance displayed on the map in meters.
28+
distanceInMeters?: number;
29+
30+
// Unit of measurement for distance
31+
unit?: Unit;
2532
};
2633

2734
type DirectionProps = {

src/libs/DistanceRequestUtils.ts

+6
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ function getDistanceForDisplay(
202202
return `${distanceInUnits} ${unitString}`;
203203
}
204204

205+
function getDistanceForDisplayLabel(distanceInMeters: number, unit: Unit): string {
206+
const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit);
207+
return `${distanceInUnits} ${unit}`;
208+
}
209+
205210
/**
206211
* @param hasRoute Whether the route exists for the distance expense
207212
* @param distanceInMeters Distance traveled
@@ -407,6 +412,7 @@ export default {
407412
getUpdatedDistanceUnit,
408413
getRate,
409414
getRateByCustomUnitRateID,
415+
getDistanceForDisplayLabel,
410416
};
411417

412418
export type {MileageRate};

src/pages/iou/request/step/IOURequestStepDistance.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,7 @@ function IOURequestStepDistance({
535535
waypoints={waypoints}
536536
navigateToWaypointEditPage={navigateToWaypointEditPage}
537537
transaction={transaction}
538+
policy={policy}
538539
/>
539540
}
540541
/>

src/styles/index.ts

+13
Original file line numberDiff line numberDiff line change
@@ -4001,6 +4001,19 @@ const styles = (theme: ThemeColors) =>
40014001
...wordBreak.breakWord,
40024002
},
40034003

4004+
distanceLabelWrapper: {
4005+
backgroundColor: colors.green500,
4006+
paddingHorizontal: 8,
4007+
paddingVertical: 4,
4008+
borderRadius: 4,
4009+
textAlign: 'center',
4010+
},
4011+
distanceLabelText: {
4012+
fontSize: 13,
4013+
fontWeight: FontUtils.fontWeight.bold,
4014+
color: colors.productLight100,
4015+
},
4016+
40044017
productTrainingTooltipWrapper: {
40054018
backgroundColor: theme.tooltipHighlightBG,
40064019
borderRadius: variables.componentBorderRadiusNormal,

0 commit comments

Comments
 (0)