Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
bd123ec
Init add alternate route for map-based mileage expenses
GCyganek Jul 15, 2026
4bef03f
Fix lint and ts
GCyganek Jul 15, 2026
d02b07d
Code cleanup, adjust styles to Figma
GCyganek Jul 15, 2026
0223d92
Fix directions layers order when switching between directions
GCyganek Jul 15, 2026
72942c4
Revert changes in Direction.tsx
GCyganek Jul 15, 2026
8319296
Add missing geojson types
GCyganek Jul 15, 2026
8676bcc
Merge branch 'main' into @GCyganek/alternate-routes
GCyganek Jul 29, 2026
f1aa5fc
Use selectedRouteDistance prop, add tests
GCyganek Jul 29, 2026
99ed0b9
Fix getSelectedRouteDistance
GCyganek Jul 29, 2026
9ed553f
Fix transactionState
GCyganek Jul 29, 2026
ed1f20d
Clear selectedRouteKey when invalidating routes
GCyganek Jul 29, 2026
1d602e2
Add web alternate routes components
GCyganek Jul 29, 2026
1eed358
Fix expense updates, add tests
GCyganek Jul 30, 2026
63d2059
Always send selectedRouteDistance
GCyganek Jul 31, 2026
6a31934
Fix problems with map expense updates
GCyganek Jul 31, 2026
2866450
Include legacy distance requests when resolving route choice
GCyganek Jul 31, 2026
d8f6942
Use transaction currency for route-change
GCyganek Jul 31, 2026
4073305
Change distance unit for all distance symbols
GCyganek Jul 31, 2026
c6c8b65
Fix eslint in test
GCyganek Jul 31, 2026
6a94d7f
Udpate route selection with the route distance fix
GCyganek Aug 3, 2026
7646a49
Fix preserving manual overwrite for split expense
GCyganek Aug 3, 2026
ad0e44d
Fix showing discard modal
GCyganek Aug 3, 2026
a07a7eb
Fix retry params
GCyganek Aug 3, 2026
b7f31a8
read troutes from updatedTransaction
GCyganek Aug 3, 2026
9466cf5
Recompute violations when selected route key changes
GCyganek Aug 3, 2026
1191b8e
Fix case where re-fetched route slightly differs
GCyganek Aug 3, 2026
ebc582d
getHasUnsavedChanges guard
GCyganek Aug 3, 2026
39450b8
Merge branch 'main' into @GCyganek/alternate-routes
GCyganek Aug 3, 2026
ffa6548
Merge branch 'main' into @GCyganek/alternate-routes
GCyganek Aug 3, 2026
bff3375
Fix getCurrencySymbol
GCyganek Aug 3, 2026
76b40d0
Fix tests
GCyganek Aug 3, 2026
949f623
Fix comment styles
GCyganek Aug 3, 2026
19ed063
Merge branch 'main' into @GCyganek/alternate-routes
GCyganek Aug 4, 2026
242822f
Use pointer cursor over routes
GCyganek Aug 4, 2026
793a7a8
Pass selectedRouteDistance to categorize/share/submit paths
GCyganek Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/eslint/eslint.seatbelt.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@
"../../src/components/DatePicker/index.tsx" "react-hooks/set-state-in-effect" 1
"../../src/components/DialogLabelContext.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/DisplayNames/DisplayNamesWithTooltip.tsx" "react-hooks/refs" 6
"../../src/components/DistanceRequest/DistanceRequestFooter.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/DistanceRequest/DistanceRequestFooter.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
"../../src/components/DistanceRequest/DistanceRequestFooter.tsx" "no-restricted-imports" 1
"../../src/components/DistanceRequest/DistanceRequestRenderItem.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
"../../src/components/Domain/CopyableTextField.tsx" "no-restricted-imports" 1
Expand Down
23 changes: 10 additions & 13 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@
"@types/canvas-size": "^1.2.2",
"@types/concurrently": "^7.0.3",
"@types/d3-scale": "^4.0.9",
"@types/geojson": "^7946.0.16",
"@types/howler": "^2.2.12",
"@types/jest": "^29.5.14",
"@types/jest-when": "^3.5.2",
Expand Down
10 changes: 10 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2438,6 +2438,8 @@ const CONST = {
TRANSACTION: {
RESULTS_PAGE_SIZE: 20,
DEFAULT_MERCHANT: 'Expense',
DEFAULT_ROUTE_KEY: 'route0',
ALTERNATE_ROUTE_KEY: 'route1',
UNKNOWN_MERCHANT: 'Unknown Merchant',
PARTIAL_TRANSACTION_MERCHANT: '(none)',
TYPE: {
Expand Down Expand Up @@ -9318,6 +9320,14 @@ const CONST = {
WAYPOINTS: 'waypoints',
},

ALTERNATE_DIRECTIONS_MAP_VIEW_LAYERS: {
SOURCE: 'alternate-directions-route-source',
UNSELECTED_FILL: 'alternate-directions-unselected-route-fill',
UNSELECTED_BORDER: 'alternate-directions-unselected-route-border',
SELECTED_FILL: 'alternate-directions-selected-route-fill',
SELECTED_BORDER: 'alternate-directions-selected-route-border',
},

MAP_CURRENT_LOCATION_FILL_COLOR: '#0185FF',

PARTNER_ID: {
Expand Down
5 changes: 3 additions & 2 deletions src/components/ConfirmedRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';

import getArrayDepth from '@libs/getArrayDepth';
import {getWaypointIndex} from '@libs/TransactionUtils';
import {getSelectedRouteKey, getWaypointIndex} from '@libs/TransactionUtils';

import {init as initMapboxToken, stop as stopMapboxToken} from '@userActions/MapboxToken';

Expand Down Expand Up @@ -45,7 +45,8 @@ type ConfirmedRouteProps = {

function ConfirmedRoute({transaction, isSmallerIcon, shouldHaveBorderRadius = true, requireRouteToDisplayMap = false, interactive, shouldDisplayCompass = true}: ConfirmedRouteProps) {
const {isOffline} = useNetwork();
const {route0: route} = transaction?.routes ?? {};
const selectedRouteKey = getSelectedRouteKey(transaction);
const route = transaction?.routes?.[selectedRouteKey] ?? transaction?.routes?.[CONST.TRANSACTION.DEFAULT_ROUTE_KEY];
const waypoints = transaction?.comment?.waypoints ?? {};
const coordinates = route?.geometry?.coordinates ?? [];
const styles = useThemeStyles();
Expand Down
38 changes: 34 additions & 4 deletions src/components/DistanceRequest/DistanceRequestFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,16 @@ import useOnyx from '@hooks/useOnyx';
import usePolicy from '@hooks/usePolicy';
import useThemeStyles from '@hooks/useThemeStyles';

import {setSelectedRoute} from '@libs/actions/Transaction';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import {getDistanceInMeters, getWaypointIndex, isCustomUnitRateIDForP2P} from '@libs/TransactionUtils';
import {getSelectedRouteKey, getWaypointIndex, isCustomUnitRateIDForP2P} from '@libs/TransactionUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Policy} from '@src/types/onyx';
import type {WaypointCollection} from '@src/types/onyx/Transaction';
import type Transaction from '@src/types/onyx/Transaction';
import type TransactionState from '@src/types/utils/TransactionStateType';

import type {StyleProp, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
Expand All @@ -41,9 +43,12 @@ type DistanceRequestFooterProps = {

/** Optional style for the map container */
mapContainerStyle?: StyleProp<ViewStyle>;

/** The state of the transaction (draft, current, etc.) used to persist route selection to the correct Onyx key */
transactionState: TransactionState;
};

function DistanceRequestFooter({waypoints, transaction, navigateToWaypointEditPage, policy, mapContainerStyle}: DistanceRequestFooterProps) {
function DistanceRequestFooter({waypoints, transaction, navigateToWaypointEditPage, policy, mapContainerStyle, transactionState}: DistanceRequestFooterProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Plus']);
Expand All @@ -60,6 +65,21 @@ function DistanceRequestFooter({waypoints, transaction, navigateToWaypointEditPa
const policyCurrency = (policy ?? activePolicy ?? personalPolicy)?.outputCurrency ?? CONST.CURRENCY.USD;
const mileageRate = isCustomUnitRateIDForP2P(transaction) ? DistanceRequestUtils.getRateForP2P(policyCurrency, transaction) : defaultMileageRate;
const {unit} = mileageRate ?? {};
const primaryRoute = transaction?.routes?.[CONST.TRANSACTION.DEFAULT_ROUTE_KEY];
const alternateRoute = transaction?.routes?.[CONST.TRANSACTION.ALTERNATE_ROUTE_KEY];
const isAlternateDirectionSelected = getSelectedRouteKey(transaction) === CONST.TRANSACTION.ALTERNATE_ROUTE_KEY;
const handleRouteSelection = (isAlternate: boolean) => {
if (isAlternate === isAlternateDirectionSelected) {
return;
}
setSelectedRoute(
transaction?.transactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID,
isAlternate ? CONST.TRANSACTION.ALTERNATE_ROUTE_KEY : CONST.TRANSACTION.DEFAULT_ROUTE_KEY,
(isAlternate ? alternateRoute : primaryRoute)?.distance ?? undefined,
DistanceRequestUtils.getDistanceUnit(transaction, mileageRate),
transactionState,
Comment thread
GCyganek marked this conversation as resolved.
);
};

const waypointMarkers: WayPoint[] = [];
for (const [key, waypoint] of Object.entries(waypoints ?? {})) {
Expand Down Expand Up @@ -105,12 +125,22 @@ function DistanceRequestFooter({waypoints, transaction, navigateToWaypointEditPa
zoom: CONST.MAPBOX.DEFAULT_ZOOM,
location: waypointMarkers?.at(0)?.coordinate ?? CONST.MAPBOX.DEFAULT_COORDINATE,
}}
directionCoordinates={(transaction?.routes?.route0?.geometry?.coordinates as Array<[number, number]>) ?? []}
directionCoordinates={(primaryRoute?.geometry?.coordinates as Array<[number, number]>) ?? []}
alternateDirection={
alternateRoute?.geometry?.coordinates && alternateRoute?.distance
? {
coordinates: (alternateRoute.geometry.coordinates as Array<[number, number]>) ?? [],
distanceInMeters: alternateRoute.distance,
isSelected: isAlternateDirectionSelected,
}
: undefined
}
setIsAlternateDirectionSelected={handleRouteSelection}
style={[styles.mapView, styles.mapEditView]}
waypoints={waypointMarkers}
styleURL={CONST.MAPBOX.STYLE_URL}
overlayStyle={styles.mapEditView}
distanceInMeters={getDistanceInMeters(transaction, undefined)}
distanceInMeters={primaryRoute?.distance !== null ? primaryRoute?.distance : undefined}
unit={unit}
/>
</View>
Expand Down
51 changes: 51 additions & 0 deletions src/components/MapView/AlternateDirections.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import useThemeStyles from '@hooks/useThemeStyles';

import Mapbox from '@rnmapbox/maps';

import type {AlternateDirectionsProps} from './MapViewTypes';

import {getAlternateDirectionsShape, SELECTED_BORDER_ID, SELECTED_FILL_ID, SOURCE_ID, UNSELECTED_BORDER_ID, UNSELECTED_FILL_ID} from './alternateDirectionsUtils';

function AlternateDirections({directionCoordinates, alternateDirection, setIsAlternateDirectionSelected}: AlternateDirectionsProps) {
const styles = useThemeStyles();
const directionShape = getAlternateDirectionsShape(directionCoordinates, alternateDirection);

return (
<Mapbox.ShapeSource
id={SOURCE_ID}
shape={directionShape}
onPress={({features}) => {
const properties = features.at(0)?.properties;
if (typeof properties?.isAlternate !== 'boolean') {
return;
}
setIsAlternateDirectionSelected?.(properties.isAlternate);
}}
>
<Mapbox.LineLayer
id={UNSELECTED_FILL_ID}
filter={['==', ['get', 'isSelected'], false]}
style={styles.alternativeMapDirection}
/>
<Mapbox.LineLayer
id={UNSELECTED_BORDER_ID}
belowLayerID={UNSELECTED_FILL_ID}
filter={['==', ['get', 'isSelected'], false]}
style={styles.mapDirectionBorder}
/>
<Mapbox.LineLayer
id={SELECTED_FILL_ID}
filter={['==', ['get', 'isSelected'], true]}
style={styles.mapDirection}
/>
<Mapbox.LineLayer
id={SELECTED_BORDER_ID}
belowLayerID={SELECTED_FILL_ID}
filter={['==', ['get', 'isSelected'], true]}
style={styles.mapDirectionBorder}
/>
</Mapbox.ShapeSource>
);
}

export default AlternateDirections;
72 changes: 72 additions & 0 deletions src/components/MapView/AlternateDirections.web.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import useThemeStyles from '@hooks/useThemeStyles';

import type {FilterSpecification} from 'mapbox-gl';

import React from 'react';
import {Layer, Source} from 'react-map-gl/mapbox';

import type {AlternateDirectionsProps} from './MapViewTypes';

import {getAlternateDirectionsShape, SELECTED_BORDER_ID, SELECTED_FILL_ID, SOURCE_ID, UNSELECTED_BORDER_ID, UNSELECTED_FILL_ID} from './alternateDirectionsUtils';

/** Layers that must be interactive on the map so that tapping a route selects it. */
const ALTERNATE_DIRECTIONS_LAYER_IDS = [UNSELECTED_BORDER_ID, UNSELECTED_FILL_ID, SELECTED_BORDER_ID, SELECTED_FILL_ID];

const UNSELECTED_FILTER: FilterSpecification = ['==', ['get', 'isSelected'], false];
const SELECTED_FILTER: FilterSpecification = ['==', ['get', 'isSelected'], true];

function AlternateDirections({directionCoordinates, alternateDirection}: AlternateDirectionsProps) {
const styles = useThemeStyles();
const layerLayoutStyle: Record<string, string> = styles.mapDirectionLayer.layout;
const layerPaintStyle: Record<string, string | number> = styles.mapDirectionLayer.paint;
const alternativeLayerLayoutStyle: Record<string, string> = styles.alternativeMapDirectionLayer.layout;
const alternativeLayerPaintStyle: Record<string, string | number> = styles.alternativeMapDirectionLayer.paint;
const layerBorderLayoutStyle: Record<string, string> = styles.mapDirectionLayerBorder.layout;
const layerBorderPaintStyle: Record<string, string | number> = styles.mapDirectionLayerBorder.paint;

const directionShape = getAlternateDirectionsShape(directionCoordinates, alternateDirection);

return (
<Source
id={SOURCE_ID}
type="geojson"
data={directionShape}
>
<Layer
id={UNSELECTED_BORDER_ID}
type="line"
source={SOURCE_ID}
filter={UNSELECTED_FILTER}
paint={layerBorderPaintStyle}
layout={layerBorderLayoutStyle}
/>
<Layer
id={UNSELECTED_FILL_ID}
type="line"
source={SOURCE_ID}
filter={UNSELECTED_FILTER}
paint={alternativeLayerPaintStyle}
layout={alternativeLayerLayoutStyle}
/>
<Layer
id={SELECTED_BORDER_ID}
type="line"
source={SOURCE_ID}
filter={SELECTED_FILTER}
paint={layerBorderPaintStyle}
layout={layerBorderLayoutStyle}
/>
<Layer
id={SELECTED_FILL_ID}
type="line"
source={SOURCE_ID}
filter={SELECTED_FILTER}
paint={layerPaintStyle}
layout={layerLayoutStyle}
/>
</Source>
);
}

export default AlternateDirections;
export {ALTERNATE_DIRECTIONS_LAYER_IDS};
54 changes: 54 additions & 0 deletions src/components/MapView/Directions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type {DirectionsProps} from './MapViewTypes';

import AlternateDirections from './AlternateDirections';
import Direction from './Direction';
import DistanceSymbol from './DistanceSymbol';
import useDistanceUnit from './useDistanceUnit';
import utils from './utils';

function Directions({directionCoordinates, alternateDirection, setIsAlternateDirectionSelected, distanceInMeters, unit, waypoints}: DirectionsProps) {
// Held here rather than in each DistanceSymbol so that toggling one label switches the unit for every label on the map.
const {distanceUnit, toggleDistanceUnit} = useDistanceUnit(unit);

if (!directionCoordinates) {
return null;
}

const alternateDirectionCoordinates = alternateDirection?.coordinates;
const hasAlternateDirection = !!alternateDirection && !!alternateDirectionCoordinates?.length;
const isAlternateDirectionSelected = !!alternateDirection?.isSelected;

return (
<>
{hasAlternateDirection ? (
<>
<AlternateDirections
directionCoordinates={directionCoordinates}
alternateDirection={alternateDirection}
setIsAlternateDirectionSelected={setIsAlternateDirectionSelected}
/>
<DistanceSymbol
distanceInMeters={alternateDirection.distanceInMeters}
distanceUnit={distanceUnit}
toggleDistanceUnit={toggleDistanceUnit}
directionCoordinates={utils.convertSegmentedRouteToSingleSegmentRoute(alternateDirectionCoordinates)}
waypoints={waypoints}
isSelected={isAlternateDirectionSelected}
/>
</>
) : (
<Direction coordinates={directionCoordinates} />
)}
<DistanceSymbol
distanceInMeters={distanceInMeters}
distanceUnit={distanceUnit}
toggleDistanceUnit={toggleDistanceUnit}
directionCoordinates={utils.convertSegmentedRouteToSingleSegmentRoute(directionCoordinates)}
waypoints={waypoints}
isSelected={!isAlternateDirectionSelected}
/>
</>
);
}

export default Directions;
Loading
Loading