Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
7be9ce2
Create GPS expenses using captured data
GCyganek Jan 19, 2026
096842c
Merge branch 'main' into @GCyganek/gps/create-gps-expenses
GCyganek Jan 20, 2026
d00a092
fix spellcheck
GCyganek Jan 20, 2026
0ee013c
Fix type error
GCyganek Jan 20, 2026
2172c3f
Replace Onyx.connect with params
GCyganek Jan 20, 2026
357945b
Merge branch 'main' into @GCyganek/gps/create-gps-expenses
GCyganek Jan 21, 2026
561271e
Revert Mobile-Expensify hash change
GCyganek Jan 21, 2026
c127bfa
do not require passing firstGpsExpenseDate param
GCyganek Jan 21, 2026
6999d5e
Revert IOURequestStepScan changes
GCyganek Jan 21, 2026
ca67a3e
Fix updating distance when changing between participants with differe…
GCyganek Jan 21, 2026
8026c0a
GPSDraftDetailsUtils
GCyganek Jan 21, 2026
3739020
Merge branch 'main' into @GCyganek/gps/create-gps-expenses
GCyganek Jan 21, 2026
327a4eb
Fix eslint warnings
GCyganek Jan 21, 2026
78cc508
Changes according to feedback
GCyganek Jan 21, 2026
183f6b1
Merge branch 'main' into @GCyganek/gps/create-gps-expenses
GCyganek Jan 21, 2026
61a4532
Merge branch 'main' into @GCyganek/gps/create-gps-expenses
GCyganek Jan 22, 2026
092445e
Revert mobile-expensify hash change
GCyganek Jan 22, 2026
2de4064
Add allBetas
GCyganek Jan 22, 2026
72b96c5
Temporary fix to tell GPS expense from Map expense
GCyganek Jan 22, 2026
94e49e6
Add missing dependency array
GCyganek Jan 22, 2026
966e5ed
Merge branch 'main' into @GCyganek/gps/create-gps-expenses
GCyganek Jan 22, 2026
f662e0c
shouldUseDefaultExpensePolicy
GCyganek Jan 22, 2026
a6714b3
Merge branch 'main' into @GCyganek/gps/create-gps-expenses
GCyganek Jan 23, 2026
7c22f9d
Typescript error fix
GCyganek Jan 23, 2026
9a47468
Fix distance conversion when no default participant
GCyganek Jan 23, 2026
caba976
revert firstCreatedGpsExpenseDateNewDot changes for now
GCyganek Jan 23, 2026
8488681
Merge branch 'main' into @GCyganek/gps/create-gps-expenses
GCyganek Jan 23, 2026
ab94746
ESLint fix
GCyganek Jan 23, 2026
da148b2
Merge branch 'main' into @GCyganek/gps/create-gps-expenses
GCyganek Jan 23, 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
20 changes: 20 additions & 0 deletions src/components/MoneyRequestConfirmationList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
setIndividualShare,
setMoneyRequestAmount,
setMoneyRequestCategory,
setMoneyRequestDistance,
setMoneyRequestMerchant,
setMoneyRequestPendingFields,
setMoneyRequestTag,
Expand All @@ -35,6 +36,7 @@ import {getIsMissingAttendeesViolation} from '@libs/AttendeeUtils';
import {isCategoryDescriptionRequired} from '@libs/CategoryUtils';
import {convertToBackendAmount, convertToDisplayString, convertToDisplayStringWithoutCurrency, getCurrencyDecimals} from '@libs/CurrencyUtils';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import {calculateGPSDistance} from '@libs/GPSDraftDetailsUtils';
import {calculateAmount, insertTagIntoTransactionTagsString, isMovingTransactionFromTrackExpense as isMovingTransactionFromTrackExpenseUtil} from '@libs/IOUUtils';
import Log from '@libs/Log';
import {validateAmount} from '@libs/MoneyRequestUtils';
Expand Down Expand Up @@ -164,6 +166,9 @@ type MoneyRequestConfirmationListProps = {
/** Whether the expense is an odometer distance expense */
isOdometerDistanceRequest?: boolean;

/** Whether the expense is a GPS distance expense */
isGPSDistanceRequest: boolean;

/** Whether the expense is a per diem expense */
isPerDiemRequest?: boolean;

Expand Down Expand Up @@ -229,6 +234,7 @@ function MoneyRequestConfirmationList({
isDistanceRequest,
isManualDistanceRequest,
isOdometerDistanceRequest = false,
isGPSDistanceRequest,
isPerDiemRequest = false,
isPolicyExpenseChat = false,
iouCategory = '',
Expand Down Expand Up @@ -388,6 +394,19 @@ function MoneyRequestConfirmationList({

const distanceRequestAmount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit ?? CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, rate ?? 0);

// Update GPS distance whenever the current distance unit differs from the one that was used
// to calculate the distance stored in transaction.comment.customUnit.quantity
const gpsDistance = transaction?.comment?.customUnit?.quantity;
const gpsDistanceWithCurrentDistanceUnit = calculateGPSDistance(distance, unit);
const shouldUpdateGpsDistance = gpsDistance !== gpsDistanceWithCurrentDistanceUnit;
useEffect(() => {
if (!shouldUpdateGpsDistance || !transactionID || isReadOnly) {
return;
}

setMoneyRequestDistance(transactionID, gpsDistanceWithCurrentDistanceUnit, true);
}, [shouldUpdateGpsDistance, transactionID, isReadOnly, gpsDistanceWithCurrentDistanceUnit]);

let amountToBeUsed = iouAmount;

if (shouldCalculateDistanceAmount) {
Expand Down Expand Up @@ -1219,6 +1238,7 @@ function MoneyRequestConfirmationList({
isDistanceRequest={isDistanceRequest}
isManualDistanceRequest={isManualDistanceRequest}
isOdometerDistanceRequest={isOdometerDistanceRequest}
isGPSDistanceRequest={isGPSDistanceRequest}
isPerDiemRequest={isPerDiemRequest}
isTimeRequest={isTimeRequest}
isMerchantEmpty={isMerchantEmpty}
Expand Down
8 changes: 6 additions & 2 deletions src/components/MoneyRequestConfirmationListFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,9 @@ type MoneyRequestConfirmationListFooterProps = {
/** Flag indicating if it is an odometer distance request */
isOdometerDistanceRequest?: boolean;

/** Flag indicating if it is a GPS distance request */
isGPSDistanceRequest: boolean;

/** Flag indicating if it is a per diem request */
isPerDiemRequest: boolean;

Expand Down Expand Up @@ -249,6 +252,7 @@ function MoneyRequestConfirmationListFooter({
isDistanceRequest,
isManualDistanceRequest,
isOdometerDistanceRequest = false,
isGPSDistanceRequest,
isPerDiemRequest,
isTimeRequest,
isMerchantEmpty,
Expand Down Expand Up @@ -510,7 +514,7 @@ function MoneyRequestConfirmationListFooter({
item: (
<MenuItemWithTopDescription
key={translate('common.distance')}
shouldShowRightIcon={!isReadOnly}
shouldShowRightIcon={!isReadOnly && !isGPSDistanceRequest}
title={DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate)}
description={translate('common.distance')}
style={[styles.moneyRequestMenuItem]}
Expand All @@ -533,7 +537,7 @@ function MoneyRequestConfirmationListFooter({
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID));
}}
disabled={didConfirm}
interactive={!isReadOnly}
interactive={!isReadOnly && !isGPSDistanceRequest}
/>
),
shouldShow: isDistanceRequest,
Expand Down
12 changes: 9 additions & 3 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,9 @@ import {
isCategoryBeingAnalyzed,
isDistanceRequest as isDistanceRequestTransactionUtils,
isExpenseUnreported as isExpenseUnreportedTransactionUtils,
isGPSDistanceRequest as isGPSDistanceRequestTransactionUtils,
isManualDistanceRequest as isManualDistanceRequestTransactionUtils,
isMapDistanceRequest as isMapDistanceRequestTransactionUtils,
isOdometerDistanceRequest as isOdometerDistanceRequestTransactionUtils,
isPerDiemRequest as isPerDiemRequestTransactionUtils,
isScanning,
Expand Down Expand Up @@ -282,8 +284,9 @@ function MoneyRequestView({
transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transactionMerchant === CONST.TRANSACTION.DEFAULT_MERCHANT;
const isDistanceRequest = isDistanceRequestTransactionUtils(transaction);
const isManualDistanceRequest = isManualDistanceRequestTransactionUtils(transaction, !!mergeTransactionID);
const isGPSDistanceRequest = isGPSDistanceRequestTransactionUtils(transaction);
const isOdometerDistanceRequest = isOdometerDistanceRequestTransactionUtils(transaction);
const isMapDistanceRequest = isDistanceRequest && !isManualDistanceRequest;
const isMapDistanceRequest = isMapDistanceRequestTransactionUtils(transaction);
const isTransactionScanning = isScanning(updatedTransaction ?? transaction);
const hasRoute = hasRouteTransactionUtils(transactionBackup ?? transaction, isDistanceRequest);

Expand Down Expand Up @@ -336,14 +339,17 @@ function MoneyRequestView({

const canEditTaxFields = canEdit && !isDistanceRequest;
const canEditAmount =
isEditable && (canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.AMOUNT, undefined, isChatReportArchived) || (isExpenseSplit && isSplitAvailable));
!isGPSDistanceRequest &&
isEditable &&
(canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.AMOUNT, undefined, isChatReportArchived) || (isExpenseSplit && isSplitAvailable));
const canEditMerchant =
isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.MERCHANT, undefined, isChatReportArchived, undefined, transaction, moneyRequestReport, policy);

const canEditDate =
isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DATE, undefined, isChatReportArchived, undefined, transaction, moneyRequestReport, policy);

const canEditDistance =
!isGPSDistanceRequest &&
isEditable &&
canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.DISTANCE, undefined, isChatReportArchived, undefined, transaction, moneyRequestReport, policy) &&
isPolicyAccessible(policy, currentUserEmailParam);
Expand Down Expand Up @@ -897,7 +903,7 @@ function MoneyRequestView({
copyable={!!descriptionCopyValue}
/>
</OfflineWithFeedback>
{isManualDistanceRequest || isOdometerDistanceRequest || (isMapDistanceRequest && transaction?.comment?.waypoints) ? (
{isManualDistanceRequest || isGPSDistanceRequest || isOdometerDistanceRequest || (isMapDistanceRequest && transaction?.comment?.waypoints) ? (
Comment thread
GCyganek marked this conversation as resolved.
distanceRequestFields
) : (
<OfflineWithFeedback pendingAction={getPendingFieldAction('merchant')}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {canEditMoneyRequest, getTransactionDetails, isPolicyExpenseChat, isRepor
import StringUtils from '@libs/StringUtils';
import type {TranslationPathOrText} from '@libs/TransactionPreviewUtils';
import {createTransactionPreviewConditionals, getIOUPayerAndReceiver, getTransactionPreviewTextAndTranslationPaths} from '@libs/TransactionPreviewUtils';
import {isManagedCardTransaction as isCardTransactionUtils, isMapDistanceRequest, isScanning} from '@libs/TransactionUtils';
import {isManagedCardTransaction as isCardTransactionUtils, isGPSDistanceRequest, isMapDistanceRequest, isScanning} from '@libs/TransactionUtils';
import ViolationsUtils from '@libs/Violations/ViolationsUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -241,7 +241,7 @@ function TransactionPreviewContent({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
isHovered={isHovered || isTransactionScanning}
size={1}
shouldUseAspectRatio={!isMapDistanceRequest(transaction)}
shouldUseAspectRatio={!isMapDistanceRequest(transaction) && !isGPSDistanceRequest(transaction)}
/>
{shouldShowSkeleton ? (
<TransactionPreviewSkeletonView transactionPreviewWidth={transactionPreviewWidth} />
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/useFetchRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {deepEqual} from 'fast-equals';
import {useEffect} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {getRoute} from '@libs/actions/Transaction';
import {getValidWaypoints, hasRoute as hasRouteTransactionUtils, isDistanceRequest, isManualDistanceRequest} from '@libs/TransactionUtils';
import {getValidWaypoints, hasRoute as hasRouteTransactionUtils, isMapDistanceRequest as isMapDistanceRequestTransactionUtils} from '@libs/TransactionUtils';
import type {IOUAction} from '@src/CONST';
import CONST from '@src/CONST';
import type {Transaction} from '@src/types/onyx';
Expand All @@ -25,7 +25,7 @@ export default function useFetchRoute(
const validatedWaypoints = getValidWaypoints(waypoints);
const previousValidatedWaypoints = usePrevious(validatedWaypoints);
const haveValidatedWaypointsChanged = !deepEqual(previousValidatedWaypoints, validatedWaypoints);
const isMapDistanceRequest = isDistanceRequest(transaction) && !isManualDistanceRequest(transaction);
const isMapDistanceRequest = isMapDistanceRequestTransactionUtils(transaction);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This caused an issue of not showing the route for split distance request.

const shouldFetchRoute = isMapDistanceRequest && (isRouteAbsentWithoutErrors || haveValidatedWaypointsChanged) && !isLoadingRoute && Object.keys(validatedWaypoints).length > 1;

useEffect(() => {
Expand Down
1 change: 1 addition & 0 deletions src/libs/API/parameters/CreateDistanceRequestParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type CreateDistanceRequestParams = {
distance?: number;
odometerStart?: number;
odometerEnd?: number;
gpsCoordinates?: string;
customUnitPolicyID?: string;
};

Expand Down
1 change: 1 addition & 0 deletions src/libs/API/parameters/TrackExpenseParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type TrackExpenseParams = {
customUnitRateID?: string;
description?: string;
distance?: number;
gpsCoordinates?: string;
isDistance?: boolean;
};

Expand Down
90 changes: 90 additions & 0 deletions src/libs/GPSDraftDetailsUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {reverseGeocodeAsync} from 'expo-location';
import type {GpsDraftDetails} from '@src/types/onyx';
import type {Unit} from '@src/types/onyx/Policy';
import type {Routes, WaypointCollection} from '@src/types/onyx/Transaction';
import DistanceRequestUtils from './DistanceRequestUtils';
import {roundToTwoDecimalPlaces} from './NumberUtils';

function getGPSWaypoints(gpsDraftDetails: GpsDraftDetails | undefined): WaypointCollection {
const gpsCoordinates = gpsDraftDetails?.gpsPoints ?? [];
const firstPoint = gpsCoordinates.at(0);
const lastPoint = gpsCoordinates.at(-1);
const startAddress = gpsDraftDetails?.startAddress.value ?? '';
const endAddress = gpsDraftDetails?.endAddress.value ?? '';

return {
...(firstPoint
? {
waypoint0: {
keyForList: 'gps_start', // temporary for hasGPSWaypoints()
lat: firstPoint.lat,
lng: firstPoint.long,
address: startAddress,
name: startAddress,
},
}
: {}),
...(lastPoint
? {
waypoint1: {
keyForList: 'gps_stop', // temporary for hasGPSWaypoints()
lat: lastPoint.lat,
lng: lastPoint.long,
address: endAddress,
name: endAddress,
},
}
: {}),
};
Comment thread
GCyganek marked this conversation as resolved.
}

function getGPSRoutes(gpsDraftDetails: GpsDraftDetails | undefined): Routes {
const distanceInMeters = roundToTwoDecimalPlaces(gpsDraftDetails?.distanceInMeters ?? 0);
const gpsCoordinates = gpsDraftDetails?.gpsPoints ?? [];

return {
route0: {
distance: distanceInMeters,
geometry: {
type: 'LineString',
coordinates: gpsCoordinates.map(({lat, long}) => [long, lat]),
},
},
};
}

function getGPSCoordinates(gpsDraftDetails: GpsDraftDetails | undefined): string | undefined {
return gpsDraftDetails?.gpsPoints ? JSON.stringify(gpsDraftDetails.gpsPoints.map((val) => ({lng: val.long, lat: val.lat}))) : undefined;
}

function calculateGPSDistance(distanceInMeters: number, unit: Unit): number {
return roundToTwoDecimalPlaces(DistanceRequestUtils.convertDistanceUnit(distanceInMeters, unit));
}

function getGPSConvertedDistance(gpsDraftDetails: GpsDraftDetails | undefined, unit: Unit): number {
const distanceInMeters = gpsDraftDetails?.distanceInMeters ?? 0;
return calculateGPSDistance(distanceInMeters, unit);
}

async function addressFromGpsPoint(gpsPoint: {lat: number; long: number}): Promise<string | null> {
try {
const [location] = await reverseGeocodeAsync({latitude: gpsPoint.lat, longitude: gpsPoint.long});

if (!location) {
return null;
}

const address: string = location?.formattedAddress ?? [location?.name, location?.city, location?.region].filter(Boolean).join(', ');

return address;
} catch (error) {
console.error('[GPS distance request] Failed to reverse geocode location to postal address: ', error);
Comment thread
GCyganek marked this conversation as resolved.
return null;
}
}

function coordinatesToString(gpsPoint: {lat: number; long: number}): string {
return `${gpsPoint.lat},${gpsPoint.long}`;
}

export {getGPSRoutes, getGPSWaypoints, getGPSConvertedDistance, getGPSCoordinates, addressFromGpsPoint, coordinatesToString, calculateGPSDistance};
1 change: 1 addition & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,7 @@ type TransactionDetails = {
odometerStart?: number;
odometerEnd?: number;
convertedAmount: number;
gpsCoordinates?: string;
};

type OptimisticIOUReport = Pick<
Expand Down
41 changes: 38 additions & 3 deletions src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ type TransactionParams = {
distance?: number;
odometerStart?: number;
odometerEnd?: number;
gpsCoordinates?: string;
type?: ValueOf<typeof CONST.TRANSACTION.TYPE>;
count?: number;
rate?: number;
Expand Down Expand Up @@ -149,8 +150,9 @@ function isDistanceRequest(transaction: OnyxEntry<Transaction>): boolean {
return (
transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE ||
transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MAP ||
transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL ||
transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER
transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER ||
transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_GPS ||
transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL
);
}

Expand All @@ -168,14 +170,43 @@ function isDistanceTypeRequest(transaction: OnyxEntry<Transaction>): boolean {
return hasDistanceCustomUnit(transaction);
}

/**
* todo: Currently there is no way to tell server map transaction object from
* server GPS transaction object, this will be discussed and updated later.
* To fix this temporarily we set keyForList of GPS waypoints to 'gps_start' and 'gps_end'
* and use that to determine if it's a GPS or Map transaction. This should be changed before
* the first GPS release.
*/
function hasGPSWaypoints(transaction: OnyxEntry<Transaction>) {
const waypoints = transaction?.comment?.waypoints;

if (!waypoints) {
return false;
}

const waypoint = Object.values(waypoints).at(0);

return !!waypoint?.keyForList?.startsWith('gps');
}

function isMapDistanceRequest(transaction: OnyxEntry<Transaction>): boolean {
// This is used during the expense creation flow before the transaction has been saved to the server
if (lodashHas(transaction, 'iouRequestType')) {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MAP;
}

// This is the case for transaction objects once they have been saved to the server
return hasDistanceCustomUnit(transaction);
return hasDistanceCustomUnit(transaction) && !hasGPSWaypoints(transaction);
}

function isGPSDistanceRequest(transaction: OnyxEntry<Transaction>): boolean {
// This is used during the expense creation flow before the transaction has been saved to the server
if (lodashHas(transaction, 'iouRequestType')) {
return transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_GPS;
}

// This is the case for transaction objects once they have been saved to the server
return hasGPSWaypoints(transaction);
}

function isManualDistanceRequest(transaction: OnyxEntry<Transaction>, isUpdatedMergeTransaction = false): boolean {
Expand Down Expand Up @@ -271,6 +302,9 @@ function getRequestType(transaction: OnyxEntry<Transaction>): IOURequestType {
if (isTimeRequest(transaction)) {
return CONST.IOU.REQUEST_TYPE.TIME;
}
if (isGPSDistanceRequest(transaction)) {
return CONST.IOU.REQUEST_TYPE.DISTANCE_GPS;
}

return CONST.IOU.REQUEST_TYPE.MANUAL;
}
Expand Down Expand Up @@ -2693,6 +2727,7 @@ export {
getValidDuplicateTransactionIDs,
isDistanceRequest,
isMapDistanceRequest,
isGPSDistanceRequest,
isManualDistanceRequest,
isOdometerDistanceRequest,
isFetchingWaypointsFromServer,
Expand Down
Loading
Loading