diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index dfb026b5592e4..2a57cfa7c4c3a 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -5806,6 +5806,7 @@ const CONST = {
RECEIPT_GENERATED_WITH_AI: 'receiptGeneratedWithAI',
OVER_TRIP_LIMIT: 'overTripLimit',
COMPANY_CARD_REQUIRED: 'companyCardRequired',
+ NO_ROUTE: 'noRoute',
MISSING_ATTENDEES: 'missingAttendees',
},
RTER_VIOLATION_TYPES: {
diff --git a/src/components/DistanceRequest/DistanceRequestRenderItem.tsx b/src/components/DistanceRequest/DistanceRequestRenderItem.tsx
index 7364cd9adf4be..98870d7d928ae 100644
--- a/src/components/DistanceRequest/DistanceRequestRenderItem.tsx
+++ b/src/components/DistanceRequest/DistanceRequestRenderItem.tsx
@@ -1,9 +1,10 @@
import React from 'react';
-import * as Expensicons from '@components/Icon/Expensicons';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
+import {isWaypointNullIsland} from '@libs/TransactionUtils';
+import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type {WaypointCollection} from '@src/types/onyx/Transaction';
@@ -32,7 +33,7 @@ type DistanceRequestProps = {
function DistanceRequestRenderItem({waypoints, item = '', onSecondaryInteraction, getIndex, isActive = false, onPress = () => {}, disabled = false}: DistanceRequestProps) {
const theme = useTheme();
- const expensifyIcons = useMemoizedLazyExpensifyIcons(['Location']);
+ const expensifyIcons = useMemoizedLazyExpensifyIcons(['Location', 'DotIndicatorUnfilled', 'DotIndicator', 'DragHandles']);
const {translate} = useLocalize();
const numberOfWaypoints = Object.keys(waypoints ?? {}).length;
const lastWaypointIndex = numberOfWaypoints - 1;
@@ -42,24 +43,25 @@ function DistanceRequestRenderItem({waypoints, item = '', onSecondaryInteraction
let waypointIcon;
if (index === 0) {
descriptionKey += 'start';
- waypointIcon = Expensicons.DotIndicatorUnfilled;
+ waypointIcon = expensifyIcons.DotIndicatorUnfilled;
} else if (index === lastWaypointIndex) {
descriptionKey += 'stop';
waypointIcon = expensifyIcons.Location;
} else {
descriptionKey += 'stop';
- waypointIcon = Expensicons.DotIndicator;
+ waypointIcon = expensifyIcons.DotIndicator;
}
const waypoint = waypoints?.[`waypoint${index}`] ?? {};
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const title = waypoint.name || waypoint.address;
+ const errorText = isWaypointNullIsland(waypoint) ? translate('violations.noRoute') : undefined;
return (
);
}
diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx
index a28f245497e61..6036931d4da16 100644
--- a/src/components/ReportActionItem/MoneyRequestView.tsx
+++ b/src/components/ReportActionItem/MoneyRequestView.tsx
@@ -613,6 +613,8 @@ function MoneyRequestView({
),
);
}}
+ brickRoadIndicator={getErrorForField('waypoints') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
+ errorText={getErrorForField('waypoints')}
copyValue={distanceCopyValue}
copyable={!!distanceCopyValue}
/>
diff --git a/src/hooks/useViolations.ts b/src/hooks/useViolations.ts
index acd96ca0987a8..bbad1d83c2faa 100644
--- a/src/hooks/useViolations.ts
+++ b/src/hooks/useViolations.ts
@@ -6,7 +6,7 @@ import type {TransactionViolation, ViolationName} from '@src/types/onyx';
/**
* Names of Fields where violations can occur.
*/
-const validationFields = ['amount', 'billable', 'category', 'comment', 'date', 'merchant', 'receipt', 'tag', 'tax', 'attendees', 'customUnitRateID', 'none'] as const;
+const validationFields = ['amount', 'billable', 'category', 'comment', 'date', 'merchant', 'receipt', 'tag', 'tax', 'attendees', 'customUnitRateID', 'waypoints', 'none'] as const;
type ViolationField = TupleToUnion;
@@ -61,6 +61,7 @@ const violationNameToField: Record 'none',
receiptGeneratedWithAI: () => 'receipt',
companyCardRequired: () => 'none',
+ noRoute: () => 'waypoints',
};
type ViolationsMap = Map;
diff --git a/src/languages/de.ts b/src/languages/de.ts
index dcb5ecbe73c43..0ec4166f0989b 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -280,7 +280,6 @@ const translations: TranslationDeepObject = {
searchWithThreeDots: 'Suchen …',
next: 'Weiter',
previous: 'Zurück',
- // @context Navigation button that returns the user to the previous screen. Should be interpreted as a UI action label.
goBack: 'Zurück',
create: 'Erstellen',
add: 'Hinzufügen',
@@ -7366,6 +7365,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
hold: 'Diese Ausgabe wurde zurückgestellt',
resolvedDuplicates: 'Duplikat behoben',
companyCardRequired: 'Firmenkartenkäufe erforderlich',
+ noRoute: 'Bitte wählen Sie eine gültige Adresse aus',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} ist erforderlich`,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index b8f6b2991e0e3..ec028d8918f62 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -7299,6 +7299,7 @@ const translations = {
hold: 'This expense was put on hold',
resolvedDuplicates: 'resolved the duplicate',
companyCardRequired: 'Company card purchases required',
+ noRoute: 'Please select a valid address',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} is required`,
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 7a93665054c41..d8cbd50e73d91 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -7451,6 +7451,7 @@ ${amount} para ${merchant} - ${date}`,
hold: 'Este gasto está retenido',
resolvedDuplicates: 'resolvió el duplicado',
companyCardRequired: 'Se requieren compras con la tarjeta de la empresa.',
+ noRoute: 'Por favor, selecciona una dirección válida',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}) => `${fieldName} es obligatorio`,
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index e0ed752099d33..307c341f65bd6 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -280,7 +280,6 @@ const translations: TranslationDeepObject = {
searchWithThreeDots: 'Rechercher...',
next: 'Suivant',
previous: 'Précédent',
- // @context Navigation button that returns the user to the previous screen. Should be interpreted as a UI action label.
goBack: 'Retour',
create: 'Créer',
add: 'Ajouter',
@@ -7376,6 +7375,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
hold: 'Cette dépense a été mise en attente',
resolvedDuplicates: 'a résolu le doublon',
companyCardRequired: 'Achats avec carte d’entreprise requis',
+ noRoute: 'Veuillez sélectionner une adresse valide',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} est requis`,
diff --git a/src/languages/it.ts b/src/languages/it.ts
index f6713dafc3235..042ecad874e42 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -280,8 +280,7 @@ const translations: TranslationDeepObject = {
searchWithThreeDots: 'Cerca...',
next: 'Avanti',
previous: 'Precedente',
- // @context Navigation button that returns the user to the previous screen. Should be interpreted as a UI action label.
- goBack: 'Torna indietro',
+ goBack: 'Indietro',
create: 'Crea',
add: 'Aggiungi',
resend: 'Reinvia',
@@ -7352,6 +7351,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori
hold: 'Questa spesa è stata messa in sospeso',
resolvedDuplicates: 'ha risolto il duplicato',
companyCardRequired: 'Acquisti con carta aziendale obbligatori',
+ noRoute: 'Seleziona un indirizzo valido',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} è obbligatorio`,
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 1e0f1465b5e01..c88dfead79cab 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -280,7 +280,6 @@ const translations: TranslationDeepObject = {
searchWithThreeDots: '検索...',
next: '次へ',
previous: '前へ',
- // @context Navigation button that returns the user to the previous screen. Should be interpreted as a UI action label.
goBack: '戻る',
create: '作成',
add: '追加',
@@ -7296,6 +7295,7 @@ ${reportName}
hold: 'この経費は保留になっています',
resolvedDuplicates: '重複を解決しました',
companyCardRequired: '法人カードでの購入が必須',
+ noRoute: '有効な住所を選択してください',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} は必須です`,
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index ff93ef78706ef..0701af7186f26 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -280,7 +280,6 @@ const translations: TranslationDeepObject = {
searchWithThreeDots: 'Zoeken...',
next: 'Volgende',
previous: 'Vorige',
- // @context Navigation button that returns the user to the previous screen. Should be interpreted as a UI action label.
goBack: 'Ga terug',
create: 'Maken',
add: 'Toevoegen',
@@ -7340,6 +7339,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten
hold: 'Deze uitgave is in de wacht gezet',
resolvedDuplicates: 'het duplicaat opgelost',
companyCardRequired: 'Aankopen met bedrijfskaart verplicht',
+ noRoute: 'Selecteer een geldig adres',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} is verplicht`,
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index dee93876af4f1..150f9e7a33837 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -280,7 +280,6 @@ const translations: TranslationDeepObject = {
searchWithThreeDots: 'Szukaj...',
next: 'Dalej',
previous: 'Wstecz',
- // @context Navigation button that returns the user to the previous screen. Should be interpreted as a UI action label.
goBack: 'Wróć',
create: 'Utwórz',
add: 'Dodaj',
@@ -7325,6 +7324,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i
hold: 'Ten wydatek został wstrzymany',
resolvedDuplicates: 'rozwiązano duplikat',
companyCardRequired: 'Wymagane zakupy kartą firmową',
+ noRoute: 'Wybierz prawidłowy adres',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `Pole ${fieldName} jest wymagane`,
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 5ee2a3e7e70b4..d7fc8da1fbfb6 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -280,7 +280,6 @@ const translations: TranslationDeepObject = {
searchWithThreeDots: 'Pesquisar...',
next: 'Próximo',
previous: 'Anterior',
- // @context Navigation button that returns the user to the previous screen. Should be interpreted as a UI action label.
goBack: 'Voltar',
create: 'Criar',
add: 'Adicionar',
@@ -7327,6 +7326,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe
hold: 'Esta despesa foi colocada em espera',
resolvedDuplicates: 'duplicata resolvida',
companyCardRequired: 'Compras com cartão da empresa obrigatórias',
+ noRoute: 'Selecione um endereço válido',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName} é obrigatório`,
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index dd16d15864724..8558c8c3fec59 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -280,7 +280,6 @@ const translations: TranslationDeepObject = {
searchWithThreeDots: '搜索…',
next: '下一步',
previous: '上一步',
- // @context Navigation button that returns the user to the previous screen. Should be interpreted as a UI action label.
goBack: '返回',
create: '创建',
add: '添加',
@@ -7175,6 +7174,7 @@ ${reportName}
hold: '此报销已被搁置',
resolvedDuplicates: '已解决重复项',
companyCardRequired: '需要公司卡消费',
+ noRoute: '请选择一个有效的地址',
},
reportViolations: {
[CONST.REPORT_VIOLATIONS.FIELD_REQUIRED]: ({fieldName}: RequiredFieldParams) => `${fieldName}为必填项`,
diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts
index 4c3970061d106..aa7a9b8eda6a3 100644
--- a/src/libs/DistanceRequestUtils.ts
+++ b/src/libs/DistanceRequestUtils.ts
@@ -181,6 +181,10 @@ function getDistanceForDisplay(
return translate('iou.fieldPending');
}
+ if (!distanceInMeters) {
+ return '';
+ }
+
const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit);
if (useShortFormUnit) {
return `${distanceInUnits} ${unit}`;
@@ -221,6 +225,10 @@ function getDistanceMerchant(
return translate('iou.fieldPending');
}
+ if (!distanceInMeters) {
+ return '';
+ }
+
const distanceInUnits = getDistanceForDisplay(hasRoute, distanceInMeters, unit, rate, translate, true);
const ratePerUnit = getRateForDisplay(unit, rate, currency, translate, toLocaleDigit, undefined, true);
diff --git a/src/libs/ReportPreviewActionUtils.ts b/src/libs/ReportPreviewActionUtils.ts
index 75bd7dd8e16f4..a2d56162c8f1c 100644
--- a/src/libs/ReportPreviewActionUtils.ts
+++ b/src/libs/ReportPreviewActionUtils.ts
@@ -19,7 +19,7 @@ import {
isReportApproved,
isSettled,
} from './ReportUtils';
-import {hasSmartScanFailedViolation, isPending, isScanning} from './TransactionUtils';
+import {hasSmartScanFailedOrNoRouteViolation, isPending, isScanning} from './TransactionUtils';
function canSubmit(
report: Report,
@@ -46,7 +46,7 @@ function canSubmit(
const isAnyReceiptBeingScanned = transactions?.some((transaction) => isScanning(transaction));
- if (transactions?.some((transaction) => hasSmartScanFailedViolation(transaction, violations, currentUserEmail, currentUserAccountID, report, policy))) {
+ if (transactions?.some((transaction) => hasSmartScanFailedOrNoRouteViolation(transaction, violations, currentUserEmail, currentUserAccountID, report, policy))) {
return false;
}
diff --git a/src/libs/ReportPrimaryActionUtils.ts b/src/libs/ReportPrimaryActionUtils.ts
index 4722e735515d5..4d8368cde5429 100644
--- a/src/libs/ReportPrimaryActionUtils.ts
+++ b/src/libs/ReportPrimaryActionUtils.ts
@@ -42,7 +42,7 @@ import {
allHavePendingRTERViolation,
getTransactionViolations,
hasPendingRTERViolation as hasPendingRTERViolationTransactionUtils,
- hasSmartScanFailedViolation,
+ hasSmartScanFailedOrNoRouteViolation,
isDuplicate,
isOnHold as isOnHoldTransactionUtils,
isPending,
@@ -115,7 +115,7 @@ function isSubmitAction(
}
if (violations && currentUserEmail && currentUserAccountID !== undefined) {
- if (reportTransactions.some((transaction) => hasSmartScanFailedViolation(transaction, violations, currentUserEmail, currentUserAccountID, report, policy))) {
+ if (reportTransactions.some((transaction) => hasSmartScanFailedOrNoRouteViolation(transaction, violations, currentUserEmail, currentUserAccountID, report, policy))) {
return false;
}
}
diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts
index b35082415cd6c..1b8570466e272 100644
--- a/src/libs/ReportSecondaryActionUtils.ts
+++ b/src/libs/ReportSecondaryActionUtils.ts
@@ -68,7 +68,7 @@ import {
allHavePendingRTERViolation,
getOriginalTransactionWithSplitInfo,
hasReceipt as hasReceiptTransactionUtils,
- hasSmartScanFailedViolation,
+ hasSmartScanFailedOrNoRouteViolation,
isDistanceRequest as isDistanceRequestTransactionUtils,
isDuplicate,
isManagedCardTransaction as isManagedCardTransactionTransactionUtils,
@@ -195,7 +195,7 @@ function isSubmitAction({
}
if (violations && currentUserLogin && currentUserAccountID !== undefined) {
- if (reportTransactions.some((transaction) => hasSmartScanFailedViolation(transaction, violations, currentUserLogin, currentUserAccountID, report, policy))) {
+ if (reportTransactions.some((transaction) => hasSmartScanFailedOrNoRouteViolation(transaction, violations, currentUserLogin, currentUserAccountID, report, policy))) {
return false;
}
}
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 3f5b2f9cdbd11..6ff3cc7cdef03 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -300,6 +300,7 @@ import {
getConvertedAmount,
getCurrency,
getDescription,
+ getDistanceInMeters,
getFormattedCreated,
getFormattedPostedDate,
getMCCGroup,
@@ -326,6 +327,7 @@ import {
isExpensifyCardTransaction,
isFetchingWaypointsFromServer,
isManualDistanceRequest as isManualDistanceRequestTransactionUtils,
+ isMapDistanceRequest,
isOnHold as isOnHoldTransactionUtils,
isPayAtEndExpense,
isPending,
@@ -5047,6 +5049,12 @@ function getTransactionReportName({
return translateLocal('iou.fieldPending');
}
+ // The unit does not matter as we are only interested in whether the distance is zero or not
+ if (isMapDistanceRequest(transaction) && !getDistanceInMeters(transaction, CONST.CUSTOM_UNITS.DISTANCE_UNIT_KILOMETERS)) {
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
+ return translateLocal('violations.noRoute');
+ }
+
if (isSentMoneyReportAction(reportAction)) {
return getIOUReportActionDisplayMessage(reportAction as ReportAction, transaction);
}
diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts
index e8a0011a2da7d..1bc6eee4671db 100644
--- a/src/libs/TransactionUtils/index.ts
+++ b/src/libs/TransactionUtils/index.ts
@@ -1675,6 +1675,10 @@ function waypointHasValidAddress(waypoint: RecentWaypoint | Waypoint): boolean {
return !!waypoint?.address?.trim();
}
+function isWaypointNullIsland(waypoint: RecentWaypoint | Waypoint): boolean {
+ return waypoint.lat === 0 && waypoint.lng === 0;
+}
+
/**
* Converts the key of a waypoint to its index
*/
@@ -1713,6 +1717,11 @@ function getValidWaypoints(waypoints: WaypointCollection | undefined, reArrangeI
return acc;
}
+ // Exclude null island
+ if (isWaypointNullIsland(currentWaypoint)) {
+ return acc;
+ }
+
// Check for adjacent waypoints with the same address or coordinate
const previousCoordinate: Coordinate | undefined = previousWaypoint?.lng && previousWaypoint?.lat ? [previousWaypoint.lng, previousWaypoint.lat] : undefined;
const currentCoordinate: Coordinate | undefined = currentWaypoint.lng && currentWaypoint.lat ? [currentWaypoint.lng, currentWaypoint.lat] : undefined;
@@ -2557,9 +2566,9 @@ function isExpenseUnreported(transaction?: Transaction): transaction is Unreport
}
/**
- * Check if there is a smartscan failed violation for the transaction.
+ * Check if there is a smartscan failed or no route violation for the transaction.
*/
-function hasSmartScanFailedViolation(
+function hasSmartScanFailedOrNoRouteViolation(
transaction: Transaction,
transactionViolations: OnyxCollection | undefined,
currentUserEmail: string,
@@ -2568,7 +2577,7 @@ function hasSmartScanFailedViolation(
policy: OnyxEntry,
): boolean {
const violations = getTransactionViolations(transaction, transactionViolations, currentUserEmail, currentUserAccountID, report, policy);
- return !!violations?.some((violation) => violation.name === CONST.VIOLATIONS.SMARTSCAN_FAILED);
+ return !!violations?.some((violation) => violation.name === CONST.VIOLATIONS.SMARTSCAN_FAILED || violation.name === CONST.VIOLATIONS.NO_ROUTE);
}
/**
@@ -2668,12 +2677,13 @@ export {
hasPendingUI,
getWaypointIndex,
waypointHasValidAddress,
+ isWaypointNullIsland,
getRecentTransactions,
hasReservationList,
hasViolation,
hasDuplicateTransactions,
hasBrokenConnectionViolation,
- hasSmartScanFailedViolation,
+ hasSmartScanFailedOrNoRouteViolation,
shouldShowBrokenConnectionViolation,
shouldShowBrokenConnectionViolationForMultipleTransactions,
hasNoticeTypeViolation,
diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts
index b1b2fccd6fa16..1580494df13f0 100644
--- a/src/libs/Violations/ViolationsUtils.ts
+++ b/src/libs/Violations/ViolationsUtils.ts
@@ -651,6 +651,8 @@ const ViolationsUtils = {
});
case CONST.VIOLATIONS.RECEIPT_GENERATED_WITH_AI:
return translate('violations.receiptGeneratedWithAI');
+ case CONST.VIOLATIONS.NO_ROUTE:
+ return translate('violations.noRoute');
default:
// The interpreter should never get here because the switch cases should be exhaustive.
// If typescript is showing an error on the assertion below it means the switch statement is out of
diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts
index 086c8b635334d..cd624d76a707f 100644
--- a/src/libs/actions/IOU/index.ts
+++ b/src/libs/actions/IOU/index.ts
@@ -4458,7 +4458,8 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U
isPaidGroupPolicy(policy) &&
!isInvoice &&
updatedTransaction &&
- (hasModifiedTag ||
+ (hasPendingWaypoints ||
+ hasModifiedTag ||
hasModifiedCategory ||
hasModifiedComment ||
hasModifiedMerchant ||
@@ -4481,6 +4482,9 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U
hasModifiedCategory && transactionChanges.category === ''
? optimisticViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.CATEGORY_OUT_OF_POLICY)
: optimisticViolations;
+ if (hasPendingWaypoints) {
+ optimisticViolations = optimisticViolations.filter((violation) => violation.name !== CONST.VIOLATIONS.NO_ROUTE);
+ }
const violationsOnyxData = ViolationsUtils.getViolationsOnyxData(
updatedTransaction,
@@ -5133,9 +5137,9 @@ type UpdateMoneyRequestDistanceParams = {
waypoints?: WaypointCollection;
distance?: number;
routes?: Routes;
- policy?: OnyxEntry;
- policyTagList?: OnyxEntry;
- policyCategories?: OnyxEntry;
+ policy: OnyxEntry;
+ policyTagList: OnyxEntry;
+ policyCategories: OnyxEntry;
transactionBackup: OnyxEntry;
currentUserAccountIDParam: number;
currentUserEmailParam: string;
@@ -5152,9 +5156,9 @@ function updateMoneyRequestDistance({
waypoints,
distance,
routes = undefined,
- policy = {} as OnyxTypes.Policy,
- policyTagList = {},
- policyCategories = {},
+ policy,
+ policyTagList,
+ policyCategories,
transactionBackup,
currentUserAccountIDParam,
currentUserEmailParam,
diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts
index 7db998adf50a7..c58ce0ac542c3 100644
--- a/src/libs/actions/Transaction.ts
+++ b/src/libs/actions/Transaction.ts
@@ -1,8 +1,6 @@
import {getUnixTime} from 'date-fns';
-import {deepEqual} from 'fast-equals';
import lodashClone from 'lodash/clone';
-import lodashHas from 'lodash/has';
-import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
+import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
import type {
@@ -54,7 +52,7 @@ import type {
} from '@src/types/onyx';
import type {OriginalMessageIOU, OriginalMessageModifiedExpense} from '@src/types/onyx/OriginalMessage';
import type {OnyxData} from '@src/types/onyx/Request';
-import type {WaypointCollection} from '@src/types/onyx/Transaction';
+import type {Waypoint, WaypointCollection} from '@src/types/onyx/Transaction';
import type TransactionState from '@src/types/utils/TransactionStateType';
import {getPolicyTagsData} from './Policy/Tag';
import {getCurrentUserAccountID} from './Report';
@@ -124,10 +122,23 @@ type SaveWaypointProps = {
};
function saveWaypoint({transactionID, index, waypoint, isDraft = false, recentWaypointsList = []}: SaveWaypointProps) {
+ // Saving a waypoint should completely overwrite the existing one at the given index (if any).
+ // Onyx merge performs noop on undefined fields. Thus we should fallback to null so the existing fields are cleared.
+ const waypointOnyxUpdate: Required> | null = waypoint
+ ? {
+ name: waypoint.name ?? null,
+ address: waypoint.address ?? null,
+ lat: waypoint.lat ?? null,
+ lng: waypoint.lng ?? null,
+ keyForList: waypoint.keyForList ?? null,
+ pendingAction: waypoint.pendingAction ?? null,
+ }
+ : null;
+
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
comment: {
waypoints: {
- [`waypoint${index}`]: waypoint,
+ [`waypoint${index}`]: waypointOnyxUpdate,
},
customUnit: {
quantity: null,
@@ -153,16 +164,9 @@ function saveWaypoint({transactionID, index, waypoint, isDraft = false, recentWa
},
});
- // You can save offline waypoints without verifying the address (we will geocode it on the backend)
- // We're going to prevent saving those addresses in the recent waypoints though since they could be invalid addresses
- // However, in the backend once we verify the address, we will save the waypoint in the recent waypoints NVP
- if (!lodashHas(waypoint, 'lat') || !lodashHas(waypoint, 'lng')) {
- return;
- }
-
// If current location is used, we would want to avoid saving it as a recent waypoint. This prevents the 'Your Location'
// text from showing up in the address search suggestions
- if (deepEqual(waypoint?.address, CONST.YOUR_LOCATION_TEXT)) {
+ if (waypoint?.address === CONST.YOUR_LOCATION_TEXT) {
return;
}
const recentWaypointAlreadyExists = recentWaypointsList.find((recentWaypoint) => recentWaypoint?.address === waypoint?.address);
@@ -364,9 +368,33 @@ function getRoute(transactionID: string, waypoints: WaypointCollection, routeTyp
* which will replace the existing ones.
*/
function updateWaypoints(transactionID: string, waypoints: WaypointCollection, isDraft = false): Promise {
+ // Updating waypoints should completely overwrite the existing ones.
+ // Onyx merge performs noop on undefined fields. Thus we should fallback to null so the existing fields are cleared.
+ const waypointsOnyxUpdate = Object.keys(waypoints).reduce(
+ (acc, key) => {
+ const waypoint = waypoints[key];
+ acc[key] = {
+ name: waypoint.name ?? null,
+ address: waypoint.address ?? null,
+ lat: waypoint.lat ?? null,
+ lng: waypoint.lng ?? null,
+ city: 'city' in waypoint ? (waypoint.city ?? null) : null,
+ state: 'state' in waypoint ? (waypoint.state ?? null) : null,
+ zipCode: 'zipCode' in waypoint ? (waypoint.zipCode ?? null) : null,
+ country: 'country' in waypoint ? (waypoint.country ?? null) : null,
+ street: 'street' in waypoint ? (waypoint.street ?? null) : null,
+ street2: 'street2' in waypoint ? (waypoint.street2 ?? null) : null,
+ pendingAction: 'pendingAction' in waypoint ? (waypoint.pendingAction ?? null) : null,
+ keyForList: waypoint.keyForList ?? null,
+ };
+ return acc;
+ },
+ {} as Record>>,
+ );
+
return Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
comment: {
- waypoints,
+ waypoints: waypointsOnyxUpdate,
customUnit: {
quantity: null,
},
diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx
index eb4907f646f4b..b85c90b00dd3d 100644
--- a/src/pages/iou/request/step/IOURequestStepDistance.tsx
+++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx
@@ -53,7 +53,7 @@ import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils';
import {getPolicy, isPaidGroupPolicy} from '@libs/PolicyUtils';
import {getPolicyExpenseChat, isArchivedReport, isPolicyExpenseChat as isPolicyExpenseChatUtil} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
-import {getDistanceInMeters, getRateID, getRequestType, getValidWaypoints, hasRoute, isCustomUnitRateIDForP2P} from '@libs/TransactionUtils';
+import {getDistanceInMeters, getRateID, getRequestType, getValidWaypoints, hasRoute, isCustomUnitRateIDForP2P, isWaypointNullIsland} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -91,6 +91,8 @@ function IOURequestStepDistance({
const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transactionID}`, {canBeMissing: true});
const policy = usePolicy(report?.policyID);
+ const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy?.id}`, {canBeMissing: true});
+ const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`, {canBeMissing: true});
const personalPolicy = usePersonalPolicy();
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false});
const defaultExpensePolicy = useDefaultExpensePolicy();
@@ -143,6 +145,7 @@ function IOURequestStepDistance({
const nonEmptyWaypointsCount = useMemo(() => Object.keys(waypoints).filter((key) => !isWaypointEmpty(waypoints[key])).length, [waypoints]);
const currentUserAccountIDParam = currentUserPersonalDetails.accountID;
const currentUserEmailParam = currentUserPersonalDetails.login ?? '';
+ const isWaypointsNullIslandError = useMemo(() => Object.values(waypoints).some(isWaypointNullIsland), [waypoints]);
const duplicateWaypointsError = useMemo(
() => nonEmptyWaypointsCount >= 2 && Object.keys(validatedWaypoints).length !== nonEmptyWaypointsCount,
[nonEmptyWaypointsCount, validatedWaypoints],
@@ -471,6 +474,9 @@ function IOURequestStepDistance({
if (hasRouteError) {
return getLatestErrorField(transaction, 'route');
}
+ if (isWaypointsNullIslandError) {
+ return {isWaypointsNullIslandError: `${translate('common.please')} ${translate('common.fixTheErrors')} ${translate('common.inTheFormBeforeContinuing')}.`} as Errors;
+ }
if (duplicateWaypointsError) {
return {duplicateWaypointsError: translate('iou.error.duplicateWaypointsErrorMessage')} as Errors;
}
@@ -539,6 +545,8 @@ function IOURequestStepDistance({
waypoints,
...(hasRouteChanged ? {routes: transaction?.routes} : {}),
policy,
+ policyTagList: policyTags,
+ policyCategories,
transactionBackup,
currentUserAccountIDParam,
currentUserEmailParam,
@@ -568,6 +576,8 @@ function IOURequestStepDistance({
navigateBack,
parentReport,
policy,
+ policyTags,
+ policyCategories,
currentUserAccountIDParam,
currentUserEmailParam,
isASAPSubmitBetaEnabled,
diff --git a/src/pages/iou/request/step/IOURequestStepDistanceManual.tsx b/src/pages/iou/request/step/IOURequestStepDistanceManual.tsx
index ebdab0a721ec1..42908f3592d47 100644
--- a/src/pages/iou/request/step/IOURequestStepDistanceManual.tsx
+++ b/src/pages/iou/request/step/IOURequestStepDistanceManual.tsx
@@ -82,6 +82,8 @@ function IOURequestStepDistanceManual({
const [selectedTab, selectedTabResult] = useOnyx(`${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.DISTANCE_REQUEST_TYPE}`, {canBeMissing: true});
const isLoadingSelectedTab = isLoadingOnyxValue(selectedTabResult);
const policy = usePolicy(report?.policyID);
+ const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy?.id}`, {canBeMissing: true});
+ const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`, {canBeMissing: true});
const personalPolicy = usePersonalPolicy();
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false});
const defaultExpensePolicy = useDefaultExpensePolicy();
@@ -180,6 +182,8 @@ function IOURequestStepDistanceManual({
// Not required for manual distance request
transactionBackup: undefined,
policy,
+ policyTagList: policyTags,
+ policyCategories,
currentUserAccountIDParam,
currentUserEmailParam,
isASAPSubmitBetaEnabled,
@@ -313,6 +317,8 @@ function IOURequestStepDistanceManual({
transaction,
parentReport,
policy,
+ policyTags,
+ policyCategories,
currentUserAccountIDParam,
currentUserEmailParam,
isASAPSubmitBetaEnabled,
diff --git a/src/pages/iou/request/step/IOURequestStepDistanceMap.tsx b/src/pages/iou/request/step/IOURequestStepDistanceMap.tsx
index d10cb0630d45b..dcf12878ffb7c 100644
--- a/src/pages/iou/request/step/IOURequestStepDistanceMap.tsx
+++ b/src/pages/iou/request/step/IOURequestStepDistanceMap.tsx
@@ -53,7 +53,7 @@ import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils';
import {getPolicy, isPaidGroupPolicy} from '@libs/PolicyUtils';
import {getPolicyExpenseChat, isArchivedReport, isPolicyExpenseChat as isPolicyExpenseChatUtil} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
-import {getDistanceInMeters, getRateID, getRequestType, getValidWaypoints, hasRoute, isCustomUnitRateIDForP2P} from '@libs/TransactionUtils';
+import {getDistanceInMeters, getRateID, getRequestType, getValidWaypoints, hasRoute, isCustomUnitRateIDForP2P, isWaypointNullIsland} from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
@@ -91,6 +91,8 @@ function IOURequestStepDistanceMap({
const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.parentReportID)}`, {canBeMissing: true});
const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transactionID}`, {canBeMissing: true});
const policy = usePolicy(report?.policyID);
+ const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy?.id}`, {canBeMissing: true});
+ const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`, {canBeMissing: true});
const personalPolicy = usePersonalPolicy();
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false});
const defaultExpensePolicy = useDefaultExpensePolicy();
@@ -140,7 +142,7 @@ function IOURequestStepDistanceMap({
return isEmpty(waypointWithoutKey);
};
const nonEmptyWaypointsCount = useMemo(() => Object.keys(waypoints).filter((key) => !isWaypointEmpty(waypoints[key])).length, [waypoints]);
-
+ const isWaypointsNullIslandError = useMemo(() => Object.values(waypoints).some(isWaypointNullIsland), [waypoints]);
const duplicateWaypointsError = useMemo(
() => nonEmptyWaypointsCount >= 2 && Object.keys(validatedWaypoints).length !== nonEmptyWaypointsCount,
[nonEmptyWaypointsCount, validatedWaypoints],
@@ -471,6 +473,9 @@ function IOURequestStepDistanceMap({
if (hasRouteError) {
return getLatestErrorField(transaction, 'route');
}
+ if (isWaypointsNullIslandError) {
+ return {isWaypointsNullIslandError: `${translate('common.please')} ${translate('common.fixTheErrors')} ${translate('common.inTheFormBeforeContinuing')}.`} as Errors;
+ }
if (duplicateWaypointsError) {
return {duplicateWaypointsError: translate('iou.error.duplicateWaypointsErrorMessage')} as Errors;
}
@@ -539,6 +544,8 @@ function IOURequestStepDistanceMap({
waypoints,
...(hasRouteChanged ? {routes: transaction?.routes} : {}),
policy,
+ policyTagList: policyTags,
+ policyCategories,
transactionBackup,
currentUserAccountIDParam,
currentUserEmailParam,
@@ -566,6 +573,8 @@ function IOURequestStepDistanceMap({
navigateToNextStep,
parentReport,
policy,
+ policyTags,
+ policyCategories,
report,
transaction?.routes,
transaction?.transactionID,
diff --git a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx
index a40afc8e479d1..d343130b842aa 100644
--- a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx
+++ b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx
@@ -105,6 +105,8 @@ function IOURequestStepDistanceOdometer({
const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`, {canBeMissing: true});
const [parentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.parentReportID)}`, {canBeMissing: true});
const policy = usePolicy(report?.policyID);
+ const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy?.id}`, {canBeMissing: true});
+ const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policy?.id}`, {canBeMissing: true});
const personalPolicy = usePersonalPolicy();
const defaultExpensePolicy = useDefaultExpensePolicy();
@@ -339,6 +341,8 @@ function IOURequestStepDistanceOdometer({
// Not required for odometer distance request
transactionBackup: undefined,
policy,
+ policyTagList: policyTags,
+ policyCategories,
currentUserAccountIDParam,
currentUserEmailParam,
isASAPSubmitBetaEnabled: false,
diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
index 0f2f0b7f1c697..755ab23b2a7c6 100644
--- a/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
+++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx
@@ -140,10 +140,10 @@ function IOURequestStepWaypoint({
// Therefore, we're going to save the waypoint as just the address, and the lat/long will be filled in on the backend
if (isOffline && waypointValue) {
const waypoint = {
- address: waypointValue ?? '',
- name: values.name ?? '',
- lat: values.lat ?? 0,
- lng: values.lng ?? 0,
+ address: waypointValue,
+ name: values.name,
+ lat: values.lat,
+ lng: values.lng,
keyForList: `${(values.name ?? 'waypoint') as string}_${Date.now()}`,
};
save(waypoint);
@@ -175,10 +175,10 @@ function IOURequestStepWaypoint({
const selectWaypoint = (values: Waypoint) => {
const waypoint = {
- lat: values.lat ?? 0,
- lng: values.lng ?? 0,
- address: values.address ?? '',
- name: values.name ?? '',
+ lat: values.lat,
+ lng: values.lng,
+ address: values.address,
+ name: values.name,
keyForList: `${values.name ?? 'waypoint'}_${Date.now()}`,
};
diff --git a/tests/unit/TransactionTest.ts b/tests/unit/TransactionTest.ts
index 23599ffeed1c7..fbdb70d78e89e 100644
--- a/tests/unit/TransactionTest.ts
+++ b/tests/unit/TransactionTest.ts
@@ -992,20 +992,6 @@ describe('Transaction', () => {
expect(updatedRecentWaypoints?.[0]?.address).toBe('123 Main St');
});
- it('should not save waypoint if missing lat/lng', async () => {
- const transactionID = 'txn2';
- const index = '1';
- const waypoint: RecentWaypoint = {
- address: 'No LatLng',
- };
- const recentWaypointsList: RecentWaypoint[] = [];
- saveWaypoint({transactionID, index, waypoint, isDraft: false, recentWaypointsList});
- await waitForBatchedUpdates();
-
- const updatedRecentWaypoints = await OnyxUtils.get(ONYXKEYS.NVP_RECENT_WAYPOINTS);
- expect(updatedRecentWaypoints?.length ?? 0).toBe(0);
- });
-
it('should not save waypoint if address is YOUR_LOCATION_TEXT', async () => {
const transactionID = 'txn3';
const index = '2';