diff --git a/src/pages/Share/SubmitDetailsPage.tsx b/src/pages/Share/SubmitDetailsPage.tsx index 03ffde5e6c1b..aba4bdee1756 100644 --- a/src/pages/Share/SubmitDetailsPage.tsx +++ b/src/pages/Share/SubmitDetailsPage.tsx @@ -182,9 +182,13 @@ function SubmitDetailsPage({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [reportOrAccountID, policy, personalPolicy, report, parentReport, currentDate, currentUserPersonalDetails, hasOnlyPersonalPolicies]); - const sharedFileSource = currentAttachment?.content ?? fileUri; - const sharedFileName = getFileName(currentAttachment?.content ?? '') || fileName; - const sharedFileType = currentAttachment?.mimeType ?? fileType; + // Use the branch-aware values computed above: for a share that needs conversion (e.g. HEIC), these resolve to the + // converted JPEG from VALIDATED_FILE_OBJECT; otherwise they fall back to the raw attachment. Re-deriving from + // currentAttachment here would discard the converted file (its content/mimeType are always set), uploading raw + // image/heic which the backend rejects. This mirrors ShareDetailsPage. + const sharedFileSource = fileUri; + const sharedFileName = fileName; + const sharedFileType = fileType; // Seed the draft so isScanRequest() returns true (enables compact mode + receipt rendering). useEffect(() => { @@ -409,7 +413,9 @@ function SubmitDetailsPage({ // Separate helper so the permission-modal callbacks don't re-enter onConfirm (deadlocked when OS permission was pre-granted). const performUpload = (locationPermissionGranted: boolean) => { - if (formHasBeenSubmitted.current || !currentAttachment) { + // Block confirm until the async HEIC→JPEG conversion has landed in VALIDATED_FILE_OBJECT, so a fast tap can't + // submit the raw file. Clearing isConfirming keeps the button from getting stuck, so a retry succeeds once ready. + if (formHasBeenSubmitted.current || !currentAttachment || (shouldUsePreValidatedFile && !validFilesToUpload)) { setIsConfirming(false); return; } diff --git a/tests/ui/SubmitDetailsPageTest.tsx b/tests/ui/SubmitDetailsPageTest.tsx index 4d84ed7716c6..7c02291a79b3 100644 --- a/tests/ui/SubmitDetailsPageTest.tsx +++ b/tests/ui/SubmitDetailsPageTest.tsx @@ -4,6 +4,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ import {act, fireEvent, render, screen} from '@testing-library/react-native'; +import {readFileAsync} from '@libs/fileDownload/FileUtils'; import Log from '@libs/Log'; import SubmitDetailsPage from '@pages/Share/SubmitDetailsPage'; @@ -214,4 +215,32 @@ describe('SubmitDetailsPage', () => { logInfoSpy.mockRestore(); }); + + it('uploads the converted JPEG (not the raw HEIC) when the shared file needed validation', async () => { + // A HEIC share: SHARE_TEMP_FILE holds the raw .heic file, VALIDATED_FILE_OBJECT holds the converted JPEG. + await act(async () => { + await Onyx.merge(ONYXKEYS.SHARE_TEMP_FILE, {content: 'file://shared.heic', mimeType: CONST.SHARE_FILE_MIMETYPE.HEIC}); + await Onyx.merge(ONYXKEYS.VALIDATED_FILE_OBJECT, {uri: 'file://converted.jpg', type: CONST.RECEIPT_ALLOWED_FILE_TYPES.JPEG}); + }); + + await renderAndConfirm(); + + // The upload must read the converted JPEG, never the raw .heic the backend rejects. + const readFileArg = jest.mocked(readFileAsync).mock.calls.at(0); + expect(readFileArg?.[0]).toBe('file://converted.jpg'); + expect(readFileArg?.[4]).toBe(CONST.RECEIPT_ALLOWED_FILE_TYPES.JPEG); + }); + + it('does not upload while a share that needs validation is still awaiting its converted file', async () => { + // A HEIC share whose conversion has not landed yet: VALIDATED_FILE_OBJECT is empty. + await act(async () => { + await Onyx.merge(ONYXKEYS.SHARE_TEMP_FILE, {content: 'file://shared.heic', mimeType: CONST.SHARE_FILE_MIMETYPE.HEIC}); + await Onyx.set(ONYXKEYS.VALIDATED_FILE_OBJECT, undefined); + }); + + await renderAndConfirm(); + + // Confirm must bail out (no raw HEIC uploaded) until the converted file is ready. + expect(jest.mocked(readFileAsync)).not.toHaveBeenCalled(); + }); });