diff --git a/src/libs/OdometerImageUtils.ts b/src/libs/OdometerImageUtils.ts new file mode 100644 index 000000000000..940036384f22 --- /dev/null +++ b/src/libs/OdometerImageUtils.ts @@ -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; diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 54b2845de348..82dab9203697 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -69,6 +69,7 @@ import {isOffline} from '@libs/Network/NetworkStore'; 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'; @@ -1700,30 +1701,15 @@ function setMoneyRequestOdometerReading(transactionID: string, startReading: num }); } -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' @@ -1736,7 +1722,9 @@ function setMoneyRequestOdometerImage(transactionID: string, imageType: Odometer }; 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, @@ -1749,12 +1737,15 @@ function setMoneyRequestOdometerImage(transactionID: string, imageType: Odometer * @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, diff --git a/src/libs/actions/TransactionEdit.ts b/src/libs/actions/TransactionEdit.ts index b80405b6fa60..bd24c03731e7 100644 --- a/src/libs/actions/TransactionEdit.ts +++ b/src/libs/actions/TransactionEdit.ts @@ -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'; @@ -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, isDraft: boolean) { +function createBackupTransaction(transaction: OnyxEntry, isDraft: boolean, shouldAlwaysCreateFreshBackup = false) { if (!transaction) { return; } @@ -24,6 +25,15 @@ function createBackupTransaction(transaction: OnyxEntry, 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}`, @@ -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, diff --git a/src/libs/stitchOdometerImages/index.native.ts b/src/libs/stitchOdometerImages/index.native.ts index 2374c276c43c..0e54df6f91ad 100644 --- a/src/libs/stitchOdometerImages/index.native.ts +++ b/src/libs/stitchOdometerImages/index.native.ts @@ -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 { - 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; diff --git a/src/libs/stitchOdometerImages/index.ts b/src/libs/stitchOdometerImages/index.ts index 51bfac1c8a1d..27885c8ff993 100644 --- a/src/libs/stitchOdometerImages/index.ts +++ b/src/libs/stitchOdometerImages/index.ts @@ -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'; @@ -6,8 +7,8 @@ import calculateStitchLayout from './stitchLayout'; let previousBlobUrl: string | null = null; function stitchOdometerImages(image1: FileObject | string | undefined, image2: FileObject | string | undefined): Promise { - 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); diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index 2f29a2b92017..a722d729b087 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -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'; @@ -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'; @@ -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 { @@ -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; @@ -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) { @@ -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; @@ -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; } @@ -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) { @@ -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(() => { diff --git a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx index 25e7e6d24779..8493e5f331c1 100644 --- a/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistanceOdometer.tsx @@ -28,6 +28,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {setMoneyRequestDistance, setMoneyRequestOdometerReading, updateMoneyRequestDistance} from '@libs/actions/IOU'; import {handleMoneyRequestStepDistanceNavigation} from '@libs/actions/IOU/MoneyRequest'; import {setDraftSplitTransaction} from '@libs/actions/IOU/Split'; +import {createBackupTransaction, removeBackupTransactionWithImageCleanup, restoreOriginalTransactionFromBackupWithImageCleanup} from '@libs/actions/TransactionEdit'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import {shouldUseTransactionDraft} from '@libs/IOUUtils'; @@ -93,6 +94,8 @@ function IOURequestStepDistanceOdometer({ const initialStartImageRef = useRef(undefined); const initialEndImageRef = useRef(undefined); const prevSelectedTabRef = useRef(undefined); + const transactionWasSaved = useRef(false); + const backupHandledManually = useRef(false); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`); const isArchived = isArchivedReport(reportNameValuePairs); @@ -232,6 +235,26 @@ function IOURequestStepDistanceOdometer({ } }, [currentTransaction?.comment?.odometerStart, currentTransaction?.comment?.odometerEnd, isEditing]); + useEffect(() => { + if (!isEditingConfirmation) { + return () => {}; + } + createBackupTransaction(transaction, isTransactionDraft, true); + + return () => { + if (backupHandledManually.current) { + return; + } + if (transactionWasSaved.current) { + removeBackupTransactionWithImageCleanup(transactionID, isTransactionDraft); + return; + } + restoreOriginalTransactionFromBackupWithImageCleanup(transactionID, isTransactionDraft); + }; + // We only want to create the backup once on mount and restore/remove it on unmount + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + // Calculate total distance - updated live after every input change const totalDistance = (() => { const start = parseFloat(DistanceRequestUtils.normalizeOdometerText(startReading, fromLocaleDigit)); @@ -349,11 +372,14 @@ function IOURequestStepDistanceOdometer({ const navigateBack = useCallback(() => { if (isEditingConfirmation) { - Navigation.goBack(confirmationRoute); + backupHandledManually.current = true; + restoreOriginalTransactionFromBackupWithImageCleanup(transactionID, isTransactionDraft, () => { + Navigation.goBack(confirmationRoute); + }); return; } Navigation.goBack(); - }, [isEditingConfirmation, confirmationRoute]); + }, [isEditingConfirmation, confirmationRoute, transactionID, isTransactionDraft]); const handlePressStartImage = useCallback(() => { if (odometerStartImage) { @@ -431,6 +457,7 @@ function IOURequestStepDistanceOdometer({ } if (isEditingConfirmation) { + transactionWasSaved.current = true; Navigation.goBack(confirmationRoute); return; } diff --git a/src/pages/iou/request/step/IOURequestStepOdometerImage/index.native.tsx b/src/pages/iou/request/step/IOURequestStepOdometerImage/index.native.tsx index d0c9a512fb85..2aacf5236ec4 100644 --- a/src/pages/iou/request/step/IOURequestStepOdometerImage/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepOdometerImage/index.native.tsx @@ -31,6 +31,7 @@ import getReceiptsUploadFolderPath from '@libs/getReceiptsUploadFolderPath'; import {shouldUseTransactionDraft} from '@libs/IOUUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; +import {getOdometerImageUri} from '@libs/OdometerImageUtils'; import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan'; import CameraPermission from '@pages/iou/request/step/IOURequestStepScan/CameraPermission'; import NavigationAwareCamera from '@pages/iou/request/step/IOURequestStepScan/components/NavigationAwareCamera/Camera'; @@ -186,7 +187,7 @@ function IOURequestStepOdometerImage({ if (!file) { return; } - setMoneyRequestOdometerImage(transactionID, imageType, file, isTransactionDraft); + setMoneyRequestOdometerImage(transactionID, imageType, getOdometerImageUri(file), isTransactionDraft, false); navigateBack(); }; @@ -250,6 +251,7 @@ function IOURequestStepOdometerImage({ size: (file as FileObject | undefined)?.size, }, isTransactionDraft, + false, ); navigateBack(); }) @@ -363,7 +365,7 @@ function IOURequestStepOdometerImage({ diff --git a/src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx b/src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx index 3c5d9a94e4ec..d41fd3bd45e1 100644 --- a/src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx @@ -88,7 +88,7 @@ function IOURequestStepOdometerImage({ }; const handleImageSelected = (file: FileObject) => { - setMoneyRequestOdometerImage(transactionID, imageType, file as File, isTransactionDraft); + setMoneyRequestOdometerImage(transactionID, imageType, file as File, isTransactionDraft, isEditingConfirmation !== 'true'); shouldRevokeOnUnmountRef.current = false; navigateBack(); }; @@ -230,7 +230,7 @@ function IOURequestStepOdometerImage({ if (source !== imageObject.source) { URL.revokeObjectURL(imageObject.source); } - setMoneyRequestOdometerImage(transactionID, imageType, file ?? source, isTransactionDraft); + setMoneyRequestOdometerImage(transactionID, imageType, file ?? source, isTransactionDraft, isEditingConfirmation !== 'true'); navigateBack(); }) .catch((error: unknown) => { diff --git a/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx b/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx index 94e73084d48c..1807835cd4ac 100644 --- a/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx +++ b/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx @@ -266,7 +266,7 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre if (!transaction?.transactionID || !imageType) { return; } - removeMoneyRequestOdometerImage(transaction.transactionID, imageType, isDraftTransaction); + removeMoneyRequestOdometerImage(transaction.transactionID, imageType, isDraftTransaction, !isEditingConfirmation); const odometerGoBackRoute = isOdometerImage && (isEditingConfirmation === true @@ -309,7 +309,7 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre const rotatedFilename = file.name ?? receiptFilename; if (isOdometerImage) { - setMoneyRequestOdometerImage(transaction.transactionID, imageType, file, isDraftTransaction); + setMoneyRequestOdometerImage(transaction.transactionID, imageType, file, isDraftTransaction, !isEditingConfirmation); } else if (isDraftTransaction) { setMoneyRequestReceipt(transaction.transactionID, imageUriResult, rotatedFilename, isDraftTransaction, fileType); } else { @@ -328,7 +328,21 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre .catch(() => { setIsRotating(false); }); - }, [transaction?.transactionID, isDraftTransaction, isOdometerImage, imageType, sourceUri, isImage, receiptFilename, fileName, fileType, policyCategories, transaction?.receipt, policy]); + }, [ + transaction?.transactionID, + isDraftTransaction, + isOdometerImage, + imageType, + sourceUri, + isImage, + receiptFilename, + fileName, + fileType, + policyCategories, + transaction?.receipt, + policy, + isEditingConfirmation, + ]); const shouldShowRotateAndCropReceiptButton = useMemo( () => @@ -367,10 +381,10 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre [ { crop: { - originX: cropRect.x, - originY: cropRect.y, - width: cropRect.width, - height: cropRect.height, + originX: Math.max(0, Math.floor(cropRect.x)), + originY: Math.max(0, Math.floor(cropRect.y)), + width: Math.max(1, Math.floor(cropRect.width)), + height: Math.max(1, Math.floor(cropRect.height)), }, }, ], @@ -396,7 +410,7 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre const croppedFilename = file.name ?? receiptFilename; if (isOdometerImage) { - setMoneyRequestOdometerImage(transaction.transactionID, imageType, file, isDraftTransaction); + setMoneyRequestOdometerImage(transaction.transactionID, imageType, file, isDraftTransaction, !isEditingConfirmation); } else if (isDraftTransaction) { setMoneyRequestReceipt(transaction.transactionID, imageUriResult, croppedFilename, isDraftTransaction, fileType); } else { @@ -428,6 +442,7 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre policyCategories, policy, exitCropMode, + isEditingConfirmation, ]); const threeDotsMenuItems: ThreeDotsMenuItemFactory = useCallback(