diff --git a/config/webpack/webpack.common.js b/config/webpack/webpack.common.js
index f3c02b286623..7dc851c95c9e 100644
--- a/config/webpack/webpack.common.js
+++ b/config/webpack/webpack.common.js
@@ -194,7 +194,7 @@ const webpackConfig = ({envFile = '.env', platform = 'web'}) => ({
// This is also why we have to use .website.js for our own web-specific files...
// Because desktop also relies on "web-specific" module implementations
// This also skips packing web only dependencies to desktop and vice versa
- extensions: ['.web.js', platform === 'web' ? '.website.js' : '.desktop.js', '.js', '.jsx', '.web.ts', platform === 'web' ? '.website.ts' : '.desktop.ts', '.ts', '.tsx'],
+ extensions: ['.web.js', platform === 'web' ? '.website.js' : '.desktop.js', '.js', '.jsx', '.web.ts', platform === 'web' ? '.website.ts' : '.desktop.ts', '.ts', '.web.tsx', '.tsx'],
fallback: {
'process/browser': require.resolve('process/browser'),
},
diff --git a/package-lock.json b/package-lock.json
index 805c80a22c03..8d87c8c87c93 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -108,7 +108,6 @@
"react-native-web-linear-gradient": "^1.1.2",
"react-native-web-lottie": "^1.4.4",
"react-native-webview": "^11.17.2",
- "react-native-x-maps": "1.0.10",
"react-pdf": "^6.2.2",
"react-plaid-link": "3.3.2",
"react-web-config": "^1.0.0",
@@ -44691,18 +44690,6 @@
"node": ">=8"
}
},
- "node_modules/react-native-x-maps": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/react-native-x-maps/-/react-native-x-maps-1.0.10.tgz",
- "integrity": "sha512-jBRl5JzP3QmGY6tj5CR9UwbREZ3tnuSa7puZozai3bRFrN68k3W6x1p6O8SGp91VvcQlaqJUPFZ+bkYiY3XRvA==",
- "peerDependencies": {
- "@rnmapbox/maps": "^10.0.11",
- "mapbox-gl": "^2.15.0",
- "react": "^18.2.0",
- "react-map-gl": "^7.1.3",
- "react-native": "^0.72.3"
- }
- },
"node_modules/react-native/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -84788,12 +84775,6 @@
}
}
},
- "react-native-x-maps": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/react-native-x-maps/-/react-native-x-maps-1.0.10.tgz",
- "integrity": "sha512-jBRl5JzP3QmGY6tj5CR9UwbREZ3tnuSa7puZozai3bRFrN68k3W6x1p6O8SGp91VvcQlaqJUPFZ+bkYiY3XRvA==",
- "requires": {}
- },
"react-pdf": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-6.2.2.tgz",
diff --git a/package.json b/package.json
index 35471a71f2fb..d1e0907e9317 100644
--- a/package.json
+++ b/package.json
@@ -148,7 +148,6 @@
"react-native-web-linear-gradient": "^1.1.2",
"react-native-web-lottie": "^1.4.4",
"react-native-webview": "^11.17.2",
- "react-native-x-maps": "1.0.10",
"react-pdf": "^6.2.2",
"react-plaid-link": "3.3.2",
"react-web-config": "^1.0.0",
diff --git a/src/CONST.ts b/src/CONST.ts
index 9aef93f1a1b8..9735e6deb0ad 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -2604,7 +2604,6 @@ const CONST = {
SF_COORDINATES: [-122.4194, 37.7749],
- MAPBOX_STYLE_URL: 'mapbox://styles/expensify/cllcoiqds00cs01r80kp34tmq',
NAVIGATION: {
TYPE: {
FORCED_UP: 'FORCED_UP',
@@ -2623,6 +2622,13 @@ const CONST = {
SAASTR: 'SaaStrDemoSetup',
SBE: 'SbeDemoSetup',
},
+
+ MAPBOX: {
+ PADDING: 50,
+ DEFAULT_ZOOM: 10,
+ DEFAULT_COORDINATE: [-122.4021, 37.7911],
+ STYLE_URL: 'mapbox://styles/expensify/cllcoiqds00cs01r80kp34tmq',
+ },
} as const;
export default CONST;
diff --git a/src/components/DistanceRequest.js b/src/components/DistanceRequest.js
index 9ba5fd76a95a..f5a0b1a50143 100644
--- a/src/components/DistanceRequest.js
+++ b/src/components/DistanceRequest.js
@@ -1,41 +1,42 @@
-import React, {useEffect, useState} from 'react';
+import React, {useEffect, useMemo, useState} from 'react';
import {ScrollView, View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
import lodashHas from 'lodash/has';
-import _ from 'underscore';
import PropTypes from 'prop-types';
-import {withOnyx} from 'react-native-onyx';
-import MapView from 'react-native-x-maps';
+import _ from 'underscore';
+
+import CONST from '../CONST';
+import ROUTES from '../ROUTES';
import ONYXKEYS from '../ONYXKEYS';
-import * as Transaction from '../libs/actions/Transaction';
-import * as TransactionUtils from '../libs/TransactionUtils';
-import MenuItemWithTopDescription from './MenuItemWithTopDescription';
-import * as Expensicons from './Icon/Expensicons';
-import theme from '../styles/themes/default';
-import Button from './Button';
+
import styles from '../styles/styles';
import variables from '../styles/variables';
-import LinearGradient from './LinearGradient';
-import * as MapboxToken from '../libs/actions/MapboxToken';
-import CONST from '../CONST';
-import BlockingView from './BlockingViews/BlockingView';
+import theme from '../styles/themes/default';
+
+import transactionPropTypes from './transactionPropTypes';
+
import useNetwork from '../hooks/useNetwork';
+import usePrevious from '../hooks/usePrevious';
import useLocalize from '../hooks/useLocalize';
+
+import * as ErrorUtils from '../libs/ErrorUtils';
import Navigation from '../libs/Navigation/Navigation';
-import ROUTES from '../ROUTES';
-import * as IOU from '../libs/actions/IOU';
-import reportPropTypes from '../pages/reportPropTypes';
-import transactionPropTypes from './transactionPropTypes';
+import * as MapboxToken from '../libs/actions/MapboxToken';
+import * as Transaction from '../libs/actions/Transaction';
+import * as TransactionUtils from '../libs/TransactionUtils';
+
+import Button from './Button';
+import MapView from './MapView';
+import LinearGradient from './LinearGradient';
+import * as Expensicons from './Icon/Expensicons';
+import BlockingView from './BlockingViews/BlockingView';
import DotIndicatorMessage from './DotIndicatorMessage';
-import * as ErrorUtils from '../libs/ErrorUtils';
-import usePrevious from '../hooks/usePrevious';
-import {iouPropTypes} from '../pages/iou/propTypes';
+import MenuItemWithTopDescription from './MenuItemWithTopDescription';
const MAX_WAYPOINTS = 25;
const MAX_WAYPOINTS_TO_DISPLAY = 4;
-const DEFAULT_ZOOM_LEVEL = 10;
-
const propTypes = {
/** Holds data related to Money Request view state, rather than the underlying Money Request data. */
iou: iouPropTypes,
@@ -75,7 +76,7 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken})
const {translate} = useLocalize();
const reportID = lodashGet(report, 'reportID', '');
- const waypoints = lodashGet(transaction, 'comment.waypoints', {});
+ const waypoints = useMemo(() => lodashGet(transaction, 'comment.waypoints', {}), [transaction]);
const numberOfWaypoints = _.size(waypoints);
const lastWaypointIndex = numberOfWaypoints - 1;
@@ -86,34 +87,39 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken})
const doesRouteExist = lodashHas(transaction, 'routes.route0.geometry.coordinates');
const shouldFetchRoute = (!doesRouteExist || haveWaypointsChanged) && !isLoadingRoute && TransactionUtils.validateWaypoints(waypoints);
- const waypointMarkers = _.filter(
- _.map(waypoints, (waypoint, key) => {
- if (!waypoint || !lodashHas(waypoint, 'lat') || !lodashHas(waypoint, 'lng')) {
- return;
- }
+ const waypointMarkers = useMemo(
+ () =>
+ _.filter(
+ _.map(waypoints, (waypoint, key) => {
+ if (!waypoint || !lodashHas(waypoint, 'lat') || !lodashHas(waypoint, 'lng')) {
+ return;
+ }
- const index = Number(key.replace('waypoint', ''));
- let MarkerComponent;
- if (index === 0) {
- MarkerComponent = Expensicons.DotIndicatorUnfilled;
- } else if (index === lastWaypointIndex) {
- MarkerComponent = Expensicons.Location;
- } else {
- MarkerComponent = Expensicons.DotIndicator;
- }
+ const index = Number(key.replace('waypoint', ''));
+ let MarkerComponent;
+ if (index === 0) {
+ MarkerComponent = Expensicons.DotIndicatorUnfilled;
+ } else if (index === lastWaypointIndex) {
+ MarkerComponent = Expensicons.Location;
+ } else {
+ MarkerComponent = Expensicons.DotIndicator;
+ }
- return {
- coordinate: [waypoint.lng, waypoint.lat],
- markerComponent: () => (
-
- ),
- };
- }),
- (waypoint) => waypoint,
+ return {
+ id: `${waypoint.lng},${waypoint.lat},${index}`,
+ coordinate: [waypoint.lng, waypoint.lat],
+ markerComponent: () => (
+
+ ),
+ };
+ }),
+ (waypoint) => waypoint,
+ ),
+ [waypoints, lastWaypointIndex],
);
// Show up to the max number of waypoints plus 1/2 of one to hint at scrolling
@@ -218,17 +224,16 @@ function DistanceRequest({iou, iouType, report, transaction, mapboxAccessToken})
{!isOffline && Boolean(mapboxAccessToken.token) ? (
) : (
diff --git a/src/components/MapView/Direction.tsx b/src/components/MapView/Direction.tsx
new file mode 100644
index 000000000000..920a3912dca4
--- /dev/null
+++ b/src/components/MapView/Direction.tsx
@@ -0,0 +1,30 @@
+import Mapbox from '@rnmapbox/maps';
+import {DirectionProps} from './MapViewTypes';
+import styles from '../../styles/styles';
+
+function Direction({coordinates}: DirectionProps) {
+ if (coordinates.length < 1) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
+
+export default Direction;
diff --git a/src/components/MapView/Direction.web.tsx b/src/components/MapView/Direction.web.tsx
new file mode 100644
index 000000000000..190ecb03cb5e
--- /dev/null
+++ b/src/components/MapView/Direction.web.tsx
@@ -0,0 +1,48 @@
+// Explanation: Different Mapbox libraries are required for web and native mobile platforms.
+// This is why we have separate components for web and native to handle the specific implementations.
+// For the web version, we use the Mapbox Web library called react-map-gl, while for the native mobile version,
+// we utilize a different Mapbox library @rnmapbox/maps tailored for mobile development.
+
+import React from 'react';
+import {View} from 'react-native';
+import {Layer, Source} from 'react-map-gl';
+import {DirectionProps} from './MapViewTypes';
+
+import styles from '../../styles/styles';
+
+function Direction({coordinates}: DirectionProps) {
+ const layerLayoutStyle: Record = styles.mapDirectionLayer.layout;
+ const layerPointStyle: Record = styles.mapDirectionLayer.paint;
+
+ if (coordinates.length < 1) {
+ return null;
+ }
+ return (
+
+ {coordinates && (
+
+
+
+ )}
+
+ );
+}
+
+export default Direction;
diff --git a/src/components/MapView/MapView.tsx b/src/components/MapView/MapView.tsx
new file mode 100644
index 000000000000..c1dd064127a9
--- /dev/null
+++ b/src/components/MapView/MapView.tsx
@@ -0,0 +1,94 @@
+import {View} from 'react-native';
+import {useFocusEffect} from '@react-navigation/native';
+import Mapbox, {MapState, MarkerView, setAccessToken} from '@rnmapbox/maps';
+import {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react';
+
+import utils from './utils';
+import Direction from './Direction';
+import CONST from '../../CONST';
+
+import {MapViewProps, MapViewHandle} from './MapViewTypes';
+
+const MapView = forwardRef(({accessToken, style, mapPadding, styleURL, pitchEnabled, initialState, waypoints, directionCoordinates}, ref) => {
+ const cameraRef = useRef(null);
+ const [isIdle, setIsIdle] = useState(false);
+
+ useImperativeHandle(
+ ref,
+ () => ({
+ flyTo: (location: [number, number], zoomLevel: number = CONST.MAPBOX.DEFAULT_ZOOM, animationDuration?: number) =>
+ cameraRef.current?.setCamera({zoomLevel, centerCoordinate: location, animationDuration}),
+ fitBounds: (northEast: [number, number], southWest: [number, number], paddingConfig?: number | number[] | undefined, animationDuration?: number | undefined) =>
+ cameraRef.current?.fitBounds(northEast, southWest, paddingConfig, animationDuration),
+ }),
+ [],
+ );
+
+ // When the page loses focus, we temporarily set the "idled" state to false.
+ // When the page regains focus, the onIdled method of the map will set the actual "idled" state,
+ // which in turn triggers the callback.
+ useFocusEffect(
+ useCallback(() => {
+ if (waypoints?.length && isIdle) {
+ if (waypoints.length === 1) {
+ cameraRef.current?.setCamera({
+ zoomLevel: 15,
+ animationDuration: 1500,
+ centerCoordinate: waypoints[0].coordinate,
+ });
+ } else {
+ const {southWest, northEast} = utils.getBounds(waypoints.map((waypoint) => waypoint.coordinate));
+ cameraRef.current?.fitBounds(northEast, southWest, mapPadding, 1000);
+ }
+ }
+ return () => {
+ setIsIdle(false);
+ };
+ }, [mapPadding, waypoints, isIdle]),
+ );
+
+ useEffect(() => {
+ setAccessToken(accessToken);
+ }, [accessToken]);
+
+ const setMapIdle = (e: MapState) => {
+ if (e.gestures.isGestureActive) return;
+ setIsIdle(true);
+ };
+
+ return (
+
+
+
+
+ {waypoints?.map(({coordinate, markerComponent, id}) => {
+ const MarkerComponent = markerComponent;
+ return (
+
+
+
+ );
+ })}
+
+ {directionCoordinates && }
+
+
+ );
+});
+
+export default memo(MapView);
diff --git a/src/components/MapView/MapView.web.tsx b/src/components/MapView/MapView.web.tsx
new file mode 100644
index 000000000000..446d375da318
--- /dev/null
+++ b/src/components/MapView/MapView.web.tsx
@@ -0,0 +1,91 @@
+// Explanation: Different Mapbox libraries are required for web and native mobile platforms.
+// This is why we have separate components for web and native to handle the specific implementations.
+// For the web version, we use the Mapbox Web library called react-map-gl, while for the native mobile version,
+// we utilize a different Mapbox library @rnmapbox/maps tailored for mobile development.
+
+import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useState} from 'react';
+import {View} from 'react-native';
+import Map, {MapRef, Marker} from 'react-map-gl';
+
+import utils from './utils';
+
+import CONST from '../../CONST';
+import Direction from './Direction';
+import {MapViewHandle, MapViewProps} from './MapViewTypes';
+
+import 'mapbox-gl/dist/mapbox-gl.css';
+
+const MapView = forwardRef(
+ ({style, styleURL, waypoints, mapPadding, accessToken, directionCoordinates, initialState = {location: CONST.MAPBOX.DEFAULT_COORDINATE, zoom: CONST.MAPBOX.DEFAULT_ZOOM}}, ref) => {
+ const [mapRef, setMapRef] = useState(null);
+ const setRef = useCallback((newRef: MapRef | null) => setMapRef(newRef), []);
+
+ useEffect(() => {
+ if (!waypoints || waypoints.length === 0) {
+ return;
+ }
+
+ if (!mapRef) {
+ return;
+ }
+
+ if (waypoints.length === 1) {
+ mapRef.flyTo({
+ center: waypoints[0].coordinate,
+ zoom: 15,
+ });
+ return;
+ }
+
+ const map = mapRef.getMap();
+
+ const {northEast, southWest} = utils.getBounds(waypoints.map((waypoint) => waypoint.coordinate));
+ map.fitBounds([northEast, southWest], {padding: mapPadding});
+ }, [waypoints, mapRef, mapPadding]);
+
+ useImperativeHandle(
+ ref,
+ () => ({
+ flyTo: (location: [number, number], zoomLevel: number = CONST.MAPBOX.DEFAULT_ZOOM, animationDuration?: number) =>
+ mapRef?.flyTo({
+ center: location,
+ zoom: zoomLevel,
+ duration: animationDuration,
+ }),
+ fitBounds: (northEast: [number, number], southWest: [number, number]) => mapRef?.fitBounds([northEast, southWest]),
+ }),
+ [mapRef],
+ );
+
+ return (
+
+
+
+ );
+ },
+);
+
+export default MapView;
diff --git a/src/components/MapView/MapViewTypes.ts b/src/components/MapView/MapViewTypes.ts
new file mode 100644
index 000000000000..cf5abeed02b2
--- /dev/null
+++ b/src/components/MapView/MapViewTypes.ts
@@ -0,0 +1,56 @@
+import {ComponentType} from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
+
+type MapViewProps = {
+ // Public access token to be used to fetch map data from Mapbox.
+ accessToken: string;
+ // Style applied to MapView component. Note some of the View Style props are not available on ViewMap
+ style: StyleProp;
+ // Link to the style JSON document.
+ styleURL?: string;
+ // Whether map can tilt in the vertical direction.
+ pitchEnabled?: boolean;
+ // Padding to apply when the map is adjusted to fit waypoints and directions
+ mapPadding?: number;
+ // Initial coordinate and zoom level
+ initialState?: InitialState;
+ // Locations on which to put markers
+ waypoints?: WayPoint[];
+ // List of coordinates which together forms a direction.
+ directionCoordinates?: Array<[number, number]>;
+};
+
+type DirectionProps = {
+ // Coordinates of points that constitute the direction
+ coordinates: Array<[number, number]>;
+};
+
+// Initial state of the map
+type InitialState = {
+ // Coordinate on which to center the map
+ location: [number, number];
+ zoom: number;
+};
+
+// Waypoint to be displayed on the map
+type WayPoint = {
+ id: string;
+ coordinate: [number, number];
+ markerComponent: ComponentType;
+};
+
+// Style used for the line that displays direction
+type DirectionStyle = {
+ width?: number;
+ color?: string;
+};
+
+// Represents a handle to interact with a map view.
+type MapViewHandle = {
+ // Fly to a location on the map
+ flyTo: (location: [number, number], zoomLevel: number, animationDuration?: number) => void;
+ // Fit the map view to a bounding box
+ fitBounds: (ne: [number, number], sw: [number, number], paddingConfig?: number | number[], animationDuration?: number) => void;
+};
+
+export type {DirectionStyle, WayPoint, MapViewProps, DirectionProps, MapViewHandle};
diff --git a/src/components/MapView/index.js b/src/components/MapView/index.js
new file mode 100644
index 000000000000..551f57e34ed2
--- /dev/null
+++ b/src/components/MapView/index.js
@@ -0,0 +1,3 @@
+import MapView from './MapView';
+
+export default MapView;
diff --git a/src/components/MapView/utils.ts b/src/components/MapView/utils.ts
new file mode 100644
index 000000000000..c37d272296e5
--- /dev/null
+++ b/src/components/MapView/utils.ts
@@ -0,0 +1,13 @@
+function getBounds(waypoints: Array<[number, number]>): {southWest: [number, number]; northEast: [number, number]} {
+ const lngs = waypoints.map((waypoint) => waypoint[0]);
+ const lats = waypoints.map((waypoint) => waypoint[1]);
+
+ return {
+ southWest: [Math.min(...lngs), Math.min(...lats)],
+ northEast: [Math.max(...lngs), Math.max(...lats)],
+ };
+}
+
+export default {
+ getBounds,
+};
diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js
index ef2ebbeac6a1..bf442fd083c5 100644
--- a/src/pages/iou/WaypointEditor.js
+++ b/src/pages/iou/WaypointEditor.js
@@ -4,6 +4,7 @@ import lodashGet from 'lodash/get';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
+import {useIsFocused} from '@react-navigation/native';
import AddressSearch from '../../components/AddressSearch';
import ScreenWrapper from '../../components/ScreenWrapper';
import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView';
@@ -76,6 +77,7 @@ const defaultProps = {
function WaypointEditor({transactionID, route: {params: {iouType = '', waypointIndex = ''} = {}} = {}, transaction, recentWaypoints}) {
const {windowWidth} = useWindowDimensions();
const [isDeleteStopModalOpen, setIsDeleteStopModalOpen] = useState(false);
+ const isFocused = useIsFocused();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
const textInput = useRef(null);
@@ -181,7 +183,7 @@ function WaypointEditor({transactionID, route: {params: {iouType = '', waypointI
onEntryTransitionEnd={() => textInput.current && textInput.current.focus()}
shouldEnableMaxHeight
>
- waypointCount - 1}>
+ waypointCount - 1) && isFocused}>