Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
e65729e
fix: preserve odometer image blob URLs during crop/rotate in edit mode
jakubkalinski0 Mar 19, 2026
c36f6f8
fix: detect expired odometer blob URLs and restart flow on web
jakubkalinski0 Mar 20, 2026
cb62a3e
fix: resolve receipt uri, name, and type fallbacks for odometer stitc…
jakubkalinski0 Mar 20, 2026
2a40b76
fix: use getMimeTypeFromUri for odometer image MIME type on native
jakubkalinski0 Mar 20, 2026
c370ae9
chore: prettier run
jakubkalinski0 Mar 20, 2026
b43f19b
fix: replace deprecated StyleSheet.absoluteFillObject with StyleSheet…
jakubkalinski0 Mar 20, 2026
a94988c
fix: add required reasonAttributes prop to ActivityIndicator in Money…
jakubkalinski0 Mar 20, 2026
544fd66
fix: add blob URL loss detection to odometer image step
jakubkalinski0 Mar 20, 2026
1badc9c
fix: import useIsFocused from @react-navigation/native instead of core
jakubkalinski0 Mar 20, 2026
a0854a9
Merge branch 'jakubkalinski0/Odometer_add_backup_transaction_for_edit…
jakubkalinski0 Mar 21, 2026
49b4d85
Merge branch 'jakubkalinski0/Odometer_add_backup_transaction_for_edit…
jakubkalinski0 Mar 27, 2026
e619e69
chore: prettier run
jakubkalinski0 Mar 27, 2026
faafb92
Merge branch 'main' into jakubkalinski0/Odometer_blob_url_loss_detection
jakubkalinski0 Apr 1, 2026
30705b8
fix: pass a required param to fix ts fail
jakubkalinski0 Apr 1, 2026
777ebbd
fix: remove unused import to fix ESLINT fail
jakubkalinski0 Apr 1, 2026
a59ed4c
fix: move transaction param to fix failing ESLINT
jakubkalinski0 Apr 1, 2026
39d5585
Merge branch 'main' into jakubkalinski0/Odometer_blob_url_loss_detection
jakubkalinski0 Apr 1, 2026
d288220
fix: pass transaction instead of transactionID to fit new signature
jakubkalinski0 Apr 1, 2026
0749e84
fix: clear stale odometer readings on blob URL failure redirect
jakubkalinski0 Apr 1, 2026
8c769ec
fix: pass iouType param to shouldUseTransactionDraft
jakubkalinski0 Apr 2, 2026
bb3326c
refactor: extract clearOdometerTransactionState to make code DRY, fix…
jakubkalinski0 Apr 2, 2026
21b8a04
refactor: fix misleading comments in blob URL restart hooks
jakubkalinski0 Apr 2, 2026
17f47d0
refactor: explicitly capture closure snapshot in useRestartOnOdometer…
jakubkalinski0 Apr 2, 2026
f33e4ed
refactor: stabilize draftTransactionIDs dep in stitch useEffect via l…
jakubkalinski0 Apr 2, 2026
d43fc6c
fix: avoid defaulting transactionID to empty string in clearOdometerT…
jakubkalinski0 Apr 2, 2026
bbbb7a3
fix: limit draft deletion in clearOdometerTransactionState to current…
jakubkalinski0 Apr 2, 2026
8d2ede7
fix: pass action and backToReport to navigateToStartMoneyRequestStep …
jakubkalinski0 Apr 2, 2026
c704795
Merge branch 'main' into jakubkalinski0/Odometer_blob_url_loss_detection
jakubkalinski0 Apr 7, 2026
4dfcd8d
Merge branch 'main' into jakubkalinski0/Odometer_blob_url_loss_detection
jakubkalinski0 Apr 8, 2026
17cd500
Merge branch 'main' into jakubkalinski0/Odometer_blob_url_loss_detection
jakubkalinski0 Apr 16, 2026
6fc1dc9
chore: prettier run
jakubkalinski0 Apr 16, 2026
36ee216
fix: remove redundant captured* aliases
jakubkalinski0 Apr 16, 2026
3aa2e60
fix: remove redundant requestType check
jakubkalinski0 Apr 16, 2026
2efd165
refactor: split useRestartOnOdometerImagesFailure hook into platform …
jakubkalinski0 Apr 16, 2026
b5aa700
fix: make useRestartOnOdometerImagesFailure hook compatible with reac…
jakubkalinski0 Apr 16, 2026
67a1cd4
Merge branch 'main' into jakubkalinski0/Odometer_blob_url_loss_detection
jakubkalinski0 Apr 16, 2026
d120e0e
Merge branch 'main' into jakubkalinski0/Odometer_blob_url_loss_detection
jakubkalinski0 Apr 17, 2026
dd85689
fix: requestType is always defined
jakubkalinski0 Apr 17, 2026
a348d1c
fix: remove backup transaction in clearOdometerTransactionState
jakubkalinski0 Apr 17, 2026
31f71a8
Merge branch 'main' into jakubkalinski0/Odometer_blob_url_loss_detection
jakubkalinski0 Apr 17, 2026
6cec2bd
chore: prettier run
jakubkalinski0 Apr 17, 2026
7779414
refactor: simplify OdometerReceiptStitcher and useRestartOnOdometerIm…
jakubkalinski0 Apr 17, 2026
7b7a390
fix: avoid defaulting transaction ID to empty string
jakubkalinski0 Apr 17, 2026
74c9a4b
Merge branch 'main' into jakubkalinski0/Odometer_blob_url_loss_detection
jakubkalinski0 Apr 20, 2026
93e7e69
refactor: move odometer related utils to a separate OdometerTransacti…
jakubkalinski0 Apr 20, 2026
8d3de76
Merge branch 'main' into jakubkalinski0/Odometer_blob_url_loss_detection
jakubkalinski0 Apr 20, 2026
9766414
refactor: remove isDraft param and rename clearOdometerTransactionSta…
jakubkalinski0 Apr 20, 2026
9a8f0fb
Merge branch 'main' into jakubkalinski0/Odometer_blob_url_loss_detection
jakubkalinski0 Apr 20, 2026
2400c8f
Merge branch 'main' into jakubkalinski0/Odometer_blob_url_loss_detection
jakubkalinski0 Apr 21, 2026
1d1a235
fix: prevent isEditingConfirmation cleanup from nullifying draft on b…
jakubkalinski0 Apr 21, 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
5 changes: 5 additions & 0 deletions src/hooks/useRestartOnOdometerImagesFailure/index.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// On native blob:// URLs don't exist, so there is nothing to check

const useRestartOnOdometerImagesFailure = () => {};

export default useRestartOnOdometerImagesFailure;
89 changes: 89 additions & 0 deletions src/hooks/useRestartOnOdometerImagesFailure/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {useEffect, useRef} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import useOnyx from '@hooks/useOnyx';
import {checkIfLocalFileIsAccessible} from '@libs/actions/IOU/Receipt';
import clearOdometerDraftTransactionState from '@libs/actions/OdometerTransactionUtils';
import {navigateToStartMoneyRequestStep} from '@libs/IOUUtils';
import {getOdometerImageUri} from '@libs/OdometerImageUtils';
import type {IOUType} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import {validTransactionDraftIDsSelector} from '@src/selectors/TransactionDraft';
import type {Transaction} from '@src/types/onyx';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';

// When the component mounts, if there are odometer images or a stitched receipt, see if the files can be read from the disk.
// If not, redirect the user to the starting step of the flow.
// This is because until the request is saved, the image files are only stored in the browser's memory as blob:// URLs
// and if the browser is refreshed, then the images cease to exist.
// The best way for the user to recover from this is to start over from the start of the request process.
const useRestartOnOdometerImagesFailure = (transaction: OnyxEntry<Transaction>, reportID: string, iouType: IOUType, backToReport: string | undefined, onBackupHandled?: () => void) => {
const [, draftTransactionsMetadata] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector});
const hasCheckedRef = useRef(false);

useEffect(() => {
if (!transaction || isLoadingOnyxValue(draftTransactionsMetadata)) {
return;
}

// Run only once after Onyx finishes loading — blob:// URLs are ephemeral and only need
// to be verified on the first render after the data is available.
// It has to be resolved this way in order to have a complete dependency array for the useEffect hook.
if (hasCheckedRef.current) {
return;
}
hasCheckedRef.current = true;

const startImage = transaction.comment?.odometerStartImage;
const endImage = transaction.comment?.odometerEndImage;
const stitchedUri = transaction.receipt?.source?.toString();

const urlsToCheck = [
{
filename: typeof startImage === 'object' ? startImage?.name : undefined,
path: getOdometerImageUri(startImage),
type: typeof startImage === 'object' ? startImage?.type : undefined,
},
{
filename: typeof endImage === 'object' ? endImage?.name : undefined,
path: getOdometerImageUri(endImage),
type: typeof endImage === 'object' ? endImage?.type : undefined,
},
{
filename: transaction.receipt?.filename,
path: stitchedUri,
type: undefined,
},
].filter(({path}) => !!path && path.startsWith('blob:'));

if (urlsToCheck.length === 0) {
return;
}

Promise.all(
urlsToCheck.map(
({filename, path, type}) =>
new Promise<boolean>((resolve) => {
checkIfLocalFileIsAccessible(
filename,
path,
type,
() => resolve(true),
() => resolve(false),
);
}),
),
).then((results) => {
const canBeRead = results.every(Boolean);
if (canBeRead) {
return;
}

onBackupHandled?.();
clearOdometerDraftTransactionState(transaction);
navigateToStartMoneyRequestStep(CONST.IOU.REQUEST_TYPE.DISTANCE_ODOMETER, iouType, transaction.transactionID, reportID, CONST.IOU.ACTION.CREATE, backToReport);
});
}, [draftTransactionsMetadata, transaction, iouType, reportID, backToReport, onBackupHandled]);
};

export default useRestartOnOdometerImagesFailure;
6 changes: 3 additions & 3 deletions src/hooks/useRestartOnReceiptFailure.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useEffect} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {checkIfScanFileCanBeRead, setMoneyRequestReceipt} from '@libs/actions/IOU/Receipt';
import {checkIfLocalFileIsAccessible, setMoneyRequestReceipt} from '@libs/actions/IOU/Receipt';
import {removeDraftTransactionsByIDs} from '@libs/actions/TransactionEdit';
import {isLocalFile as isLocalFileUtil} from '@libs/fileDownload/FileUtils';
import {navigateToStartMoneyRequestStep} from '@libs/IOUUtils';
Expand Down Expand Up @@ -40,7 +40,7 @@ const useRestartOnReceiptFailure = (transaction: OnyxEntry<Transaction>, reportI
setMoneyRequestReceipt(transaction.transactionID, '', '', true);
};

checkIfScanFileCanBeRead(itemReceiptFilename, itemReceiptPath, itemReceiptType, () => {}, onFailure)?.then(() => {
checkIfLocalFileIsAccessible(itemReceiptFilename, itemReceiptPath, itemReceiptType, () => {}, onFailure)?.then(() => {
const requestType = getRequestType(transaction);
if (isScanFilesCanBeRead || requestType !== CONST.IOU.REQUEST_TYPE.SCAN) {
return;
Expand All @@ -50,7 +50,7 @@ const useRestartOnReceiptFailure = (transaction: OnyxEntry<Transaction>, reportI
navigateToStartMoneyRequestStep(requestType, iouType, transaction.transactionID, reportID);
});

// We want this hook to run on mounting only
// We want this hook to run once after Onyx finishes loading the draft transactions
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [draftTransactionsMetadata]);
};
Expand Down
4 changes: 2 additions & 2 deletions src/libs/actions/IOU/Receipt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,7 @@ function navigateToStartStepIfScanFileCannotBeRead(
readFileAsync(receiptPath.toString(), receiptFilename, onSuccess, onFailure, receiptType);
}

function checkIfScanFileCanBeRead(
function checkIfLocalFileIsAccessible(
receiptFilename: string | undefined,
receiptPath: ReceiptSource | undefined,
receiptType: string | undefined,
Expand All @@ -352,4 +352,4 @@ function checkIfScanFileCanBeRead(
return readFileAsync(receiptPath.toString(), receiptFilename, onSuccess, onFailure, receiptType);
}

export {checkIfScanFileCanBeRead, detachReceipt, navigateToStartStepIfScanFileCannotBeRead, replaceReceipt, setMoneyRequestReceipt};
export {checkIfLocalFileIsAccessible, detachReceipt, navigateToStartStepIfScanFileCannotBeRead, replaceReceipt, setMoneyRequestReceipt};
79 changes: 1 addition & 78 deletions src/libs/actions/IOU/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import {buildNextStepNew, buildOptimisticNextStep} from '@libs/NextStepUtils';
import {roundToTwoDecimalPlaces} from '@libs/NumberUtils';
import * as NumberUtils from '@libs/NumberUtils';
import revokeOdometerImageUri from '@libs/OdometerImageUtils';
import {getManagerMcTestParticipant} from '@libs/OptionsListUtils';
import {getCustomUnitID} from '@libs/PerDiemRequestUtils';
import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber';
Expand Down Expand Up @@ -96,7 +95,7 @@
import {notifyNewAction} from '@userActions/Report';
import {mergeTransactionIdsHighlightOnSearchRoute, sanitizeWaypointsForAPI} from '@userActions/Transaction';
import {getRemoveDraftTransactionsByIDsData, removeDraftTransaction, removeDraftTransactionsByIDs} from '@userActions/TransactionEdit';
import type {IOUAction, IOUActionParams, OdometerImageType} from '@src/CONST';
import type {IOUAction, IOUActionParams} from '@src/CONST';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -113,7 +112,6 @@
import type {OnyxData} from '@src/types/onyx/Request';
import type {SearchDataTypes} from '@src/types/onyx/SearchResults';
import type {Comment, Receipt, ReceiptSource, SplitShares, TransactionChanges, TransactionCustomUnit, WaypointCollection} from '@src/types/onyx/Transaction';
import type {FileObject} from '@src/types/utils/Attachment';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type BasePolicyParams from './types/BasePolicyParams';
import type BaseTransactionParams from './types/BaseTransactionParams';
Expand Down Expand Up @@ -379,7 +377,7 @@
};

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

Check warning on line 380 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 @@ -431,7 +429,7 @@
};

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

Check warning on line 432 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 @@ -445,7 +443,7 @@
});

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

Check warning on line 446 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 @@ -454,7 +452,7 @@
});

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

Check warning on line 455 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 @@ -468,7 +466,7 @@
});

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

Check warning on line 469 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 @@ -481,7 +479,7 @@
});

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

Check warning on line 482 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 @@ -490,7 +488,7 @@
});

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

Check warning on line 491 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 @@ -500,7 +498,7 @@

let deprecatedUserAccountID = -1;
let deprecatedCurrentUserEmail = '';
Onyx.connect({

Check warning on line 501 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) => {
deprecatedCurrentUserEmail = value?.email ?? '';
Expand All @@ -509,7 +507,7 @@
});

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

Check warning on line 510 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?.[deprecatedUserAccountID] ?? undefined;
Expand All @@ -517,7 +515,7 @@
});

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

Check warning on line 518 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 @@ -1241,78 +1239,6 @@
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {comment: {customUnit: {quantity: distanceAsFloat, distanceUnit}}});
}

/**
* Set the odometer readings for a transaction
*/
function setMoneyRequestOdometerReading(transactionID: string, startReading: number, endReading: number, isDraft: boolean) {
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
comment: {
odometerStart: startReading,
odometerEnd: endReading,
},
});
}

/**
* Set odometer image for a transaction
* @param transaction - The transaction or transaction draft
* @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(
transaction: OnyxEntry<OnyxTypes.Transaction>,
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'
? file
: {
uri: file.uri ?? (typeof URL !== 'undefined' ? URL.createObjectURL(file as Blob) : undefined),
name: file.name,
type: file.type,
size: file.size,
};
const transactionID = transaction?.transactionID;
const existingImage = transaction?.comment?.[imageKey];
if (shouldRevokeOldImage) {
revokeOdometerImageUri(existingImage, normalizedFile);
}
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
comment: {
[imageKey]: normalizedFile,
},
});
}

/**
* Remove odometer image from a transaction
* @param transaction - The transaction or transaction draft
* @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(transaction: OnyxEntry<OnyxTypes.Transaction>, imageType: OdometerImageType, isDraft: boolean, shouldRevokeOldImage: boolean) {
if (!transaction?.transactionID) {
return;
}
const imageKey = imageType === CONST.IOU.ODOMETER_IMAGE_TYPE.START ? 'odometerStartImage' : 'odometerEndImage';
const existingImage = transaction?.comment?.[imageKey];
if (shouldRevokeOldImage) {
revokeOdometerImageUri(existingImage);
}
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`, {
comment: {
[imageKey]: null,
},
});
}

/**
* Set the distance rate of a transaction.
* Used when creating a new transaction or moving an existing one from Self DM
Expand Down Expand Up @@ -3885,9 +3811,6 @@
setMoneyRequestDescription,
setMoneyRequestDistance,
setMoneyRequestDistanceRate,
setMoneyRequestOdometerReading,
setMoneyRequestOdometerImage,
removeMoneyRequestOdometerImage,
setMoneyRequestMerchant,
setMoneyRequestParticipants,
setMoneyRequestParticipantsFromReport,
Expand Down
90 changes: 90 additions & 0 deletions src/libs/actions/OdometerTransactionUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import Onyx from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import revokeOdometerImageUri from '@libs/OdometerImageUtils';
import CONST from '@src/CONST';
import type {OdometerImageType} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Transaction} from '@src/types/onyx';
import type {FileObject} from '@src/types/utils/Attachment';
import {setMoneyRequestReceipt} from './IOU/Receipt';
import {removeBackupTransaction} from './TransactionEdit';

/**
* Set the odometer readings for a transaction
*/
function setMoneyRequestOdometerReading(transactionID: string, startReading: number | null, endReading: number | null, isDraft: boolean) {
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
comment: {
odometerStart: startReading,
odometerEnd: endReading,
},
});
}

/**
* Set odometer image for a transaction
* @param transaction - The transaction or transaction draft
* @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(transaction: OnyxEntry<Transaction>, 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'
? file
: {
uri: file.uri ?? (typeof URL !== 'undefined' ? URL.createObjectURL(file as Blob) : undefined),
name: file.name,
type: file.type,
size: file.size,
};
const transactionID = transaction?.transactionID;
const existingImage = transaction?.comment?.[imageKey];
if (shouldRevokeOldImage) {
revokeOdometerImageUri(existingImage, normalizedFile);
}
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
comment: {
[imageKey]: normalizedFile,
},
});
}

/**
* Remove odometer image from a transaction
* @param transaction - The transaction or transaction draft
* @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(transaction: OnyxEntry<Transaction>, imageType: OdometerImageType, isDraft: boolean, shouldRevokeOldImage: boolean) {
if (!transaction?.transactionID) {
return;
}
const imageKey = imageType === CONST.IOU.ODOMETER_IMAGE_TYPE.START ? 'odometerStartImage' : 'odometerEndImage';
const existingImage = transaction?.comment?.[imageKey];
if (shouldRevokeOldImage) {
revokeOdometerImageUri(existingImage);
}
Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`, {
comment: {
[imageKey]: null,
},
});
}

function clearOdometerDraftTransactionState(transaction: OnyxEntry<Transaction>): void {
if (!transaction) {
return;
}
setMoneyRequestReceipt(transaction.transactionID, '', '', true);
setMoneyRequestOdometerReading(transaction.transactionID, null, null, true);
removeMoneyRequestOdometerImage(transaction, CONST.IOU.ODOMETER_IMAGE_TYPE.START, true, true);
removeMoneyRequestOdometerImage(transaction, CONST.IOU.ODOMETER_IMAGE_TYPE.END, true, true);
removeBackupTransaction(transaction.transactionID);
}

export {setMoneyRequestOdometerReading, setMoneyRequestOdometerImage, removeMoneyRequestOdometerImage};
export default clearOdometerDraftTransactionState;
4 changes: 2 additions & 2 deletions src/libs/fileDownload/validateReceiptFile/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {checkIfScanFileCanBeRead} from '@libs/actions/IOU/Receipt';
import {checkIfLocalFileIsAccessible} from '@libs/actions/IOU/Receipt';
import type {ReceiptSource} from '@src/types/onyx/Transaction';

/**
Expand All @@ -12,7 +12,7 @@ function validateReceiptFile(
onSuccess: (file: File) => void,
onFailure: () => void,
): Promise<void | File> | undefined {
return checkIfScanFileCanBeRead(receiptFilename, receiptPath, receiptType, onSuccess, onFailure);
return checkIfLocalFileIsAccessible(receiptFilename, receiptPath, receiptType, onSuccess, onFailure);
}

export default validateReceiptFile;
8 changes: 6 additions & 2 deletions src/pages/iou/request/step/IOURequestStepConfirmation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import useOptimisticDraftTransactions from '@hooks/useOptimisticDraftTransaction
import usePolicyForTransaction from '@hooks/usePolicyForTransaction';
import usePrivateIsArchivedMap from '@hooks/usePrivateIsArchivedMap';
import useReportAttributes from '@hooks/useReportAttributes';
import useRestartOnOdometerImagesFailure from '@hooks/useRestartOnOdometerImagesFailure';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {isMobileSafari} from '@libs/Browser';
Expand Down Expand Up @@ -237,6 +238,7 @@ function IOURequestStepConfirmation({

const odometerStartImage = transaction?.comment?.odometerStartImage;
const odometerEndImage = transaction?.comment?.odometerEndImage;
useRestartOnOdometerImagesFailure(isOdometerDistanceRequest ? transaction : undefined, reportID, iouType, backToReport);

// Pre-insert Search is only useful for flows whose submit ends in handleNavigateAfterExpenseCreate
// (which navigates to Search). Flows that use dismissModalAndOpenReportInInboxTab (PAY,
Expand Down Expand Up @@ -513,10 +515,11 @@ function IOURequestStepConfirmation({
/>
<OdometerReceiptStitcher
isOdometerDistanceRequest={isOdometerDistanceRequest}
currentTransactionID={currentTransactionID}
odometerStartImage={odometerStartImage}
odometerEndImage={odometerEndImage}
action={action}
transaction={transaction}
reportID={reportID}
backToReport={backToReport}
iouType={iouType}
onStitchingChange={setIsStitchingReceipt}
onStitchError={setStitchError}
Expand All @@ -528,6 +531,7 @@ function IOURequestStepConfirmation({
initialTransactionID={initialTransactionID}
reportID={reportID}
action={action}
backToReport={backToReport}
report={report}
participants={participants}
draftTransactionIDs={draftTransactionIDs}
Expand Down
Loading
Loading