Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b2a5ddf
fix: restore odometer images when navigating back from confirmation page
jakubkalinski0 Mar 19, 2026
6d6cc38
fix: prevent auto-redirect to odometer tab after replacing image and …
jakubkalinski0 Mar 19, 2026
6ba3f4c
fix: clear stale odometer backup transaction on distance tab view focus
jakubkalinski0 Mar 19, 2026
afba5ed
fix: bypass AsyncStorage race condition when creating fresh odometer …
jakubkalinski0 Mar 19, 2026
213c255
fix: skip odometer image re-stitching when source images unchanged
jakubkalinski0 Mar 19, 2026
a72272b
Merge branch 'jakubkalinski0/Odometer_move_odometer_image_stitching_t…
jakubkalinski0 Mar 21, 2026
8f182c0
Merge branch 'jakubkalinski0/Odometer_move_odometer_image_stitching_t…
jakubkalinski0 Mar 23, 2026
0d4d3d1
Merge branch 'jakubkalinski0/Odometer_move_odometer_image_stitching_t…
jakubkalinski0 Mar 24, 2026
f7c0578
fix: replace deprecated absoluteFillObject with absoluteFill
jakubkalinski0 Mar 24, 2026
c8c6d73
Merge branch 'main' into jakubkalinski0/Odometer_add_backup_transacti…
jakubkalinski0 Mar 25, 2026
f5883ba
Merge branch 'main' into jakubkalinski0/Odometer_add_backup_transacti…
jakubkalinski0 Mar 25, 2026
5b2b817
fix: revoke odometer image blob URLs before navigating back from conf…
jakubkalinski0 Mar 25, 2026
528aab2
fix: use correct useIsFocused import to fix test failures
jakubkalinski0 Mar 25, 2026
7c5e330
refactor: extract odometer image helpers into OdometerImageUtils
jakubkalinski0 Mar 25, 2026
b78dcb8
fix: remove redundant backup cleanup
jakubkalinski0 Mar 25, 2026
d670c3f
refactor: replace manual uri cast with getOdometerImageUri util
jakubkalinski0 Mar 25, 2026
be47ca0
fix: don't revoke backup blob on odometer image rotate/crop
jakubkalinski0 Mar 25, 2026
98cd8cb
chore: add comment explaining empty deps in backup useEffect
jakubkalinski0 Mar 25, 2026
95aaada
Merge branch 'main' into jakubkalinski0/Odometer_add_backup_transacti…
jakubkalinski0 Mar 26, 2026
9b638b2
Merge branch 'main' into jakubkalinski0/Odometer_add_backup_transacti…
jakubkalinski0 Mar 26, 2026
0d1f94a
fix: move odometerStartImage/EndImage declarations inside useEffect
jakubkalinski0 Mar 26, 2026
38d405a
Merge branch 'main' into jakubkalinski0/Odometer_add_backup_transacti…
jakubkalinski0 Mar 27, 2026
f929b17
fix: restore null check for file in image handleImageSelected
jakubkalinski0 Mar 27, 2026
a677660
refactor: make shouldRevokeOldImage a required param
jakubkalinski0 Mar 27, 2026
6e3dd3b
Merge branch 'main' into jakubkalinski0/Odometer_add_backup_transacti…
jakubkalinski0 Mar 27, 2026
b25b8db
fix: floor crop rect coords to prevent IEEE 754 float precision overf…
jakubkalinski0 Mar 30, 2026
6dff585
Merge branch 'main' into jakubkalinski0/Odometer_add_backup_transacti…
jakubkalinski0 Mar 30, 2026
7d41773
Merge branch 'main' into jakubkalinski0/Odometer_add_backup_transacti…
jakubkalinski0 Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/libs/OdometerImageUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type {FileObject} from '@src/types/utils/Attachment';
import {getMimeTypeFromUri} from './fileDownload/FileUtils';

function getOdometerImageUri(image: FileObject | string | null | undefined): string {
return typeof image === 'string' ? image : (image?.uri ?? '');
}

function getOdometerImageName(image: FileObject | string | null | undefined): string {
return typeof image === 'string' ? (image.split('/').pop() ?? '') : (image?.name ?? '');
}

function getOdometerImageType(image: FileObject | string | null | undefined): string | undefined {
return typeof image === 'string' ? getMimeTypeFromUri(image) : (image?.type ?? getMimeTypeFromUri(image?.uri ?? ''));
}

/**
* Revokes a blob URL previously associated with an odometer image, but only when
* the image has actually changed (i.e. the old URL differs from the new one).
*
* Skips revocation when:
* - The `URL` API is not available (non-browser environments / native)
* - The URI is not a blob: URL (e.g. file:// on native, https:// for uploaded images)
* - The old and new URIs are identical (image was not replaced)
*/
function revokeOdometerImageUri(image: FileObject | string | null | undefined, nextImage?: FileObject | string | null): void {
if (typeof URL === 'undefined') {
return;
}

const currentUri = getOdometerImageUri(image);
if (!currentUri?.startsWith('blob:')) {
return;
}
const nextUri = getOdometerImageUri(nextImage);
if (currentUri === nextUri) {
return;
}
URL.revokeObjectURL(currentUri);
}

export {getOdometerImageUri, getOdometerImageName, getOdometerImageType};
export default revokeOdometerImageUri;
31 changes: 11 additions & 20 deletions src/libs/actions/IOU/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
import {buildNextStepNew, buildOptimisticNextStep} from '@libs/NextStepUtils';
import {roundToTwoDecimalPlaces} from '@libs/NumberUtils';
import * as NumberUtils from '@libs/NumberUtils';
import revokeOdometerImageUri from '@libs/OdometerImageUtils';
import {getManagerMcTestParticipant, getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils';
import Parser from '@libs/Parser';
import {getCustomUnitID} from '@libs/PerDiemRequestUtils';
Expand Down Expand Up @@ -790,7 +791,7 @@
};

let allPersonalDetails: OnyxTypes.PersonalDetailsList = {};
Onyx.connect({

Check warning on line 794 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
allPersonalDetails = value ?? {};
Expand Down Expand Up @@ -923,7 +924,7 @@
};

let allTransactions: NonNullable<OnyxCollection<OnyxTypes.Transaction>> = {};
Onyx.connect({

Check warning on line 927 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -937,7 +938,7 @@
});

let allTransactionDrafts: NonNullable<OnyxCollection<OnyxTypes.Transaction>> = {};
Onyx.connect({

Check warning on line 941 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_DRAFT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -946,7 +947,7 @@
});

let allTransactionViolations: NonNullable<OnyxCollection<OnyxTypes.TransactionViolations>> = {};
Onyx.connect({

Check warning on line 950 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -960,7 +961,7 @@
});

let allPolicyTags: OnyxCollection<OnyxTypes.PolicyTagLists> = {};
Onyx.connect({

Check warning on line 964 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY_TAGS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -973,7 +974,7 @@
});

let allReports: OnyxCollection<OnyxTypes.Report>;
Onyx.connect({

Check warning on line 977 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -982,7 +983,7 @@
});

let allReportNameValuePairs: OnyxCollection<OnyxTypes.ReportNameValuePairs>;
Onyx.connect({

Check warning on line 986 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -992,7 +993,7 @@

let userAccountID = -1;
let currentUserEmail = '';
Onyx.connect({

Check warning on line 996 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
currentUserEmail = value?.email ?? '';
Expand All @@ -1001,7 +1002,7 @@
});

let deprecatedCurrentUserPersonalDetails: OnyxEntry<OnyxTypes.PersonalDetails>;
Onyx.connect({

Check warning on line 1005 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
deprecatedCurrentUserPersonalDetails = value?.[userAccountID] ?? undefined;
Expand All @@ -1009,7 +1010,7 @@
});

let allReportActions: OnyxCollection<OnyxTypes.ReportActions>;
Onyx.connect({

Check warning on line 1013 in src/libs/actions/IOU/index.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand Down Expand Up @@ -1700,30 +1701,15 @@
});
}

function revokeOdometerImageUri(image: FileObject | string | null | undefined, nextImage?: FileObject | string | null): void {
if (typeof URL === 'undefined') {
return;
}

const currentUri = typeof image === 'string' ? image : image?.uri;
if (!currentUri?.startsWith('blob:')) {
return;
}
const nextUri = typeof nextImage === 'string' ? nextImage : nextImage?.uri;
if (currentUri === nextUri) {
return;
}
URL.revokeObjectURL(currentUri);
}

/**
* Set odometer image for a transaction
* @param transactionID - The transaction ID
* @param imageType - 'start' or 'end'
* @param file - The image file (File object on web, URI string on native)
* @param isDraft - Whether this is a draft transaction
* @param shouldRevokeOldImage - Whether to revoke the previous blob URL immediately (always false on native where blob URLs don't exist; false on web when a backup transaction exists making the caller responsible for revoking)
*/
function setMoneyRequestOdometerImage(transactionID: string, imageType: OdometerImageType, file: FileObject | string, isDraft: boolean) {
function setMoneyRequestOdometerImage(transactionID: string, imageType: OdometerImageType, file: FileObject | string, isDraft: boolean, shouldRevokeOldImage: boolean) {
const imageKey = imageType === CONST.IOU.ODOMETER_IMAGE_TYPE.START ? 'odometerStartImage' : 'odometerEndImage';
const normalizedFile: FileObject | string =
typeof file === 'string'
Expand All @@ -1736,7 +1722,9 @@
};
const transaction = isDraft ? allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`] : allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
const existingImage = transaction?.comment?.[imageKey];
revokeOdometerImageUri(existingImage, normalizedFile);
if (shouldRevokeOldImage) {
revokeOdometerImageUri(existingImage, normalizedFile);
}
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
comment: {
[imageKey]: normalizedFile,
Expand All @@ -1749,12 +1737,15 @@
* @param transactionID - The transaction ID
* @param imageType - 'start' or 'end'
* @param isDraft - Whether this is a draft transaction
* @param shouldRevokeOldImage - Whether to revoke the previous blob URL immediately (always false on native where blob URLs don't exist; false on web when a backup transaction exists making the caller responsible for revoking)
*/
function removeMoneyRequestOdometerImage(transactionID: string, imageType: OdometerImageType, isDraft: boolean) {
function removeMoneyRequestOdometerImage(transactionID: string, imageType: OdometerImageType, isDraft: boolean, shouldRevokeOldImage: boolean) {
const imageKey = imageType === CONST.IOU.ODOMETER_IMAGE_TYPE.START ? 'odometerStartImage' : 'odometerEndImage';
const transaction = isDraft ? allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`] : allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
const existingImage = transaction?.comment?.[imageKey];
revokeOdometerImageUri(existingImage);
if (shouldRevokeOldImage) {
revokeOdometerImageUri(existingImage);
}
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
comment: {
[imageKey]: null,
Expand Down
59 changes: 58 additions & 1 deletion src/libs/actions/TransactionEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {format} from 'date-fns';
import Onyx from 'react-native-onyx';
import type {Connection, OnyxEntry} from 'react-native-onyx';
import {formatCurrentUserToAttendee} from '@libs/IOUUtils';
import revokeOdometerImageUri from '@libs/OdometerImageUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {PersonalDetails, Transaction} from '@src/types/onyx';
Expand All @@ -12,7 +13,7 @@ let connection: Connection;
/**
* Makes a backup copy of a transaction object that can be restored when the user cancels editing a transaction.
*/
function createBackupTransaction(transaction: OnyxEntry<Transaction>, isDraft: boolean) {
function createBackupTransaction(transaction: OnyxEntry<Transaction>, isDraft: boolean, shouldAlwaysCreateFreshBackup = false) {
if (!transaction) {
return;
}
Expand All @@ -24,6 +25,15 @@ function createBackupTransaction(transaction: OnyxEntry<Transaction>, isDraft: b
const newTransaction = {
...transaction,
};

// When shouldAlwaysCreateFreshBackup is true, skip reading the existing backup entirely and directly overwrite it.
// This avoids a race condition where connectWithoutView would fall back to reading from AsyncStorage (which may still
// contain a stale backup from a previous session even if the cache entry was dropped), restoring corrupted data.
if (shouldAlwaysCreateFreshBackup) {
Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transaction.transactionID}`, newTransaction);
return;
}

// We need to read the old transaction backup first before writing a new one, otherwise we might overwrite an existing backup. It does not update impact UI rendering since this function is called on page mount.
const conn = Onyx.connectWithoutView({
key: `${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transaction.transactionID}`,
Expand Down Expand Up @@ -177,10 +187,57 @@ function buildOptimisticTransactionAndCreateDraft({initialTransaction, currentUs
return newTransaction;
}

function removeBackupTransactionWithImageCleanup(transactionID: string | undefined, isDraft: boolean, onComplete?: () => void) {
if (!transactionID) {
return;
}
const backupConn = Onyx.connectWithoutView({
key: `${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transactionID}`,
callback: (backupTransaction) => {
Onyx.disconnect(backupConn);
const currentConn = Onyx.connectWithoutView({
key: `${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
callback: (currentTransaction) => {
Onyx.disconnect(currentConn);
revokeOdometerImageUri(backupTransaction?.comment?.odometerStartImage, currentTransaction?.comment?.odometerStartImage);
revokeOdometerImageUri(backupTransaction?.comment?.odometerEndImage, currentTransaction?.comment?.odometerEndImage);
removeBackupTransaction(transactionID);
onComplete?.();
},
});
},
});
}

function restoreOriginalTransactionFromBackupWithImageCleanup(transactionID: string | undefined, isDraft: boolean, onComplete?: () => void) {
if (!transactionID) {
return;
}
connection = Onyx.connectWithoutView({
key: `${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${transactionID}`,
callback: (backupTransaction) => {
Onyx.disconnect(connection);
const currentConn = Onyx.connectWithoutView({
key: `${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
callback: (currentTransaction) => {
Onyx.disconnect(currentConn);
revokeOdometerImageUri(currentTransaction?.comment?.odometerStartImage, backupTransaction?.comment?.odometerStartImage);
revokeOdometerImageUri(currentTransaction?.comment?.odometerEndImage, backupTransaction?.comment?.odometerEndImage);
Onyx.set(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, backupTransaction ?? null);
removeBackupTransaction(transactionID);
onComplete?.();
},
});
},
});
}

export {
createBackupTransaction,
removeBackupTransaction,
removeBackupTransactionWithImageCleanup,
restoreOriginalTransactionFromBackup,
restoreOriginalTransactionFromBackupWithImageCleanup,
createDraftTransaction,
removeDraftTransaction,
removeTransactionReceipt,
Expand Down
5 changes: 3 additions & 2 deletions src/libs/stitchOdometerImages/index.native.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import {ImageFormat, Skia} from '@shopify/react-native-skia';
import RNFS from 'react-native-fs';
import Log from '@libs/Log';
import {getOdometerImageUri} from '@libs/OdometerImageUtils';
import type {FileObject} from '@src/types/utils/Attachment';
import STITCHED_ODOMETER_FILENAME_PREFIX from './constants';
import calculateStitchLayout from './stitchLayout';

async function stitchOdometerImages(image1: FileObject | string | undefined, image2: FileObject | string | undefined): Promise<FileObject | null> {
const source1 = typeof image1 === 'string' ? image1 : (image1?.uri ?? null);
const source2 = typeof image2 === 'string' ? image2 : (image2?.uri ?? null);
const source1 = getOdometerImageUri(image1);
const source2 = getOdometerImageUri(image2);

if (!source1 || !source2) {
return null;
Expand Down
5 changes: 3 additions & 2 deletions src/libs/stitchOdometerImages/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {getOdometerImageUri} from '@libs/OdometerImageUtils';
import type {FileObject} from '@src/types/utils/Attachment';
import STITCHED_ODOMETER_FILENAME_PREFIX from './constants';
import calculateStitchLayout from './stitchLayout';
Expand All @@ -6,8 +7,8 @@ import calculateStitchLayout from './stitchLayout';
let previousBlobUrl: string | null = null;

function stitchOdometerImages(image1: FileObject | string | undefined, image2: FileObject | string | undefined): Promise<FileObject | null> {
const source1 = typeof image1 === 'string' ? image1 : (image1?.uri ?? null);
const source2 = typeof image2 === 'string' ? image2 : (image2?.uri ?? null);
const source1 = getOdometerImageUri(image1);
const source2 = getOdometerImageUri(image2);

if (!source1 || !source2) {
return Promise.resolve(null);
Expand Down
50 changes: 36 additions & 14 deletions src/pages/iou/request/step/IOURequestStepConfirmation.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {useIsFocused} from '@react-navigation/native';
import {hasSeenTourSelector} from '@selectors/Onboarding';
import {validTransactionDraftIDsSelector} from '@selectors/TransactionDraft';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
Expand Down Expand Up @@ -39,7 +40,7 @@ import {getCurrencySymbol} from '@libs/CurrencyUtils';
import DateUtils from '@libs/DateUtils';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import DistanceRequestUtils from '@libs/DistanceRequestUtils';
import {getMimeTypeFromUri, isLocalFile as isLocalFileFileUtils} from '@libs/fileDownload/FileUtils';
import {isLocalFile as isLocalFileFileUtils} from '@libs/fileDownload/FileUtils';
import validateReceiptFile from '@libs/fileDownload/validateReceiptFile';
import getCurrentPosition from '@libs/getCurrentPosition';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
Expand All @@ -55,6 +56,7 @@ import Log from '@libs/Log';
import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction';
import Navigation from '@libs/Navigation/Navigation';
import {rand64, roundToTwoDecimalPlaces} from '@libs/NumberUtils';
import {getOdometerImageName, getOdometerImageType, getOdometerImageUri} from '@libs/OdometerImageUtils';
import {getParticipantsOption, getReportOption} from '@libs/OptionsListUtils';
import {isPaidGroupPolicy} from '@libs/PolicyUtils';
import {
Expand Down Expand Up @@ -289,6 +291,7 @@ function IOURequestStepConfirmation({
const isDistanceRequest = isDistanceRequestTransactionUtils(transaction);
const isManualDistanceRequest = isManualDistanceRequestTransactionUtils(transaction);
const isOdometerDistanceRequest = isOdometerDistanceRequestTransactionUtils(transaction);
const isFocused = useIsFocused();
const isGPSDistanceRequest = isGPSDistanceRequestTransactionUtils(transaction);
const transactionDistance = isManualDistanceRequest || isOdometerDistanceRequest || isGPSDistanceRequest ? (transaction?.comment?.customUnit?.quantity ?? undefined) : undefined;
const isTimeRequest = requestType === CONST.IOU.REQUEST_TYPE.TIME;
Expand Down Expand Up @@ -320,6 +323,10 @@ function IOURequestStepConfirmation({
const [isConfirming, setIsConfirming] = useState(false);
const [isStitchingReceipt, setIsStitchingReceipt] = useState(false);
const [stitchError, setStitchError] = useState('');
const lastStitchedImages = useRef<{
startImage: FileObject | string | undefined;
endImage: FileObject | string | undefined;
} | null>(null);

const headerTitle = useMemo(() => {
if (isCategorizingTrackExpense) {
Expand Down Expand Up @@ -427,18 +434,25 @@ function IOURequestStepConfirmation({
}
}, [isOffline, policy?.pendingAction, policyExpenseChatPolicyID, senderPolicyID]);

const odometerStartImage = transaction?.comment?.odometerStartImage;
const odometerEndImage = transaction?.comment?.odometerEndImage;

useEffect(() => {
if (!isOdometerDistanceRequest) {
if (!isOdometerDistanceRequest || !isFocused) {
return;
}

const getImageUri = (img: FileObject | string | null | undefined): string => (typeof img === 'string' ? img : (img?.uri ?? ''));
const getImageName = (img: FileObject | string | null | undefined): string => (typeof img === 'string' ? (img.split('/').pop() ?? '') : (img?.name ?? ''));
const getImageType = (img: FileObject | string | null | undefined): string | undefined =>
typeof img === 'string' ? getMimeTypeFromUri(img) : (img?.type ?? getMimeTypeFromUri(img?.uri ?? ''));
const odometerStartImage = transaction?.comment?.odometerStartImage;
const odometerEndImage = transaction?.comment?.odometerEndImage;

// Skip stitching when source images haven't changed (compare by URI not reference
// because Onyx may create new object instances when restoring a backup transaction)
const startUri = getOdometerImageUri(odometerStartImage);
const endUri = getOdometerImageUri(odometerEndImage);
if (
lastStitchedImages.current !== null &&
getOdometerImageUri(lastStitchedImages.current.startImage) === startUri &&
getOdometerImageUri(lastStitchedImages.current.endImage) === endUri
) {
return;
}

if (!odometerStartImage || !odometerEndImage) {
const singleImage = odometerStartImage ?? odometerEndImage;
Expand All @@ -447,7 +461,14 @@ function IOURequestStepConfirmation({
return;
}

setMoneyRequestReceipt(currentTransactionID, getImageUri(singleImage), getImageName(singleImage), shouldUseTransactionDraft(action, iouType), getImageType(singleImage));
setMoneyRequestReceipt(
currentTransactionID,
getOdometerImageUri(singleImage),
getOdometerImageName(singleImage),
shouldUseTransactionDraft(action, iouType),
getOdometerImageType(singleImage),
);
lastStitchedImages.current = {startImage: odometerStartImage, endImage: odometerEndImage};
return;
}

Expand All @@ -462,11 +483,12 @@ function IOURequestStepConfirmation({
}
setMoneyRequestReceipt(
currentTransactionID,
getImageUri(stitchedImage),
getImageName(stitchedImage),
getOdometerImageUri(stitchedImage),
getOdometerImageName(stitchedImage),
shouldUseTransactionDraft(action, iouType),
getImageType(stitchedImage),
getOdometerImageType(stitchedImage),
);
lastStitchedImages.current = {startImage: odometerStartImage, endImage: odometerEndImage};
})
.catch((error: unknown) => {
if (ignore) {
Expand All @@ -485,7 +507,7 @@ function IOURequestStepConfirmation({
return () => {
ignore = true;
};
}, [isOdometerDistanceRequest, currentTransactionID, odometerStartImage, odometerEndImage, action, translate, iouType]);
}, [isOdometerDistanceRequest, isFocused, currentTransactionID, transaction?.comment?.odometerStartImage, transaction?.comment?.odometerEndImage, action, translate, iouType]);

const defaultBillable = !!policy?.defaultBillable;
useEffect(() => {
Expand Down
Loading
Loading