diff --git a/src/libs/actions/IOU/index.ts b/src/libs/actions/IOU/index.ts index 143c3f6a808..201eed6d02b 100644 --- a/src/libs/actions/IOU/index.ts +++ b/src/libs/actions/IOU/index.ts @@ -1698,13 +1698,19 @@ function setMoneyRequestOdometerReading(transactionID: string, startReading: num /** * Set odometer image for a transaction - * @param transactionID - The transaction ID + * @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(transactionID: string, imageType: OdometerImageType, file: FileObject | string, isDraft: boolean, shouldRevokeOldImage: boolean) { +function setMoneyRequestOdometerImage( + transaction: OnyxEntry, + 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' @@ -1715,7 +1721,7 @@ function setMoneyRequestOdometerImage(transactionID: string, imageType: Odometer type: file.type, size: file.size, }; - const transaction = isDraft ? allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`] : allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const transactionID = transaction?.transactionID; const existingImage = transaction?.comment?.[imageKey]; if (shouldRevokeOldImage) { revokeOdometerImageUri(existingImage, normalizedFile); @@ -1729,19 +1735,21 @@ function setMoneyRequestOdometerImage(transactionID: string, imageType: Odometer /** * Remove odometer image from a transaction - * @param transactionID - The transaction ID + * @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(transactionID: string, imageType: OdometerImageType, isDraft: boolean, shouldRevokeOldImage: boolean) { +function removeMoneyRequestOdometerImage(transaction: OnyxEntry, imageType: OdometerImageType, isDraft: boolean, shouldRevokeOldImage: boolean) { + if (!transaction?.transactionID) { + return; + } 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]; if (shouldRevokeOldImage) { revokeOdometerImageUri(existingImage); } - Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { + Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`, { comment: { [imageKey]: null, }, diff --git a/src/pages/iou/request/step/IOURequestStepOdometerImage/index.native.tsx b/src/pages/iou/request/step/IOURequestStepOdometerImage/index.native.tsx index 2aacf5236ec..e668e522396 100644 --- a/src/pages/iou/request/step/IOURequestStepOdometerImage/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepOdometerImage/index.native.tsx @@ -68,6 +68,7 @@ function IOURequestStepOdometerImage({ route: { params: {action, iouType, transactionID, reportID, backToReport, imageType, isEditingConfirmation}, }, + transaction, }: IOURequestStepOdometerImageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -187,7 +188,7 @@ function IOURequestStepOdometerImage({ if (!file) { return; } - setMoneyRequestOdometerImage(transactionID, imageType, getOdometerImageUri(file), isTransactionDraft, false); + setMoneyRequestOdometerImage(transaction, imageType, getOdometerImageUri(file), isTransactionDraft, false); navigateBack(); }; @@ -242,7 +243,7 @@ function IOURequestStepOdometerImage({ cropImageToAspectRatio(imageObject, viewfinderLayout.current?.width, viewfinderLayout.current?.height, undefined, photo.orientation) .then(({file, filename, source}) => { setMoneyRequestOdometerImage( - transactionID, + transaction, imageType, { uri: source, diff --git a/src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx b/src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx index d41fd3bd45e..a5512ea5e60 100644 --- a/src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepOdometerImage/index.tsx @@ -46,6 +46,7 @@ function IOURequestStepOdometerImage({ route: { params: {action, iouType, transactionID, reportID, backToReport, imageType, isEditingConfirmation}, }, + transaction, }: IOURequestStepOdometerImageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -88,7 +89,7 @@ function IOURequestStepOdometerImage({ }; const handleImageSelected = (file: FileObject) => { - setMoneyRequestOdometerImage(transactionID, imageType, file as File, isTransactionDraft, isEditingConfirmation !== 'true'); + setMoneyRequestOdometerImage(transaction, imageType, file as File, isTransactionDraft, isEditingConfirmation !== 'true'); shouldRevokeOnUnmountRef.current = false; navigateBack(); }; @@ -230,7 +231,7 @@ function IOURequestStepOdometerImage({ if (source !== imageObject.source) { URL.revokeObjectURL(imageObject.source); } - setMoneyRequestOdometerImage(transactionID, imageType, file ?? source, isTransactionDraft, isEditingConfirmation !== 'true'); + setMoneyRequestOdometerImage(transaction, 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 1807835cd4a..4b96afbce98 100644 --- a/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx +++ b/src/pages/media/AttachmentModalScreen/routes/TransactionReceiptModalContent.tsx @@ -266,14 +266,14 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre if (!transaction?.transactionID || !imageType) { return; } - removeMoneyRequestOdometerImage(transaction.transactionID, imageType, isDraftTransaction, !isEditingConfirmation); + removeMoneyRequestOdometerImage(transaction, imageType, isDraftTransaction, !isEditingConfirmation); const odometerGoBackRoute = isOdometerImage && (isEditingConfirmation === true ? ROUTES.MONEY_REQUEST_STEP_DISTANCE_ODOMETER.getRoute(action ?? CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID) : ROUTES.DISTANCE_REQUEST_CREATE_TAB_ODOMETER.getRoute(action ?? CONST.IOU.ACTION.CREATE, iouType, transactionID, reportID, backToReport)); Navigation.goBack(odometerGoBackRoute || undefined); - }, [transaction?.transactionID, imageType, isOdometerImage, isDraftTransaction, isEditingConfirmation, action, iouType, transactionID, reportID, backToReport]); + }, [transaction, imageType, isDraftTransaction, isOdometerImage, isEditingConfirmation, action, iouType, transactionID, reportID, backToReport]); const onDownloadAttachment = useDownloadAttachment({ isAuthTokenRequired, @@ -309,7 +309,7 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre const rotatedFilename = file.name ?? receiptFilename; if (isOdometerImage) { - setMoneyRequestOdometerImage(transaction.transactionID, imageType, file, isDraftTransaction, !isEditingConfirmation); + setMoneyRequestOdometerImage(transaction, imageType, file, isDraftTransaction, !isEditingConfirmation); } else if (isDraftTransaction) { setMoneyRequestReceipt(transaction.transactionID, imageUriResult, rotatedFilename, isDraftTransaction, fileType); } else { @@ -328,21 +328,7 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre .catch(() => { setIsRotating(false); }); - }, [ - transaction?.transactionID, - isDraftTransaction, - isOdometerImage, - imageType, - sourceUri, - isImage, - receiptFilename, - fileName, - fileType, - policyCategories, - transaction?.receipt, - policy, - isEditingConfirmation, - ]); + }, [transaction, sourceUri, isImage, fileName, fileType, receiptFilename, isOdometerImage, isDraftTransaction, imageType, isEditingConfirmation, policyCategories, policy]); const shouldShowRotateAndCropReceiptButton = useMemo( () => @@ -410,7 +396,7 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre const croppedFilename = file.name ?? receiptFilename; if (isOdometerImage) { - setMoneyRequestOdometerImage(transaction.transactionID, imageType, file, isDraftTransaction, !isEditingConfirmation); + setMoneyRequestOdometerImage(transaction, imageType, file, isDraftTransaction, !isEditingConfirmation); } else if (isDraftTransaction) { setMoneyRequestReceipt(transaction.transactionID, imageUriResult, croppedFilename, isDraftTransaction, fileType); } else { @@ -429,20 +415,20 @@ function TransactionReceiptModalContent({navigation, route}: AttachmentModalScre setIsCropSaving(false); }); }, [ - transaction?.transactionID, - isDraftTransaction, - isOdometerImage, - imageType, + transaction, sourceUri, isImage, cropRect, - receiptFilename, fileName, fileType, - policyCategories, - policy, exitCropMode, + receiptFilename, + isOdometerImage, + isDraftTransaction, + imageType, isEditingConfirmation, + policyCategories, + policy, ]); const threeDotsMenuItems: ThreeDotsMenuItemFactory = useCallback( diff --git a/tests/actions/IOUTest.ts b/tests/actions/IOUTest.ts index 4b28d2b742f..51b06440720 100644 --- a/tests/actions/IOUTest.ts +++ b/tests/actions/IOUTest.ts @@ -35,12 +35,14 @@ import { payMoneyRequest, rejectExpenseReport, rejectMoneyRequest, + removeMoneyRequestOdometerImage, replaceReceipt, requestMoney, resetDraftTransactionsCustomUnit, retractReport, setMoneyRequestCategory, setMoneyRequestDistanceRate, + setMoneyRequestOdometerImage, shouldOptimisticallyUpdateSearch, submitReport, trackExpense, @@ -17516,4 +17518,85 @@ describe('actions/IOU', () => { expect(updatedTransaction?.comment?.customUnit?.quantity).toBe(100); }); }); + + describe('setMoneyRequestOdometerImage and removeMoneyRequestOdometerImage', () => { + beforeEach(() => { + jest.mock('@libs/OdometerImageUtils', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + __esModule: true, + default: jest.fn(), + })); + }); + + afterEach(() => { + jest.unmock('@libs/OdometerImageUtils'); + }); + it('should set odometer start image on a draft transaction', async () => { + const transaction = createRandomTransaction(1); + const transactionID = transaction.transactionID; + const file = {uri: 'image.uri', name: 'image.jpg', type: 'image/jpeg', size: 1234}; + const imageType = CONST.IOU.ODOMETER_IMAGE_TYPE.START; + + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, transaction); + + setMoneyRequestOdometerImage(transaction, imageType, file, true, false); + await waitForBatchedUpdates(); + + const draftTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`); + expect(draftTransaction?.comment?.odometerStartImage).toEqual(file); + }); + + it('should set odometer end image on a non-draft transaction', async () => { + const transaction = createRandomTransaction(1); + const transactionID = transaction.transactionID; + const file = {uri: 'image.uri', name: 'image.jpg', type: 'image/jpeg', size: 1234}; + const imageType = CONST.IOU.ODOMETER_IMAGE_TYPE.END; + + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, transaction); + + setMoneyRequestOdometerImage(transaction, imageType, file, false, false); + await waitForBatchedUpdates(); + + const updatedTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + expect(updatedTransaction?.comment?.odometerEndImage).toEqual(file); + }); + + it('should remove odometer start image from a draft transaction', async () => { + const transaction = { + ...createRandomTransaction(1), + comment: { + odometerStartImage: {uri: 'image.uri'}, + }, + } as Transaction; + const transactionID = transaction.transactionID; + const imageType = CONST.IOU.ODOMETER_IMAGE_TYPE.START; + + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, transaction); + + removeMoneyRequestOdometerImage(transaction, imageType, true, false); + await waitForBatchedUpdates(); + + const draftTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`); + expect(draftTransaction?.comment?.odometerStartImage).toBeUndefined(); + }); + + it('should remove odometer end image from a non-draft transaction', async () => { + const transaction = { + ...createRandomTransaction(1), + comment: { + odometerEndImage: {uri: 'image.uri'}, + }, + } as Transaction; + const transactionID = transaction.transactionID; + const imageType = CONST.IOU.ODOMETER_IMAGE_TYPE.END; + + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, transaction); + + removeMoneyRequestOdometerImage(transaction, imageType, false, false); + await waitForBatchedUpdates(); + + const updatedTransaction = await getOnyxValue(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`); + expect(updatedTransaction?.comment?.odometerEndImage).toBeUndefined(); + }); + }); });