From 87e07f1d4a373843f4eaa52a2e700dcc843fad7f Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 3 Feb 2026 12:35:43 -0800 Subject: [PATCH 1/6] Sanitize waypoint fields before sending to API This enforces a strict schema for waypoint objects to prevent data pollution. Only name, address, lat, and lng fields are now included when sending waypoints to the API. All other fields (like keyForList, pendingAction, city, state, etc.) are stripped out. Fixes https://github.com/Expensify/Expensify/issues/502939 Co-authored-by: Cursor --- src/libs/actions/IOU/index.ts | 10 ++-- src/libs/actions/Transaction.ts | 32 +++++++++---- tests/unit/TransactionTest.ts | 83 ++++++++++++++++++++++++++++++++- 3 files changed, 109 insertions(+), 16 deletions(-) diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 446434d5c53d8..f50e416d3a7da 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -234,7 +234,7 @@ import {buildOptimisticPolicyRecentlyUsedTags} from '@userActions/Policy/Tag'; import type {GuidedSetupData} from '@userActions/Report'; import {buildInviteToRoomOnyxData, completeOnboarding, notifyNewAction, optimisticReportLastData} from '@userActions/Report'; import {clearAllRelatedReportActionErrors} from '@userActions/ReportActions'; -import {mergeTransactionIdsHighlightOnSearchRoute, sanitizeRecentWaypoints} from '@userActions/Transaction'; +import {mergeTransactionIdsHighlightOnSearchRoute, sanitizeWaypointsForAPI} from '@userActions/Transaction'; import {removeDraftTransaction, removeDraftTransactions} from '@userActions/TransactionEdit'; import {getOnboardingMessages} from '@userActions/Welcome/OnboardingFlow'; import type {OnboardingCompanySize} from '@userActions/Welcome/OnboardingFlow'; @@ -5502,7 +5502,7 @@ function updateMoneyRequestDistance({ parentReportNextStep, }: UpdateMoneyRequestDistanceParams) { const transactionChanges: TransactionChanges = { - ...(waypoints && {waypoints: sanitizeRecentWaypoints(waypoints)}), + ...(waypoints && {waypoints: sanitizeWaypointsForAPI(waypoints)}), routes, ...(distance && {distance}), ...(odometerStart !== undefined && {odometerStart}), @@ -6399,7 +6399,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation): {iouRep const testDriveCommentReportActionID = isTestDrive ? NumberUtils.rand64() : undefined; - const sanitizedWaypoints = waypoints ? JSON.stringify(sanitizeRecentWaypoints(waypoints)) : undefined; + const sanitizedWaypoints = waypoints ? JSON.stringify(sanitizeWaypointsForAPI(waypoints)) : undefined; // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = isMoneyRequestReportReportUtils(report); @@ -6789,7 +6789,7 @@ function trackExpense(params: CreateTrackExpenseParams) { // Pass an open receipt so the distance expense will show a map with the route optimistically const trackedReceipt = validWaypoints ? {source: ReceiptGeneric as ReceiptSource, state: CONST.IOU.RECEIPT_STATE.OPEN, name: 'receipt-generic.png'} : receipt; - const sanitizedWaypoints = validWaypoints ? JSON.stringify(sanitizeRecentWaypoints(validWaypoints)) : undefined; + const sanitizedWaypoints = validWaypoints ? JSON.stringify(sanitizeWaypointsForAPI(validWaypoints)) : undefined; const retryParams: CreateTrackExpenseParams = { ...params, @@ -7749,7 +7749,7 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest let parameters: CreateDistanceRequestParams; let onyxData: OnyxData; - const sanitizedWaypoints = !isManualDistanceRequest ? sanitizeRecentWaypoints(validWaypoints) : null; + const sanitizedWaypoints = !isManualDistanceRequest ? sanitizeWaypointsForAPI(validWaypoints) : null; if (iouType === CONST.IOU.TYPE.SPLIT) { const { splitData, diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index f8b49c7c995f0..a88270a95a1c7 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -313,19 +313,31 @@ function getOnyxDataForRouteRequest( } /** - * Sanitizes the waypoints by removing the pendingAction property. + * Sanitizes the waypoints data to only include allowed fields for API requests. + * Only keeps: name (optional), address, lat, lng * * @param waypoints - The collection of waypoints to sanitize. - * @returns The sanitized collection of waypoints. + * @returns The sanitized collection of waypoints with only allowed fields. */ -function sanitizeRecentWaypoints(waypoints: WaypointCollection): WaypointCollection { +function sanitizeWaypointsForAPI(waypoints: WaypointCollection): WaypointCollection { return Object.entries(waypoints).reduce((acc: WaypointCollection, [key, waypoint]) => { - if ('pendingAction' in waypoint) { - const {pendingAction, ...rest} = waypoint; - acc[key] = rest; - } else { - acc[key] = waypoint; + const sanitizedWaypoint: Record = {}; + + // Only include allowed fields + if (waypoint.name !== undefined) { + sanitizedWaypoint.name = waypoint.name; + } + if (waypoint.address !== undefined) { + sanitizedWaypoint.address = waypoint.address; } + if (waypoint.lat !== undefined) { + sanitizedWaypoint.lat = waypoint.lat; + } + if (waypoint.lng !== undefined) { + sanitizedWaypoint.lng = waypoint.lng; + } + + acc[key] = sanitizedWaypoint; return acc; }, {}); } @@ -338,7 +350,7 @@ function sanitizeRecentWaypoints(waypoints: WaypointCollection): WaypointCollect function getRoute(transactionID: string, waypoints: WaypointCollection, routeType: TransactionState = CONST.TRANSACTION.STATE.CURRENT) { const parameters: GetRouteParams = { transactionID, - waypoints: JSON.stringify(sanitizeRecentWaypoints(waypoints)), + waypoints: JSON.stringify(sanitizeWaypointsForAPI(waypoints)), }; let command; @@ -1571,7 +1583,7 @@ export { setReviewDuplicatesKey, abandonReviewDuplicateTransactions, openDraftDistanceExpense, - sanitizeRecentWaypoints, + sanitizeWaypointsForAPI, getLastModifiedExpense, revert, changeTransactionsReport, diff --git a/tests/unit/TransactionTest.ts b/tests/unit/TransactionTest.ts index be7871abfb346..d460b56e71a7d 100644 --- a/tests/unit/TransactionTest.ts +++ b/tests/unit/TransactionTest.ts @@ -3,7 +3,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import OnyxUtils from 'react-native-onyx/dist/OnyxUtils'; import useOnyx from '@hooks/useOnyx'; -import {changeTransactionsReport, dismissDuplicateTransactionViolation, markAsCash, saveWaypoint} from '@libs/actions/Transaction'; +import {changeTransactionsReport, dismissDuplicateTransactionViolation, markAsCash, sanitizeWaypointsForAPI, saveWaypoint} from '@libs/actions/Transaction'; import DateUtils from '@libs/DateUtils'; import {getAllNonDeletedTransactions} from '@libs/MoneyRequestReportUtils'; import type {buildOptimisticNextStep} from '@libs/NextStepUtils'; @@ -1060,6 +1060,87 @@ describe('Transaction', () => { }); }); + describe('sanitizeWaypointsForAPI', () => { + it('should only include allowed fields (name, address, lat, lng)', () => { + // Given waypoints with extra fields that should be stripped out + const waypointsWithExtraFields = { + waypoint0: { + name: 'Start Location', + address: '123 Main St', + lat: 40.7128, + lng: -74.006, + city: 'New York', + state: 'NY', + zipCode: '10001', + country: 'US', + street: '123 Main St', + street2: 'Apt 4B', + keyForList: 'unique-key-1', + pendingAction: 'add', + extraField: 'should be removed', + }, + waypoint1: { + address: '456 Oak Ave', + lat: 40.7589, + lng: -73.9851, + city: 'New York', + state: 'NY', + zipCode: '10002', + keyForList: 'unique-key-2', + anotherExtraField: 'should also be removed', + }, + }; + + // When sanitizing the waypoints + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sanitizedWaypoints = sanitizeWaypointsForAPI(waypointsWithExtraFields as any); + + // Then only allowed fields should remain + expect(sanitizedWaypoints.waypoint0).toEqual({ + name: 'Start Location', + address: '123 Main St', + lat: 40.7128, + lng: -74.006, + }); + + // waypoint1 has no name field, so it should not be included + expect(sanitizedWaypoints.waypoint1).toEqual({ + address: '456 Oak Ave', + lat: 40.7589, + lng: -73.9851, + }); + }); + + it('should handle waypoints with only some allowed fields', () => { + // Given waypoints with only address + const waypointsWithPartialFields = { + waypoint0: { + address: 'Partial Address Only', + }, + }; + + // When sanitizing the waypoints + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const sanitizedWaypoints = sanitizeWaypointsForAPI(waypointsWithPartialFields as any); + + // Then only the address should be present + expect(sanitizedWaypoints.waypoint0).toEqual({ + address: 'Partial Address Only', + }); + }); + + it('should handle empty waypoints', () => { + // Given empty waypoints + const emptyWaypoints = {}; + + // When sanitizing the waypoints + const sanitizedWaypoints = sanitizeWaypointsForAPI(emptyWaypoints); + + // Then the result should also be empty + expect(sanitizedWaypoints).toEqual({}); + }); + }); + describe('markAsCash', () => { it('should optimistically remove RTER violation and add dismissed violation report action', async () => { // Given a transaction with an RTER violation From 4cb9be488c5d875a54df42efa590b26505b4a2c0 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 3 Feb 2026 13:34:38 -0800 Subject: [PATCH 2/6] Fix: Preserve keyForList in Onyx optimistic data The AI reviewer correctly identified that sanitizing waypoints in transactionChanges would strip keyForList from Onyx optimistic data, which is needed for UI list rendering and GPS waypoint detection. This change: - Removes sanitization from transactionChanges (preserves keyForList in Onyx) - Adds sanitization only when building API params (JSON.stringify step) This ensures Onyx has complete waypoint data while API requests only contain the allowed fields (name, address, lat, lng). Co-authored-by: Cursor --- src/libs/actions/IOU/index.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index f50e416d3a7da..96f001efaece6 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -4369,8 +4369,9 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U const transactionDetails = getTransactionDetails(updatedTransaction, undefined, undefined, allowNegative); if (transactionDetails?.waypoints) { - // This needs to be a JSON string since we're sending this to the MapBox API - transactionDetails.waypoints = JSON.stringify(transactionDetails.waypoints); + // Sanitize waypoints for API - only keep allowed fields (name, address, lat, lng) + // This is done here (not in transactionChanges) to preserve keyForList in Onyx optimistic data + transactionDetails.waypoints = JSON.stringify(sanitizeWaypointsForAPI(transactionDetails.waypoints as WaypointCollection)); } const dataToIncludeInParams: Partial = Object.fromEntries(Object.entries(transactionDetails ?? {}).filter(([key]) => key in transactionChanges)); @@ -4907,8 +4908,9 @@ function getUpdateTrackExpenseParams( const transactionDetails = getTransactionDetails(updatedTransaction); if (transactionDetails?.waypoints) { - // This needs to be a JSON string since we're sending this to the MapBox API - transactionDetails.waypoints = JSON.stringify(transactionDetails.waypoints); + // Sanitize waypoints for API - only keep allowed fields (name, address, lat, lng) + // This is done here (not in transactionChanges) to preserve keyForList in Onyx optimistic data + transactionDetails.waypoints = JSON.stringify(sanitizeWaypointsForAPI(transactionDetails.waypoints as WaypointCollection)); } const dataToIncludeInParams: Partial = Object.fromEntries(Object.entries(transactionDetails ?? {}).filter(([key]) => key in transactionChanges)); @@ -5502,7 +5504,9 @@ function updateMoneyRequestDistance({ parentReportNextStep, }: UpdateMoneyRequestDistanceParams) { const transactionChanges: TransactionChanges = { - ...(waypoints && {waypoints: sanitizeWaypointsForAPI(waypoints)}), + // Don't sanitize waypoints here - keep all fields for Onyx optimistic data (e.g., keyForList) + // Sanitization happens when building API params + ...(waypoints && {waypoints}), routes, ...(distance && {distance}), ...(odometerStart !== undefined && {odometerStart}), From 5441e05d7e929c17d806253b0c641c0bae8721f0 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 3 Feb 2026 15:09:36 -0800 Subject: [PATCH 3/6] Fix ESLint errors in unit tests Add @typescript-eslint/no-unsafe-argument to disable comments since we intentionally pass objects with extra fields to test sanitization. Co-authored-by: Cursor --- tests/unit/TransactionTest.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/TransactionTest.ts b/tests/unit/TransactionTest.ts index d460b56e71a7d..c819e15ccc52e 100644 --- a/tests/unit/TransactionTest.ts +++ b/tests/unit/TransactionTest.ts @@ -1092,7 +1092,7 @@ describe('Transaction', () => { }; // When sanitizing the waypoints - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument const sanitizedWaypoints = sanitizeWaypointsForAPI(waypointsWithExtraFields as any); // Then only allowed fields should remain @@ -1120,7 +1120,7 @@ describe('Transaction', () => { }; // When sanitizing the waypoints - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument const sanitizedWaypoints = sanitizeWaypointsForAPI(waypointsWithPartialFields as any); // Then only the address should be present From 0644dc1e6f6314ea2aef0718a5944a1d3874f65d Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 10 Mar 2026 13:04:49 -0700 Subject: [PATCH 4/6] Replace sanitizeRecentWaypoints with sanitizeWaypointsForAPI sanitizeRecentWaypoints no longer exists in Transaction.ts - it was replaced by sanitizeWaypointsForAPI in this PR. Update the call site from main's bulk convert flow to use the correct function. Made-with: Cursor --- src/libs/actions/IOU/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 2e0da9c950aa0..280d384b6efd2 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -240,7 +240,7 @@ import type {BuildPolicyDataKeys} from '@userActions/Policy/Policy'; import {buildOptimisticPolicyRecentlyUsedTags} from '@userActions/Policy/Tag'; import type {GuidedSetupData} from '@userActions/Report'; import {buildInviteToRoomOnyxData, completeOnboarding, notifyNewAction, optimisticReportLastData} from '@userActions/Report'; -import {mergeTransactionIdsHighlightOnSearchRoute, sanitizeRecentWaypoints, sanitizeWaypointsForAPI} from '@userActions/Transaction'; +import {mergeTransactionIdsHighlightOnSearchRoute, sanitizeWaypointsForAPI} from '@userActions/Transaction'; import {removeDraftTransaction, removeDraftTransactions, removeDraftTransactionsByIDs} from '@userActions/TransactionEdit'; import {getOnboardingMessages} from '@userActions/Welcome/OnboardingFlow'; import type {OnboardingCompanySize} from '@userActions/Welcome/OnboardingFlow'; @@ -6417,7 +6417,7 @@ function convertBulkTrackedExpensesToIOU({ const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); const transactionWaypoints = getWaypoints(transaction); - const sanitizedWaypointsForBulk = transactionWaypoints ? JSON.stringify(sanitizeRecentWaypoints(transactionWaypoints)) : undefined; + const sanitizedWaypointsForBulk = transactionWaypoints ? JSON.stringify(sanitizeWaypointsForAPI(transactionWaypoints)) : undefined; const convertParams: ConvertTrackedExpenseToRequestParams = { payerParams: { From 04141b68479f16cca4d0948877c2a0369df596e5 Mon Sep 17 00:00:00 2001 From: neil-marcellini Date: Tue, 10 Mar 2026 15:53:13 -0700 Subject: [PATCH 5/6] Address AI review feedback on waypoint sanitization - Add null guard in sanitizeWaypointsForAPI to handle null waypoint entries that can occur during rollback of failed distance edits - Extract stringifyWaypointsForAPI helper to deduplicate sanitize+stringify pattern in getUpdateMoneyRequestParams and getUpdateTrackExpenseParams - Add justification comments to ESLint disable directives in tests - Add test for null waypoint entry handling Made-with: Cursor --- src/libs/actions/IOU/index.ts | 10 +++------ src/libs/actions/Transaction.ts | 15 ++++++++++++- tests/unit/TransactionTest.ts | 39 +++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 280d384b6efd2..a757cbe9173f3 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -240,7 +240,7 @@ import type {BuildPolicyDataKeys} from '@userActions/Policy/Policy'; import {buildOptimisticPolicyRecentlyUsedTags} from '@userActions/Policy/Tag'; import type {GuidedSetupData} from '@userActions/Report'; import {buildInviteToRoomOnyxData, completeOnboarding, notifyNewAction, optimisticReportLastData} from '@userActions/Report'; -import {mergeTransactionIdsHighlightOnSearchRoute, sanitizeWaypointsForAPI} from '@userActions/Transaction'; +import {mergeTransactionIdsHighlightOnSearchRoute, sanitizeWaypointsForAPI, stringifyWaypointsForAPI} from '@userActions/Transaction'; import {removeDraftTransaction, removeDraftTransactions, removeDraftTransactionsByIDs} from '@userActions/TransactionEdit'; import {getOnboardingMessages} from '@userActions/Welcome/OnboardingFlow'; import type {OnboardingCompanySize} from '@userActions/Welcome/OnboardingFlow'; @@ -4611,9 +4611,7 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U const transactionDetails = getTransactionDetails(updatedTransaction, undefined, undefined, allowNegative); if (transactionDetails?.waypoints) { - // Sanitize waypoints for API - only keep allowed fields (name, address, lat, lng) - // This is done here (not in transactionChanges) to preserve keyForList in Onyx optimistic data - transactionDetails.waypoints = JSON.stringify(sanitizeWaypointsForAPI(transactionDetails.waypoints as WaypointCollection)); + transactionDetails.waypoints = stringifyWaypointsForAPI(transactionDetails.waypoints as WaypointCollection); } const dataToIncludeInParams: Partial = Object.fromEntries(Object.entries(transactionDetails ?? {}).filter(([key]) => key in transactionChanges)); @@ -5160,9 +5158,7 @@ function getUpdateTrackExpenseParams( const transactionDetails = getTransactionDetails(updatedTransaction); if (transactionDetails?.waypoints) { - // Sanitize waypoints for API - only keep allowed fields (name, address, lat, lng) - // This is done here (not in transactionChanges) to preserve keyForList in Onyx optimistic data - transactionDetails.waypoints = JSON.stringify(sanitizeWaypointsForAPI(transactionDetails.waypoints as WaypointCollection)); + transactionDetails.waypoints = stringifyWaypointsForAPI(transactionDetails.waypoints as WaypointCollection); } const dataToIncludeInParams: Partial = Object.fromEntries(Object.entries(transactionDetails ?? {}).filter(([key]) => key in transactionChanges)); diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 0dfe77f7feb9c..139aaea7724d6 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -355,9 +355,12 @@ function getOnyxDataForRouteRequest( */ function sanitizeWaypointsForAPI(waypoints: WaypointCollection): WaypointCollection { return Object.entries(waypoints).reduce((acc: WaypointCollection, [key, waypoint]) => { + if (!waypoint) { + return acc; + } + const sanitizedWaypoint: Record = {}; - // Only include allowed fields if (waypoint.name !== undefined) { sanitizedWaypoint.name = waypoint.name; } @@ -376,6 +379,15 @@ function sanitizeWaypointsForAPI(waypoints: WaypointCollection): WaypointCollect }, {}); } +/** + * Sanitizes waypoints and serializes them to a JSON string for API params. + * Preserves keyForList and other Onyx-only fields by sanitizing at the serialization boundary + * rather than when building transactionChanges. + */ +function stringifyWaypointsForAPI(waypoints: WaypointCollection): string { + return JSON.stringify(sanitizeWaypointsForAPI(waypoints)); +} + /** * Gets the route for a set of waypoints * Used so we can generate a map view of the provided waypoints @@ -1695,6 +1707,7 @@ export { abandonReviewDuplicateTransactions, openDraftDistanceExpense, sanitizeWaypointsForAPI, + stringifyWaypointsForAPI, getLastModifiedExpense, revert, changeTransactionsReport, diff --git a/tests/unit/TransactionTest.ts b/tests/unit/TransactionTest.ts index 27ca91fa6a2e1..8103eca0d10b9 100644 --- a/tests/unit/TransactionTest.ts +++ b/tests/unit/TransactionTest.ts @@ -1133,6 +1133,7 @@ describe('Transaction', () => { }; // When sanitizing the waypoints + // Test intentionally passes extra fields not in WaypointCollection to verify they are stripped // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument const sanitizedWaypoints = sanitizeWaypointsForAPI(waypointsWithExtraFields as any); @@ -1161,6 +1162,7 @@ describe('Transaction', () => { }; // When sanitizing the waypoints + // Test uses a partial waypoint object to verify sanitization handles missing fields // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument const sanitizedWaypoints = sanitizeWaypointsForAPI(waypointsWithPartialFields as any); @@ -1180,6 +1182,43 @@ describe('Transaction', () => { // Then the result should also be empty expect(sanitizedWaypoints).toEqual({}); }); + + it('should skip null waypoint entries without crashing', () => { + // Given waypoints where some entries are null (can happen during rollback of failed distance edits) + const waypointsWithNulls = { + waypoint0: { + address: '123 Main St', + lat: 40.7128, + lng: -74.006, + }, + waypoint1: null, + waypoint2: { + address: '789 Pine Rd', + lat: 40.73, + lng: -73.99, + }, + }; + + // When sanitizing the waypoints + // Null entries can occur at runtime even though WaypointCollection type doesn't include null + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument + const sanitizedWaypoints = sanitizeWaypointsForAPI(waypointsWithNulls as any); + + // Then null entries should be dropped and valid entries sanitized + expect(sanitizedWaypoints).toEqual({ + waypoint0: { + address: '123 Main St', + lat: 40.7128, + lng: -74.006, + }, + waypoint2: { + address: '789 Pine Rd', + lat: 40.73, + lng: -73.99, + }, + }); + expect(sanitizedWaypoints.waypoint1).toBeUndefined(); + }); }); describe('markAsCash', () => { From 588d3a1f9c0c5a5f0a4cbefabb876ccc8db8ea7a Mon Sep 17 00:00:00 2001 From: "Neil Marcellini (via MelvinBot)" Date: Mon, 16 Mar 2026 22:02:21 +0000 Subject: [PATCH 6/6] Use stringifyWaypointsForAPI consistently across all call sites Replace manual JSON.stringify(sanitizeWaypointsForAPI(...)) calls with the stringifyWaypointsForAPI helper in getRoute, convertBulkTrackedExpensesToIOU, requestMoney, and trackExpense for consistency. Co-authored-by: Neil Marcellini --- src/libs/actions/IOU/index.ts | 6 +++--- src/libs/actions/Transaction.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index a757cbe9173f3..4c0b40a3516f5 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -6413,7 +6413,7 @@ function convertBulkTrackedExpensesToIOU({ const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); const transactionWaypoints = getWaypoints(transaction); - const sanitizedWaypointsForBulk = transactionWaypoints ? JSON.stringify(sanitizeWaypointsForAPI(transactionWaypoints)) : undefined; + const sanitizedWaypointsForBulk = transactionWaypoints ? stringifyWaypointsForAPI(transactionWaypoints) : undefined; const convertParams: ConvertTrackedExpenseToRequestParams = { payerParams: { @@ -6697,7 +6697,7 @@ function requestMoney(requestMoneyInformation: RequestMoneyInformation): {iouRep const testDriveCommentReportActionID = isTestDrive ? NumberUtils.rand64() : undefined; - const sanitizedWaypoints = waypoints ? JSON.stringify(sanitizeWaypointsForAPI(waypoints)) : undefined; + const sanitizedWaypoints = waypoints ? stringifyWaypointsForAPI(waypoints) : undefined; // If the report is iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function const isMoneyRequestReport = isMoneyRequestReportReportUtils(report); @@ -7496,7 +7496,7 @@ function trackExpense(params: CreateTrackExpenseParams) { // Pass an open receipt so the distance expense will show a map with the route optimistically const trackedReceipt = validWaypoints ? {source: ReceiptGeneric as ReceiptSource, state: CONST.IOU.RECEIPT_STATE.OPEN, name: 'receipt-generic.png'} : receipt; - const sanitizedWaypoints = validWaypoints ? JSON.stringify(sanitizeWaypointsForAPI(validWaypoints)) : undefined; + const sanitizedWaypoints = validWaypoints ? stringifyWaypointsForAPI(validWaypoints) : undefined; const retryParams: CreateTrackExpenseParams = { ...params, diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 139aaea7724d6..c954d67886ecd 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -396,7 +396,7 @@ function stringifyWaypointsForAPI(waypoints: WaypointCollection): string { function getRoute(transactionID: string, waypoints: WaypointCollection, routeType: TransactionState = CONST.TRANSACTION.STATE.CURRENT) { const parameters: GetRouteParams = { transactionID, - waypoints: JSON.stringify(sanitizeWaypointsForAPI(waypoints)), + waypoints: stringifyWaypointsForAPI(waypoints), }; let command;