Skip to content

Commit 9ca496f

Browse files
committed
fix(distance): draw the New Expensify e-receipt instead of the Classic PDF
1 parent eef9c94 commit 9ca496f

8 files changed

Lines changed: 37 additions & 106 deletions

File tree

src/components/Attachments/AttachmentView/index.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
2828
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
2929
import {getFileResolution, isHighResolutionImage} from '@libs/fileDownload/FileUtils';
3030
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
31-
import {hasEReceipt, hasReceiptSource, isPerDiemRequest, shouldRenderLocalDistanceEReceipt} from '@libs/TransactionUtils';
31+
import {hasEReceipt, hasReceiptSource, isMapBasedDistanceRequest, isPerDiemRequest} from '@libs/TransactionUtils';
3232

3333
import type {ColorValue} from '@styles/utils/types';
3434
import variables from '@styles/variables';
@@ -255,10 +255,13 @@ function AttachmentView({
255255
);
256256
}
257257

258-
// A distance expense normally shows the receipt file that the server generated, which the PDF branch below
259-
// renders. Draw the card only for the cases that file cannot cover, and do it before that branch so the
260-
// stale URL of a receipt that the server is rebuilding is never requested.
261-
if (transaction && shouldRenderLocalDistanceEReceipt(transaction)) {
258+
// New Expensify builds the distance e-receipt from the expense, which is why the server stores only the route
259+
// map as the thumbnail for it to draw around. The generated PDF beside it is for Expensify Classic, which
260+
// cannot build one in the frontend, and it prints the routed trip rather than what the expense bills. Showing
261+
// that PDF here made the enlarged receipt contradict every other surface, so draw the card instead. This runs
262+
// before the PDF branch below, which would otherwise return first.
263+
// See https://github.com/Expensify/Expensify/issues/545298 and https://github.com/Expensify/App/issues/97013.
264+
if (transaction && isMapBasedDistanceRequest(transaction)) {
262265
return <DistanceEReceipt transaction={transaction} />;
263266
}
264267

src/components/DistanceEReceipt.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
77

88
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
99
import {getTransactionDetails} from '@libs/ReportUtils';
10-
import {getWaypointIndex, hasReceipt, hasUsableStoredDistanceReceipt} from '@libs/TransactionUtils';
10+
import {getWaypointIndex, hasDistanceRouteErrors, hasPendingDistanceReceiptRegeneration, hasReceipt} from '@libs/TransactionUtils';
1111
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
1212

1313
import type {TranslationPaths} from '@src/languages/types';
@@ -41,9 +41,9 @@ function DistanceEReceipt({transaction, hoverPreview = false}: DistanceEReceiptP
4141
const {amount: transactionAmount, currency: transactionCurrency, merchant: transactionMerchant, created: transactionDate} = getTransactionDetails(transaction) ?? {};
4242
const formattedTransactionAmount = convertToDisplayString(transactionAmount, transactionCurrency);
4343
const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? '');
44-
// The thumbnail is a page of the stored receipt file, so this card must not show it once the rest of the app
45-
// has stopped trusting that file.
46-
const canShowStoredReceiptPage = hasUsableStoredDistanceReceipt(transaction);
44+
// The thumbnail this card draws is the stored route map. An edit that makes the server rebuild the receipt
45+
// invalidates its URL, and a route error means there is no trip to draw, so show the pending map for both.
46+
const canShowStoredRouteMap = !hasPendingDistanceReceiptRegeneration(transaction) && !hasDistanceRouteErrors(transaction);
4747
const waypoints = useMemo(() => transaction?.comment?.waypoints ?? {}, [transaction?.comment?.waypoints]);
4848
const sortedWaypoints = useMemo<WaypointCollection>(
4949
() =>
@@ -68,7 +68,7 @@ function DistanceEReceipt({transaction, hoverPreview = false}: DistanceEReceiptP
6868
/>
6969

7070
<View style={[styles.moneyRequestViewImage, styles.mh0, styles.mt0, styles.mb5, styles.borderNone]}>
71-
{!canShowStoredReceiptPage || !thumbnailSource ? (
71+
{!canShowStoredRouteMap || !thumbnailSource ? (
7272
<PendingMapView />
7373
) : (
7474
<ReceiptImage

src/components/ReportActionItem/MoneyRequestReceiptView.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ import {
5757
isDistanceRequest as isDistanceRequestTransactionUtils,
5858
isMapBasedDistanceRequest,
5959
isScanning,
60-
shouldRenderLocalDistanceEReceipt,
6160
} from '@libs/TransactionUtils';
6261
import ViolationsUtils, {filterReceiptViolations} from '@libs/Violations/ViolationsUtils';
6362

@@ -186,12 +185,13 @@ function MoneyRequestReceiptView({
186185
const displayedTransaction = updatedTransaction ?? transaction;
187186
const isDistanceRequest = isDistanceRequestTransactionUtils(displayedTransaction);
188187

188+
// The hover overlay shows the full distance e-receipt (map + amount + waypoints), so only surface it for
189+
// map/route-based distance expenses. Odometer and pure-manual distance expenses have no map and must be excluded.
189190
const isMapDistanceRequest = isMapBasedDistanceRequest(displayedTransaction);
190191
const isPendingReceiptRegeneration = hasPendingDistanceReceiptRegeneration(displayedTransaction);
191-
// The overlay draws the distance e-receipt card from the expense, therefore it must appear only where the
192-
// receipt box has no generated file to show. Over a generated file it would state a total and a mileage that
193-
// the file itself can contradict, which is the whole of https://github.com/Expensify/App/issues/97013.
194-
const canShowDistanceEReceipt = shouldRenderLocalDistanceEReceipt(displayedTransaction);
192+
// While the receipt is regenerating (e.g. after an offline waypoint edit) the stored map is stale and can't be
193+
// redrawn locally, so don't surface the e-receipt overlay — the receipt box already shows the pending map.
194+
const canShowDistanceEReceipt = isMapDistanceRequest && !isPendingReceiptRegeneration;
195195
// The Expand button opens the full-screen receipt on the stored map. While regeneration is pending that map is
196196
// stale and can't be redrawn locally, so disable Expand for map distance requests until the refreshed receipt arrives.
197197
const shouldDisableExpandReceipt = isMapDistanceRequest && isPendingReceiptRegeneration;

src/components/ReportActionItem/ReportActionItemImage.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ import {getReportIDForExpense} from '@libs/MergeTransactionUtils';
1414
import Navigation from '@libs/Navigation/Navigation';
1515
import {getThumbnailAndImageURIs} from '@libs/ReceiptUtils';
1616
import {
17-
hasDistanceRouteErrors,
1817
hasEReceipt,
1918
hasPendingDistanceReceiptRegeneration,
2019
hasReceiptSource,
2120
isDistanceRequest,
2221
isManualDistanceRequest,
22+
isMapBasedDistanceRequest,
2323
isPerDiemRequest,
2424
} from '@libs/TransactionUtils';
2525
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
@@ -138,9 +138,7 @@ function ReportActionItemImage({
138138
const icons = useMemoizedLazyExpensifyIcons(['Receipt']);
139139
const {report: contextReport, transactionThreadReport} = useShowContextMenuState();
140140
const isMapDistanceRequest = !!transaction && isDistanceRequest(transaction) && !isManualDistanceRequest(transaction);
141-
// Any error on the expense keeps the tile on the live route, not the route errors alone, because a tile that
142-
// shows a red dot next to a stale receipt reads as though the receipt itself failed.
143-
const hasErrors = !isEmptyObject(transaction?.errors) || hasDistanceRouteErrors(transaction);
141+
const hasErrors = !isEmptyObject(transaction?.errors) || !isEmptyObject(transaction?.errorFields?.route) || !isEmptyObject(transaction?.errorFields?.waypoints);
144142
// While the receipt is regenerating its stored URL is stale, so draw the live route from `routes.coordinates`
145143
// (via `ConfirmedRoute`) instead of loading the now-404'd image.
146144
const showMapAsImage = isMapDistanceRequest && (hasErrors || hasPendingDistanceReceiptRegeneration(transaction));
@@ -223,9 +221,10 @@ function ReportActionItemImage({
223221
// A remote PDF is shown as the server's low-resolution JPG thumbnail, which blurs when hover-zoomed.
224222
// Where zooming is available (web only), render the actual PDF on top of the thumbnail so the magnified
225223
// view stays sharp. The thumbnail stays underneath as an instant preview and as a fallback if the PDF fails.
226-
// A distance receipt is a PDF too, therefore it zooms the same way as any other receipt.
224+
// Map/route distance requests are excluded: their hover overlay is a DistanceEReceipt card, not the PDF.
225+
// isMapBasedDistanceRequest covers map, GPS, and manual-typed transactions that still carry waypoints.
227226
const pdfSourceURL = typeof originalImageSource === 'string' && !!originalImageSource ? originalImageSource : undefined;
228-
const isRemotePDF = !!isPDF && !effectiveIsLocalFile && !isEReceipt && !!pdfSourceURL;
227+
const isRemotePDF = !!isPDF && !effectiveIsLocalFile && !isEReceipt && !isMapBasedDistanceRequest(transaction) && !!pdfSourceURL;
229228
const shouldOverlayHighResPDF = canZoomReceipt && isRemotePDF && hasHoverSupport();
230229

231230
const renderReceiptContent = (receiptImage: React.ReactNode) =>

src/components/TransactionItemRow/ReceiptPreview/index.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import useResponsiveLayoutOnWideRHP from '@hooks/useResponsiveLayoutOnWideRHP';
99
import useThemeStyles from '@hooks/useThemeStyles';
1010
import useWindowDimensions from '@hooks/useWindowDimensions';
1111

12-
import {hasReceiptSource, isPerDiemRequest, shouldRenderLocalDistanceEReceipt} from '@libs/TransactionUtils';
12+
import {hasReceiptSource, isDistanceRequest, isManualDistanceRequest, isPerDiemRequest} from '@libs/TransactionUtils';
1313

1414
import variables from '@styles/variables';
1515

@@ -45,9 +45,7 @@ type ReceiptPreviewProps = {
4545
};
4646

4747
function ReceiptPreview({source, hovered, isEReceipt = false, transactionItem, anchorPosition}: ReceiptPreviewProps) {
48-
// A distance expense usually has a generated receipt file, and `source` already points at a page of it, so the
49-
// preview shows the file. The card is for the expenses that have no file to show.
50-
const isDistanceEReceipt = shouldRenderLocalDistanceEReceipt(transactionItem);
48+
const isDistanceEReceipt = isDistanceRequest(transactionItem) && !isManualDistanceRequest(transactionItem);
5149
const isPerDiemEReceipt = isPerDiemRequest(transactionItem) && !hasReceiptSource(transactionItem) && !!transactionItem.transactionID;
5250
const styles = useThemeStyles();
5351
const [eReceiptScaleFactor, setEReceiptScaleFactor] = useState(0);

src/libs/TransactionUtils/index.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,40 +1291,6 @@ function hasDistanceRouteErrors(transaction: OnyxInputOrEntry<Transaction>): boo
12911291
return !isEmptyObject(transaction?.errorFields?.route) || !isEmptyObject(transaction?.errorFields?.waypoints);
12921292
}
12931293

1294-
/**
1295-
* Whether the receipt file that the server generated for a distance expense can be shown. It cannot when there
1296-
* is no file, when the server is building a new one after an edit, or when the route failed.
1297-
*/
1298-
function hasUsableStoredDistanceReceipt(transaction: OnyxEntry<Transaction>): boolean {
1299-
return hasReceiptSource(transaction) && !hasPendingDistanceReceiptRegeneration(transaction) && !hasDistanceRouteErrors(transaction);
1300-
}
1301-
1302-
/**
1303-
* Whether a distance expense must render the local `DistanceEReceipt` card instead of the receipt file the
1304-
* server generated.
1305-
*
1306-
* The generated file records the routed trip, therefore its total and its "84.57 mi @ $0.725 / mi" line can
1307-
* differ from what the expense bills. Expensify Classic shows that file on every surface, and New Expensify
1308-
* must do the same. If one surface draws the card and another draws the file, the two contradict each other.
1309-
* The card is only the fallback for a receipt file we cannot show.
1310-
*/
1311-
function shouldRenderLocalDistanceEReceipt(transaction: OnyxEntry<Transaction>): boolean {
1312-
// Only map and GPS distance expenses get a generated map receipt. An odometer expense shows the photos of
1313-
// the odometer that the user took, and a manual distance expense has no map. Both keep their own handling.
1314-
if (!isDistanceRequest(transaction) || isOdometerDistanceRequest(transaction) || isManualDistanceRequest(transaction)) {
1315-
return false;
1316-
}
1317-
1318-
if (!hasUsableStoredDistanceReceipt(transaction)) {
1319-
return true;
1320-
}
1321-
1322-
// The server generates a distance receipt as a PDF that carries its own total and mileage. An older expense
1323-
// stored the bare map image instead, therefore the card must supply those two lines around it.
1324-
const receiptSource = typeof transaction?.receipt?.source === 'string' ? transaction.receipt.source : '';
1325-
return !Str.isPDF(transaction?.receipt?.filename ?? '') && !Str.isPDF(receiptSource);
1326-
}
1327-
13281294
/**
13291295
* Return the merchant field from the transaction, return the modifiedMerchant if present.
13301296
*/
@@ -3472,8 +3438,6 @@ export {
34723438
hasLocallyKnownDistance,
34733439
hasPendingDistanceReceiptRegeneration,
34743440
hasDistanceRouteErrors,
3475-
hasUsableStoredDistanceReceipt,
3476-
shouldRenderLocalDistanceEReceipt,
34773441
isExpensifyCardTransaction,
34783442
isManagedCardTransaction,
34793443
isDuplicate,

src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,8 @@ import {
3232
hasOdometerImageSource,
3333
hasReceipt,
3434
hasReceiptSource,
35-
hasUsableStoredDistanceReceipt,
3635
isOdometerDistanceRequest,
3736
isReceiptBeingScanned,
38-
shouldRenderLocalDistanceEReceipt,
3937
} from '@libs/TransactionUtils';
4038
import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot';
4139

@@ -294,9 +292,7 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre
294292
draftTransactionID,
295293
});
296294

297-
// Withhold Download only when the card on screen is not backed by a stored file. An older distance expense
298-
// also shows the card, but around the very image it would download, so that one stays available.
299-
const allowDownload = !isEReceipt && (!shouldRenderLocalDistanceEReceipt(transaction) || hasUsableStoredDistanceReceipt(transaction));
295+
const allowDownload = !isEReceipt;
300296

301297
const applyDurableReceipt = useCallback(
302298
(imageUri: string, filename: string, file: File, isSameReceipt?: boolean) => {

tests/unit/TransactionUtilsTest.ts

Lines changed: 11 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,57 +1266,28 @@ describe('TransactionUtils', () => {
12661266
});
12671267
});
12681268

1269-
describe('shouldRenderLocalDistanceEReceipt', () => {
1269+
describe('isMapBasedDistanceRequest', () => {
12701270
const UPDATE = CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE;
12711271
const PDF_RECEIPT = {source: 'https://www.expensify.com/receipts/w_abc123.pdf', filename: 'w_abc123.pdf'};
12721272

12731273
function generateMapDistanceTransaction(values: Partial<Transaction> = {}): Transaction {
12741274
return generateTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MAP, receipt: PDF_RECEIPT, ...values});
12751275
}
12761276

1277-
it('returns false for a map distance expense whose generated PDF receipt is current', () => {
1278-
expect(TransactionUtils.shouldRenderLocalDistanceEReceipt(generateMapDistanceTransaction())).toBe(false);
1279-
});
1280-
1281-
it('returns true when there is no receipt file yet', () => {
1282-
expect(TransactionUtils.shouldRenderLocalDistanceEReceipt(generateMapDistanceTransaction({receipt: undefined}))).toBe(true);
1283-
});
1284-
1285-
it('returns true while the server rebuilds the receipt after an edit', () => {
1286-
expect(TransactionUtils.shouldRenderLocalDistanceEReceipt(generateMapDistanceTransaction({pendingFields: {waypoints: UPDATE}}))).toBe(true);
1287-
expect(TransactionUtils.shouldRenderLocalDistanceEReceipt(generateMapDistanceTransaction({pendingFields: {merchant: UPDATE}}))).toBe(true);
1288-
});
1289-
1290-
it('returns true when the route failed', () => {
1291-
expect(TransactionUtils.shouldRenderLocalDistanceEReceipt(generateMapDistanceTransaction({errorFields: {route: {someError: 'No route found'}}}))).toBe(true);
1277+
// New Expensify draws its own distance e-receipt for these, so the generated PDF beside them is never shown.
1278+
it('is true for a map distance expense whichever receipt it stores', () => {
1279+
expect(TransactionUtils.isMapBasedDistanceRequest(generateMapDistanceTransaction())).toBe(true);
1280+
expect(TransactionUtils.isMapBasedDistanceRequest(generateMapDistanceTransaction({receipt: undefined}))).toBe(true);
1281+
expect(TransactionUtils.isMapBasedDistanceRequest(generateMapDistanceTransaction({pendingFields: {merchant: UPDATE}}))).toBe(true);
12921282
});
12931283

1294-
it('returns true for an older expense that stored the bare map image', () => {
1295-
const imageReceipt = {source: 'https://www.expensify.com/receipts/w_abc123.jpg', filename: 'w_abc123.jpg'};
1296-
expect(TransactionUtils.shouldRenderLocalDistanceEReceipt(generateMapDistanceTransaction({receipt: imageReceipt}))).toBe(true);
1297-
// That file is still the one the card draws, so it stays downloadable.
1298-
expect(TransactionUtils.hasUsableStoredDistanceReceipt(generateMapDistanceTransaction({receipt: imageReceipt}))).toBe(true);
1284+
it('is true for a GPS distance expense, which also has a route to draw', () => {
1285+
expect(TransactionUtils.isMapBasedDistanceRequest(generateMapDistanceTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS}))).toBe(true);
12991286
});
13001287

1301-
it('keeps showing the generated receipt when an unrelated error lands on the expense', () => {
1302-
expect(TransactionUtils.shouldRenderLocalDistanceEReceipt(generateMapDistanceTransaction({errors: {someError: 'Payment failed'}}))).toBe(false);
1303-
});
1304-
1305-
it('returns false for odometer, manual distance and non-distance expenses', () => {
1306-
expect(TransactionUtils.shouldRenderLocalDistanceEReceipt(generateMapDistanceTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER, receipt: undefined}))).toBe(
1307-
false,
1308-
);
1309-
expect(TransactionUtils.shouldRenderLocalDistanceEReceipt(generateMapDistanceTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_MANUAL, receipt: undefined}))).toBe(
1310-
false,
1311-
);
1312-
expect(TransactionUtils.shouldRenderLocalDistanceEReceipt(generateTransaction({receipt: undefined}))).toBe(false);
1313-
});
1314-
1315-
it('treats a GPS distance expense the same way, because it also gets a generated receipt', () => {
1316-
expect(TransactionUtils.shouldRenderLocalDistanceEReceipt(generateMapDistanceTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS}))).toBe(false);
1317-
expect(
1318-
TransactionUtils.shouldRenderLocalDistanceEReceipt(generateMapDistanceTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_GPS, pendingFields: {merchant: UPDATE}})),
1319-
).toBe(true);
1288+
it('is false for odometer and non-distance expenses, which keep their own receipt', () => {
1289+
expect(TransactionUtils.isMapBasedDistanceRequest(generateMapDistanceTransaction({iouRequestType: CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER}))).toBe(false);
1290+
expect(TransactionUtils.isMapBasedDistanceRequest(generateTransaction({receipt: undefined}))).toBe(false);
13201291
});
13211292
});
13221293

0 commit comments

Comments
 (0)