From f4aff81e8292c86d1a2ad4325fb9bfdde9082079 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 9 Jan 2024 11:50:14 +0100 Subject: [PATCH 01/19] Use transaction.isLoading to identify pending Distance requests --- src/components/DistanceEReceipt.js | 4 +--- .../ReportActionItem/MoneyRequestPreview.js | 17 ++++++++--------- .../ReportActionItem/MoneyRequestView.js | 9 ++------- .../ReportActionItem/ReportPreview.js | 10 ++-------- src/libs/ReportUtils.ts | 17 +++++++++++++++-- src/libs/TransactionUtils.ts | 18 +++++++++++++++++- src/types/onyx/Transaction.ts | 2 ++ 7 files changed, 47 insertions(+), 30 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 0241eea44063..f566fb77b912 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -7,7 +7,6 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -35,8 +34,7 @@ function DistanceEReceipt({transaction}) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; - const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); - const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : translate('common.tbd'); + const {formattedAmount: formattedTransactionAmount, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); const waypoints = lodashGet(transaction, 'comment.waypoints', {}); const sortedWaypoints = useMemo( diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index c052a885245f..f66b5b90efbd 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -152,7 +152,13 @@ function MoneyRequestPreview(props) { // Pay button should only be visible to the manager of the report. const isCurrentUserManager = managerID === sessionAccountID; - const {amount: requestAmount, currency: requestCurrency, comment: requestComment, merchant} = ReportUtils.getTransactionDetails(props.transaction); + const { + amount: requestAmount, + formattedAmount: formattedRequestAmount, + currency: requestCurrency, + comment: requestComment, + merchant, + } = ReportUtils.getTransactionDetails(props.transaction); const description = truncate(requestComment, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const hasReceipt = TransactionUtils.hasReceipt(props.transaction); @@ -166,13 +172,10 @@ function MoneyRequestPreview(props) { // Show the merchant for IOUs and expenses only if they are custom or not related to scanning smartscan const shouldShowMerchant = !_.isEmpty(requestMerchant) && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT; const shouldShowDescription = !_.isEmpty(description) && !shouldShowMerchant && !isScanning; - const hasPendingWaypoints = lodashGet(props.transaction, 'pendingFields.waypoints', null); let merchantOrDescription = requestMerchant; if (!shouldShowMerchant) { merchantOrDescription = description || ''; - } else if (hasPendingWaypoints) { - merchantOrDescription = requestMerchant.replace(CONST.REGEX.FIRST_SPACE, translate('common.tbd')); } const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(props.transaction)] : []; @@ -221,10 +224,6 @@ function MoneyRequestPreview(props) { }; const getDisplayAmountText = () => { - if (isDistanceRequest) { - return requestAmount && !hasPendingWaypoints ? CurrencyUtils.convertToDisplayString(requestAmount, props.transaction.currency) : translate('common.tbd'); - } - if (isScanning) { return translate('iou.receiptScanning'); } @@ -233,7 +232,7 @@ function MoneyRequestPreview(props) { return Localize.translateLocal('iou.receiptMissingDetails'); } - return CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency); + return formattedRequestAmount; }; const getDisplayDeleteAmountText = () => { diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 37ff163f23c8..42de7b461f9d 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -128,7 +128,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate const { created: transactionDate, amount: transactionAmount, - currency: transactionCurrency, + formattedAmount: formattedTransactionAmount, comment: transactionDescription, merchant: transactionMerchant, billable: transactionBillable, @@ -140,11 +140,6 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate } = ReportUtils.getTransactionDetails(transaction); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); - let formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; - const hasPendingWaypoints = lodashGet(transaction, 'pendingFields.waypoints', null); - if (isDistanceRequest && (!formattedTransactionAmount || hasPendingWaypoints)) { - formattedTransactionAmount = translate('common.tbd'); - } const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); const isCardTransaction = TransactionUtils.isCardTransaction(transaction); const cardProgramName = isCardTransaction ? CardUtils.getCardDescription(transactionCardID) : ''; @@ -274,7 +269,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate ReceiptUtils.getThumbnailAndImageURIs(transaction)); let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; - const hasPendingWaypoints = formattedMerchant && hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => lodashGet(transaction, 'pendingFields.waypoints', null)); - if (hasPendingWaypoints) { - formattedMerchant = formattedMerchant.replace(CONST.REGEX.FIRST_SPACE, props.translate('common.tbd')); - } + const hasOnlyLoadingDistanceRequests = hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => TransactionUtils.isLoadingDistanceRequest(transaction)); const previewSubtitle = formattedMerchant || props.translate('iou.requestCount', { @@ -178,7 +175,7 @@ function ReportPreview(props) { ); const getDisplayAmount = () => { - if (hasPendingWaypoints) { + if (hasOnlyLoadingDistanceRequests) { return props.translate('common.tbd'); } if (totalDisplaySpend) { @@ -187,9 +184,6 @@ function ReportPreview(props) { if (isScanning) { return props.translate('iou.receiptScanning'); } - if (hasOnlyDistanceRequests) { - return props.translate('common.tbd'); - } // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") let displayAmount = ''; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0d7658adf180..dad7314168ae 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -292,6 +292,7 @@ type TransactionDetails = cardID: number; originalAmount: number; originalCurrency: string; + formattedAmount: string; } | undefined; @@ -1835,11 +1836,23 @@ function getTransactionDetails(transaction: OnyxEntry, createdDateF if (!transaction) { return; } + const report = getReport(transaction?.reportID); + const amount = TransactionUtils.getAmount(transaction, isNotEmptyObject(report) && isExpenseReport(report)); + const currency = TransactionUtils.getCurrency(transaction); + + let formattedAmount; + if (TransactionUtils.isLoadingDistanceRequest(transaction)) { + formattedAmount = Localize.translateLocal('common.tbd'); + } else { + formattedAmount = amount ? CurrencyUtils.convertToDisplayString(amount, currency) : ''; + } + return { created: TransactionUtils.getCreated(transaction, createdDateFormat), - amount: TransactionUtils.getAmount(transaction, isNotEmptyObject(report) && isExpenseReport(report)), - currency: TransactionUtils.getCurrency(transaction), + amount, + currency, + formattedAmount, comment: TransactionUtils.getDescription(transaction), merchant: TransactionUtils.getMerchant(transaction), waypoints: TransactionUtils.getWaypoints(transaction), diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index c34a6753c1d5..7badbc524085 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -2,6 +2,7 @@ import lodashHas from 'lodash/has'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, ReportAction, Transaction} from '@src/types/onyx'; @@ -49,6 +50,15 @@ function isDistanceRequest(transaction: Transaction): boolean { return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE; } +function isLoadingDistanceRequest(transaction: OnyxEntry): boolean { + if (!transaction) { + return false; + } + + const amount = getAmount(transaction, false); + return isDistanceRequest(transaction) && (!!transaction?.isLoading || amount === 0); +} + function isScanRequest(transaction: Transaction): boolean { // This is used during the request creation flow before the transaction has been saved to the server if (lodashHas(transaction, 'iouRequestType')) { @@ -311,7 +321,12 @@ function getOriginalAmount(transaction: Transaction): number { * Return the merchant field from the transaction, return the modifiedMerchant if present. */ function getMerchant(transaction: OnyxEntry): string { - return transaction?.modifiedMerchant ? transaction.modifiedMerchant : transaction?.merchant ?? ''; + if (!transaction) { + return ''; + } + + const merchant = transaction.modifiedMerchant ? transaction.modifiedMerchant : transaction.merchant ?? ''; + return isLoadingDistanceRequest(transaction) ? merchant.replace(CONST.REGEX.FIRST_SPACE, Localize.translateLocal('common.tbd')) : merchant; } function getDistance(transaction: Transaction): number { @@ -566,6 +581,7 @@ export { isReceiptBeingScanned, getValidWaypoints, isDistanceRequest, + isLoadingDistanceRequest, isExpensifyCardTransaction, isCardTransaction, isPending, diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 8b7e26280305..4b8a0a1cf9b3 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -94,6 +94,8 @@ type Transaction = { /** If the transaction was made in a foreign currency, we send the original amount and currency */ originalAmount?: number; originalCurrency?: string; + + isLoading?: boolean; }; export default Transaction; From b30a5e7eb90279177b4c51febdc1cfa5a7c4db36 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 9 Jan 2024 15:15:38 +0100 Subject: [PATCH 02/19] Use transaction.isLoading to identify pending Distance requests --- .../ReportActionItem/ReportPreview.js | 2 +- src/libs/TransactionUtils.ts | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index e0346e3c6bcb..d50811c15fbe 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -157,7 +157,7 @@ function ReportPreview(props) { const hasErrors = hasReceipts && hasMissingSmartscanFields; const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); - let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; + const formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; const hasOnlyLoadingDistanceRequests = hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => TransactionUtils.isLoadingDistanceRequest(transaction)); const previewSubtitle = formattedMerchant || diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 7badbc524085..9ab09a1978e1 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -2,7 +2,6 @@ import lodashHas from 'lodash/has'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; -import * as Localize from '@libs/Localize'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, ReportAction, Transaction} from '@src/types/onyx'; @@ -12,6 +11,7 @@ import type {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/on import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; +import * as Localize from './Localize'; import * as NumberUtils from './NumberUtils'; type AdditionalTransactionChanges = {comment?: string; waypoints?: WaypointCollection}; @@ -50,15 +50,6 @@ function isDistanceRequest(transaction: Transaction): boolean { return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE; } -function isLoadingDistanceRequest(transaction: OnyxEntry): boolean { - if (!transaction) { - return false; - } - - const amount = getAmount(transaction, false); - return isDistanceRequest(transaction) && (!!transaction?.isLoading || amount === 0); -} - function isScanRequest(transaction: Transaction): boolean { // This is used during the request creation flow before the transaction has been saved to the server if (lodashHas(transaction, 'iouRequestType')) { @@ -317,6 +308,20 @@ function getOriginalAmount(transaction: Transaction): number { return Math.abs(amount); } +/** + * Verify if the transaction is of Distance request and is not fully ready: + * - it has a zero amount, which means the request was created offline and expects the distance calculation from the server + * - it is in `isLoading` state, which means the waypoints were updated offline and the distance requires re-calculation + */ +function isLoadingDistanceRequest(transaction: OnyxEntry): boolean { + if (!transaction) { + return false; + } + + const amount = getAmount(transaction, false); + return isDistanceRequest(transaction) && (!!transaction?.isLoading || amount === 0); +} + /** * Return the merchant field from the transaction, return the modifiedMerchant if present. */ From 46dec5946ecb854a0c32b6c6724606bac09089cc Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 16 Jan 2024 22:48:17 +0100 Subject: [PATCH 03/19] Rename isLoadingDistanceRequest -> isDistanceBeingCalculated --- src/components/ReportActionItem/ReportPreview.js | 2 +- src/libs/ReportUtils.ts | 2 +- src/libs/TransactionUtils.ts | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 4772ec8a55ac..332e45d32cb1 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -166,7 +166,7 @@ function ReportPreview(props) { const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); const formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; - const hasOnlyLoadingDistanceRequests = hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => TransactionUtils.isLoadingDistanceRequest(transaction)); + const hasOnlyLoadingDistanceRequests = hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => TransactionUtils.isDistanceBeingCalculated(transaction)); const previewSubtitle = formattedMerchant || props.translate('iou.requestCount', { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 6458ae7368c7..b3218d4747d8 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1857,7 +1857,7 @@ function getTransactionDetails(transaction: OnyxEntry, createdDateF const currency = TransactionUtils.getCurrency(transaction); let formattedAmount; - if (TransactionUtils.isLoadingDistanceRequest(transaction)) { + if (TransactionUtils.isDistanceBeingCalculated(transaction)) { formattedAmount = Localize.translateLocal('common.tbd'); } else { formattedAmount = amount ? CurrencyUtils.convertToDisplayString(amount, currency) : ''; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 9ab09a1978e1..e91ffb9490b8 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -309,11 +309,11 @@ function getOriginalAmount(transaction: Transaction): number { } /** - * Verify if the transaction is of Distance request and is not fully ready: + * Verify if the transaction is of Distance request and is expecting the distance to be calculated on the server: * - it has a zero amount, which means the request was created offline and expects the distance calculation from the server * - it is in `isLoading` state, which means the waypoints were updated offline and the distance requires re-calculation */ -function isLoadingDistanceRequest(transaction: OnyxEntry): boolean { +function isDistanceBeingCalculated(transaction: OnyxEntry): boolean { if (!transaction) { return false; } @@ -331,7 +331,7 @@ function getMerchant(transaction: OnyxEntry): string { } const merchant = transaction.modifiedMerchant ? transaction.modifiedMerchant : transaction.merchant ?? ''; - return isLoadingDistanceRequest(transaction) ? merchant.replace(CONST.REGEX.FIRST_SPACE, Localize.translateLocal('common.tbd')) : merchant; + return isDistanceBeingCalculated(transaction) ? merchant.replace(CONST.REGEX.FIRST_SPACE, Localize.translateLocal('common.tbd')) : merchant; } function getDistance(transaction: Transaction): number { @@ -586,7 +586,7 @@ export { isReceiptBeingScanned, getValidWaypoints, isDistanceRequest, - isLoadingDistanceRequest, + isDistanceBeingCalculated, isExpensifyCardTransaction, isCardTransaction, isPending, From b87642aad54f4f134e799b1a6406baa88ccb2caa Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 19 Jan 2024 13:29:28 +0100 Subject: [PATCH 04/19] Use "Route pending" instead of "TBD" --- src/components/DistanceEReceipt.js | 4 +- .../MoneyRequestConfirmationList.js | 7 ++- ...oraryForRefactorRequestConfirmationList.js | 7 ++- .../ReportActionItem/MoneyReportView.tsx | 2 +- .../ReportActionItem/MoneyRequestPreview.js | 18 +++---- .../ReportActionItem/MoneyRequestView.js | 4 +- .../ReportActionItem/ReportPreview.js | 11 ++-- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/CurrencyUtils.ts | 8 +-- src/libs/DistanceRequestUtils.ts | 5 +- src/libs/ReportUtils.ts | 38 ++++++-------- src/libs/TransactionUtils.ts | 19 ++----- src/libs/actions/IOU.js | 52 +++++++++++++++++-- src/libs/actions/Transaction.ts | 12 +++-- 15 files changed, 119 insertions(+), 70 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index f566fb77b912..75457f2a7cc9 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -7,6 +7,7 @@ import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; @@ -34,7 +35,8 @@ function DistanceEReceipt({transaction}) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; - const {formattedAmount: formattedTransactionAmount, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); + const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); + const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); const waypoints = lodashGet(transaction, 'comment.waypoints', {}); const sortedWaypoints = useMemo( diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index f66e73a2ef02..a54ca8c7a424 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -256,7 +256,7 @@ function MoneyRequestConfirmationList(props) { const hasRoute = TransactionUtils.hasRoute(transaction); const isDistanceRequestWithoutRoute = props.isDistanceRequest && !hasRoute; const formattedAmount = isDistanceRequestWithoutRoute - ? translate('common.tbd') + ? '' : CurrencyUtils.convertToDisplayString( shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount, props.isDistanceRequest ? currency : props.iouCurrencyCode, @@ -425,6 +425,11 @@ function MoneyRequestConfirmationList(props) { if (!props.isDistanceRequest) { return; } + + if (!hasRoute) { + IOU.setMoneyRequestPendingFields_temporaryForRefactor(props.transactionID, {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); + } + const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); IOU.setMoneyRequestMerchant_temporaryForRefactor(props.transactionID, distanceMerchant); }, [hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, props.isDistanceRequest, props.transactionID]); diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 36d424ea28f2..c6e044b12918 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -285,7 +285,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const hasRoute = TransactionUtils.hasRoute(transaction); const isDistanceRequestWithoutRoute = isDistanceRequest && !hasRoute; const formattedAmount = isDistanceRequestWithoutRoute - ? translate('common.tbd') + ? '' : CurrencyUtils.convertToDisplayString( shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : iouAmount, isDistanceRequest ? currency : iouCurrencyCode, @@ -472,6 +472,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ if (!isDistanceRequest) { return; } + + if (!hasRoute) { + IOU.setMoneyRequestPendingFields_temporaryForRefactor(transaction.transactionID, {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); + } + const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); IOU.setMoneyRequestMerchant_temporaryForRefactor(transaction.transactionID, distanceMerchant); }, [hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction]); diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 4fcca3e518a5..f092822b94e8 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -42,7 +42,7 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: const {totalDisplaySpend, nonReimbursableSpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(report); const shouldShowBreakdown = nonReimbursableSpend && reimbursableSpend; - const formattedTotalAmount = CurrencyUtils.convertToDisplayString(totalDisplaySpend, report.currency, ReportUtils.hasOnlyDistanceRequestTransactions(report.reportID)); + const formattedTotalAmount = CurrencyUtils.convertToDisplayString(totalDisplaySpend, report.currency); const formattedOutOfPocketAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, report.currency); const formattedCompanySpendAmount = CurrencyUtils.convertToDisplayString(nonReimbursableSpend, report.currency); diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index f8ddef88663a..fd182b1d96ae 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -155,25 +155,21 @@ function MoneyRequestPreview(props) { // Pay button should only be visible to the manager of the report. const isCurrentUserManager = managerID === sessionAccountID; - const { - amount: requestAmount, - formattedAmount: formattedRequestAmount, - currency: requestCurrency, - comment: requestComment, - merchant, - } = ReportUtils.getTransactionDetails(props.transaction); + const {amount: requestAmount, currency: requestCurrency, comment: requestComment, merchant} = ReportUtils.getTransactionDetails(props.transaction); const description = truncate(requestComment, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const requestMerchant = truncate(merchant, {length: CONST.REQUEST_PREVIEW.MAX_LENGTH}); const hasReceipt = TransactionUtils.hasReceipt(props.transaction); const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(props.transaction); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(props.transaction); const isDistanceRequest = TransactionUtils.isDistanceRequest(props.transaction); + const hasPendingRoute = TransactionUtils.hasPendingRoute(props.transaction); const isExpensifyCardTransaction = TransactionUtils.isExpensifyCardTransaction(props.transaction); const isSettled = ReportUtils.isSettled(props.iouReport.reportID); const isDeleted = lodashGet(props.action, 'pendingAction', null) === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; // Show the merchant for IOUs and expenses only if they are custom or not related to scanning smartscan - const shouldShowMerchant = !_.isEmpty(requestMerchant) && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT; + const shouldShowMerchant = + !_.isEmpty(requestMerchant) && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && !hasPendingRoute; const shouldShowDescription = !_.isEmpty(description) && !shouldShowMerchant && !isScanning; let merchantOrDescription = requestMerchant; @@ -231,11 +227,15 @@ function MoneyRequestPreview(props) { return translate('iou.receiptScanning'); } + if (hasPendingRoute) { + return translate('iou.routePending'); + } + if (TransactionUtils.hasMissingSmartscanFields(props.transaction)) { return Localize.translateLocal('iou.receiptMissingDetails'); } - return formattedRequestAmount; + return CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency); }; const getDisplayDeleteAmountText = () => { diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 677a75453f0e..7703940ea13f 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -128,7 +128,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate const { created: transactionDate, amount: transactionAmount, - formattedAmount: formattedTransactionAmount, + currency: transactionCurrency, comment: transactionDescription, merchant: transactionMerchant, billable: transactionBillable, @@ -140,6 +140,8 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate } = ReportUtils.getTransactionDetails(transaction); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); + const hasPendingRoute = TransactionUtils.hasPendingRoute(transaction); + const formattedTransactionAmount = transactionAmount && !hasPendingRoute ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); const isCardTransaction = TransactionUtils.isCardTransaction(transaction); const cardProgramName = isCardTransaction ? CardUtils.getCardDescription(transactionCardID) : ''; diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 6c76b49a89eb..cd682d814ba5 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -141,11 +141,11 @@ function ReportPreview(props) { const {translate} = useLocalize(); const {canUseViolations} = usePermissions(); - const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyDistanceRequests, hasNonReimbursableTransactions} = useMemo( + const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyPendingDistanceRequests, hasNonReimbursableTransactions} = useMemo( () => ({ hasMissingSmartscanFields: ReportUtils.hasMissingSmartscanFields(props.iouReportID), areAllRequestsBeingSmartScanned: ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action), - hasOnlyDistanceRequests: ReportUtils.hasOnlyDistanceRequestTransactions(props.iouReportID), + hasOnlyPendingDistanceRequests: ReportUtils.hasOnlyTransactionsWithPendingRoutes(props.iouReportID), hasNonReimbursableTransactions: ReportUtils.hasNonReimbursableTransactions(props.iouReportID), }), // When transactions get updated these status may have changed, so that is a case where we also want to run this. @@ -174,11 +174,10 @@ function ReportPreview(props) { const hasErrors = (hasReceipts && hasMissingSmartscanFields) || (canUseViolations && ReportUtils.hasViolations(props.iouReportID, props.transactionViolations)); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); - const formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; + let formattedMerchant = numberOfRequests === 1 && hasReceipts && !hasOnlyPendingDistanceRequests ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; if (TransactionUtils.isPartialMerchant(formattedMerchant)) { formattedMerchant = null; } - const hasOnlyLoadingDistanceRequests = hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => TransactionUtils.isDistanceBeingCalculated(transaction)); const previewSubtitle = formattedMerchant || props.translate('iou.requestCount', { @@ -195,8 +194,8 @@ function ReportPreview(props) { ); const getDisplayAmount = () => { - if (hasOnlyLoadingDistanceRequests) { - return props.translate('common.tbd'); + if (hasOnlyPendingDistanceRequests) { + return props.translate('iou.routePending'); } if (totalDisplaySpend) { return CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.iouReport.currency); diff --git a/src/languages/en.ts b/src/languages/en.ts index b6da38df21a0..f1fe2c0367de 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -580,6 +580,7 @@ export default { canceled: 'Canceled', posted: 'Posted', deleteReceipt: 'Delete receipt', + routePending: 'Route pending...', receiptScanning: 'Receipt scan in progress…', receiptMissingDetails: 'Receipt missing details', receiptStatusTitle: 'Scanning…', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2478c8ba8bd2..d163a7bc2fa0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -573,6 +573,7 @@ export default { canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', + routePending: 'Ruta pendiente...', receiptScanning: 'Escaneo de recibo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', receiptStatusTitle: 'Escaneando…', diff --git a/src/libs/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index 42387e03c80b..cec9d1e09088 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -2,7 +2,6 @@ import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; -import * as Localize from './Localize'; import BaseLocaleListener from './Localize/LocaleListener/BaseLocaleListener'; import * as NumberFormatUtils from './NumberFormatUtils'; @@ -98,13 +97,8 @@ function convertToFrontendAmount(amountAsInt: number): number { * * @param amountInCents – should be an integer. Anything after a decimal place will be dropped. * @param currency - IOU currency - * @param shouldFallbackToTbd - whether to return 'TBD' instead of a falsy value (e.g. 0.00) */ -function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURRENCY.USD, shouldFallbackToTbd = false): string { - if (shouldFallbackToTbd && !amountInCents) { - return Localize.translateLocal('common.tbd'); - } - +function convertToDisplayString(amountInCents = 0, currency: string = CONST.CURRENCY.USD): string { const convertedAmount = convertToFrontendAmount(amountInCents); return NumberFormatUtils.format(BaseLocaleListener.getPreferredLocale(), convertedAmount, { style: 'currency', diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index c92e9bfd3f67..2309d517df6d 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -95,8 +95,11 @@ function getDistanceMerchant( translate: LocaleContextProps['translate'], toLocaleDigit: LocaleContextProps['toLocaleDigit'], ): string { - const distanceInUnits = hasRoute ? getRoundedDistanceInUnits(distanceInMeters, unit) : translate('common.tbd'); + if (!hasRoute) { + return translate('iou.routePending'); + } + const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit); const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9260d39bde56..64985920454d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -335,7 +335,6 @@ type TransactionDetails = cardID: number; originalAmount: number; originalCurrency: string; - formattedAmount: string; } | undefined; @@ -1094,10 +1093,9 @@ function hasSingleParticipant(report: OnyxEntry): boolean { } /** - * Checks whether all the transactions linked to the IOU report are of the Distance Request type - * + * Checks whether all the transactions linked to the IOU report are of the Distance Request type with pending routes */ -function hasOnlyDistanceRequestTransactions(iouReportID: string | undefined): boolean { +function hasOnlyTransactionsWithPendingRoutes(iouReportID: string | undefined): boolean { const transactions = TransactionUtils.getAllReportTransactions(iouReportID); // Early return false in case not having any transaction @@ -1105,7 +1103,7 @@ function hasOnlyDistanceRequestTransactions(iouReportID: string | undefined): bo return false; } - return transactions.every((transaction) => TransactionUtils.isDistanceRequest(transaction)); + return transactions.every((transaction) => TransactionUtils.hasPendingRoute(transaction)); } /** @@ -1902,7 +1900,7 @@ function getPolicyExpenseChatName(report: OnyxEntry, policy: OnyxEntry

, policy: OnyxEntry | undefined = undefined): string { const moneyRequestTotal = getMoneyRequestReimbursableTotal(report); - const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency, hasOnlyDistanceRequestTransactions(report?.reportID)); + const formattedAmount = CurrencyUtils.convertToDisplayString(moneyRequestTotal, report?.currency); const payerOrApproverName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report?.managerID) ?? ''; const payerPaidAmountMessage = Localize.translateLocal('iou.payerPaidAmount', { payer: payerOrApproverName, @@ -1944,18 +1942,7 @@ function getTransactionDetails(transaction: OnyxEntry, createdDateF if (!transaction) { return; } - const report = getReport(transaction?.reportID); - const amount = TransactionUtils.getAmount(transaction, isNotEmptyObject(report) && isExpenseReport(report)); - const currency = TransactionUtils.getCurrency(transaction); - - let formattedAmount; - if (TransactionUtils.isDistanceBeingCalculated(transaction)) { - formattedAmount = Localize.translateLocal('common.tbd'); - } else { - formattedAmount = amount ? CurrencyUtils.convertToDisplayString(amount, currency) : ''; - } - return { created: TransactionUtils.getCreated(transaction, createdDateFormat), amount: TransactionUtils.getAmount(transaction, !isEmptyObject(report) && isExpenseReport(report)), @@ -2153,6 +2140,11 @@ function getTransactionReportName(reportAction: OnyxEntry): string // Transaction data might be empty on app's first load, if so we fallback to Request return Localize.translateLocal('iou.request'); } + + if (TransactionUtils.hasPendingRoute(transaction)) { + return Localize.translateLocal('iou.routePending'); + } + if (TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { return Localize.translateLocal('iou.receiptScanning'); } @@ -2164,7 +2156,7 @@ function getTransactionReportName(reportAction: OnyxEntry): string const transactionDetails = getTransactionDetails(transaction); return Localize.translateLocal(ReportActionsUtils.isSentMoneyReportAction(reportAction) ? 'iou.threadSentMoneyReportName' : 'iou.threadRequestReportName', { - formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency, TransactionUtils.isDistanceRequest(transaction)) ?? '', + formattedAmount: CurrencyUtils.convertToDisplayString(transactionDetails?.amount ?? 0, transactionDetails?.currency) ?? '', comment: transactionDetails?.comment ?? '', }); } @@ -2177,7 +2169,7 @@ function getTransactionReportName(reportAction: OnyxEntry): string function getReportPreviewMessage( report: OnyxEntry | EmptyObject, reportAction: OnyxEntry | EmptyObject = {}, - shouldConsiderReceiptBeingScanned = false, + shouldConsiderScanningReceiptOrPendingRoute = false, isPreviewMessageForParentChatReport = false, policy: OnyxEntry = null, isForListPreview = false, @@ -2225,12 +2217,16 @@ function getReportPreviewMessage( }); } - if (!isEmptyObject(reportAction) && shouldConsiderReceiptBeingScanned && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { + if (!isEmptyObject(reportAction) && shouldConsiderScanningReceiptOrPendingRoute && reportAction && ReportActionsUtils.isMoneyRequestAction(reportAction)) { const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction); if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasReceipt(linkedTransaction) && TransactionUtils.isReceiptBeingScanned(linkedTransaction)) { return Localize.translateLocal('iou.receiptScanning'); } + + if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasPendingRoute(linkedTransaction)) { + return Localize.translateLocal('iou.routePending'); + } } const originalMessage = reportAction?.originalMessage as IOUMessage | undefined; @@ -4717,7 +4713,7 @@ export { buildTransactionThread, areAllRequestsBeingSmartScanned, getTransactionsWithReceipts, - hasOnlyDistanceRequestTransactions, + hasOnlyTransactionsWithPendingRoutes, hasNonReimbursableTransactions, hasMissingSmartscanFields, getIOUReportActionDisplayMessage, diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index bdb37a3ccd76..f6b60b35bce8 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -11,7 +11,6 @@ import type {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/on import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; -import * as Localize from './Localize'; import * as NumberUtils from './NumberUtils'; type AdditionalTransactionChanges = {comment?: string; waypoints?: WaypointCollection}; @@ -317,29 +316,21 @@ function getOriginalAmount(transaction: Transaction): number { } /** - * Verify if the transaction is of Distance request and is expecting the distance to be calculated on the server: - * - it has a zero amount, which means the request was created offline and expects the distance calculation from the server - * - it is in `isLoading` state, which means the waypoints were updated offline and the distance requires re-calculation + * Verify if the transaction is of Distance request and is expecting the distance to be calculated on the server */ -function isDistanceBeingCalculated(transaction: OnyxEntry): boolean { +function hasPendingRoute(transaction: OnyxEntry): boolean { if (!transaction) { return false; } - const amount = getAmount(transaction, false); - return isDistanceRequest(transaction) && (!!transaction?.isLoading || amount === 0); + return isDistanceRequest(transaction) && !!transaction.pendingFields?.waypoints; } /** * Return the merchant field from the transaction, return the modifiedMerchant if present. */ function getMerchant(transaction: OnyxEntry): string { - if (!transaction) { - return ''; - } - - const merchant = transaction.modifiedMerchant ? transaction.modifiedMerchant : transaction.merchant ?? ''; - return isDistanceBeingCalculated(transaction) ? merchant.replace(CONST.REGEX.FIRST_SPACE, Localize.translateLocal('common.tbd')) : merchant; + return transaction?.modifiedMerchant ? transaction.modifiedMerchant : transaction?.merchant ?? ''; } function getDistance(transaction: Transaction): number { @@ -601,7 +592,7 @@ export { isReceiptBeingScanned, getValidWaypoints, isDistanceRequest, - isDistanceBeingCalculated, + hasPendingRoute, isExpensifyCardTransaction, isCardTransaction, isPending, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7ee752a1f0ef..1a9030c89cc4 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -214,6 +214,14 @@ function setMoneyRequestMerchant_temporaryForRefactor(transactionID, merchant) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {merchant: merchant.trim()}); } +/** + * @param {String} transactionID + * @param {Object} pendingFields + */ +function setMoneyRequestPendingFields_temporaryForRefactor(transactionID, pendingFields) { + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {pendingFields}); +} + /** * @param {String} transactionID * @param {String} category @@ -964,7 +972,8 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t // We don't create a modified report action if we're updating the waypoints, // since there isn't actually any optimistic data we can create for them and the report action is created on the server // with the response from the MapBox API - if (!_.has(transactionChanges, 'waypoints')) { + const hasPendingWaypoints = _.has(transactionChanges, 'waypoints'); + if (!hasPendingWaypoints) { const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport); params.reportActionID = updatedReportAction.reportActionID; @@ -1026,7 +1035,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t value: { ...updatedTransaction, pendingFields, - isLoading: _.has(transactionChanges, 'waypoints'), + isLoading: hasPendingWaypoints, errorFields: null, }, }); @@ -1090,13 +1099,47 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t }, }); - if (_.has(transactionChanges, 'waypoints')) { + if (hasPendingWaypoints) { + // When updating waypoints, we need to explicitly set the transaction's amount and IOU report's total to 0. + // These values must be calculated on the server because they depend on the distance. + optimisticData.push( + ...[ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: { + total: CONST.IOU.DEFAULT_AMOUNT, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + amount: CONST.IOU.DEFAULT_AMOUNT, + modifiedAmount: CONST.IOU.DEFAULT_AMOUNT, + modifiedMerchant: Localize.translateLocal('iou.routePending'), + }, + }, + ], + ); + // Delete the draft transaction when editing waypoints when the server responds successfully and there are no errors successData.push({ onyxMethod: Onyx.METHOD.SET, key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, value: null, }); + + // Revert the transaction's amount to the original value on failure. + // The IOU Report will be fully reverted in the failureData further below. + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + amount: transaction.amount, + modifiedAmount: transaction.modifiedAmount, + }, + }); } // Clear out loading states, pending fields, and add the error fields @@ -1110,7 +1153,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t }, }); - // Reset the iouReport to it's original state + // Reset the iouReport to its original state failureData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, @@ -3672,6 +3715,7 @@ export { setMoneyRequestDescription_temporaryForRefactor, setMoneyRequestMerchant_temporaryForRefactor, setMoneyRequestParticipants_temporaryForRefactor, + setMoneyRequestPendingFields_temporaryForRefactor, setMoneyRequestReceipt, setMoneyRequestTag_temporaryForRefactor, setMoneyRequestAmount, diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 430de0557674..8dc83ed37a75 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -64,7 +64,9 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp [`waypoint${index}`]: waypoint, }, }, - amount: CONST.IOU.DEFAULT_AMOUNT, + // We want to reset the amount only for draft transactions (when creating the request). + // When modifying an existing transaction, the amount will be updated on the actual IOU update operation. + ...(isDraft && {amount: CONST.IOU.DEFAULT_AMOUNT}), // Empty out errors when we're saving a new waypoint as this indicates the user is updating their input errorFields: { route: null, @@ -133,7 +135,9 @@ function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: ...transaction.comment, waypoints: reIndexedWaypoints, }, - amount: CONST.IOU.DEFAULT_AMOUNT, + // We want to reset the amount only for draft transactions (when creating the request). + // When modifying an existing transaction, the amount will be updated on the actual IOU update operation. + ...(isDraft && {amount: CONST.IOU.DEFAULT_AMOUNT}), }; if (!isRemovedWaypointEmpty) { @@ -246,7 +250,9 @@ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, i comment: { waypoints, }, - amount: CONST.IOU.DEFAULT_AMOUNT, + // We want to reset the amount only for draft transactions (when creating the request). + // When modifying an existing transaction, the amount will be updated on the actual IOU update operation. + ...(isDraft && {amount: CONST.IOU.DEFAULT_AMOUNT}), // Empty out errors when we're saving new waypoints as this indicates the user is updating their input errorFields: { route: null, From 5a24138c6aad5b5a2ed01a511ccd5a2e53dd82fa Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 19 Jan 2024 13:49:52 +0100 Subject: [PATCH 05/19] Fix the pending route logic in Distance e-Receipt --- src/components/DistanceEReceipt.js | 11 ++++---- .../ReportActionItem/ReportPreview.js | 12 +++++---- src/libs/ReceiptUtils.ts | 26 ++++++++++--------- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 75457f2a7cc9..3f337281c548 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -4,7 +4,6 @@ import {ScrollView, View} from 'react-native'; import _ from 'underscore'; import EReceiptBackground from '@assets/images/eReceipt_background.svg'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as CurrencyUtils from '@libs/CurrencyUtils'; @@ -33,10 +32,10 @@ function DistanceEReceipt({transaction}) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); - const {isOffline} = useNetwork(); const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); - const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; + const formattedTransactionAmount = CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency); + const hasPendingRoute = TransactionUtils.hasPendingRoute(transaction); const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); const waypoints = lodashGet(transaction, 'comment.waypoints', {}); const sortedWaypoints = useMemo( @@ -64,7 +63,7 @@ function DistanceEReceipt({transaction}) { /> - {isOffline || !thumbnailSource ? ( + {hasPendingRoute || !thumbnailSource ? ( ) : ( - {formattedTransactionAmount} + {!hasPendingRoute && ( + {formattedTransactionAmount} + )} {transactionMerchant} diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index cd682d814ba5..2e5c73b04f54 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -179,11 +179,13 @@ function ReportPreview(props) { formattedMerchant = null; } const previewSubtitle = - formattedMerchant || - props.translate('iou.requestCount', { - count: numberOfRequests - numberOfScanningReceipts, - scanningReceipts: numberOfScanningReceipts, - }); + numberOfRequests === 1 && hasOnlyPendingDistanceRequests + ? '' + : formattedMerchant || + props.translate('iou.requestCount', { + count: numberOfRequests - numberOfScanningReceipts, + scanningReceipts: numberOfScanningReceipts, + }); const shouldShowSubmitButton = isDraftExpenseReport && reimbursableSpend !== 0; diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index bcba68a3a0bd..9e985f0bd60e 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -8,6 +8,7 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; import * as FileUtils from './fileDownload/FileUtils'; +import * as TransactionUtils from './TransactionUtils'; type ThumbnailAndImageURI = { image: ImageSourcePropType | string; @@ -29,27 +30,28 @@ type FileNameAndExtension = { * @param receiptFileName */ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { + if (TransactionUtils.hasPendingRoute(transaction)) { + return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true}; + } + // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg const path = transaction?.receipt?.source ?? receiptPath ?? ''; // filename of uploaded image or last part of remote URI const filename = transaction?.filename ?? receiptFileName ?? ''; const isReceiptImage = Str.isImage(filename); - const hasEReceipt = transaction?.hasEReceipt; - if (!Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { - if (hasEReceipt) { - return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; - } + if (hasEReceipt) { + return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; + } - // For local files, we won't have a thumbnail yet - if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { - return {thumbnail: null, image: path, isLocalFile: true}; - } + // For local files, we won't have a thumbnail yet + if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { + return {thumbnail: null, image: path, isLocalFile: true}; + } - if (isReceiptImage) { - return {thumbnail: `${path}.1024.jpg`, image: path}; - } + if (isReceiptImage) { + return {thumbnail: `${path}.1024.jpg`, image: path}; } const {fileExtension} = FileUtils.splitExtensionFromFileName(filename) as FileNameAndExtension; From dd94a01030f0d6ef3050f34dfb2db0b8e67ccb78 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sat, 20 Jan 2024 14:12:24 +0100 Subject: [PATCH 06/19] Show the "Pending route..." as merchant for requests with manual amount --- src/components/DistanceEReceipt.js | 4 +--- .../ReportActionItem/MoneyRequestPreview.js | 7 ++++-- .../ReportActionItem/MoneyRequestView.js | 3 +-- .../ReportActionItem/ReportPreview.js | 24 +++++++++---------- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 3f337281c548..c507e713b546 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -75,9 +75,7 @@ function DistanceEReceipt({transaction}) { )} - {!hasPendingRoute && ( - {formattedTransactionAmount} - )} + {!hasPendingRoute && {formattedTransactionAmount}} {transactionMerchant} diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index fd182b1d96ae..5f555e6993d4 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -169,7 +169,10 @@ function MoneyRequestPreview(props) { // Show the merchant for IOUs and expenses only if they are custom or not related to scanning smartscan const shouldShowMerchant = - !_.isEmpty(requestMerchant) && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && !hasPendingRoute; + !_.isEmpty(requestMerchant) && + requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && + requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && + !(hasPendingRoute && !requestAmount); const shouldShowDescription = !_.isEmpty(description) && !shouldShowMerchant && !isScanning; let merchantOrDescription = requestMerchant; @@ -227,7 +230,7 @@ function MoneyRequestPreview(props) { return translate('iou.receiptScanning'); } - if (hasPendingRoute) { + if (hasPendingRoute && !requestAmount) { return translate('iou.routePending'); } diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 7703940ea13f..5a84a5e6de21 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -140,8 +140,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate } = ReportUtils.getTransactionDetails(transaction); const isEmptyMerchant = transactionMerchant === '' || transactionMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); - const hasPendingRoute = TransactionUtils.hasPendingRoute(transaction); - const formattedTransactionAmount = transactionAmount && !hasPendingRoute ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; + const formattedTransactionAmount = transactionAmount ? CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency) : ''; const formattedOriginalAmount = transactionOriginalAmount && transactionOriginalCurrency && CurrencyUtils.convertToDisplayString(transactionOriginalAmount, transactionOriginalCurrency); const isCardTransaction = TransactionUtils.isCardTransaction(transaction); const cardProgramName = isCardTransaction ? CardUtils.getCardDescription(transactionCardID) : ''; diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 2e5c73b04f54..1f9755c2a3e8 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -174,18 +174,16 @@ function ReportPreview(props) { const hasErrors = (hasReceipts && hasMissingSmartscanFields) || (canUseViolations && ReportUtils.hasViolations(props.iouReportID, props.transactionViolations)); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); - let formattedMerchant = numberOfRequests === 1 && hasReceipts && !hasOnlyPendingDistanceRequests ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; + let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; if (TransactionUtils.isPartialMerchant(formattedMerchant)) { formattedMerchant = null; } const previewSubtitle = - numberOfRequests === 1 && hasOnlyPendingDistanceRequests - ? '' - : formattedMerchant || - props.translate('iou.requestCount', { - count: numberOfRequests - numberOfScanningReceipts, - scanningReceipts: numberOfScanningReceipts, - }); + formattedMerchant || + props.translate('iou.requestCount', { + count: numberOfRequests - numberOfScanningReceipts, + scanningReceipts: numberOfScanningReceipts, + }); const shouldShowSubmitButton = isDraftExpenseReport && reimbursableSpend !== 0; @@ -196,15 +194,15 @@ function ReportPreview(props) { ); const getDisplayAmount = () => { - if (hasOnlyPendingDistanceRequests) { - return props.translate('iou.routePending'); - } if (totalDisplaySpend) { return CurrencyUtils.convertToDisplayString(totalDisplaySpend, props.iouReport.currency); } if (isScanning) { return props.translate('iou.receiptScanning'); } + if (hasOnlyPendingDistanceRequests) { + return props.translate('iou.routePending'); + } // If iouReport is not available, get amount from the action message (Ex: "Domain20821's Workspace owes $33.00" or "paid ₫60" or "paid -₫60 elsewhere") let displayAmount = ''; @@ -253,6 +251,8 @@ function ReportPreview(props) { return isCurrentUserManager && !isDraftExpenseReport && !isApproved && !iouSettled; }, [isPaidGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, iouSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; + const shouldShowSubtitle = + !isScanning && (numberOfRequests > 1 || (hasReceipts && numberOfRequests === 1 && formattedMerchant && !(hasOnlyPendingDistanceRequests && !totalDisplaySpend))); return ( @@ -301,7 +301,7 @@ function ReportPreview(props) { )} - {!isScanning && (numberOfRequests > 1 || (hasReceipts && numberOfRequests === 1 && formattedMerchant)) && ( + {shouldShowSubtitle && ( {previewSubtitle || moneyRequestComment} From 373c670b35dd8d332503c042c267bf9d4dc06c2c Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sat, 20 Jan 2024 15:53:13 +0100 Subject: [PATCH 07/19] Update hasPendingRoute logic --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- src/libs/TransactionUtils.ts | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 97f97877aa10..f8484373cad8 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -172,7 +172,7 @@ function MoneyRequestPreview(props) { !_.isEmpty(requestMerchant) && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && - !(hasPendingRoute && !requestAmount); + !(isDistanceRequest && hasPendingRoute && !requestAmount); const shouldShowDescription = !_.isEmpty(description) && !shouldShowMerchant && !isScanning; let merchantOrDescription = requestMerchant; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index f6b60b35bce8..f4b3b464d9d1 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -316,14 +316,10 @@ function getOriginalAmount(transaction: Transaction): number { } /** - * Verify if the transaction is of Distance request and is expecting the distance to be calculated on the server + * Verify if the transaction is expecting the distance to be calculated on the server */ function hasPendingRoute(transaction: OnyxEntry): boolean { - if (!transaction) { - return false; - } - - return isDistanceRequest(transaction) && !!transaction.pendingFields?.waypoints; + return !!transaction?.pendingFields?.waypoints; } /** From 634b5fa8f1b1f1dd7c1122b1ad9e391f2d4174e6 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sat, 20 Jan 2024 21:17:59 +0100 Subject: [PATCH 08/19] Update "Route pending" display based on amount --- src/components/DistanceEReceipt.js | 2 +- src/libs/ReportUtils.ts | 2 +- src/libs/TransactionUtils.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index c507e713b546..55ac6d8d6979 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -75,7 +75,7 @@ function DistanceEReceipt({transaction}) { )} - {!hasPendingRoute && {formattedTransactionAmount}} + {!!transactionAmount && {formattedTransactionAmount}} {transactionMerchant} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2a7fd4c74338..e88a62953b3e 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2225,7 +2225,7 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.receiptScanning'); } - if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasPendingRoute(linkedTransaction)) { + if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasPendingRoute(linkedTransaction) && !TransactionUtils.getAmount(linkedTransaction)) { return Localize.translateLocal('iou.routePending'); } } diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index f4b3b464d9d1..103f66d7a683 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -266,8 +266,8 @@ function getDescription(transaction: OnyxEntry): string { /** * Return the amount field from the transaction, return the modifiedAmount if present. */ -function getAmount(transaction: OnyxEntry, isFromExpenseReport: boolean): number { - // IOU requests cannot have negative values but they can be stored as negative values, let's return absolute value +function getAmount(transaction: OnyxEntry, isFromExpenseReport = false): number { + // IOU requests cannot have negative values, but they can be stored as negative values, let's return absolute value if (!isFromExpenseReport) { const amount = transaction?.modifiedAmount ?? 0; if (amount) { From f2a7a06d85a3cfff8e11bbcf8e4b30c205f95ef7 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sat, 20 Jan 2024 21:46:18 +0100 Subject: [PATCH 09/19] Revert some changes --- src/libs/ReceiptUtils.ts | 26 ++++++++++++-------------- src/types/onyx/Transaction.ts | 2 -- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 9e985f0bd60e..bcba68a3a0bd 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -8,7 +8,6 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; import * as FileUtils from './fileDownload/FileUtils'; -import * as TransactionUtils from './TransactionUtils'; type ThumbnailAndImageURI = { image: ImageSourcePropType | string; @@ -30,28 +29,27 @@ type FileNameAndExtension = { * @param receiptFileName */ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { - if (TransactionUtils.hasPendingRoute(transaction)) { - return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true}; - } - // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg const path = transaction?.receipt?.source ?? receiptPath ?? ''; // filename of uploaded image or last part of remote URI const filename = transaction?.filename ?? receiptFileName ?? ''; const isReceiptImage = Str.isImage(filename); + const hasEReceipt = transaction?.hasEReceipt; - if (hasEReceipt) { - return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; - } + if (!Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { + if (hasEReceipt) { + return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction}; + } - // For local files, we won't have a thumbnail yet - if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { - return {thumbnail: null, image: path, isLocalFile: true}; - } + // For local files, we won't have a thumbnail yet + if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { + return {thumbnail: null, image: path, isLocalFile: true}; + } - if (isReceiptImage) { - return {thumbnail: `${path}.1024.jpg`, image: path}; + if (isReceiptImage) { + return {thumbnail: `${path}.1024.jpg`, image: path}; + } } const {fileExtension} = FileUtils.splitExtensionFromFileName(filename) as FileNameAndExtension; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 4b8a0a1cf9b3..8b7e26280305 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -94,8 +94,6 @@ type Transaction = { /** If the transaction was made in a foreign currency, we send the original amount and currency */ originalAmount?: number; originalCurrency?: string; - - isLoading?: boolean; }; export default Transaction; From 98ec065bba2d026e96fb8242cc607279b42905c9 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sun, 21 Jan 2024 21:47:33 +0100 Subject: [PATCH 10/19] Update IOU Report total on waypoints change --- src/libs/actions/IOU.js | 85 +++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 49 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 1a9030c89cc4..e5e45dd5de32 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -954,7 +954,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t const iouReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${transactionThread.parentReportID}`]; const isFromExpenseReport = ReportUtils.isExpenseReport(iouReport); const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); - const updatedTransaction = TransactionUtils.getUpdatedTransaction(transaction, transactionChanges, isFromExpenseReport); + let updatedTransaction = TransactionUtils.getUpdatedTransaction(transaction, transactionChanges, isFromExpenseReport); const transactionDetails = ReportUtils.getTransactionDetails(updatedTransaction); // This needs to be a JSON string since we're sending this to the MapBox API @@ -968,13 +968,22 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t transactionID, }; + const hasPendingWaypoints = _.has(transactionChanges, 'waypoints'); + if (hasPendingWaypoints) { + updatedTransaction = { + ...updatedTransaction, + amount: CONST.IOU.DEFAULT_AMOUNT, + modifiedAmount: CONST.IOU.DEFAULT_AMOUNT, + modifiedMerchant: Localize.translateLocal('iou.routePending'), + }; + } + // Step 3: Build the modified expense report actions // We don't create a modified report action if we're updating the waypoints, // since there isn't actually any optimistic data we can create for them and the report action is created on the server // with the response from the MapBox API - const hasPendingWaypoints = _.has(transactionChanges, 'waypoints'); + const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport); if (!hasPendingWaypoints) { - const updatedReportAction = ReportUtils.buildOptimisticModifiedExpenseReportAction(transactionThread, transaction, transactionChanges, isFromExpenseReport); params.reportActionID = updatedReportAction.reportActionID; optimisticData.push({ @@ -1001,31 +1010,31 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t }, }, }); + } - // Step 4: Compute the IOU total and update the report preview message (and report header) so LHN amount owed is correct. - // Should only update if the transaction matches the currency of the report, else we wait for the update - // from the server with the currency conversion - let updatedMoneyRequestReport = {...iouReport}; - if (updatedTransaction.currency === iouReport.currency && updatedTransaction.modifiedAmount) { - const diff = TransactionUtils.getAmount(transaction, true) - TransactionUtils.getAmount(updatedTransaction, true); - if (ReportUtils.isExpenseReport(iouReport)) { - updatedMoneyRequestReport.total += diff; - } else { - updatedMoneyRequestReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, updatedReportAction.actorAccountID, diff, TransactionUtils.getCurrency(transaction), false); - } - - updatedMoneyRequestReport.cachedTotal = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedTransaction.currency); - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: updatedMoneyRequestReport, - }); - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: {pendingAction: null}, - }); + // Step 4: Compute the IOU total and update the report preview message (and report header) so LHN amount owed is correct. + // Should only update if the transaction matches the currency of the report, else we wait for the update + // from the server with the currency conversion + let updatedMoneyRequestReport = {...iouReport}; + if (updatedTransaction.currency === iouReport.currency && (updatedTransaction.modifiedAmount || hasPendingWaypoints)) { + const diff = TransactionUtils.getAmount(transaction, true) - TransactionUtils.getAmount(updatedTransaction, true); + if (ReportUtils.isExpenseReport(iouReport)) { + updatedMoneyRequestReport.total += diff; + } else { + updatedMoneyRequestReport = IOUUtils.updateIOUOwnerAndTotal(iouReport, updatedReportAction.actorAccountID, diff, TransactionUtils.getCurrency(transaction), false); } + + updatedMoneyRequestReport.cachedTotal = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedTransaction.currency); + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: updatedMoneyRequestReport, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, + value: {pendingAction: null}, + }); } // Optimistically modify the transaction @@ -1100,29 +1109,6 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t }); if (hasPendingWaypoints) { - // When updating waypoints, we need to explicitly set the transaction's amount and IOU report's total to 0. - // These values must be calculated on the server because they depend on the distance. - optimisticData.push( - ...[ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: { - total: CONST.IOU.DEFAULT_AMOUNT, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - amount: CONST.IOU.DEFAULT_AMOUNT, - modifiedAmount: CONST.IOU.DEFAULT_AMOUNT, - modifiedMerchant: Localize.translateLocal('iou.routePending'), - }, - }, - ], - ); - // Delete the draft transaction when editing waypoints when the server responds successfully and there are no errors successData.push({ onyxMethod: Onyx.METHOD.SET, @@ -1138,6 +1124,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t value: { amount: transaction.amount, modifiedAmount: transaction.modifiedAmount, + modifiedMerchant: transaction.modifiedMerchant, }, }); } From c6ed187f899e58c632c89ce60633bd9d5f27ed17 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 23 Jan 2024 12:37:34 +0100 Subject: [PATCH 11/19] Use TransactionUtils.hasPendingRoute to check pending route for receipt --- src/libs/ReceiptUtils.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 7fca9f54b744..9e985f0bd60e 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -8,6 +8,7 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {Transaction} from '@src/types/onyx'; import * as FileUtils from './fileDownload/FileUtils'; +import * as TransactionUtils from './TransactionUtils'; type ThumbnailAndImageURI = { image: ImageSourcePropType | string; @@ -29,7 +30,7 @@ type FileNameAndExtension = { * @param receiptFileName */ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { - if (Object.hasOwn(transaction?.pendingFields ?? {}, 'waypoints')) { + if (TransactionUtils.hasPendingRoute(transaction)) { return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true}; } From 3f3564c7c50f46d65aa0675511f051d74c0f8d66 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 26 Jan 2024 16:43:20 +0100 Subject: [PATCH 12/19] Show "Route pending" when the rate is not loaded yet --- src/components/MoneyRequestConfirmationList.js | 16 ++++++++-------- ...emporaryForRefactorRequestConfirmationList.js | 16 ++++++++-------- src/libs/DistanceRequestUtils.ts | 6 +++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 1e8a7613cbeb..0a170db92e9a 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -255,8 +255,8 @@ function MoneyRequestConfirmationList(props) { const shouldShowBillable = !lodashGet(props.policy, 'disabledFields.defaultBillable', true); const hasRoute = TransactionUtils.hasRoute(transaction); - const isDistanceRequestWithoutRoute = props.isDistanceRequest && !hasRoute; - const formattedAmount = isDistanceRequestWithoutRoute + const isDistanceRequestWithPendingRoute = props.isDistanceRequest && (!hasRoute || !rate); + const formattedAmount = isDistanceRequestWithPendingRoute ? '' : CurrencyUtils.convertToDisplayString( shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : props.iouAmount, @@ -332,7 +332,7 @@ function MoneyRequestConfirmationList(props) { let text; if (isSplitBill && props.iouAmount === 0) { text = translate('iou.split'); - } else if ((props.receiptPath && isTypeRequest) || isDistanceRequestWithoutRoute) { + } else if ((props.receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { text = translate('iou.request'); if (props.iouAmount !== 0) { text = translate('iou.requestAmount', {amount: formattedAmount}); @@ -347,7 +347,7 @@ function MoneyRequestConfirmationList(props) { value: props.iouType, }, ]; - }, [isSplitBill, isTypeRequest, props.iouType, props.iouAmount, props.receiptPath, formattedAmount, isDistanceRequestWithoutRoute, translate]); + }, [isSplitBill, isTypeRequest, props.iouType, props.iouAmount, props.receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]); const selectedParticipants = useMemo(() => _.filter(props.selectedParticipants, (participant) => participant.selected), [props.selectedParticipants]); const payeePersonalDetails = useMemo(() => props.payeePersonalDetails || props.currentUserPersonalDetails, [props.payeePersonalDetails, props.currentUserPersonalDetails]); @@ -427,13 +427,13 @@ function MoneyRequestConfirmationList(props) { return; } - if (!hasRoute) { + if (isDistanceRequestWithPendingRoute) { IOU.setMoneyRequestPendingFields_temporaryForRefactor(props.transactionID, {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); } const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); IOU.setMoneyRequestMerchant_temporaryForRefactor(props.transactionID, distanceMerchant); - }, [hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, props.isDistanceRequest, props.transactionID]); + }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, props.isDistanceRequest, props.transactionID]); /** * @param {Object} option @@ -487,7 +487,7 @@ function MoneyRequestConfirmationList(props) { } else { // validate the amount for distance requests const decimals = CurrencyUtils.getCurrencyDecimals(props.iouCurrencyCode); - if (props.isDistanceRequest && !isDistanceRequestWithoutRoute && !MoneyRequestUtils.validateAmount(String(props.iouAmount), decimals)) { + if (props.isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(props.iouAmount), decimals)) { setFormError('common.error.invalidAmount'); return; } @@ -510,7 +510,7 @@ function MoneyRequestConfirmationList(props) { props.iouType, props.isDistanceRequest, props.iouCategory, - isDistanceRequestWithoutRoute, + isDistanceRequestWithPendingRoute, props.iouCurrencyCode, props.iouAmount, transaction, diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 7ee3010acda5..f9ad8670d52e 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -284,8 +284,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const shouldShowBillable = !lodashGet(policy, 'disabledFields.defaultBillable', true); const hasRoute = TransactionUtils.hasRoute(transaction); - const isDistanceRequestWithoutRoute = isDistanceRequest && !hasRoute; - const formattedAmount = isDistanceRequestWithoutRoute + const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate); + const formattedAmount = isDistanceRequestWithPendingRoute ? '' : CurrencyUtils.convertToDisplayString( shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : iouAmount, @@ -376,7 +376,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ let text; if (isTypeSplit && iouAmount === 0) { text = translate('iou.split'); - } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithoutRoute) { + } else if ((receiptPath && isTypeRequest) || isDistanceRequestWithPendingRoute) { text = translate('iou.request'); if (iouAmount !== 0) { text = translate('iou.requestAmount', {amount: formattedAmount}); @@ -391,7 +391,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ value: iouType, }, ]; - }, [isTypeSplit, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithoutRoute, translate]); + }, [isTypeSplit, isTypeRequest, iouType, iouAmount, receiptPath, formattedAmount, isDistanceRequestWithPendingRoute, translate]); const selectedParticipants = useMemo(() => _.filter(pickedParticipants, (participant) => participant.selected), [pickedParticipants]); const personalDetailsOfPayee = useMemo(() => payeePersonalDetails || currentUserPersonalDetails, [payeePersonalDetails, currentUserPersonalDetails]); @@ -474,13 +474,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ return; } - if (!hasRoute) { + if (isDistanceRequestWithPendingRoute) { IOU.setMoneyRequestPendingFields_temporaryForRefactor(transaction.transactionID, {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); } const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); IOU.setMoneyRequestMerchant_temporaryForRefactor(transaction.transactionID, distanceMerchant); - }, [hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction]); + }, [isDistanceRequestWithPendingRoute, hasRoute, distance, unit, rate, currency, translate, toLocaleDigit, isDistanceRequest, transaction]); /** * @param {Object} option @@ -535,7 +535,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ } else { // validate the amount for distance requests const decimals = CurrencyUtils.getCurrencyDecimals(iouCurrencyCode); - if (isDistanceRequest && !isDistanceRequestWithoutRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) { + if (isDistanceRequest && !isDistanceRequestWithPendingRoute && !MoneyRequestUtils.validateAmount(String(iouAmount), decimals)) { setFormError('common.error.invalidAmount'); return; } @@ -560,7 +560,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ onSendMoney, iouCurrencyCode, isDistanceRequest, - isDistanceRequestWithoutRoute, + isDistanceRequestWithPendingRoute, iouAmount, isEditingSplitBill, onConfirm, diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 2309d517df6d..a42cb6a8f756 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -95,7 +95,7 @@ function getDistanceMerchant( translate: LocaleContextProps['translate'], toLocaleDigit: LocaleContextProps['toLocaleDigit'], ): string { - if (!hasRoute) { + if (!hasRoute || !rate) { return translate('iou.routePending'); } @@ -103,9 +103,9 @@ function getDistanceMerchant( const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit; - const ratePerUnit = rate ? PolicyUtils.getUnitRateValue({rate}, toLocaleDigit) : translate('common.tbd'); + const ratePerUnit = PolicyUtils.getUnitRateValue({rate}, toLocaleDigit); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const currencySymbol = rate ? CurrencyUtils.getCurrencySymbol(currency) || `${currency} ` : ''; + const currencySymbol = CurrencyUtils.getCurrencySymbol(currency) || `${currency} `; return `${distanceInUnits} ${unitString} @ ${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`; } From 945281983bf8adb3e8ade3cbaf61ca9fa8a4e324 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sat, 27 Jan 2024 23:04:43 +0100 Subject: [PATCH 13/19] Cosmetic change for readability --- src/libs/actions/IOU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 730a4e2f1ddc..71582c2fa3a1 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1038,7 +1038,7 @@ function getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, t // Should only update if the transaction matches the currency of the report, else we wait for the update // from the server with the currency conversion let updatedMoneyRequestReport = {...iouReport}; - if (updatedTransaction.currency === iouReport.currency && (updatedTransaction.modifiedAmount || hasPendingWaypoints)) { + if ((hasPendingWaypoints || updatedTransaction.modifiedAmount) && updatedTransaction.currency === iouReport.currency) { const diff = TransactionUtils.getAmount(transaction, true) - TransactionUtils.getAmount(updatedTransaction, true); if (ReportUtils.isExpenseReport(iouReport)) { updatedMoneyRequestReport.total += diff; From 16f6b54141297987374a8a60b9110cc5a24dd56d Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 30 Jan 2024 19:09:25 +0100 Subject: [PATCH 14/19] Change hasPendingRoute to isFetchingWaypointsFromServer --- src/components/DistanceEReceipt.js | 4 ++-- src/components/MoneyRequestConfirmationList.js | 2 +- ...eyTemporaryForRefactorRequestConfirmationList.js | 2 +- .../ReportActionItem/MoneyRequestPreview.js | 13 +++++++++---- src/components/ReportActionItem/ReportPreview.tsx | 9 +++++++++ src/libs/ReceiptUtils.ts | 2 +- src/libs/ReportUtils.ts | 6 +++--- src/libs/TransactionUtils.ts | 4 ++-- src/libs/actions/IOU.js | 4 ++-- 9 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 796a1822ad6e..3842e26750fc 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -35,7 +35,7 @@ function DistanceEReceipt({transaction}) { const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); const formattedTransactionAmount = CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency); - const hasPendingRoute = TransactionUtils.hasPendingRoute(transaction); + const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(transaction); const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); const waypoints = lodashGet(transaction, 'comment.waypoints', {}); const sortedWaypoints = useMemo( @@ -63,7 +63,7 @@ function DistanceEReceipt({transaction}) { /> - {hasPendingRoute || !thumbnailSource ? ( + {isFetchingWaypointsFromServer || !thumbnailSource ? ( ) : ( 1 || (hasReceipts && numberOfRequests === 1 && formattedMerchant && !(hasOnlyPendingDistanceRequests && !totalDisplaySpend))); + return ( diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 3776cdc27d40..48859cce105e 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -30,7 +30,7 @@ type FileNameAndExtension = { * @param receiptFileName */ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI { - if (TransactionUtils.hasPendingRoute(transaction)) { + if (TransactionUtils.isFetchingWaypointsFromServer(transaction)) { return {thumbnail: null, image: ReceiptGeneric, isLocalFile: true}; } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 3d09876d1bd5..72151ebf6025 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1080,7 +1080,7 @@ function hasOnlyTransactionsWithPendingRoutes(iouReportID: string | undefined): return false; } - return transactions.every((transaction) => TransactionUtils.hasPendingRoute(transaction)); + return transactions.every((transaction) => TransactionUtils.isFetchingWaypointsFromServer(transaction)); } /** @@ -2095,7 +2095,7 @@ function getTransactionReportName(reportAction: OnyxEntry): string return Localize.translateLocal('iou.request'); } - if (TransactionUtils.hasPendingRoute(transaction)) { + if (TransactionUtils.isFetchingWaypointsFromServer(transaction)) { return Localize.translateLocal('iou.routePending'); } @@ -2178,7 +2178,7 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.receiptScanning'); } - if (!isEmptyObject(linkedTransaction) && TransactionUtils.hasPendingRoute(linkedTransaction) && !TransactionUtils.getAmount(linkedTransaction)) { + if (!isEmptyObject(linkedTransaction) && TransactionUtils.isFetchingWaypointsFromServer(linkedTransaction) && !TransactionUtils.getAmount(linkedTransaction)) { return Localize.translateLocal('iou.routePending'); } } diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 47da46129f9d..10f1c5d154fa 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -318,7 +318,7 @@ function getOriginalAmount(transaction: Transaction): number { /** * Verify if the transaction is expecting the distance to be calculated on the server */ -function hasPendingRoute(transaction: OnyxEntry): boolean { +function isFetchingWaypointsFromServer(transaction: OnyxEntry): boolean { return !!transaction?.pendingFields?.waypoints; } @@ -588,7 +588,7 @@ export { isReceiptBeingScanned, getValidWaypoints, isDistanceRequest, - hasPendingRoute, + isFetchingWaypointsFromServer, isExpensifyCardTransaction, isCardTransaction, isPending, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index c0c250698478..731fbed2cee6 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -219,7 +219,7 @@ function setMoneyRequestMerchant_temporaryForRefactor(transactionID, merchant) { * @param {String} transactionID * @param {Object} pendingFields */ -function setMoneyRequestPendingFields_temporaryForRefactor(transactionID, pendingFields) { +function setMoneyRequestPendingFields(transactionID, pendingFields) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {pendingFields}); } @@ -3812,7 +3812,7 @@ export { setMoneyRequestDescription_temporaryForRefactor, setMoneyRequestMerchant_temporaryForRefactor, setMoneyRequestParticipants_temporaryForRefactor, - setMoneyRequestPendingFields_temporaryForRefactor, + setMoneyRequestPendingFields, setMoneyRequestReceipt, setMoneyRequestTag_temporaryForRefactor, setMoneyRequestAmount, From 19d97aac1913dcb6b5da3f132213e0f8181e638a Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 2 Feb 2024 00:33:19 +0100 Subject: [PATCH 15/19] Update comment for readability Co-authored-by: Amy Evans --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 72502e53c2ae..00f02c866bb1 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -172,7 +172,7 @@ function MoneyRequestPreview(props) { /* Show the merchant for IOUs and expenses only if: - - the merchant is not empty, is custom or not related to scanning smartscan; + - the merchant is not empty, is custom, or is not related to scanning smartscan; - the request is not a distance request with a pending route and amount = 0 - in this case, the merchant says: "Route pending...", which is already shown in the amount field; */ From 3ff6f9a4e2e601204e276855eb195f6a88f915e1 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sat, 3 Feb 2024 11:47:59 +0100 Subject: [PATCH 16/19] Refactor checks for readability --- .../ReportActionItem/MoneyRequestPreview.tsx | 12 +++++------- src/components/ReportActionItem/ReportPreview.tsx | 11 +++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.tsx b/src/components/ReportActionItem/MoneyRequestPreview.tsx index a238a3a71065..622382dea64b 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview.tsx @@ -155,8 +155,9 @@ function MoneyRequestPreview({ const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(transaction); const isDistanceRequest = TransactionUtils.isDistanceRequest(transaction); - const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(props.transaction); - const isExpensifyCardTransaction = TransactionUtils.isExpensifyCardTransaction(props.transaction); + const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(transaction); + const isPendingDistanceRequest = isDistanceRequest && isFetchingWaypointsFromServer && !requestAmount; + const isCardTransaction = TransactionUtils.isCardTransaction(transaction); const isSettled = ReportUtils.isSettled(iouReport?.reportID); const isDeleted = action?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; @@ -167,10 +168,7 @@ function MoneyRequestPreview({ the merchant says: "Route pending...", which is already shown in the amount field; */ const shouldShowMerchant = - !!requestMerchant && - requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && - requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && - !(isDistanceRequest && isFetchingWaypointsFromServer && !requestAmount); + !!requestMerchant && requestMerchant !== CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT && requestMerchant !== CONST.TRANSACTION.DEFAULT_MERCHANT && !isPendingDistanceRequest; const shouldShowDescription = !!description && !shouldShowMerchant && !isScanning; let merchantOrDescription = requestMerchant; @@ -228,7 +226,7 @@ function MoneyRequestPreview({ return translate('iou.receiptScanning'); } - if (isFetchingWaypointsFromServer && !requestAmount) { + if (isPendingDistanceRequest) { return translate('iou.routePending'); } diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 3cd8471c04cc..4792ac7f9d3d 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -104,11 +104,11 @@ function ReportPreview({ const {translate} = useLocalize(); const {canUseViolations} = usePermissions(); - const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyPendingDistanceRequests, hasNonReimbursableTransactions} = useMemo( + const {hasMissingSmartscanFields, areAllRequestsBeingSmartScanned, hasOnlyTransactionsWithPendingRoutes, hasNonReimbursableTransactions} = useMemo( () => ({ hasMissingSmartscanFields: ReportUtils.hasMissingSmartscanFields(iouReportID), areAllRequestsBeingSmartScanned: ReportUtils.areAllRequestsBeingSmartScanned(iouReportID, action), - hasOnlyPendingDistanceRequests: ReportUtils.hasOnlyTransactionsWithPendingRoutes(iouReportID), + hasOnlyTransactionsWithPendingRoutes: ReportUtils.hasOnlyTransactionsWithPendingRoutes(iouReportID), hasNonReimbursableTransactions: ReportUtils.hasNonReimbursableTransactions(iouReportID), }), // When transactions get updated these status may have changed, so that is a case where we also want to run this. @@ -138,6 +138,8 @@ function ReportPreview({ const hasErrors = (hasReceipts && hasMissingSmartscanFields) || (canUseViolations && ReportUtils.hasViolations(iouReportID, transactionViolations)); const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3); const lastThreeReceipts = lastThreeTransactionsWithReceipts.map((transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction)); + const hasOnlyPendingDistanceRequests = hasOnlyTransactionsWithPendingRoutes && !totalDisplaySpend; + let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; if (TransactionUtils.isPartialMerchant(formattedMerchant ?? '')) { formattedMerchant = null; @@ -221,6 +223,7 @@ function ReportPreview({ return isCurrentUserManager && !isDraftExpenseReport && !isApproved && !iouSettled; }, [isPaidGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, iouSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; + /* Show subtitle if at least one of the money requests is not being smart scanned, and either: - There is more than one money request – in this case, the "X requests, Y scanning" subtitle is shown; @@ -229,8 +232,8 @@ function ReportPreview({ * There is an edge case when there is only one distance request with a pending route and amount = 0. In this case, we don't want to show the merchant because it says: "Pending route...", which is already displayed in the amount field. */ - const shouldShowSubtitle = - !isScanning && (numberOfRequests > 1 || (hasReceipts && numberOfRequests === 1 && formattedMerchant && !(hasOnlyPendingDistanceRequests && !totalDisplaySpend))); + const shouldShowSingleRequestMerchant = numberOfRequests === 1 && !!formattedMerchant && !hasOnlyPendingDistanceRequests; + const shouldShowSubtitle = !isScanning && (shouldShowSingleRequestMerchant || numberOfRequests > 1); return ( Date: Tue, 6 Feb 2024 09:07:14 +0100 Subject: [PATCH 17/19] TS migration cleanup --- src/libs/actions/IOU.ts | 78 +++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 9a5baa6bbf7f..c6a675ab3da2 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -49,7 +49,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; import type {Participant, Split} from '@src/types/onyx/IOU'; -import type {ErrorFields, Errors} from '@src/types/onyx/OnyxCommon'; +import type {ErrorFields, Errors, PendingFields} from '@src/types/onyx/OnyxCommon'; import type ReportAction from '@src/types/onyx/ReportAction'; import type {OnyxData} from '@src/types/onyx/Request'; import type {Comment, Receipt, ReceiptSource, TaxRate, TransactionChanges, WaypointCollection} from '@src/types/onyx/Transaction'; @@ -298,11 +298,7 @@ function setMoneyRequestMerchant_temporaryForRefactor(transactionID: string, mer Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {merchant: merchant.trim()}); } -/** - * @param {String} transactionID - * @param {Object} pendingFields - */ -function setMoneyRequestPendingFields(transactionID, pendingFields) { +function setMoneyRequestPendingFields(transactionID: string, pendingFields: PendingFields) { Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {pendingFields}); } @@ -1025,13 +1021,32 @@ function getUpdateMoneyRequestParams( }; const hasPendingWaypoints = 'waypoints' in transactionChanges; - if (hasPendingWaypoints) { + if (transaction && updatedTransaction && hasPendingWaypoints) { updatedTransaction = { ...updatedTransaction, amount: CONST.IOU.DEFAULT_AMOUNT, modifiedAmount: CONST.IOU.DEFAULT_AMOUNT, modifiedMerchant: Localize.translateLocal('iou.routePending'), }; + + // Delete the draft transaction when editing waypoints when the server responds successfully and there are no errors + successData.push({ + onyxMethod: Onyx.METHOD.SET, + key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, + value: null, + }); + + // Revert the transaction's amount to the original value on failure. + // The IOU Report will be fully reverted in the failureData further below. + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + amount: transaction.amount, + modifiedAmount: transaction.modifiedAmount, + modifiedMerchant: transaction.modifiedMerchant, + }, + }); } // Step 3: Build the modified expense report actions @@ -1068,21 +1083,21 @@ function getUpdateMoneyRequestParams( }); } - // Step 4: Compute the IOU total and update the report preview message (and report header) so LHN amount owed is correct. - // Should only update if the transaction matches the currency of the report, else we wait for the update - // from the server with the currency conversion - let updatedMoneyRequestReport = {...iouReport}; - if (updatedTransaction?.currency === iouReport?.currency && (hasPendingWaypoints || updatedTransaction?.modifiedAmount)) { - const diff = TransactionUtils.getAmount(transaction, true) - TransactionUtils.getAmount(updatedTransaction, true); - if (ReportUtils.isExpenseReport(iouReport) && typeof updatedMoneyRequestReport.total === 'number') { - updatedMoneyRequestReport.total += diff; - } else { - updatedMoneyRequestReport = iouReport - ? IOUUtils.updateIOUOwnerAndTotal(iouReport, updatedReportAction.actorAccountID ?? -1, diff, TransactionUtils.getCurrency(transaction), false) - : {}; - } + // Step 4: Compute the IOU total and update the report preview message (and report header) so LHN amount owed is correct. + // Should only update if the transaction matches the currency of the report, else we wait for the update + // from the server with the currency conversion + let updatedMoneyRequestReport = {...iouReport}; + if ((hasPendingWaypoints || updatedTransaction?.modifiedAmount) && updatedTransaction?.currency === iouReport?.currency) { + const diff = TransactionUtils.getAmount(transaction, true) - TransactionUtils.getAmount(updatedTransaction, true); + if (ReportUtils.isExpenseReport(iouReport) && typeof updatedMoneyRequestReport.total === 'number') { + updatedMoneyRequestReport.total += diff; + } else { + updatedMoneyRequestReport = iouReport + ? IOUUtils.updateIOUOwnerAndTotal(iouReport, updatedReportAction.actorAccountID ?? -1, diff, TransactionUtils.getCurrency(transaction), false) + : {}; + } - updatedMoneyRequestReport.cachedTotal = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedTransaction.currency); + updatedMoneyRequestReport.cachedTotal = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedTransaction?.currency); optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, @@ -1165,27 +1180,6 @@ function getUpdateMoneyRequestParams( }, }); - if (hasPendingWaypoints) { - // Delete the draft transaction when editing waypoints when the server responds successfully and there are no errors - successData.push({ - onyxMethod: Onyx.METHOD.SET, - key: `${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, - value: null, - }); - - // Revert the transaction's amount to the original value on failure. - // The IOU Report will be fully reverted in the failureData further below. - failureData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: { - amount: transaction.amount, - modifiedAmount: transaction.modifiedAmount, - modifiedMerchant: transaction.modifiedMerchant, - }, - }); - } - // Clear out loading states, pending fields, and add the error fields failureData.push({ onyxMethod: Onyx.METHOD.MERGE, From a020a2383a53209a6b66cc848810d2f5315b83c7 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 6 Feb 2024 18:08:46 +0100 Subject: [PATCH 18/19] Remove hasOnlyPendingDistanceRequests vars --- src/components/DistanceEReceipt.js | 3 +-- src/components/ReportActionItem/MoneyRequestPreview.tsx | 8 +++++--- src/components/ReportActionItem/ReportPreview.tsx | 5 ++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 3842e26750fc..794c1c9aa53f 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -35,7 +35,6 @@ function DistanceEReceipt({transaction}) { const {thumbnail} = TransactionUtils.hasReceipt(transaction) ? ReceiptUtils.getThumbnailAndImageURIs(transaction) : {}; const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = ReportUtils.getTransactionDetails(transaction); const formattedTransactionAmount = CurrencyUtils.convertToDisplayString(transactionAmount, transactionCurrency); - const isFetchingWaypointsFromServer = TransactionUtils.isFetchingWaypointsFromServer(transaction); const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail || ''); const waypoints = lodashGet(transaction, 'comment.waypoints', {}); const sortedWaypoints = useMemo( @@ -63,7 +62,7 @@ function DistanceEReceipt({transaction}) { /> - {isFetchingWaypointsFromServer || !thumbnailSource ? ( + {TransactionUtils.isFetchingWaypointsFromServer(transaction) || !thumbnailSource ? ( ) : ( ReceiptUtils.getThumbnailAndImageURIs(transaction)); - const hasOnlyPendingDistanceRequests = hasOnlyTransactionsWithPendingRoutes && !totalDisplaySpend; let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null; if (TransactionUtils.isPartialMerchant(formattedMerchant ?? '')) { @@ -170,7 +169,7 @@ function ReportPreview({ if (isScanning) { return translate('iou.receiptScanning'); } - if (hasOnlyPendingDistanceRequests) { + if (hasOnlyTransactionsWithPendingRoutes) { return translate('iou.routePending'); } @@ -234,7 +233,7 @@ function ReportPreview({ * There is an edge case when there is only one distance request with a pending route and amount = 0. In this case, we don't want to show the merchant because it says: "Pending route...", which is already displayed in the amount field. */ - const shouldShowSingleRequestMerchant = numberOfRequests === 1 && !!formattedMerchant && !hasOnlyPendingDistanceRequests; + const shouldShowSingleRequestMerchant = numberOfRequests === 1 && !!formattedMerchant && !(hasOnlyTransactionsWithPendingRoutes && !totalDisplaySpend); const shouldShowSubtitle = !isScanning && (shouldShowSingleRequestMerchant || numberOfRequests > 1); return ( From 534b2c1a8b59c7557fcf5fe6c77372b3138e6e43 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 6 Feb 2024 23:13:01 +0100 Subject: [PATCH 19/19] Explain the setMoneyRequestPendingFields call --- src/components/MoneyRequestConfirmationList.js | 9 ++++++--- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 9 ++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index d8476d0d2bb0..5355c302b742 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -427,9 +427,12 @@ function MoneyRequestConfirmationList(props) { return; } - if (isDistanceRequestWithPendingRoute) { - IOU.setMoneyRequestPendingFields(props.transactionID, {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); - } + /* + Set pending waypoints based on the route status. We should handle this dynamically to cover cases such as: + When the user completes the initial steps of the IOU flow offline and then goes online on the confirmation page. + In this scenario, the route will be fetched from the server, and the waypoints will no longer be pending. + */ + IOU.setMoneyRequestPendingFields(props.transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); IOU.setMoneyRequestMerchant_temporaryForRefactor(props.transactionID, distanceMerchant); diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index a5e04dda8d3f..5de9492b50c1 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -474,9 +474,12 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ return; } - if (isDistanceRequestWithPendingRoute) { - IOU.setMoneyRequestPendingFields(transaction.transactionID, {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}); - } + /* + Set pending waypoints based on the route status. We should handle this dynamically to cover cases such as: + When the user completes the initial steps of the IOU flow offline and then goes online on the confirmation page. + In this scenario, the route will be fetched from the server, and the waypoints will no longer be pending. + */ + IOU.setMoneyRequestPendingFields(transaction.transactionID, {waypoints: isDistanceRequestWithPendingRoute ? CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD : null}); const distanceMerchant = DistanceRequestUtils.getDistanceMerchant(hasRoute, distance, unit, rate, currency, translate, toLocaleDigit); IOU.setMoneyRequestMerchant_temporaryForRefactor(transaction.transactionID, distanceMerchant);