diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 3098f434f6c4..c956e97e736c 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -2913,6 +2913,7 @@ const CONST = {
DISTANCE_MAP: 'distance-map',
DISTANCE_MANUAL: 'distance-manual',
DISTANCE_GPS: 'distance-gps',
+ DISTANCE_ODOMETER: 'distance-odometer',
},
EXPENSE_TYPE: {
DISTANCE: 'distance',
@@ -2924,7 +2925,9 @@ const CONST = {
DISTANCE_MAP: 'distance-map',
DISTANCE_MANUAL: 'distance-manual',
DISTANCE_GPS: 'distance-gps',
+ DISTANCE_ODOMETER: 'distance-odometer',
},
+
REPORT_ACTION_TYPE: {
PAY: 'pay',
CREATE: 'create',
@@ -5505,6 +5508,7 @@ const CONST = {
DISTANCE_MAP: 'distance-map',
DISTANCE_MANUAL: 'distance-manual',
DISTANCE_GPS: 'distance-gps',
+ DISTANCE_ODOMETER: 'distance-odometer',
},
STATUS_TEXT_MAX_LENGTH: 100,
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 641e74f94a1d..87cf0b33472d 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -1175,6 +1175,17 @@ const ROUTES = {
return getUrlWithBackToParam(`${action as string}/${iouType as string}/distance-manual/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo);
},
},
+ MONEY_REQUEST_STEP_DISTANCE_ODOMETER: {
+ route: ':action/:iouType/distance-odometer/:transactionID/:reportID',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '') => {
+ if (!transactionID || !reportID) {
+ Log.warn('Invalid transactionID or reportID is used to build the MONEY_REQUEST_STEP_DISTANCE_ODOMETER route');
+ }
+
+ // eslint-disable-next-line no-restricted-syntax -- Legacy route generation
+ return getUrlWithBackToParam(`${action as string}/${iouType as string}/distance-odometer/${transactionID}/${reportID}`, backTo);
+ },
+ },
MONEY_REQUEST_STEP_DISTANCE_RATE: {
route: ':action/:iouType/distanceRate/:transactionID/:reportID/:reportActionID?',
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backTo = '', reportActionID?: string) => {
@@ -1294,6 +1305,11 @@ const ROUTES = {
getRoute: (action: IOUAction, iouType: IOUType, transactionID: string | undefined, reportID: string | undefined, backToReport?: string) =>
`${action as string}/${iouType as string}/start/${transactionID}/${reportID}/distance-new${backToReport ? `/${backToReport}` : ''}/distance-gps` as const,
},
+ DISTANCE_REQUEST_CREATE_TAB_ODOMETER: {
+ route: 'distance-odometer',
+ getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backToReport?: string) =>
+ `${action as string}/${iouType as string}/start/${transactionID}/${reportID}/distance-new${backToReport ? `/${backToReport}` : ''}/distance-odometer` as const,
+ },
IOU_SEND_ADD_BANK_ACCOUNT: 'pay/new/add-bank-account',
IOU_SEND_ADD_DEBIT_CARD: 'pay/new/add-debit-card',
IOU_SEND_ENABLE_PAYMENTS: 'pay/new/enable-payments',
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 860419234f2d..1393fcf63841 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -329,6 +329,7 @@ const SCREENS = {
STEP_DISTANCE_MAP: 'Money_Request_Step_Distance_Map',
STEP_DISTANCE_MANUAL: 'Money_Request_Step_Distance_Manual',
STEP_DISTANCE_GPS: 'Money_Request_Step_Distance_GPS',
+ STEP_DISTANCE_ODOMETER: 'Money_Request_Step_Distance_Odometer',
RECEIPT_PREVIEW: 'Money_Request_Receipt_preview',
},
diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx
index 76c09481b5d8..102bd778c953 100755
--- a/src/components/MoneyRequestConfirmationList.tsx
+++ b/src/components/MoneyRequestConfirmationList.tsx
@@ -152,6 +152,9 @@ type MoneyRequestConfirmationListProps = {
/** Whether the expense is a manual distance expense */
isManualDistanceRequest: boolean;
+ /** Whether the expense is an odometer distance expense */
+ isOdometerDistanceRequest?: boolean;
+
/** Whether the expense is a per diem expense */
isPerDiemRequest?: boolean;
@@ -213,6 +216,7 @@ function MoneyRequestConfirmationList({
iouAmount,
isDistanceRequest,
isManualDistanceRequest,
+ isOdometerDistanceRequest = false,
isPerDiemRequest = false,
isPolicyExpenseChat = false,
iouCategory = '',
@@ -1144,6 +1148,7 @@ function MoneyRequestConfirmationList({
isCategoryRequired={isCategoryRequired}
isDistanceRequest={isDistanceRequest}
isManualDistanceRequest={isManualDistanceRequest}
+ isOdometerDistanceRequest={isOdometerDistanceRequest}
isPerDiemRequest={isPerDiemRequest}
isMerchantEmpty={isMerchantEmpty}
isMerchantRequired={isMerchantRequired}
diff --git a/src/components/MoneyRequestConfirmationListFooter.tsx b/src/components/MoneyRequestConfirmationListFooter.tsx
index 4e4acba9a7df..bce89a190df1 100644
--- a/src/components/MoneyRequestConfirmationListFooter.tsx
+++ b/src/components/MoneyRequestConfirmationListFooter.tsx
@@ -41,6 +41,7 @@ import CONST from '@src/CONST';
import type {IOUAction, IOUType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type {Route} from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {Attendee, Participant} from '@src/types/onyx/IOU';
import type {Unit} from '@src/types/onyx/Policy';
@@ -114,6 +115,9 @@ type MoneyRequestConfirmationListFooterProps = {
/** Flag indicating if it is a manual distance request */
isManualDistanceRequest: boolean;
+ /** Flag indicating if it is an odometer distance request */
+ isOdometerDistanceRequest?: boolean;
+
/** Flag indicating if it is a per diem request */
isPerDiemRequest: boolean;
@@ -231,6 +235,7 @@ function MoneyRequestConfirmationListFooter({
isCategoryRequired,
isDistanceRequest,
isManualDistanceRequest,
+ isOdometerDistanceRequest = false,
isPerDiemRequest,
isMerchantEmpty,
isMerchantRequired,
@@ -301,8 +306,8 @@ function MoneyRequestConfirmationListFooter({
const hasPendingWaypoints = transaction && isFetchingWaypointsFromServer(transaction);
const hasErrors = !isEmptyObject(transaction?.errors) || !isEmptyObject(transaction?.errorFields?.route) || !isEmptyObject(transaction?.errorFields?.waypoints);
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- const shouldShowMap = isDistanceRequest && !isManualDistanceRequest && !!(hasErrors || hasPendingWaypoints || iouType !== CONST.IOU.TYPE.SPLIT || !isReadOnly);
+ const shouldShowMap =
+ isDistanceRequest && !isManualDistanceRequest && !isOdometerDistanceRequest && [hasErrors, hasPendingWaypoints, iouType !== CONST.IOU.TYPE.SPLIT, !isReadOnly].some(Boolean);
const isFromGlobalCreate = !!transaction?.isFromGlobalCreate;
const senderWorkspace = useMemo(() => {
@@ -504,6 +509,11 @@ function MoneyRequestConfirmationListFooter({
return;
}
+ if (isOdometerDistanceRequest) {
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE_ODOMETER.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute()) as Route);
+ return;
+ }
+
Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(action, iouType, transactionID, reportID, Navigation.getActiveRoute(), reportActionID));
}}
disabled={didConfirm}
@@ -1031,7 +1041,7 @@ function MoneyRequestConfirmationListFooter({
>
)}
- {(!shouldShowMap || isManualDistanceRequest) && (
+ {(!shouldShowMap || isManualDistanceRequest || isOdometerDistanceRequest) && (
{hasReceiptImageOrThumbnail
? receiptThumbnailContent
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index 56c41e036a7b..6b4e58fb292a 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -90,6 +90,7 @@ import {
isDistanceRequest as isDistanceRequestTransactionUtils,
isExpenseUnreported as isExpenseUnreportedTransactionUtils,
isManualDistanceRequest as isManualDistanceRequestTransactionUtils,
+ isOdometerDistanceRequest as isOdometerDistanceRequestTransactionUtils,
isPerDiemRequest as isPerDiemRequestTransactionUtils,
isScanning,
isTimeRequest as isTimeRequestTransactionUtils,
@@ -278,6 +279,7 @@ function MoneyRequestView({
transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transactionMerchant === CONST.TRANSACTION.DEFAULT_MERCHANT;
const isDistanceRequest = isDistanceRequestTransactionUtils(transaction);
const isManualDistanceRequest = isManualDistanceRequestTransactionUtils(transaction, !!mergeTransactionID);
+ const isOdometerDistanceRequest = isOdometerDistanceRequestTransactionUtils(transaction);
const isMapDistanceRequest = isDistanceRequest && !isManualDistanceRequest;
const isTransactionScanning = isScanning(updatedTransaction ?? transaction);
const hasRoute = hasRouteTransactionUtils(transactionBackup ?? transaction, isDistanceRequest);
@@ -555,6 +557,19 @@ function MoneyRequestView({
return;
}
+ if (isOdometerDistanceRequest) {
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_DISTANCE_ODOMETER.getRoute(
+ CONST.IOU.ACTION.EDIT,
+ iouType,
+ transaction.transactionID,
+ transactionThreadReport.reportID,
+ getReportRHPActiveRoute(),
+ ),
+ );
+ return;
+ }
+
if (isManualDistanceRequest) {
Navigation.navigate(
ROUTES.MONEY_REQUEST_STEP_DISTANCE_MANUAL.getRoute(
@@ -834,7 +849,7 @@ function MoneyRequestView({
copyable={!!descriptionCopyValue}
/>
- {isManualDistanceRequest || (isMapDistanceRequest && transaction?.comment?.waypoints) ? (
+ {isManualDistanceRequest || isOdometerDistanceRequest || (isMapDistanceRequest && transaction?.comment?.waypoints) ? (
distanceRequestFields
) : (
diff --git a/src/components/TabSelector/TabSelector.tsx b/src/components/TabSelector/TabSelector.tsx
index 53364fafb1e3..52fc9ea147c2 100644
--- a/src/components/TabSelector/TabSelector.tsx
+++ b/src/components/TabSelector/TabSelector.tsx
@@ -37,7 +37,21 @@ type IconTitleAndTestID = {
testID?: string;
};
-const MEMOIZED_LAZY_TAB_SELECTOR_ICONS = ['CalendarSolid', 'UploadAlt', 'User', 'Car', 'Hashtag', 'Map', 'Pencil', 'ReceiptScan', 'Receipt', 'MoneyCircle', 'Percent', 'Crosshair'] as const;
+const MEMOIZED_LAZY_TAB_SELECTOR_ICONS = [
+ 'CalendarSolid',
+ 'UploadAlt',
+ 'User',
+ 'Car',
+ 'Hashtag',
+ 'Map',
+ 'Pencil',
+ 'ReceiptScan',
+ 'Receipt',
+ 'MoneyCircle',
+ 'Percent',
+ 'Crosshair',
+ 'Meter',
+] as const;
function getIconTitleAndTestID(
icons: Record, IconAsset>,
@@ -73,6 +87,8 @@ function getIconTitleAndTestID(
return {icon: icons.Pencil, title: translate('tabSelector.manual'), testID: 'distanceManual'};
case CONST.TAB_REQUEST.DISTANCE_GPS:
return {icon: icons.Crosshair, title: translate('tabSelector.gps'), testID: 'distanceGPS'};
+ case CONST.TAB_REQUEST.DISTANCE_ODOMETER:
+ return {icon: icons.Meter, title: translate('tabSelector.odometer'), testID: 'distanceOdometer'};
case CONST.TAB.SPLIT.AMOUNT:
return {icon: icons.MoneyCircle, title: translate('iou.amount'), testID: 'split-amount'};
case CONST.TAB.SPLIT.PERCENTAGE:
diff --git a/src/hooks/useBeforeRemove.tsx b/src/hooks/useBeforeRemove.tsx
index 835d5a30babe..908bb659960c 100644
--- a/src/hooks/useBeforeRemove.tsx
+++ b/src/hooks/useBeforeRemove.tsx
@@ -3,13 +3,16 @@ import type {EventListenerCallback, EventMapCore, NavigationState} from '@react-
import {useEffect} from 'react';
// beforeRemove have some limitations. When the react-navigation is upgraded to 7.x, update this to use usePreventRemove hook.
-const useBeforeRemove = (onBeforeRemove: EventListenerCallback, 'beforeRemove'>) => {
+const useBeforeRemove = (onBeforeRemove: EventListenerCallback, 'beforeRemove'>, isEnabled = true) => {
const navigation = useNavigation();
useEffect(() => {
+ if (!isEnabled) {
+ return undefined;
+ }
const unsubscribe = navigation.addListener('beforeRemove', onBeforeRemove);
return unsubscribe;
- }, [navigation, onBeforeRemove]);
+ }, [navigation, onBeforeRemove, isEnabled]);
};
export default useBeforeRemove;
diff --git a/src/languages/de.ts b/src/languages/de.ts
index d484e3951f26..42e5aa3a5f4c 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -956,15 +956,7 @@ const translations: TranslationDeepObject = {
subscription: 'Abonnement',
domains: 'Domänen',
},
- tabSelector: {
- chat: 'Chat',
- room: 'Raum',
- distance: 'Entfernung',
- manual: 'Manuell',
- scan: 'Scannen',
- map: 'Karte',
- gps: 'GPS',
- },
+ tabSelector: {chat: 'Chat', room: 'Raum', distance: 'Entfernung', manual: 'Manuell', scan: 'Scannen', map: 'Karte', gps: 'GPS', odometer: 'Kilometerzähler'},
spreadsheet: {
upload: 'Eine Tabellenkalkulation hochladen',
import: 'Tabellenkalkulation importieren',
@@ -1292,6 +1284,8 @@ const translations: TranslationDeepObject = {
invalidRate: 'Satz für diesen Workspace ungültig. Bitte wählen Sie einen verfügbaren Satz aus dem Workspace aus.',
endDateBeforeStartDate: 'Das Enddatum darf nicht vor dem Startdatum liegen',
endDateSameAsStartDate: 'Das Enddatum darf nicht mit dem Startdatum identisch sein',
+ invalidReadings: 'Bitte geben Sie sowohl Anfangs- als auch Endstand ein',
+ negativeDistanceNotAllowed: 'Endablesung muss größer als Startablesung sein',
},
dismissReceiptError: 'Fehler ausblenden',
dismissReceiptErrorConfirmation: 'Achtung! Wenn du diesen Fehler verwirfst, wird dein hochgeladener Beleg vollständig entfernt. Bist du sicher?',
@@ -7104,6 +7098,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
error: {
selectSuggestedAddress: 'Bitte wählen Sie eine vorgeschlagene Adresse aus oder verwenden Sie den aktuellen Standort',
},
+ odometer: {startReading: 'Mit dem Lesen beginnen', endReading: 'Lesen beenden', saveForLater: 'Für später speichern', totalDistance: 'Gesamtdistanz'},
},
reportCardLostOrDamaged: {
screenTitle: 'Zeugnis verloren oder beschädigt',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 54356f33f9a3..1442ab13454b 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -946,6 +946,7 @@ const translations = {
scan: 'Scan',
map: 'Map',
gps: 'GPS',
+ odometer: 'Odometer',
},
spreadsheet: {
upload: 'Upload a spreadsheet',
@@ -1242,6 +1243,8 @@ const translations = {
invalidTagLength: 'The tag name exceeds 255 characters. Please shorten it or choose a different tag.',
invalidAmount: 'Please enter a valid amount before continuing',
invalidDistance: 'Please enter a valid distance before continuing',
+ invalidReadings: 'Please enter both start and end readings',
+ negativeDistanceNotAllowed: 'End reading must be greater than start reading',
invalidIntegerAmount: 'Please enter a whole dollar amount before continuing',
invalidTaxAmount: ({amount}: RequestAmountParams) => `Maximum tax amount is ${amount}`,
invalidSplit: 'The sum of splits must equal the total amount',
@@ -6974,6 +6977,12 @@ const translations = {
error: {
selectSuggestedAddress: 'Please select a suggested address or use current location',
},
+ odometer: {
+ startReading: 'Start reading',
+ endReading: 'End reading',
+ saveForLater: 'Save for later',
+ totalDistance: 'Total distance',
+ },
},
gps: {
tooltip: "GPS tracking in progress! When you're done, stop tracking below.",
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 7612dead1b77..5cd2eb601de2 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -692,6 +692,7 @@ const translations: TranslationDeepObject = {
scan: 'Escanear',
map: 'Map',
gps: 'GPS',
+ odometer: 'Odómetro',
},
spreadsheet: {
upload: 'Importar',
@@ -989,6 +990,8 @@ const translations: TranslationDeepObject = {
invalidTagLength: 'La longitud de la etiqueta escogida excede el máximo permitido (255). Por favor, escoge otra etiqueta o acorta la etiqueta primero.',
invalidAmount: 'Por favor, ingresa un importe válido antes de continuar',
invalidDistance: 'Por favor, ingresa una distancia válida antes de continuar',
+ invalidReadings: 'Por favor ingrese ambas lecturas de inicio y fin',
+ negativeDistanceNotAllowed: 'La lectura final debe ser mayor que la lectura inicial',
invalidIntegerAmount: 'Por favor, introduce una cantidad entera en dólares antes de continuar',
invalidTaxAmount: ({amount}) => `El importe máximo del impuesto es ${amount}`,
invalidSplit: 'La suma de las partes debe ser igual al importe total',
@@ -7183,6 +7186,12 @@ ${amount} para ${merchant} - ${date}`,
error: {
selectSuggestedAddress: 'Por favor, selecciona una dirección sugerida o usa la ubicación actual',
},
+ odometer: {
+ startReading: 'Lectura inicial',
+ endReading: 'Lectura final',
+ saveForLater: 'Guardar para después',
+ totalDistance: 'Distancia total',
+ },
},
reportCardLostOrDamaged: {
screenTitle: 'Notificar la pérdida o deterioro de la tarjeta',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 2a542dc40119..790d9a445998 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -957,15 +957,7 @@ const translations: TranslationDeepObject = {
subscription: 'Abonnement',
domains: 'Domaines',
},
- tabSelector: {
- chat: 'Discussion',
- room: 'Salle',
- distance: 'Distance',
- manual: 'Manuel',
- scan: 'Scanner',
- map: 'Carte',
- gps: 'GPS',
- },
+ tabSelector: {chat: 'Discussion', room: 'Salle', distance: 'Distance', manual: 'Manuel', scan: 'Scanner', map: 'Carte', gps: 'GPS', odometer: 'Compteur kilométrique'},
spreadsheet: {
upload: 'Téléverser une feuille de calcul',
import: 'Importer une feuille de calcul',
@@ -1294,6 +1286,8 @@ const translations: TranslationDeepObject = {
invalidRate: 'Taux non valide pour cet espace de travail. Veuillez sélectionner un taux disponible dans l’espace de travail.',
endDateBeforeStartDate: 'La date de fin ne peut pas être antérieure à la date de début',
endDateSameAsStartDate: 'La date de fin ne peut pas être identique à la date de début',
+ invalidReadings: 'Veuillez saisir les relevés de début et de fin',
+ negativeDistanceNotAllowed: 'La lecture de fin doit être supérieure à la lecture de début',
},
dismissReceiptError: 'Ignorer l’erreur',
dismissReceiptErrorConfirmation: 'Attention ! Ignorer cette erreur supprimera entièrement votre reçu téléchargé. Êtes-vous sûr ?',
@@ -7117,6 +7111,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
error: {
selectSuggestedAddress: 'Veuillez sélectionner une adresse suggérée ou utiliser la position actuelle',
},
+ odometer: {startReading: 'Commencer la lecture', endReading: 'Fin de lecture', saveForLater: 'Enregistrer pour plus tard', totalDistance: 'Distance totale'},
},
reportCardLostOrDamaged: {
screenTitle: 'Bulletin perdu ou endommagé',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 39ec147b302b..b4f61df4e698 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -954,15 +954,7 @@ const translations: TranslationDeepObject = {
subscription: 'Abbonamento',
domains: 'Domini',
},
- tabSelector: {
- chat: 'Chat',
- room: 'Stanza',
- distance: 'Distanza',
- manual: 'Manuale',
- scan: 'Scannerizza',
- map: 'Mappa',
- gps: 'GPS',
- },
+ tabSelector: {chat: 'Chat', room: 'Stanza', distance: 'Distanza', manual: 'Manuale', scan: 'Scannerizza', map: 'Mappa', gps: 'GPS', odometer: 'Contachilometri'},
spreadsheet: {
upload: 'Carica un foglio di calcolo',
import: 'Importa foglio di calcolo',
@@ -1289,6 +1281,8 @@ const translations: TranslationDeepObject = {
invalidRate: 'Tariffa non valida per questo workspace. Seleziona una tariffa disponibile dal workspace.',
endDateBeforeStartDate: 'La data di fine non può essere precedente alla data di inizio',
endDateSameAsStartDate: 'La data di fine non può essere uguale alla data di inizio',
+ invalidReadings: 'Inserisci sia la lettura iniziale che quella finale',
+ negativeDistanceNotAllowed: 'La lettura finale deve essere maggiore della lettura iniziale',
},
dismissReceiptError: 'Ignora errore',
dismissReceiptErrorConfirmation: 'Attenzione! Se ignori questo errore, la ricevuta caricata verrà rimossa completamente. Sei sicuro?',
@@ -7091,6 +7085,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
error: {
selectSuggestedAddress: 'Seleziona un indirizzo suggerito o usa la posizione attuale',
},
+ odometer: {startReading: 'Inizia a leggere', endReading: 'Termina lettura', saveForLater: 'Salva per dopo', totalDistance: 'Distanza totale'},
},
reportCardLostOrDamaged: {
screenTitle: 'Pagella smarrita o danneggiata',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 585887c4c221..a243c75852fd 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -953,15 +953,7 @@ const translations: TranslationDeepObject = {
subscription: 'サブスクリプション',
domains: 'ドメイン',
},
- tabSelector: {
- chat: 'チャット',
- room: '部屋',
- distance: '距離',
- manual: '手動',
- scan: 'スキャン',
- map: '地図',
- gps: 'GPS',
- },
+ tabSelector: {chat: 'チャット', room: '部屋', distance: '距離', manual: '手動', scan: 'スキャン', map: '地図', gps: 'GPS', odometer: 'オドメーター'},
spreadsheet: {
upload: 'スプレッドシートをアップロード',
import: 'スプレッドシートをインポート',
@@ -1287,6 +1279,8 @@ const translations: TranslationDeepObject = {
invalidRate: 'このワークスペースでは無効なレートです。ワークスペースから利用可能なレートを選択してください。',
endDateBeforeStartDate: '終了日は開始日より前にはできません',
endDateSameAsStartDate: '終了日は開始日と同じにはできません',
+ invalidReadings: '開始と終了の両方の読みを入力してください',
+ negativeDistanceNotAllowed: '終了値は開始値より大きくなければなりません',
},
dismissReceiptError: 'エラーを閉じる',
dismissReceiptErrorConfirmation: '注意!このエラーを無視すると、アップロードした領収書が完全に削除されます。本当に実行しますか?',
@@ -7035,6 +7029,7 @@ ${reportName}
error: {
selectSuggestedAddress: '候補の住所を選択するか、現在地を使用してください',
},
+ odometer: {startReading: '読み始める', endReading: '読み取り終了', saveForLater: '後で保存', totalDistance: '合計距離'},
},
reportCardLostOrDamaged: {
screenTitle: '成績証明書の紛失または損傷',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 026e47d8dd03..36e16819de4f 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -954,15 +954,7 @@ const translations: TranslationDeepObject = {
subscription: 'Abonnement',
domains: 'Domeinen',
},
- tabSelector: {
- chat: 'Chat',
- room: 'Kamer',
- distance: 'Afstand',
- manual: 'Handmatig',
- scan: 'Scannen',
- map: 'Kaart',
- gps: 'GPS',
- },
+ tabSelector: {chat: 'Chat', room: 'Kamer', distance: 'Afstand', manual: 'Handmatig', scan: 'Scannen', map: 'Kaart', gps: 'GPS', odometer: 'Kilometerstand'},
spreadsheet: {
upload: 'Een spreadsheet uploaden',
import: 'Spreadsheet importeren',
@@ -1288,6 +1280,8 @@ const translations: TranslationDeepObject = {
invalidRate: 'Tarief is niet geldig voor deze workspace. Selecteer een beschikbaar tarief uit de workspace.',
endDateBeforeStartDate: 'De einddatum kan niet vóór de startdatum liggen',
endDateSameAsStartDate: 'De einddatum mag niet hetzelfde zijn als de startdatum',
+ negativeDistanceNotAllowed: 'Eindstand moet groter zijn dan beginstand',
+ invalidReadings: 'Voer zowel de begin- als eindstanden in',
},
dismissReceiptError: 'Foutmelding sluiten',
dismissReceiptErrorConfirmation: 'Let op! Als je deze foutmelding negeert, wordt je geüploade bon volledig verwijderd. Weet je het zeker?',
@@ -7078,6 +7072,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten
error: {
selectSuggestedAddress: 'Selecteer een voorgesteld adres of gebruik huidige locatie',
},
+ odometer: {startReading: 'Begin met lezen', endReading: 'Lezen beëindigen', saveForLater: 'Voor later bewaren', totalDistance: 'Totale afstand'},
},
reportCardLostOrDamaged: {
screenTitle: 'Rapportkaart kwijt of beschadigd',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 069ba463425b..01845991375c 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -954,15 +954,7 @@ const translations: TranslationDeepObject = {
subscription: 'Subskrypcja',
domains: 'Domeny',
},
- tabSelector: {
- chat: 'Czat',
- room: 'Pokój',
- distance: 'Dystans',
- manual: 'Ręczny',
- scan: 'Skanuj',
- map: 'Mapa',
- gps: 'GPS',
- },
+ tabSelector: {chat: 'Czat', room: 'Pokój', distance: 'Dystans', manual: 'Ręczny', scan: 'Skanuj', map: 'Mapa', gps: 'GPS', odometer: 'Licznik przebiegu'},
spreadsheet: {
upload: 'Prześlij arkusz kalkulacyjny',
import: 'Importuj arkusz kalkulacyjny',
@@ -1287,6 +1279,8 @@ const translations: TranslationDeepObject = {
invalidRate: 'Stawka nie jest prawidłowa dla tego przestrzeni roboczej. Wybierz dostępną stawkę z tej przestrzeni roboczej.',
endDateBeforeStartDate: 'Data zakończenia nie może być wcześniejsza niż data rozpoczęcia',
endDateSameAsStartDate: 'Data zakończenia nie może być taka sama jak data rozpoczęcia',
+ negativeDistanceNotAllowed: 'Odczyt końcowy musi być większy niż odczyt początkowy',
+ invalidReadings: 'Wprowadź zarówno odczyt początkowy, jak i końcowy',
},
dismissReceiptError: 'Odrzuć błąd',
dismissReceiptErrorConfirmation: 'Uwaga! Odrzucenie tego błędu spowoduje całkowite usunięcie przesłanego paragonu. Czy na pewno chcesz kontynuować?',
@@ -7067,6 +7061,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
error: {
selectSuggestedAddress: 'Wybierz sugerowany adres lub użyj bieżącej lokalizacji',
},
+ odometer: {startReading: 'Rozpocznij czytanie', endReading: 'Zakończ czytanie', saveForLater: 'Zapisz na później', totalDistance: 'Całkowity dystans'},
},
reportCardLostOrDamaged: {
screenTitle: 'Świadectwo zgubione lub uszkodzone',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 74ee00186045..9a2ce1915a2c 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -953,15 +953,7 @@ const translations: TranslationDeepObject = {
subscription: 'Assinatura',
domains: 'Domínios',
},
- tabSelector: {
- chat: 'Chat',
- room: 'Sala',
- distance: 'Distância',
- manual: 'Manual',
- scan: 'Escanear',
- map: 'Mapa',
- gps: 'GPS',
- },
+ tabSelector: {chat: 'Chat', room: 'Sala', distance: 'Distância', manual: 'Manual', scan: 'Escanear', map: 'Mapa', gps: 'GPS', odometer: 'Odômetro'},
spreadsheet: {
upload: 'Enviar uma planilha',
import: 'Importar planilha',
@@ -1285,6 +1277,8 @@ const translations: TranslationDeepObject = {
invalidRate: 'Taxa inválida para este workspace. Selecione uma taxa disponível do workspace.',
endDateBeforeStartDate: 'A data de término não pode ser anterior à data de início',
endDateSameAsStartDate: 'A data de término não pode ser igual à data de início',
+ invalidReadings: 'Insira as leituras de início e fim',
+ negativeDistanceNotAllowed: 'A leitura final deve ser maior que a leitura inicial',
},
dismissReceiptError: 'Dispensar erro',
dismissReceiptErrorConfirmation: 'Atenção! Ignorar este erro removerá completamente o seu recibo enviado. Tem certeza?',
@@ -7070,6 +7064,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe
error: {
selectSuggestedAddress: 'Selecione um endereço sugerido ou use a localização atual',
},
+ odometer: {startReading: 'Começar a ler', endReading: 'Encerrar leitura', saveForLater: 'Salvar para depois', totalDistance: 'Distância total'},
},
reportCardLostOrDamaged: {
screenTitle: 'Boletim perdido ou danificado',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 80332e27b648..c120fa37a572 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -941,15 +941,7 @@ const translations: TranslationDeepObject = {
subscription: '订阅',
domains: '域名',
},
- tabSelector: {
- chat: '聊天',
- room: '房间',
- distance: '距离',
- manual: '手动',
- scan: '扫描',
- map: '地图',
- gps: 'GPS',
- },
+ tabSelector: {chat: '聊天', room: '房间', distance: '距离', manual: '手动', scan: '扫描', map: '地图', gps: 'GPS', odometer: '里程表'},
spreadsheet: {
upload: '上传电子表格',
import: '导入电子表格',
@@ -1266,6 +1258,8 @@ const translations: TranslationDeepObject = {
invalidRate: '此汇率对该工作区无效。请选择此工作区中的可用汇率。',
endDateBeforeStartDate: '结束日期不能早于开始日期',
endDateSameAsStartDate: '结束日期不能与开始日期相同',
+ invalidReadings: '请输入起始读数和结束读数',
+ negativeDistanceNotAllowed: '结束读数必须大于开始读数',
},
dismissReceiptError: '忽略错误',
dismissReceiptErrorConfirmation: '提醒!关闭此错误会完全删除你上传的收据。确定要继续吗?',
@@ -6918,6 +6912,7 @@ ${reportName}
error: {
selectSuggestedAddress: '请选择一个建议地址或使用当前位置',
},
+ odometer: {startReading: '开始阅读', endReading: '结束阅读', saveForLater: '稍后保存', totalDistance: '总距离'},
},
reportCardLostOrDamaged: {
screenTitle: '成绩单遗失或损坏',
diff --git a/src/libs/API/parameters/CreateDistanceRequestParams.ts b/src/libs/API/parameters/CreateDistanceRequestParams.ts
index f7c7e8e65060..04a853d17be6 100644
--- a/src/libs/API/parameters/CreateDistanceRequestParams.ts
+++ b/src/libs/API/parameters/CreateDistanceRequestParams.ts
@@ -27,6 +27,8 @@ type CreateDistanceRequestParams = {
description?: string;
attendees?: string;
distance?: number;
+ odometerStart?: number;
+ odometerEnd?: number;
};
export default CreateDistanceRequestParams;
diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts
index 128a9613e254..8d7af62b6891 100644
--- a/src/libs/DebugUtils.ts
+++ b/src/libs/DebugUtils.ts
@@ -1054,6 +1054,8 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string)
name: CONST.RED_BRICK_ROAD_PENDING_ACTION,
defaultP2PRate: CONST.RED_BRICK_ROAD_PENDING_ACTION,
distanceUnit: CONST.RED_BRICK_ROAD_PENDING_ACTION,
+ odometerStart: CONST.RED_BRICK_ROAD_PENDING_ACTION,
+ odometerEnd: CONST.RED_BRICK_ROAD_PENDING_ACTION,
attendees: CONST.RED_BRICK_ROAD_PENDING_ACTION,
amount: CONST.RED_BRICK_ROAD_PENDING_ACTION,
taxAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION,
@@ -1160,6 +1162,8 @@ function validateTransactionDraftProperty(key: keyof Transaction, value: string)
units: 'object',
splitsStartDate: 'string',
splitsEndDate: 'string',
+ odometerStart: 'number',
+ odometerEnd: 'number',
});
case 'accountant':
return validateObject>(value, {
diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts
index 8f74ac4ae6df..04d6f7baebf2 100644
--- a/src/libs/IOUUtils.ts
+++ b/src/libs/IOUUtils.ts
@@ -32,6 +32,9 @@ function navigateToStartMoneyRequestStep(requestType: IOURequestType, iouType: I
case CONST.IOU.REQUEST_TYPE.DISTANCE_GPS:
Navigation.goBack(ROUTES.DISTANCE_REQUEST_CREATE_TAB_GPS.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID), {compareParams: false});
break;
+ case CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER:
+ Navigation.goBack(ROUTES.DISTANCE_REQUEST_CREATE_TAB_ODOMETER.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID), {compareParams: false});
+ break;
case CONST.IOU.REQUEST_TYPE.SCAN:
Navigation.goBack(ROUTES.MONEY_REQUEST_CREATE_TAB_SCAN.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID), {compareParams: false});
break;
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index c4cf60aedee8..74b9b20d7b44 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -183,6 +183,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../../pages/iou/request/step/IOURequestStepDistanceMap').default,
[SCREENS.MONEY_REQUEST.STEP_DISTANCE_MANUAL]: () => require('../../../../pages/iou/request/step/IOURequestStepDistanceManual').default,
[SCREENS.MONEY_REQUEST.STEP_DISTANCE_GPS]: () => require('../../../../pages/iou/request/step/IOURequestStepDistanceGPS').default,
+ [SCREENS.MONEY_REQUEST.STEP_DISTANCE_ODOMETER]: () => require('../../../../pages/iou/request/step/IOURequestStepDistanceOdometer').default,
[SCREENS.SET_DEFAULT_WORKSPACE]: () => require('../../../../pages/SetDefaultWorkspacePage').default,
});
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index ab2dc0ca7a99..245b34e64cb8 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1458,6 +1458,10 @@ const config: LinkingOptions['config'] = {
'distance-gps': {
path: ROUTES.DISTANCE_REQUEST_CREATE_TAB_GPS.route,
},
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'distance-odometer': {
+ path: ROUTES.DISTANCE_REQUEST_CREATE_TAB_ODOMETER.route,
+ },
},
},
[SCREENS.SETTINGS_CATEGORIES.SETTINGS_CATEGORIES_ROOT]: ROUTES.SETTINGS_CATEGORIES_ROOT.route,
@@ -1474,6 +1478,7 @@ const config: LinkingOptions['config'] = {
[SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.route,
[SCREENS.MONEY_REQUEST.STEP_DISTANCE]: ROUTES.MONEY_REQUEST_STEP_DISTANCE.route,
[SCREENS.MONEY_REQUEST.STEP_DISTANCE_MANUAL]: ROUTES.MONEY_REQUEST_STEP_DISTANCE_MANUAL.route,
+ [SCREENS.MONEY_REQUEST.STEP_DISTANCE_ODOMETER]: ROUTES.MONEY_REQUEST_STEP_DISTANCE_ODOMETER.route,
[SCREENS.MONEY_REQUEST.STEP_DISTANCE_RATE]: ROUTES.MONEY_REQUEST_STEP_DISTANCE_RATE.route,
[SCREENS.MONEY_REQUEST.HOLD]: ROUTES.MONEY_REQUEST_HOLD_REASON.route,
[SCREENS.MONEY_REQUEST.REJECT]: ROUTES.REJECT_MONEY_REQUEST_REASON.route,
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index f24b231bb490..30eaaae910ff 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1727,6 +1727,16 @@ type MoneyRequestNavigatorParamList = {
backToReport?: string;
reportActionID?: string;
};
+ [SCREENS.MONEY_REQUEST.STEP_DISTANCE_ODOMETER]: {
+ action: IOUAction;
+ iouType: IOUType;
+ transactionID: string;
+ reportID: string;
+ // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md
+ backTo: Routes;
+ backToReport?: string;
+ reportActionID?: string;
+ };
[SCREENS.MONEY_REQUEST.CREATE]: {
iouType: IOUType;
reportID: string;
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 14b3b0da5791..15a85201a2ae 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -786,6 +786,8 @@ type TransactionDetails = {
postedDate: string;
transactionID: string;
distance?: number;
+ odometerStart?: number;
+ odometerEnd?: number;
};
type OptimisticIOUReport = Pick<
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index 62213616a440..416eee2f3dac 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -114,6 +114,8 @@ type TransactionParams = {
splitsStartDate?: string;
splitsEndDate?: string;
distance?: number;
+ odometerStart?: number;
+ odometerEnd?: number;
};
type BuildOptimisticTransactionParams = {
@@ -159,7 +161,8 @@ function isDistanceRequest(transaction: OnyxEntry): 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_MANUAL ||
+ transaction?.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER
);
}
@@ -194,7 +197,28 @@ function isManualDistanceRequest(transaction: OnyxEntry, isUpdatedM
}
// This is the case for transaction objects once they have been saved to the server
- return hasDistanceCustomUnit(transaction) && isEmptyObject(transaction?.comment?.waypoints);
+ // Exclude odometer requests which also have no waypoints but have odometer readings
+ return (
+ hasDistanceCustomUnit(transaction) &&
+ isEmptyObject(transaction?.comment?.waypoints) &&
+ transaction?.comment?.odometerStart === undefined &&
+ transaction?.comment?.odometerEnd === undefined
+ );
+}
+
+function isOdometerDistanceRequest(transaction: OnyxEntry): 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_ODOMETER;
+ }
+
+ // This is the case for transaction objects once they have been saved to the server
+ // Odometer requests have odometerStart and odometerEnd in comment, and no waypoints
+ return (
+ hasDistanceCustomUnit(transaction) &&
+ isEmptyObject(transaction?.comment?.waypoints) &&
+ (transaction?.comment?.odometerStart !== undefined || transaction?.comment?.odometerEnd !== undefined)
+ );
}
function isScanRequest(transaction: OnyxEntry | Partial): boolean {
@@ -232,6 +256,9 @@ function isCorporateCardTransaction(transaction: OnyxEntry): boolea
}
function getRequestType(transaction: OnyxEntry): IOURequestType {
+ if (isOdometerDistanceRequest(transaction)) {
+ return CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER;
+ }
if (isManualDistanceRequest(transaction)) {
return CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL;
}
@@ -267,7 +294,8 @@ function getExpenseType(transaction: OnyxEntry): ValueOf;
}
/**
@@ -373,12 +401,20 @@ function buildOptimisticTransaction(params: BuildOptimisticTransactionParams): T
splitExpensesTotal,
participants,
pendingAction = CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ odometerStart,
+ odometerEnd,
} = transactionParams;
// transactionIDs are random, positive, 64-bit numeric strings.
// Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID)
const transactionID = existingTransactionID ?? rand64();
const commentJSON: Comment = {comment, attendees};
+ if (odometerStart !== undefined) {
+ commentJSON.odometerStart = odometerStart;
+ }
+ if (odometerEnd !== undefined) {
+ commentJSON.odometerEnd = odometerEnd;
+ }
if (isDemoTransactionParam) {
commentJSON.isDemoTransaction = true;
}
@@ -403,7 +439,8 @@ function buildOptimisticTransaction(params: BuildOptimisticTransactionParams): T
const isMapDistanceTransaction = !!pendingFields?.waypoints;
const isManualDistanceTransaction = isManualDistanceRequest(existingTransaction);
- if (isMapDistanceTransaction || isManualDistanceTransaction) {
+ const isOdometerDistanceTransaction = isOdometerDistanceRequest(existingTransaction);
+ if (isMapDistanceTransaction || isManualDistanceTransaction || isOdometerDistanceTransaction) {
// Set the distance unit, which comes from the policy distance unit or the P2P rate data
lodashSet(commentJSON, 'customUnit.distanceUnit', DistanceRequestUtils.getUpdatedDistanceUnit({transaction: existingTransaction, policy}));
lodashSet(commentJSON, 'customUnit.quantity', distance);
@@ -2550,6 +2587,7 @@ export {
isDistanceRequest,
isMapDistanceRequest,
isManualDistanceRequest,
+ isOdometerDistanceRequest,
isFetchingWaypointsFromServer,
isExpensifyCardTransaction,
isManagedCardTransaction,
diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts
index 746b3ad83c7b..85a177980975 100644
--- a/src/libs/actions/IOU/index.ts
+++ b/src/libs/actions/IOU/index.ts
@@ -223,6 +223,7 @@ import {
isFetchingWaypointsFromServer,
isManualDistanceRequest as isManualDistanceRequestTransactionUtils,
isMapDistanceRequest,
+ isOdometerDistanceRequest as isOdometerDistanceRequestTransactionUtils,
isOnHold,
isPending,
isPendingCardOrScanningTransaction,
@@ -604,6 +605,8 @@ type DistanceRequestTransactionParams = BaseTransactionParams & {
splitShares?: SplitShares;
distance?: number;
receipt?: Receipt;
+ odometerStart?: number;
+ odometerEnd?: number;
};
type CreateDistanceRequestInformation = {
@@ -664,6 +667,8 @@ type TrackExpenseTransactionParams = {
customUnitRateID?: string;
attendees?: Attendee[];
isLinkedTrackedExpenseReportArchived?: boolean;
+ odometerStart?: number;
+ odometerEnd?: number;
};
type TrackExpenseAccountantParams = {
@@ -705,6 +710,8 @@ type GetTrackExpenseInformationTransactionParams = {
linkedTrackedExpenseReportAction?: OnyxTypes.ReportAction;
attendees?: Attendee[];
distance?: number;
+ odometerStart?: number;
+ odometerEnd?: number;
};
type GetTrackExpenseInformationParticipantParams = {
@@ -1056,7 +1063,12 @@ function initMoneyRequest({
let requestCategory: string | null = null;
// Set up initial distance expense state
- if (newIouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE || newIouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MAP || newIouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL) {
+ if (
+ newIouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE ||
+ newIouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MAP ||
+ newIouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL ||
+ newIouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER
+ ) {
if (!isFromGlobalCreate) {
const isPolicyExpenseChat = isPolicyExpenseChatReportUtil(report) || isPolicyExpenseChatReportUtil(parentReport);
const customUnitRateID = DistanceRequestUtils.getCustomUnitRateID({reportID, isPolicyExpenseChat, policy, lastSelectedDistanceRates});
@@ -1067,7 +1079,7 @@ function initMoneyRequest({
if (comment.customUnit) {
comment.customUnit.quantity = null;
}
- if (newIouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL) {
+ if (newIouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL || newIouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER) {
comment.waypoints = undefined;
} else {
comment.waypoints = {
@@ -1075,6 +1087,11 @@ function initMoneyRequest({
waypoint1: {keyForList: 'stop_waypoint'},
};
}
+ // Initialize odometer readings for odometer type
+ if (newIouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER) {
+ comment.odometerStart = undefined;
+ comment.odometerEnd = undefined;
+ }
}
if (newIouRequestType === CONST.IOU.REQUEST_TYPE.PER_DIEM) {
@@ -1423,6 +1440,18 @@ function setMoneyRequestDistance(transactionID: string, distanceAsFloat: number,
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {comment: {customUnit: {quantity: distanceAsFloat}}});
}
+/**
+ * Set the odometer readings for a transaction
+ */
+function setMoneyRequestOdometerReading(transactionID: string, startReading: number, endReading: number, isDraft: boolean) {
+ Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
+ comment: {
+ odometerStart: startReading,
+ odometerEnd: endReading,
+ },
+ });
+}
+
/**
* Set the distance rate of a transaction.
* Used when creating a new transaction or moving an existing one from Self DM
@@ -3045,7 +3074,8 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma
existingTransaction &&
(existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE ||
existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MAP ||
- existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL);
+ existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL ||
+ existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER);
const isManualDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL;
let optimisticTransaction = buildOptimisticTransaction({
existingTransactionID,
@@ -3575,8 +3605,25 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T
} = params;
const {payeeAccountID = userAccountID, payeeEmail = currentUserEmail, participant} = participantParams;
const {policy, policyCategories, policyTagList} = policyParams;
- const {comment, amount, currency, created, distance, merchant, receipt, category, tag, taxCode, taxAmount, billable, reimbursable, linkedTrackedExpenseReportAction, attendees} =
- transactionParams;
+ const {
+ comment,
+ amount,
+ currency,
+ created,
+ distance,
+ merchant,
+ receipt,
+ category,
+ tag,
+ taxCode,
+ taxAmount,
+ billable,
+ reimbursable,
+ linkedTrackedExpenseReportAction,
+ attendees,
+ odometerStart,
+ odometerEnd,
+ } = transactionParams;
const optimisticData: OnyxUpdate[] = [];
const successData: OnyxUpdate[] = [];
@@ -3736,6 +3783,7 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T
const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${existingTransactionID ?? CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`];
const isDistanceRequest = existingTransaction && isDistanceRequestTransactionUtils(existingTransaction);
const isManualDistanceRequest = existingTransaction && isManualDistanceRequestTransactionUtils(existingTransaction);
+ const isOdometerDistanceRequest = existingTransaction && isOdometerDistanceRequestTransactionUtils(existingTransaction);
let optimisticTransaction = buildOptimisticTransaction({
existingTransactionID,
existingTransaction,
@@ -3758,6 +3806,8 @@ function getTrackExpenseInformation(params: GetTrackExpenseInformationParams): T
reimbursable,
filename: existingTransaction?.receipt?.filename,
attendees,
+ odometerStart: isOdometerDistanceRequest ? odometerStart : undefined,
+ odometerEnd: isOdometerDistanceRequest ? odometerEnd : undefined,
},
});
if (iouReport) {
@@ -4950,6 +5000,8 @@ type UpdateMoneyRequestDistanceParams = {
currentUserAccountIDParam: number;
currentUserEmailParam: string;
isASAPSubmitBetaEnabled: boolean;
+ odometerStart?: number;
+ odometerEnd?: number;
};
/** Updates the waypoints of a distance expense */
@@ -4966,11 +5018,15 @@ function updateMoneyRequestDistance({
currentUserAccountIDParam,
currentUserEmailParam,
isASAPSubmitBetaEnabled,
+ odometerStart,
+ odometerEnd,
}: UpdateMoneyRequestDistanceParams) {
const transactionChanges: TransactionChanges = {
...(waypoints && {waypoints: sanitizeRecentWaypoints(waypoints)}),
routes,
...(distance && {distance}),
+ ...(odometerStart !== undefined && {odometerStart}),
+ ...(odometerEnd !== undefined && {odometerEnd}),
};
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`] ?? null;
const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReport?.parentReportID}`] ?? null;
@@ -4993,6 +5049,13 @@ function updateMoneyRequestDistance({
}
const {params, onyxData} = data;
+ if (odometerStart !== undefined) {
+ params.odometerStart = odometerStart;
+ }
+ if (odometerEnd !== undefined) {
+ params.odometerEnd = odometerEnd;
+ }
+
if (!distance) {
const recentServerValidatedWaypoints = recentWaypoints.filter((item) => !item.pendingAction);
onyxData?.failureData?.push({
@@ -6168,6 +6231,8 @@ function trackExpense(params: CreateTrackExpenseParams) {
linkedTrackedExpenseReportID,
customUnitRateID,
attendees,
+ odometerStart,
+ odometerEnd,
} = transactionData;
const isMoneyRequestReport = isMoneyRequestReportReportUtils(report);
const currentChatReport = isMoneyRequestReport ? getReportOrDraftReport(report?.chatReportID) : report;
@@ -6256,6 +6321,8 @@ function trackExpense(params: CreateTrackExpenseParams) {
reimbursable,
linkedTrackedExpenseReportAction,
attendees,
+ odometerStart,
+ odometerEnd,
},
policyParams: {
policy,
@@ -6279,7 +6346,7 @@ function trackExpense(params: CreateTrackExpenseParams) {
value: recentServerValidatedWaypoints,
});
- if (isMapDistanceRequest(transaction) || isManualDistanceRequestTransactionUtils(transaction)) {
+ if (isMapDistanceRequest(transaction) || isManualDistanceRequestTransactionUtils(transaction) || isOdometerDistanceRequestTransactionUtils(transaction)) {
// @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
onyxData?.optimisticData?.push({
onyxMethod: Onyx.METHOD.SET,
@@ -8007,6 +8074,8 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest
splitShares = {},
attendees,
receipt,
+ odometerStart,
+ odometerEnd,
} = transactionParams;
// If the report is an iou or expense report, we should get the linked chat report to be passed to the getMoneyRequestInformation function
@@ -8080,6 +8149,8 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest
chatType: splitData.chatType,
description: parsedComment,
attendees: attendees ? JSON.stringify(attendees) : undefined,
+ odometerStart,
+ odometerEnd,
};
} else {
const participant = participants.at(0) ?? {};
@@ -8136,7 +8207,7 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest
onyxData = moneyRequestOnyxData;
- if (transaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MAP || isManualDistanceRequest) {
+ if (transaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_MAP || isManualDistanceRequest || transaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER) {
// @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830
onyxData?.optimisticData?.push({
onyxMethod: Onyx.METHOD.SET,
@@ -8157,6 +8228,8 @@ function createDistanceRequest(distanceRequestInformation: CreateDistanceRequest
waypoints: JSON.stringify(sanitizedWaypoints),
distance,
receipt,
+ odometerStart,
+ odometerEnd,
created,
category,
tag,
@@ -14597,6 +14670,7 @@ export {
setMoneyRequestDescription,
setMoneyRequestDistance,
setMoneyRequestDistanceRate,
+ setMoneyRequestOdometerReading,
setMoneyRequestMerchant,
setMoneyRequestParticipants,
setMoneyRequestParticipantsFromReport,
diff --git a/src/pages/iou/request/DistanceRequestStartPage.tsx b/src/pages/iou/request/DistanceRequestStartPage.tsx
index 74dd77260d17..e2bd8a4a32ea 100644
--- a/src/pages/iou/request/DistanceRequestStartPage.tsx
+++ b/src/pages/iou/request/DistanceRequestStartPage.tsx
@@ -33,6 +33,7 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import IOURequestStepDistanceGPS from './step/IOURequestStepDistanceGPS';
import IOURequestStepDistanceManual from './step/IOURequestStepDistanceManual';
import IOURequestStepDistanceMap from './step/IOURequestStepDistanceMap';
+import IOURequestStepDistanceOdometer from './step/IOURequestStepDistanceOdometer';
import type {WithWritableReportOrNotFoundProps} from './step/withWritableReportOrNotFound';
type DistanceRequestStartPageProps = WithWritableReportOrNotFoundProps & {
@@ -220,6 +221,18 @@ function DistanceRequestStartPage({
)}
)}
+ {false && (
+
+ {() => (
+
+
+
+ )}
+
+ )}
diff --git a/src/pages/iou/request/IOURequestRedirectToStartPage.tsx b/src/pages/iou/request/IOURequestRedirectToStartPage.tsx
index 084b14c02a84..f0e07e68c011 100644
--- a/src/pages/iou/request/IOURequestRedirectToStartPage.tsx
+++ b/src/pages/iou/request/IOURequestRedirectToStartPage.tsx
@@ -40,6 +40,8 @@ function IOURequestRedirectToStartPage({
Navigation.navigate(ROUTES.DISTANCE_REQUEST_CREATE_TAB_MANUAL.getRoute(CONST.IOU.ACTION.CREATE, iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, optimisticReportID));
} else if (iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_GPS) {
Navigation.navigate(ROUTES.DISTANCE_REQUEST_CREATE_TAB_GPS.getRoute(CONST.IOU.ACTION.CREATE, iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, optimisticReportID));
+ } else if (iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER) {
+ Navigation.navigate(ROUTES.DISTANCE_REQUEST_CREATE_TAB_ODOMETER.getRoute(CONST.IOU.ACTION.CREATE, iouType, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, optimisticReportID));
}
// This useEffect should only run on mount which is why there are no dependencies being passed in the second parameter
diff --git a/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx b/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx
index 31cae7433fd7..da853ae17a96 100644
--- a/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx
+++ b/src/pages/iou/request/step/DiscardChangesConfirmation/index.native.tsx
@@ -1,18 +1,19 @@
import type {NavigationAction} from '@react-navigation/native';
-import {usePreventRemove} from '@react-navigation/native';
+import {useIsFocused, usePreventRemove} from '@react-navigation/native';
import React, {memo, useCallback, useRef, useState} from 'react';
import ConfirmModal from '@components/ConfirmModal';
import useLocalize from '@hooks/useLocalize';
import navigationRef from '@libs/Navigation/navigationRef';
import type DiscardChangesConfirmationProps from './types';
-function DiscardChangesConfirmation({getHasUnsavedChanges}: DiscardChangesConfirmationProps) {
+function DiscardChangesConfirmation({getHasUnsavedChanges, isEnabled = true}: DiscardChangesConfirmationProps) {
const {translate} = useLocalize();
+ const isFocused = useIsFocused();
const [isVisible, setIsVisible] = useState(false);
const shouldAllowNavigation = useRef(false);
const blockedNavigationAction = useRef(undefined);
- const hasUnsavedChanges = getHasUnsavedChanges();
+ const hasUnsavedChanges = isEnabled && isFocused && getHasUnsavedChanges();
const shouldPrevent = hasUnsavedChanges && !shouldAllowNavigation.current;
usePreventRemove(
diff --git a/src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx b/src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx
index 9b213b878e81..40a2ab96c88c 100644
--- a/src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx
+++ b/src/pages/iou/request/step/DiscardChangesConfirmation/index.tsx
@@ -1,5 +1,5 @@
import type {NavigationAction} from '@react-navigation/native';
-import {useNavigation} from '@react-navigation/native';
+import {useIsFocused, useNavigation} from '@react-navigation/native';
import React, {memo, useCallback, useEffect, useRef, useState} from 'react';
import ConfirmModal from '@components/ConfirmModal';
import useBeforeRemove from '@hooks/useBeforeRemove';
@@ -11,8 +11,9 @@ import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNa
import type {RootNavigatorParamList} from '@libs/Navigation/types';
import type DiscardChangesConfirmationProps from './types';
-function DiscardChangesConfirmation({getHasUnsavedChanges, onCancel}: DiscardChangesConfirmationProps) {
+function DiscardChangesConfirmation({getHasUnsavedChanges, onCancel, isEnabled = true}: DiscardChangesConfirmationProps) {
const navigation = useNavigation>();
+ const isFocused = useIsFocused();
const {translate} = useLocalize();
const [isVisible, setIsVisible] = useState(false);
const blockedNavigationAction = useRef(undefined);
@@ -22,7 +23,7 @@ function DiscardChangesConfirmation({getHasUnsavedChanges, onCancel}: DiscardCha
useBeforeRemove(
useCallback(
(e) => {
- if (!getHasUnsavedChanges() || shouldNavigateBack.current) {
+ if (!isEnabled || !isFocused || !getHasUnsavedChanges() || shouldNavigateBack.current) {
return;
}
@@ -30,8 +31,9 @@ function DiscardChangesConfirmation({getHasUnsavedChanges, onCancel}: DiscardCha
blockedNavigationAction.current = e.data.action;
navigateAfterInteraction(() => setIsVisible((prev) => !prev));
},
- [getHasUnsavedChanges],
+ [getHasUnsavedChanges, isFocused, isEnabled],
),
+ isEnabled && isFocused,
);
/**
@@ -40,6 +42,9 @@ function DiscardChangesConfirmation({getHasUnsavedChanges, onCancel}: DiscardCha
* So we need to go forward to get back to the current page
*/
useEffect(() => {
+ if (!isEnabled || !isFocused) {
+ return undefined;
+ }
// transitionStart is triggered before the previous page is fully loaded so RHP sliding animation
// could be less "glitchy" when going back and forth between the previous and current pages
const unsubscribe = navigation.addListener('transitionStart', ({data: {closing}}) => {
@@ -58,7 +63,16 @@ function DiscardChangesConfirmation({getHasUnsavedChanges, onCancel}: DiscardCha
});
return unsubscribe;
- }, [navigation, getHasUnsavedChanges]);
+ }, [navigation, getHasUnsavedChanges, isFocused, isEnabled]);
+
+ useEffect(() => {
+ if ((isFocused && isEnabled) || !isVisible) {
+ return;
+ }
+ setIsVisible(false);
+ blockedNavigationAction.current = undefined;
+ shouldNavigateBack.current = false;
+ }, [isFocused, isVisible, isEnabled]);
const navigateBack = useCallback(() => {
if (blockedNavigationAction.current) {
diff --git a/src/pages/iou/request/step/DiscardChangesConfirmation/types.ts b/src/pages/iou/request/step/DiscardChangesConfirmation/types.ts
index 2fdd9d24b5f2..95ee6cfa4da4 100644
--- a/src/pages/iou/request/step/DiscardChangesConfirmation/types.ts
+++ b/src/pages/iou/request/step/DiscardChangesConfirmation/types.ts
@@ -1,6 +1,7 @@
type DiscardChangesConfirmationProps = {
getHasUnsavedChanges: () => boolean;
onCancel?: () => void;
+ isEnabled?: boolean;
};
export default DiscardChangesConfirmationProps;
diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
index 2458ceaad6b7..01165442aadb 100644
--- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
+++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx
@@ -68,6 +68,7 @@ import {
hasReceipt,
isDistanceRequest as isDistanceRequestTransactionUtils,
isManualDistanceRequest as isManualDistanceRequestTransactionUtils,
+ isOdometerDistanceRequest as isOdometerDistanceRequestTransactionUtils,
isScanRequest,
} from '@libs/TransactionUtils';
import type {GpsPoint} from '@userActions/IOU';
@@ -240,6 +241,8 @@ function IOURequestStepConfirmation({
const requestType = getRequestType(transaction);
const isDistanceRequest = isDistanceRequestTransactionUtils(transaction);
const isManualDistanceRequest = isManualDistanceRequestTransactionUtils(transaction);
+ const isOdometerDistanceRequest = isOdometerDistanceRequestTransactionUtils(transaction);
+ const transactionDistance = isManualDistanceRequest || isOdometerDistanceRequest ? (transaction?.comment?.customUnit?.quantity ?? undefined) : undefined;
const isPerDiemRequest = requestType === CONST.IOU.REQUEST_TYPE.PER_DIEM;
const [lastLocationPermissionPrompt] = useOnyx(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, {canBeMissing: true});
const {
@@ -723,6 +726,7 @@ function IOURequestStepConfirmation({
for (const [index, item] of transactions.entries()) {
const isLinkedTrackedExpenseReportArchived =
!!item.linkedTrackedExpenseReportID && archivedReportsIdSet.has(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${item.linkedTrackedExpenseReportID}`);
+ const itemDistance = isManualDistanceRequest || isOdometerDistanceRequest ? (item.comment?.customUnit?.quantity ?? undefined) : undefined;
trackExpenseIOUActions({
report,
@@ -740,7 +744,7 @@ function IOURequestStepConfirmation({
},
transactionParams: {
amount: item.amount,
- distance: isManualDistanceRequest ? (item.comment?.customUnit?.quantity ?? undefined) : undefined,
+ distance: itemDistance,
currency: item.currency,
created: item.created,
merchant: item.merchant,
@@ -760,6 +764,8 @@ function IOURequestStepConfirmation({
customUnitRateID,
attendees: item.comment?.attendees,
isLinkedTrackedExpenseReportArchived,
+ odometerStart: isOdometerDistanceRequest ? item.comment?.odometerStart : undefined,
+ odometerEnd: isOdometerDistanceRequest ? item.comment?.odometerEnd : undefined,
},
accountantParams: {
accountant: item.accountant,
@@ -789,6 +795,7 @@ function IOURequestStepConfirmation({
customUnitRateID,
isDraftPolicy,
isManualDistanceRequest,
+ isOdometerDistanceRequest,
archivedReportsIdSet,
isASAPSubmitBetaEnabled,
introSelected,
@@ -802,6 +809,7 @@ function IOURequestStepConfirmation({
if (!transaction) {
return;
}
+
createDistanceRequestIOUActions({
report,
participants: selectedParticipants,
@@ -819,7 +827,7 @@ function IOURequestStepConfirmation({
transactionParams: {
amount: transaction.amount,
comment: trimmedComment,
- distance: isManualDistanceRequest ? (transaction.comment?.customUnit?.quantity ?? undefined) : undefined,
+ distance: transactionDistance,
created: transaction.created,
currency: transaction.currency,
merchant: transaction.merchant,
@@ -833,7 +841,9 @@ function IOURequestStepConfirmation({
billable: transaction.billable,
reimbursable: transaction.reimbursable,
attendees: transaction.comment?.attendees,
- receipt: isManualDistanceRequest ? receiptFiles[transaction.transactionID] : undefined,
+ receipt: isManualDistanceRequest || isOdometerDistanceRequest ? receiptFiles[transaction.transactionID] : undefined,
+ odometerStart: isOdometerDistanceRequest ? transaction.comment?.odometerStart : undefined,
+ odometerEnd: isOdometerDistanceRequest ? transaction.comment?.odometerEnd : undefined,
},
backToReport,
isASAPSubmitBetaEnabled,
@@ -849,11 +859,13 @@ function IOURequestStepConfirmation({
currentUserPersonalDetails.accountID,
iouType,
policy,
+ isOdometerDistanceRequest,
policyCategories,
policyTags,
policyRecentlyUsedCategories,
policyRecentlyUsedTags,
isManualDistanceRequest,
+ transactionDistance,
transactionTaxCode,
transactionTaxAmount,
customUnitRateID,
@@ -1364,13 +1376,14 @@ function IOURequestStepConfirmation({
receiptFilename={receiptFilename}
iouType={iouType}
reportID={reportID}
- shouldDisplayReceipt={!isMovingTransactionFromTrackExpense && (!isDistanceRequest || isManualDistanceRequest) && !isPerDiemRequest}
+ shouldDisplayReceipt={!isMovingTransactionFromTrackExpense && (!isDistanceRequest || isManualDistanceRequest || isOdometerDistanceRequest) && !isPerDiemRequest}
isPolicyExpenseChat={isPolicyExpenseChat}
policyID={policyID}
iouMerchant={transaction?.merchant}
iouCreated={transaction?.created}
isDistanceRequest={isDistanceRequest}
isManualDistanceRequest={isManualDistanceRequest}
+ isOdometerDistanceRequest={isOdometerDistanceRequest}
isPerDiemRequest={isPerDiemRequest}
shouldShowSmartScanFields={shouldShowSmartScanFields}
action={action}
diff --git a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx
new file mode 100644
index 000000000000..c443be8c93bf
--- /dev/null
+++ b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx
@@ -0,0 +1,581 @@
+import {useIsFocused} from '@react-navigation/native';
+import reportsSelector from '@selectors/Attributes';
+import React, {useEffect, useMemo, useRef, useState} from 'react';
+import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import Button from '@components/Button';
+import FormHelpMessage from '@components/FormHelpMessage';
+import Text from '@components/Text';
+import TextInput from '@components/TextInput';
+import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
+import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
+import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import useDefaultExpensePolicy from '@hooks/useDefaultExpensePolicy';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import usePersonalPolicy from '@hooks/usePersonalPolicy';
+import usePolicy from '@hooks/usePolicy';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {
+ createDistanceRequest,
+ getMoneyRequestParticipantsFromReport,
+ setCustomUnitRateID,
+ setMoneyRequestDistance,
+ setMoneyRequestMerchant,
+ setMoneyRequestOdometerReading,
+ setMoneyRequestParticipantsFromReport,
+ setMoneyRequestPendingFields,
+ trackExpense,
+ updateMoneyRequestDistance,
+} from '@libs/actions/IOU';
+import {setTransactionReport} from '@libs/actions/Transaction';
+import DistanceRequestUtils from '@libs/DistanceRequestUtils';
+import {navigateToParticipantPage, shouldUseTransactionDraft} from '@libs/IOUUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import {roundToTwoDecimalPlaces} from '@libs/NumberUtils';
+import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils';
+import {isPaidGroupPolicy} from '@libs/PolicyUtils';
+import {getPolicyExpenseChat, isArchivedReport, isPolicyExpenseChat as isPolicyExpenseChatUtils} from '@libs/ReportUtils';
+import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import type {ReportAttributesDerivedValue} from '@src/types/onyx/DerivedValues';
+import type Transaction from '@src/types/onyx/Transaction';
+import DiscardChangesConfirmation from './DiscardChangesConfirmation';
+import StepScreenWrapper from './StepScreenWrapper';
+import withFullTransactionOrNotFound from './withFullTransactionOrNotFound';
+import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound';
+import withWritableReportOrNotFound from './withWritableReportOrNotFound';
+
+type IOURequestStepDistanceOdometerProps = WithCurrentUserPersonalDetailsProps &
+ WithWritableReportOrNotFoundProps & {
+ /** The transaction object being modified in Onyx */
+ transaction: OnyxEntry;
+ };
+
+function IOURequestStepDistanceOdometer({
+ report,
+ route: {
+ params: {action, iouType, reportID, transactionID, backTo, backToReport},
+ },
+ transaction,
+ currentUserPersonalDetails,
+}: IOURequestStepDistanceOdometerProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const {isExtraSmallScreenHeight} = useResponsiveLayout();
+ const isFocused = useIsFocused();
+
+ const startReadingInputRef = useRef(null);
+ const endReadingInputRef = useRef(null);
+
+ const [startReading, setStartReading] = useState('');
+ const [endReading, setEndReading] = useState('');
+ const [formError, setFormError] = useState('');
+ // Key to force TextInput remount when resetting state after tab switch
+ const [inputKey, setInputKey] = useState(0);
+
+ // Track initial values for DiscardChangesConfirmation
+ const initialStartReadingRef = useRef('');
+ const initialEndReadingRef = useRef('');
+ const hasInitializedRefs = useRef(false);
+ // Track previous transaction values to detect when transaction is cleared (e.g., tab switch)
+ const prevTransactionStartRef = useRef(undefined);
+ const prevTransactionEndRef = useRef(undefined);
+ // Track local state via refs to avoid including them in useEffect dependencies
+ const startReadingRef = useRef('');
+ const endReadingRef = useRef('');
+
+ const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`, {canBeMissing: true});
+ const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, {canBeMissing: true});
+ const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false});
+ const [reportAttributesDerived] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {canBeMissing: true, selector: reportsSelector});
+ const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true});
+ const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE, {canBeMissing: true});
+ const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
+ const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true});
+ const [policyRecentlyUsedCurrencies] = useOnyx(ONYXKEYS.RECENTLY_USED_CURRENCIES, {canBeMissing: true});
+ const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`, {canBeMissing: true});
+ const policy = usePolicy(report?.policyID);
+ const personalPolicy = usePersonalPolicy();
+ const defaultExpensePolicy = useDefaultExpensePolicy();
+
+ const isEditing = action === CONST.IOU.ACTION.EDIT;
+ const isCreatingNewRequest = !(backTo ?? isEditing);
+ const isTransactionDraft = shouldUseTransactionDraft(action, iouType);
+ const currentUserAccountIDParam = currentUserPersonalDetails.accountID;
+ const currentUserEmailParam = currentUserPersonalDetails.login ?? '';
+
+ const shouldUseDefaultExpensePolicy = useMemo(
+ () =>
+ iouType === CONST.IOU.TYPE.CREATE &&
+ isPaidGroupPolicy(defaultExpensePolicy) &&
+ defaultExpensePolicy?.isPolicyExpenseChatEnabled &&
+ !shouldRestrictUserBillableActions(defaultExpensePolicy.id),
+ [iouType, defaultExpensePolicy],
+ );
+
+ const unit = DistanceRequestUtils.getRate({transaction, policy: shouldUseDefaultExpensePolicy ? defaultExpensePolicy : policy}).unit;
+
+ const shouldSkipConfirmation: boolean = !skipConfirmation || !report?.reportID ? false : !(isArchivedReport(reportNameValuePairs) || isPolicyExpenseChatUtils(report));
+
+ // Reset component state when transaction has no odometer data (happens when switching tabs)
+ // In Phase 1, we don't persist data from transaction since users can't save and exit
+ useEffect(() => {
+ if (!isFocused) {
+ return;
+ }
+
+ const currentStart = transaction?.comment?.odometerStart;
+ const currentEnd = transaction?.comment?.odometerEnd;
+
+ // Check if transaction was cleared (had values before, now null/undefined)
+ // This happens when switching tabs, not during normal typing
+ const wasCleared =
+ (prevTransactionStartRef.current !== null && prevTransactionStartRef.current !== undefined && (currentStart === null || currentStart === undefined)) ||
+ (prevTransactionEndRef.current !== null && prevTransactionEndRef.current !== undefined && (currentEnd === null || currentEnd === undefined));
+
+ const hasTransactionData = (currentStart !== null && currentStart !== undefined) || (currentEnd !== null && currentEnd !== undefined);
+
+ // Reset if transaction was cleared (had values before, now null)
+ // This happens when switching tabs - transaction data is cleared but local state persists
+ // Also reset if transaction is empty (component remounted after tab switch)
+ // Don't reset in edit mode as we want to preserve user's changes
+ const shouldReset =
+ hasInitializedRefs.current &&
+ !isEditing &&
+ !backToReport &&
+ !hasTransactionData &&
+ (wasCleared || (prevTransactionStartRef.current === undefined && prevTransactionEndRef.current === undefined));
+
+ if (shouldReset) {
+ setStartReading('');
+ setEndReading('');
+ startReadingRef.current = '';
+ endReadingRef.current = '';
+ initialStartReadingRef.current = '';
+ initialEndReadingRef.current = '';
+ setFormError('');
+ // Force TextInput remount to reset label position
+ setInputKey((prev) => prev + 1);
+ }
+
+ // Update refs to track previous values
+ prevTransactionStartRef.current = currentStart;
+ prevTransactionEndRef.current = currentEnd;
+ }, [isFocused, isEditing, backToReport, transaction?.comment?.odometerStart, transaction?.comment?.odometerEnd]);
+
+ // Initialize initial values refs on mount for DiscardChangesConfirmation
+ // These should never be updated after mount - they represent the "baseline" state
+ useEffect(() => {
+ if (hasInitializedRefs.current) {
+ return;
+ }
+ const currentStart = transaction?.comment?.odometerStart;
+ const currentEnd = transaction?.comment?.odometerEnd;
+ const startValue = currentStart !== null && currentStart !== undefined ? currentStart.toString() : '';
+ const endValue = currentEnd !== null && currentEnd !== undefined ? currentEnd.toString() : '';
+ initialStartReadingRef.current = startValue;
+ initialEndReadingRef.current = endValue;
+ hasInitializedRefs.current = true;
+ }, [transaction?.comment?.odometerStart, transaction?.comment?.odometerEnd]);
+
+ // Initialize values from transaction when editing or when transaction has data (but not when switching tabs)
+ // This updates the current state, but NOT the initial refs (those are set only once on mount)
+ useEffect(() => {
+ const currentStart = transaction?.comment?.odometerStart;
+ const currentEnd = transaction?.comment?.odometerEnd;
+
+ // Only initialize if:
+ // 1. We haven't initialized yet AND transaction has data, OR
+ // 2. We're editing and transaction has data (to load existing values), OR
+ // 3. Transaction has data but local state is empty (user navigated back from another page)
+ const hasTransactionData = (currentStart !== null && currentStart !== undefined) || (currentEnd !== null && currentEnd !== undefined);
+ const hasLocalState = startReadingRef.current || endReadingRef.current;
+ const shouldInitialize =
+ (!hasInitializedRefs.current && hasTransactionData) ||
+ (isEditing && hasTransactionData && !hasLocalState) ||
+ (hasTransactionData && !hasLocalState && hasInitializedRefs.current);
+
+ if (shouldInitialize) {
+ const startValue = currentStart !== null && currentStart !== undefined ? currentStart.toString() : '';
+ const endValue = currentEnd !== null && currentEnd !== undefined ? currentEnd.toString() : '';
+
+ if (startValue || endValue) {
+ setStartReading(startValue);
+ setEndReading(endValue);
+ startReadingRef.current = startValue;
+ endReadingRef.current = endValue;
+ }
+ }
+ }, [transaction?.comment?.odometerStart, transaction?.comment?.odometerEnd, isEditing]);
+
+ // Calculate total distance - updated live after every input change
+ const totalDistance = (() => {
+ const start = parseFloat(startReading);
+ const end = parseFloat(endReading);
+ if (Number.isNaN(start) || Number.isNaN(end) || !startReading || !endReading) {
+ return null;
+ }
+ const distance = end - start;
+ // Show 0 if distance is negative
+ return distance <= 0 ? 0 : distance;
+ })();
+
+ const buttonText = (() => {
+ if (shouldSkipConfirmation) {
+ return translate('iou.createExpense');
+ }
+ const shouldShowSave = isEditing || backToReport;
+ return shouldShowSave ? translate('common.save') : translate('common.next');
+ })();
+
+ const cleanOdometerReading = (text: string): string => {
+ // Allow digits and one decimal point or comma
+ // Remove all characters except digits, dots, and commas
+ let cleaned = text.replaceAll(/[^0-9.,]/g, '');
+ // Replace comma with dot for consistency
+ cleaned = cleaned.replaceAll(',', '.');
+ // Allow only one decimal point
+ const parts = cleaned.split('.');
+ if (parts.length > 2) {
+ cleaned = `${parts.at(0) ?? ''}.${parts.slice(1).join('')}`;
+ }
+ // Don't allow decimal point at the start
+ if (cleaned.startsWith('.')) {
+ cleaned = `0${cleaned}`;
+ }
+ return cleaned;
+ };
+
+ const handleStartReadingChange = (text: string) => {
+ const cleaned = cleanOdometerReading(text);
+ setStartReading(cleaned);
+ startReadingRef.current = cleaned;
+ if (formError) {
+ setFormError('');
+ }
+ };
+
+ const handleEndReadingChange = (text: string) => {
+ const cleaned = cleanOdometerReading(text);
+ setEndReading(cleaned);
+ endReadingRef.current = cleaned;
+ if (formError) {
+ setFormError('');
+ }
+ };
+
+ // Navigate to confirmation page helper - following Manual tab pattern
+ const navigateToConfirmationPage = () => {
+ if (!transactionID || !reportID) {
+ return;
+ }
+ switch (iouType) {
+ case CONST.IOU.TYPE.REQUEST:
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, CONST.IOU.TYPE.SUBMIT, transactionID, reportID, backToReport));
+ break;
+ default:
+ Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, backToReport));
+ }
+ };
+
+ const navigateBack = () => {
+ Navigation.goBack(backTo);
+ };
+
+ // Navigate to next page following Manual tab pattern
+ const navigateToNextPage = () => {
+ const start = parseFloat(startReading);
+ const end = parseFloat(endReading);
+
+ // Store odometer readings in transaction.comment.odometerStart/odometerEnd
+ setMoneyRequestOdometerReading(transactionID, start, end, isTransactionDraft);
+
+ // Calculate total distance (endReading - startReading)
+ const distance = end - start;
+ const calculatedDistance = roundToTwoDecimalPlaces(distance);
+
+ // Store total distance in transaction.comment.customUnit.quantity
+ setMoneyRequestDistance(transactionID, calculatedDistance, isTransactionDraft);
+
+ if (isEditing) {
+ // Update existing transaction
+ const previousDistance = transaction?.comment?.customUnit?.quantity;
+ const previousStart = transaction?.comment?.odometerStart;
+ const previousEnd = transaction?.comment?.odometerEnd;
+ const hasChanges = previousDistance !== calculatedDistance || previousStart !== start || previousEnd !== end;
+
+ if (hasChanges) {
+ // Update distance (which will also update amount and merchant)
+ updateMoneyRequestDistance({
+ transactionID: transaction?.transactionID,
+ transactionThreadReportID: reportID,
+ distance: calculatedDistance,
+ odometerStart: start,
+ odometerEnd: end,
+ // Not required for odometer distance request
+ transactionBackup: undefined,
+ policy,
+ currentUserAccountIDParam,
+ currentUserEmailParam,
+ isASAPSubmitBetaEnabled: false,
+ });
+ }
+ Navigation.goBack();
+ return;
+ }
+
+ if (backToReport) {
+ Navigation.goBack(backTo);
+ return;
+ }
+
+ // If a reportID exists in the report object, use it to set participants and navigate to confirmation
+ // Following Manual tab pattern
+ if (report?.reportID && !isArchivedReport(reportNameValuePairs) && iouType !== CONST.IOU.TYPE.CREATE) {
+ const selectedParticipants = getMoneyRequestParticipantsFromReport(report, currentUserPersonalDetails.accountID);
+ const derivedReports = (reportAttributesDerived as ReportAttributesDerivedValue | undefined)?.reports;
+ const participants = selectedParticipants.map((participant) => {
+ const participantAccountID = participant?.accountID ?? CONST.DEFAULT_NUMBER_ID;
+ return participantAccountID ? getParticipantsOption(participant, personalDetails) : getReportOption(participant, derivedReports);
+ });
+
+ if (shouldSkipConfirmation) {
+ setMoneyRequestPendingFields(transactionID, {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD});
+ setMoneyRequestMerchant(transactionID, translate('iou.fieldPending'), false);
+
+ const participant = participants.at(0);
+ const customUnitRateID = DistanceRequestUtils.getCustomUnitRateID({
+ reportID: report.reportID,
+ isPolicyExpenseChat: !!participant?.isPolicyExpenseChat,
+ policy,
+ lastSelectedDistanceRates,
+ });
+
+ if (iouType === CONST.IOU.TYPE.TRACK && participant) {
+ trackExpense({
+ report,
+ isDraftPolicy: false,
+ participantParams: {
+ payeeEmail: currentUserEmailParam,
+ payeeAccountID: currentUserAccountIDParam,
+ participant,
+ },
+ policyParams: {
+ policy,
+ },
+ transactionParams: {
+ amount: 0,
+ distance: calculatedDistance,
+ currency: transaction?.currency ?? 'USD',
+ created: transaction?.created ?? '',
+ merchant: translate('iou.fieldPending'),
+ receipt: {},
+ billable: false,
+ customUnitRateID,
+ attendees: transaction?.comment?.attendees,
+ odometerStart: start,
+ odometerEnd: end,
+ },
+ isASAPSubmitBetaEnabled: false,
+ currentUserAccountIDParam,
+ currentUserEmailParam,
+ introSelected,
+ activePolicyID,
+ quickAction,
+ });
+ return;
+ }
+
+ createDistanceRequest({
+ report,
+ participants,
+ currentUserLogin: currentUserEmailParam,
+ currentUserAccountID: currentUserAccountIDParam,
+ iouType,
+ existingTransaction: transaction,
+ transactionParams: {
+ amount: 0,
+ distance: calculatedDistance,
+ comment: '',
+ created: transaction?.created ?? '',
+ currency: transaction?.currency ?? 'USD',
+ merchant: translate('iou.fieldPending'),
+ billable: !!policy?.defaultBillable,
+ customUnitRateID,
+ splitShares: transaction?.splitShares,
+ attendees: transaction?.comment?.attendees,
+ odometerStart: start,
+ odometerEnd: end,
+ },
+ backToReport,
+ isASAPSubmitBetaEnabled: false,
+ transactionViolations,
+ quickAction,
+ policyRecentlyUsedCurrencies: policyRecentlyUsedCurrencies ?? [],
+ });
+ return;
+ }
+
+ setMoneyRequestParticipantsFromReport(transactionID, report, currentUserPersonalDetails.accountID).then(() => {
+ navigateToConfirmationPage();
+ });
+ return;
+ }
+
+ // If there was no reportID, then that means the user started this flow from the global menu
+ // and an optimistic reportID was generated. In that case, the next step is to select the participants for this expense.
+ if (shouldUseDefaultExpensePolicy) {
+ const activePolicyExpenseChat = getPolicyExpenseChat(currentUserAccountIDParam, defaultExpensePolicy?.id);
+ const shouldAutoReport = !!defaultExpensePolicy?.autoReporting || !!personalPolicy?.autoReporting;
+ const transactionReportID = shouldAutoReport ? activePolicyExpenseChat?.reportID : CONST.REPORT.UNREPORTED_REPORT_ID;
+ const rateID = DistanceRequestUtils.getCustomUnitRateID({
+ reportID: transactionReportID,
+ isPolicyExpenseChat: true,
+ policy: defaultExpensePolicy,
+ lastSelectedDistanceRates,
+ });
+ setTransactionReport(transactionID, {reportID: transactionReportID}, true);
+ setCustomUnitRateID(transactionID, rateID);
+ setMoneyRequestParticipantsFromReport(transactionID, activePolicyExpenseChat, currentUserPersonalDetails.accountID).then(() => {
+ Navigation.navigate(
+ ROUTES.MONEY_REQUEST_STEP_CONFIRMATION.getRoute(
+ CONST.IOU.ACTION.CREATE,
+ iouType === CONST.IOU.TYPE.CREATE ? CONST.IOU.TYPE.SUBMIT : iouType,
+ transactionID,
+ activePolicyExpenseChat?.reportID,
+ ),
+ );
+ });
+ } else if (transactionID && reportID) {
+ navigateToParticipantPage(iouType, transactionID, reportID);
+ }
+ };
+
+ // Handle form submission with validation
+ const handleNext = () => {
+ // Validation: Start and end readings must not be empty
+ if (!startReading || !endReading) {
+ setFormError(translate('iou.error.invalidReadings'));
+ return;
+ }
+
+ const start = parseFloat(startReading);
+ const end = parseFloat(endReading);
+
+ if (Number.isNaN(start) || Number.isNaN(end)) {
+ setFormError(translate('iou.error.invalidReadings'));
+ return;
+ }
+
+ // Validation: Calculated distance (end - start) must be > 0
+ const distance = end - start;
+ if (distance <= 0) {
+ setFormError(translate('iou.error.negativeDistanceNotAllowed'));
+ return;
+ }
+
+ // When validation passes, call navigateToNextPage
+ navigateToNextPage();
+ };
+
+ const shouldEnableDiscardConfirmation = !backToReport && !shouldSkipConfirmation && !isEditing;
+
+ return (
+
+
+
+ {/* Start Reading */}
+
+
+
+
+
+ {/* End Reading */}
+
+
+
+
+
+
+ {/* Total Distance Display - always shown, updated live */}
+
+
+ {`${translate('distance.odometer.totalDistance')}: ${totalDistance !== null ? roundToTwoDecimalPlaces(totalDistance) : 0} ${unit}`}
+
+
+
+
+ {/* Form Error Message */}
+ {!!formError && (
+
+ )}
+
+ {/* Next/Save Button */}
+
+
+
+ {
+ const hasReadingChanges = startReadingRef.current !== initialStartReadingRef.current || endReadingRef.current !== initialEndReadingRef.current;
+ return hasReadingChanges;
+ }}
+ />
+
+ );
+}
+
+IOURequestStepDistanceOdometer.displayName = 'IOURequestStepDistanceOdometer';
+
+const IOURequestStepDistanceOdometerWithCurrentUserPersonalDetails = withCurrentUserPersonalDetails(IOURequestStepDistanceOdometer);
+// eslint-disable-next-line rulesdir/no-negated-variables
+const IOURequestStepDistanceOdometerWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepDistanceOdometerWithCurrentUserPersonalDetails, true);
+// eslint-disable-next-line rulesdir/no-negated-variables
+const IOURequestStepDistanceOdometerWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepDistanceOdometerWithWritableReportOrNotFound);
+
+export default IOURequestStepDistanceOdometerWithFullTransactionOrNotFound;
diff --git a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx
index 07a081ade89a..05e8af7a788f 100644
--- a/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx
+++ b/src/pages/iou/request/step/withFullTransactionOrNotFound.tsx
@@ -50,7 +50,8 @@ type MoneyRequestRouteName =
| typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_MAP
| typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_GPS
| typeof SCREENS.MONEY_REQUEST.DISTANCE_CREATE
- | typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_MANUAL;
+ | typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_MANUAL
+ | typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_ODOMETER;
type WithFullTransactionOrNotFoundProps = WithFullTransactionOrNotFoundOnyxProps &
PlatformStackScreenProps;
diff --git a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
index 098d0f04bf3c..ff1462107428 100644
--- a/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
+++ b/src/pages/iou/request/step/withWritableReportOrNotFound.tsx
@@ -54,6 +54,7 @@ type MoneyRequestRouteName =
| typeof SCREENS.MONEY_REQUEST.DISTANCE_CREATE
| typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_MAP
| typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_GPS
+ | typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_ODOMETER
| typeof SCREENS.MONEY_REQUEST.STEP_DISTANCE_MANUAL;
type WithWritableReportOrNotFoundProps = WithWritableReportOrNotFoundOnyxProps & PlatformStackScreenProps;
diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts
index 7daf58c9856a..ae3f8e9b5d84 100644
--- a/src/types/onyx/Transaction.ts
+++ b/src/types/onyx/Transaction.ts
@@ -119,6 +119,12 @@ type Comment = {
/** Timestamp when auto-categorization was initiated (format: "YYYY-MM-DD HH:MM:SS") */
pendingAutoCategorizationTime?: string;
+
+ /** Odometer start reading for distance expenses */
+ odometerStart?: number;
+
+ /** Odometer end reading for distance expenses */
+ odometerEnd?: number;
};
/** Model of transaction custom unit */