diff --git a/src/CONST/index.ts b/src/CONST/index.ts index dcb098982e9f..353d210eaf7d 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -253,8 +253,6 @@ const CONST = { // Allowed extensions for receipts ALLOWED_RECEIPT_EXTENSIONS: ['heif', 'heic', 'jpg', 'jpeg', 'gif', 'png', 'pdf', 'htm', 'html', 'text', 'rtf', 'doc', 'tif', 'tiff', 'msword', 'zip', 'xml', 'message'], - - MAX_FILE_LIMIT: 30, }, // Allowed extensions for spreadsheets import @@ -1881,19 +1879,6 @@ const CONST = { // Video MimeTypes allowed by iOS photos app. VIDEO: /\.(mov|mp4)$/, }, - - FILE_VALIDATION_ERRORS: { - WRONG_FILE_TYPE: 'wrongFileType', - WRONG_FILE_TYPE_MULTIPLE: 'wrongFileTypeMultiple', - FILE_TOO_LARGE: 'fileTooLarge', - FILE_TOO_LARGE_MULTIPLE: 'fileTooLargeMultiple', - FILE_TOO_SMALL: 'fileTooSmall', - FILE_CORRUPTED: 'fileCorrupted', - FOLDER_NOT_ALLOWED: 'folderNotAllowed', - MAX_FILE_LIMIT_EXCEEDED: 'fileLimitExceeded', - PROTECTED_FILE: 'protectedFile', - }, - IOS_CAMERA_ROLL_ACCESS_ERROR: 'Access to photo library was denied', ADD_PAYMENT_MENU_POSITION_Y: 226, ADD_PAYMENT_MENU_POSITION_X: 356, diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 82d04c9c847d..2bfe11782fc9 100644 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -14,7 +14,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import attachmentModalHandler from '@libs/AttachmentModalHandler'; import fileDownload from '@libs/fileDownload'; -import {cleanFileName, getFileName, getFileValidationErrorText, validateAttachment, validateImageForCorruption} from '@libs/fileDownload/FileUtils'; +import {cleanFileName, getFileName, validateImageForCorruption} from '@libs/fileDownload/FileUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {hasEReceipt, hasMissingSmartscanFields, hasReceipt, hasReceiptSource, isReceiptBeingScanned} from '@libs/TransactionUtils'; @@ -23,6 +23,7 @@ import variables from '@styles/variables'; import {detachReceipt} from '@userActions/IOU'; import type {IOUAction, IOUType} from '@src/CONST'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; @@ -63,7 +64,6 @@ type FileObject = Partial; type ChildrenProps = { displayFileInModal: (data: FileObject) => void; - displayMultipleFilesInModal: (data: FileObject[]) => void; show: () => void; }; @@ -75,7 +75,7 @@ type AttachmentModalProps = { attachmentID?: string; /** Optional callback to fire when we want to preview an image and approve it for use. */ - onConfirm?: ((file: FileObject | FileObject[]) => void) | null; + onConfirm?: ((file: FileObject) => void) | null; /** Whether the modal should be open by default */ defaultOpen?: boolean; @@ -196,10 +196,11 @@ function AttachmentModal({ const styles = useThemeStyles(); const [isModalOpen, setIsModalOpen] = useState(defaultOpen); const [shouldLoadAttachment, setShouldLoadAttachment] = useState(false); - const [fileError, setFileError] = useState | null>(null); - const [isFileErrorModalVisible, setIsFileErrorModalVisible] = useState(false); + const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); const [isDeleteReceiptConfirmModalVisible, setIsDeleteReceiptConfirmModalVisible] = useState(false); const [isAuthTokenRequiredState, setIsAuthTokenRequiredState] = useState(isAuthTokenRequired); + const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(null); + const [attachmentInvalidReason, setAttachmentInvalidReason] = useState(null); const [sourceState, setSourceState] = useState(() => source); const [modalType, setModalType] = useState(CONST.MODAL.MODAL_TYPE.CENTERED_UNSWIPEABLE); const [isConfirmButtonDisabled, setIsConfirmButtonDisabled] = useState(false); @@ -209,14 +210,13 @@ function AttachmentModal({ const {windowWidth} = useWindowDimensions(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const nope = useSharedValue(false); - const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && fileError); + const isOverlayModalVisible = (isReceiptAttachment && isDeleteReceiptConfirmModalVisible) || (!isReceiptAttachment && isAttachmentInvalid); const iouType = useMemo(() => iouTypeProp ?? (isTrackExpenseAction ? CONST.IOU.TYPE.TRACK : CONST.IOU.TYPE.SUBMIT), [isTrackExpenseAction, iouTypeProp]); const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const transactionID = (isMoneyRequestAction(parentReportAction) && getOriginalMessage(parentReportAction)?.IOUTransactionID) || CONST.DEFAULT_NUMBER_ID; const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {canBeMissing: true}); const [currentAttachmentLink, setCurrentAttachmentLink] = useState(attachmentLink); - const [validFilesToUpload, setValidFilesToUpload] = useState([]); const {setAttachmentError, isErrorInAttachment, clearAttachmentErrors} = useAttachmentErrors(); const [file, setFile] = useState( @@ -300,11 +300,7 @@ function AttachmentModal({ } if (onConfirm) { - if (validFilesToUpload.length) { - onConfirm(validFilesToUpload); - } else { - onConfirm(Object.assign(file ?? {}, {source: sourceState} as FileObject)); - } + onConfirm(Object.assign(file ?? {}, {source: sourceState} as FileObject)); } setIsModalOpen(false); @@ -315,7 +311,7 @@ function AttachmentModal({ * Close the confirm modals. */ const closeConfirmModal = useCallback(() => { - setIsFileErrorModalVisible(false); + setIsAttachmentInvalid(false); setIsDeleteReceiptConfirmModalVisible(false); }, []); @@ -329,20 +325,29 @@ function AttachmentModal({ }, [transaction]); const isValidFile = useCallback( - (fileObject: FileObject, isCheckingMultipleFiles?: boolean) => + (fileObject: FileObject) => validateImageForCorruption(fileObject) .then(() => { - const error = validateAttachment(fileObject, isCheckingMultipleFiles); - if (error) { - setFileError(error); - setIsFileErrorModalVisible(true); + if (fileObject.size && fileObject.size > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { + setIsAttachmentInvalid(true); + setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooLarge'); + setAttachmentInvalidReason('attachmentPicker.sizeExceeded'); + return false; + } + + if (fileObject.size && fileObject.size < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { + setIsAttachmentInvalid(true); + setAttachmentInvalidReasonTitle('attachmentPicker.attachmentTooSmall'); + setAttachmentInvalidReason('attachmentPicker.sizeNotMet'); return false; } + return true; }) .catch(() => { - setFileError(CONST.FILE_VALIDATION_ERRORS.FILE_CORRUPTED); - setIsFileErrorModalVisible(true); + setIsAttachmentInvalid(true); + setAttachmentInvalidReasonTitle('attachmentPicker.attachmentError'); + setAttachmentInvalidReason('attachmentPicker.errorWhileSelectingCorruptedAttachment'); return false; }), [], @@ -350,80 +355,14 @@ function AttachmentModal({ const isDirectoryCheck = useCallback((data: FileObject) => { if ('webkitGetAsEntry' in data && (data as DataTransferItem).webkitGetAsEntry()?.isDirectory) { - setFileError(CONST.FILE_VALIDATION_ERRORS.FOLDER_NOT_ALLOWED); - setIsFileErrorModalVisible(true); + setIsAttachmentInvalid(true); + setAttachmentInvalidReasonTitle('attachmentPicker.attachmentError'); + setAttachmentInvalidReason('attachmentPicker.folderNotAllowedMessage'); return false; } return true; }, []); - const handleOpenModal = useCallback( - (inputSource: string, fileObject: FileObject) => { - const inputModalType = getModalType(inputSource, fileObject); - setIsModalOpen(true); - setSourceState(inputSource); - setFile(fileObject); - setModalType(inputModalType); - }, - [getModalType, setSourceState, setFile, setModalType], - ); - - useEffect(() => { - if (!validFilesToUpload.length) { - return; - } - - if (validFilesToUpload.length > 0) { - if (fileError) { - return; - } - const fileToDisplay = validFilesToUpload.at(0); - if (fileToDisplay) { - const inputSource = fileToDisplay.uri ?? ''; - handleOpenModal(inputSource, fileToDisplay); - } - } - }, [fileError, handleOpenModal, validFilesToUpload]); - - const validateFiles = useCallback( - (data: FileObject[]) => { - let validFiles: FileObject[] = []; - - Promise.all(data.map((fileToUpload) => isValidFile(fileToUpload, true).then((isValid) => (isValid ? fileToUpload : null)))).then((results) => { - validFiles = results.filter((validFile): validFile is FileObject => validFile !== null); - setValidFilesToUpload(validFiles); - }); - }, - [isValidFile], - ); - - const confirmAndContinue = () => { - if (fileError === CONST.FILE_VALIDATION_ERRORS.MAX_FILE_LIMIT_EXCEEDED) { - validateFiles(validFilesToUpload); - } - setIsFileErrorModalVisible(false); - InteractionManager.runAfterInteractions(() => { - setFileError(null); - }); - }; - - const validateAndDisplayMultipleFilesToUpload = useCallback( - (data: FileObject[]) => { - if (!data?.length || data.some((fileObject) => !isDirectoryCheck(fileObject))) { - return; - } - if (data.length > CONST.API_ATTACHMENT_VALIDATIONS.MAX_FILE_LIMIT) { - const validFiles = data.slice(0, CONST.API_ATTACHMENT_VALIDATIONS.MAX_FILE_LIMIT); - setValidFilesToUpload(validFiles); - setFileError(CONST.FILE_VALIDATION_ERRORS.MAX_FILE_LIMIT_EXCEEDED); - setIsFileErrorModalVisible(true); - return; - } - validateFiles(data); - }, - [isDirectoryCheck, validateFiles], - ); - const validateAndDisplayFileToUpload = useCallback( (data: FileObject) => { if (!data || !isDirectoryCheck(data)) { @@ -453,13 +392,21 @@ function AttachmentModal({ } const inputSource = URL.createObjectURL(updatedFile); updatedFile.uri = inputSource; - handleOpenModal(inputSource, updatedFile); + const inputModalType = getModalType(inputSource, updatedFile); + setIsModalOpen(true); + setSourceState(inputSource); + setFile(updatedFile); + setModalType(inputModalType); } else if (fileObject.uri) { - handleOpenModal(fileObject.uri, fileObject); + const inputModalType = getModalType(fileObject.uri, fileObject); + setIsModalOpen(true); + setSourceState(fileObject.uri); + setFile(fileObject); + setModalType(inputModalType); } }); }, - [isDirectoryCheck, isValidFile, handleOpenModal], + [isValidFile, getModalType, isDirectoryCheck], ); /** @@ -486,15 +433,6 @@ function AttachmentModal({ [onModalClose], ); - const closeAndResetModal = useCallback(() => { - closeConfirmModal(); - closeModal(); - InteractionManager.runAfterInteractions(() => { - setFileError(null); - setValidFilesToUpload([]); - }); - }, [closeConfirmModal, closeModal]); - /** * open the modal */ @@ -593,10 +531,10 @@ function AttachmentModal({ } setShouldLoadAttachment(false); clearAttachmentErrors(); - setValidFilesToUpload([]); if (isPDFLoadError.current) { - setFileError(CONST.FILE_VALIDATION_ERRORS.FILE_CORRUPTED); - setIsFileErrorModalVisible(true); + setIsAttachmentInvalid(true); + setAttachmentInvalidReasonTitle('attachmentPicker.attachmentError'); + setAttachmentInvalidReason('attachmentPicker.errorWhileSelectingCorruptedAttachment'); return; } @@ -744,14 +682,13 @@ function AttachmentModal({ {!isReceiptAttachment && ( { if (!isPDFLoadError.current) { return; @@ -764,7 +701,6 @@ function AttachmentModal({ {children?.({ displayFileInModal: validateAndDisplayFileToUpload, - displayMultipleFilesInModal: validateAndDisplayMultipleFilesToUpload, show: openModal, })} diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index 0560e14ec9c7..edcd5e698d67 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -153,68 +153,45 @@ function AttachmentPicker({ return reject(new Error(`Error during attachment selection: ${response.errorMessage}`)); } - const assets = response.assets; - if (!assets || assets.length === 0) { + const targetAsset = response.assets?.[0]; + const targetAssetUri = targetAsset?.uri; + + if (!targetAssetUri) { return resolve(); } - const processedAssets: Asset[] = []; - let processedCount = 0; - - const checkAllProcessed = () => { - processedCount++; - if (processedCount === assets.length) { - resolve(processedAssets.length > 0 ? processedAssets : undefined); - } - }; - - assets.forEach((asset) => { - if (!asset.uri) { - checkAllProcessed(); - return; - } - - if (asset.type?.startsWith('image')) { - verifyFileFormat({fileUri: asset.uri, formatSignatures: CONST.HEIC_SIGNATURES}) - .then((isHEIC) => { - // react-native-image-picker incorrectly changes file extension without transcoding the HEIC file, so we are doing it manually if we detect HEIC signature - if (isHEIC && asset.uri) { - ImageManipulator.manipulate(asset.uri) - .renderAsync() - .then((manipulatedImage) => manipulatedImage.saveAsync({format: SaveFormat.JPEG})) - .then((manipulationResult) => { - const uri = manipulationResult.uri; - const convertedAsset = { - uri, - name: uri - .substring(uri.lastIndexOf('/') + 1) - .split('?') - .at(0), - type: 'image/jpeg', - width: manipulationResult.width, - height: manipulationResult.height, - }; - processedAssets.push(convertedAsset); - checkAllProcessed(); - }) - .catch((error: Error) => { - showGeneralAlert(error.message ?? 'An unknown error occurred'); - checkAllProcessed(); - }); - } else { - processedAssets.push(asset); - checkAllProcessed(); - } - }) - .catch((error: Error) => { - showGeneralAlert(error.message ?? 'An unknown error occurred'); - checkAllProcessed(); - }); - } else { - processedAssets.push(asset); - checkAllProcessed(); - } - }); + if (targetAsset?.type?.startsWith('image')) { + verifyFileFormat({fileUri: targetAssetUri, formatSignatures: CONST.HEIC_SIGNATURES}) + .then((isHEIC) => { + // react-native-image-picker incorrectly changes file extension without transcoding the HEIC file, so we are doing it manually if we detect HEIC signature + if (isHEIC && targetAssetUri) { + ImageManipulator.manipulate(targetAssetUri) + .renderAsync() + .then((manipulatedImage) => manipulatedImage.saveAsync({format: SaveFormat.JPEG})) + .then((manipulationResult) => { + const uri = manipulationResult.uri; + const convertedAsset = { + uri, + name: uri + .substring(uri.lastIndexOf('/') + 1) + .split('?') + .at(0), + type: 'image/jpeg', + width: manipulationResult.width, + height: manipulationResult.height, + }; + + return resolve([convertedAsset]); + }) + .catch((err) => reject(err)); + } else { + return resolve(response.assets); + } + }) + .catch((err) => reject(err)); + } else { + return resolve(response.assets); + } }); }), [fileLimit, showGeneralAlert, type], @@ -310,20 +287,42 @@ function AttachmentPicker({ setIsVisible(false); }; + const validateAndCompleteAttachmentSelection = useCallback( + (fileData: FileResponse) => { + // Check if the file dimensions indicate corruption + // The width/height for a corrupted file is -1 on android native and 0 on ios native + // We must check only numeric values because the width/height can be undefined for non-image files + if ((typeof fileData.width === 'number' && fileData.width <= 0) || (typeof fileData.height === 'number' && fileData.height <= 0)) { + showImageCorruptionAlert(); + return Promise.resolve(); + } + return getDataForUpload(fileData) + .then((result) => { + completeAttachmentSelection.current([result]); + }) + .catch((error: Error) => { + showGeneralAlert(error.message); + throw error; + }); + }, + [showGeneralAlert, showImageCorruptionAlert], + ); + /** * Handles the image/document picker result and * sends the selected attachment to the caller (parent component) */ const pickAttachment = useCallback( - (attachments: Asset[] | LocalCopy[] | void = []): Promise | undefined => { + (attachments: Asset[] | LocalCopy[] | void = []): Promise | undefined => { if (!attachments || attachments.length === 0) { onCanceled.current(); - return Promise.resolve(); + return Promise.resolve([]); } const filesToProcess = attachments.map((fileData) => { if (!fileData) { - return Promise.resolve(null); + onCanceled.current(); + return Promise.resolve(); } /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ @@ -346,10 +345,20 @@ function AttachmentPicker({ fileDataObject.height = height; return fileDataObject; }) - .then((file) => getDataForUpload(file)) + .then((file) => { + return getDataForUpload(file) + .then((result) => completeAttachmentSelection.current([result])) + .catch((error) => { + if (error instanceof Error) { + showGeneralAlert(error.message); + } else { + showGeneralAlert('An unknown error occurred'); + } + throw error; + }); + }) .catch(() => { showImageCorruptionAlert(); - return null; }); } @@ -361,41 +370,21 @@ function AttachmentPicker({ if (fileDataObject.width <= 0 || fileDataObject.height <= 0) { showImageCorruptionAlert(); - return null; + return Promise.resolve(); // Skip processing this corrupted file } - return getDataForUpload(fileDataObject); + return validateAndCompleteAttachmentSelection(fileDataObject); }) .catch(() => { showImageCorruptionAlert(); - return null; }); } - - return getDataForUpload(fileDataObject).catch((error: Error) => { - showGeneralAlert(error.message); - return null; - }); + return validateAndCompleteAttachmentSelection(fileDataObject); }); - return Promise.all(filesToProcess) - .then((results) => { - const validResults = results.filter((result): result is FileObject => result !== null); - if (validResults.length > 0) { - completeAttachmentSelection.current(validResults); - } else { - onCanceled.current(); - } - }) - .catch((error) => { - if (error instanceof Error) { - showGeneralAlert(error.message); - } else { - showGeneralAlert('An unknown error occurred'); - } - }); + return Promise.all(filesToProcess); }, - [shouldValidateImage, showGeneralAlert, showImageCorruptionAlert], + [shouldValidateImage, validateAndCompleteAttachmentSelection, showGeneralAlert, showImageCorruptionAlert], ); /** diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx index 6b11c82e60b3..1b11bdcf047e 100644 --- a/src/components/AttachmentPicker/index.tsx +++ b/src/components/AttachmentPicker/index.tsx @@ -60,15 +60,9 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE, a return; } - if (allowMultiple && e.target.files.length > 1) { - const files = Array.from(e.target.files).map((currentFile) => { - // eslint-disable-next-line no-param-reassign - currentFile.uri = URL.createObjectURL(currentFile); - return currentFile as FileObject; - }); - onPicked.current(files); - } else if (e.target.files[0]) { - const file = e.target.files[0]; + const file = e.target.files[0]; + + if (file) { file.uri = URL.createObjectURL(file); onPicked.current([file]); } diff --git a/src/hooks/useFileValidation.ts b/src/hooks/useFileValidation.ts index bbbe9504f9e4..88621ef8dbb7 100644 --- a/src/hooks/useFileValidation.ts +++ b/src/hooks/useFileValidation.ts @@ -1,65 +1,61 @@ import {Str} from 'expensify-common'; import {useState} from 'react'; -import type {ValueOf} from 'type-fest'; -import {resizeImageIfNeeded, validateAttachment, validateImageForCorruption} from '@libs/fileDownload/FileUtils'; +import {resizeImageIfNeeded, validateReceipt} from '@libs/fileDownload/FileUtils'; import type {FileObject} from '@pages/media/AttachmentModalScreen/types'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; function useFileValidation() { const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false); + const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState(); + const [attachmentInvalidReason, setAttachmentValidReason] = useState(); const [pdfFile, setPdfFile] = useState(null); const [isLoadingReceipt, setIsLoadingReceipt] = useState(false); - const [fileError, setFileError] = useState | null>(null); /** * Sets the upload receipt error modal content when an invalid receipt is uploaded */ - const setUploadReceiptError = (error: ValueOf) => { - setIsAttachmentInvalid(true); - setFileError(error); + const setUploadReceiptError = (isInvalid: boolean, title: TranslationPaths, reason: TranslationPaths) => { + setIsAttachmentInvalid(isInvalid); + setAttachmentInvalidReasonTitle(title); + setAttachmentValidReason(reason); setPdfFile(null); }; - const validateAndResizeFile = (originalFile: FileObject, setReceiptAndNavigate: (file: FileObject) => void, isPdfValidated?: boolean, isCheckingMultipleFiles?: boolean) => { - validateImageForCorruption(originalFile) - .then(() => { - const error = validateAttachment(originalFile, isCheckingMultipleFiles, true); - if (error) { - setIsAttachmentInvalid(true); - setFileError(error); - return false; - } - // If we have a pdf file and if it is not validated then set the pdf file for validation and return - if (Str.isPDF(originalFile.name ?? '') && !isPdfValidated) { - setPdfFile(originalFile); - return; - } + const validateAndResizeFile = (originalFile: FileObject, setReceiptAndNavigate: (file: FileObject) => void, isPdfValidated?: boolean) => { + validateReceipt(originalFile, setUploadReceiptError).then((isFileValid) => { + if (!isFileValid) { + return; + } - // With the image size > 24MB, we use manipulateAsync to resize the image. - // It takes a long time so we should display a loading indicator while the resize image progresses. - if (Str.isImage(originalFile.name ?? '') && (originalFile?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { - setIsLoadingReceipt(true); - } - resizeImageIfNeeded(originalFile).then((resizedFile) => { - setIsLoadingReceipt(false); - setReceiptAndNavigate(resizedFile); - }); - }) - .catch(() => { - setFileError(CONST.FILE_VALIDATION_ERRORS.FILE_CORRUPTED); - return false; + // If we have a pdf file and if it is not validated then set the pdf file for validation and return + if (Str.isPDF(originalFile.name ?? '') && !isPdfValidated) { + setPdfFile(originalFile); + return; + } + + // With the image size > 24MB, we use manipulateAsync to resize the image. + // It takes a long time so we should display a loading indicator while the resize image progresses. + if (Str.isImage(originalFile.name ?? '') && (originalFile?.size ?? 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { + setIsLoadingReceipt(true); + } + resizeImageIfNeeded(originalFile).then((resizedFile) => { + setIsLoadingReceipt(false); + setReceiptAndNavigate(resizedFile); }); + }); }; return { validateAndResizeFile, isAttachmentInvalid, setIsAttachmentInvalid, + attachmentInvalidReason, + attachmentInvalidReasonTitle, + setUploadReceiptError, pdfFile, setPdfFile, - setUploadReceiptError, isLoadingReceipt, - fileError, }; } diff --git a/src/languages/de.ts b/src/languages/de.ts index 1a3e78c99d76..3755d65bc5b8 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -99,7 +99,6 @@ import type { ExportIntegrationSelectedParams, FeatureNameParams, FileLimitParams, - FileTypeParams, FiltersAmountBetweenParams, FlightLayoverParams, FlightParams, @@ -666,12 +665,6 @@ const translations = { attachmentImageTooLarge: 'Dieses Bild ist zu groß, um es vor dem Hochladen in der Vorschau anzuzeigen.', tooManyFiles: ({fileLimit}: FileLimitParams) => `Sie können jeweils nur bis zu ${fileLimit} Dateien hochladen.`, sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `Dateien überschreiten ${maxUploadSizeInMB} MB. Bitte versuchen Sie es erneut.`, - someFilesCantBeUploaded: 'Einige Dateien können nicht hochgeladen werden', - sizeLimitExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `Dateien müssen unter ${maxUploadSizeInMB} MB sein. Größere Dateien werden nicht hochgeladen.`, - maxFileLimitExceeded: 'Sie können bis zu 30 Belege gleichzeitig hochladen. Weitere werden nicht hochgeladen.', - unsupportedFileType: ({fileType}: FileTypeParams) => `${fileType} Dateien werden nicht unterstützt. Nur unterstützte Dateitypen werden hochgeladen.`, - learnMoreAboutSupportedFiles: 'Erfahren Sie mehr über unterstützte Formate.', - passwordProtected: 'Passwortgeschützte PDFs werden nicht unterstützt. Nur unterstützte Dateien werden hochgeladen.', }, dropzone: { addAttachments: 'Anhänge hinzufügen', diff --git a/src/languages/en.ts b/src/languages/en.ts index 7e780b6b9c6f..b221c0f4cfd7 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -87,7 +87,6 @@ import type { ExportIntegrationSelectedParams, FeatureNameParams, FileLimitParams, - FileTypeParams, FiltersAmountBetweenParams, FlightLayoverParams, FlightParams, @@ -656,12 +655,6 @@ const translations = { attachmentImageTooLarge: 'This image is too large to preview before uploading.', tooManyFiles: ({fileLimit}: FileLimitParams) => `You can only upload up to ${fileLimit} files at a time.`, sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `Files exceeds ${maxUploadSizeInMB} MB. Please try again.`, - someFilesCantBeUploaded: "Some files can't be uploaded", - sizeLimitExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `Files must be under ${maxUploadSizeInMB} MB. Any larger files won't be uploaded.`, - maxFileLimitExceeded: "You can upload up to 30 receipts at a time. Any extras won't be uploaded.", - unsupportedFileType: ({fileType}: FileTypeParams) => `${fileType} files aren't supported. Only supported file types will be uploaded.`, - learnMoreAboutSupportedFiles: 'Learn more about supported formats.', - passwordProtected: "Password-protected PDFs aren't supported. Only supported files will be uploaded.", }, dropzone: { addAttachments: 'Add attachments', diff --git a/src/languages/es.ts b/src/languages/es.ts index 67fc5de7bfb4..341db9f025c3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -86,7 +86,6 @@ import type { ExportIntegrationSelectedParams, FeatureNameParams, FileLimitParams, - FileTypeParams, FiltersAmountBetweenParams, FlightLayoverParams, FlightParams, @@ -651,12 +650,6 @@ const translations = { attachmentImageTooLarge: 'Esta imagen es demasiado grande para obtener una vista previa antes de subirla.', tooManyFiles: ({fileLimit}: FileLimitParams) => `Solamente puedes suber ${fileLimit} archivos a la vez.`, sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `El archivo supera los ${maxUploadSizeInMB} MB. Por favor, vuelve a intentarlo.`, - someFilesCantBeUploaded: 'Algunos archivos no se pueden subir', - sizeLimitExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `Los archivos deben ser menores a ${maxUploadSizeInMB} MB. Los archivos más grandes no se subirán.`, - maxFileLimitExceeded: 'Puedes subir hasta 30 recibos a la vez. Los extras no se subirán.', - unsupportedFileType: ({fileType}: FileTypeParams) => `${fileType} archivos no son compatibles. Solo se subirán los archivos compatibles.`, - learnMoreAboutSupportedFiles: 'Obtén más información sobre los formatos compatibles.', - passwordProtected: 'Los PDFs con contraseña no son compatibles. Solo se subirán los archivos compatibles', }, dropzone: { addAttachments: 'Añadir archivos adjuntos', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 081c37a31b01..30947ec2b51e 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -99,7 +99,6 @@ import type { ExportIntegrationSelectedParams, FeatureNameParams, FileLimitParams, - FileTypeParams, FiltersAmountBetweenParams, FlightLayoverParams, FlightParams, @@ -666,13 +665,6 @@ const translations = { attachmentImageTooLarge: 'Cette image est trop grande pour être prévisualisée avant le téléchargement.', tooManyFiles: ({fileLimit}: FileLimitParams) => `Vous pouvez télécharger jusqu'à ${fileLimit} fichiers à la fois.`, sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `Les fichiers dépassent ${maxUploadSizeInMB} MB. Veuillez réessayer.`, - someFilesCantBeUploaded: 'Certains fichiers ne peuvent pas être téléchargés', - sizeLimitExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => - `Les fichiers doivent faire moins de ${maxUploadSizeInMB} MB. Les fichiers plus volumineux ne seront pas téléchargés.`, - maxFileLimitExceeded: "Vous pouvez télécharger jusqu'à 30 reçus à la fois. Les fichiers supplémentaires ne seront pas téléchargés.", - unsupportedFileType: ({fileType}: FileTypeParams) => `Les fichiers ${fileType} ne sont pas pris en charge. Seuls les types de fichiers pris en charge seront téléchargés.`, - learnMoreAboutSupportedFiles: 'En savoir plus sur les formats pris en charge.', - passwordProtected: 'Les PDF protégés par mot de passe ne sont pas pris en charge. Seuls les fichiers pris en charge seront téléchargés.', }, dropzone: { addAttachments: 'Ajouter des pièces jointes', diff --git a/src/languages/it.ts b/src/languages/it.ts index 8a79252ffb84..0e4c78a32066 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -99,7 +99,6 @@ import type { ExportIntegrationSelectedParams, FeatureNameParams, FileLimitParams, - FileTypeParams, FiltersAmountBetweenParams, FlightLayoverParams, FlightParams, @@ -666,12 +665,6 @@ const translations = { attachmentImageTooLarge: 'Questa immagine è troppo grande per essere visualizzata in anteprima prima del caricamento.', tooManyFiles: ({fileLimit}: FileLimitParams) => `Puoi caricare solo fino a ${fileLimit} file alla volta.`, sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `I file superano ${maxUploadSizeInMB} MB. Per favore riprova.`, - someFilesCantBeUploaded: 'Alcuni file non possono essere caricati', - sizeLimitExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `I file devono essere inferiori a ${maxUploadSizeInMB} MB. I file più grandi non verranno caricati.`, - maxFileLimitExceeded: 'Puoi caricare fino a 30 ricevute alla volta. Quelle in eccesso non verranno caricate.', - unsupportedFileType: ({fileType}: FileTypeParams) => `I file ${fileType} non sono supportati. Verranno caricati solo i tipi di file supportati.`, - learnMoreAboutSupportedFiles: 'Scopri di più sui formati supportati.', - passwordProtected: 'I PDF protetti da password non sono supportati. Verranno caricati solo i file supportati.', }, dropzone: { addAttachments: 'Aggiungi allegati', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 65fd02149a8d..6fbcfe51f200 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -99,7 +99,6 @@ import type { ExportIntegrationSelectedParams, FeatureNameParams, FileLimitParams, - FileTypeParams, FiltersAmountBetweenParams, FlightLayoverParams, FlightParams, @@ -666,12 +665,6 @@ const translations = { attachmentImageTooLarge: 'この画像はアップロード前にプレビューするには大きすぎます。', tooManyFiles: ({fileLimit}: FileLimitParams) => `一度にアップロードできるファイルは${fileLimit}個までです。`, sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `ファイルが ${maxUploadSizeInMB} MB を超えています。もう一度お試しください。`, - someFilesCantBeUploaded: '一部のファイルはアップロードできません', - sizeLimitExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `ファイルは${maxUploadSizeInMB}MB未満である必要があります。それより大きいファイルはアップロードされません。`, - maxFileLimitExceeded: '一度に最大30枚の領収書をアップロードできます。超過分はアップロードされません。', - unsupportedFileType: ({fileType}: FileTypeParams) => `${fileType}ファイルはサポートされていません。サポートされているファイルタイプのみがアップロードされます。`, - learnMoreAboutSupportedFiles: 'サポートされているフォーマットについて詳しく知る。', - passwordProtected: 'パスワード保護されたPDFはサポートされていません。サポートされているファイルのみがアップロードされます。', }, dropzone: { addAttachments: '添付ファイルを追加', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index b0e36467e276..51869c270d8a 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -99,7 +99,6 @@ import type { ExportIntegrationSelectedParams, FeatureNameParams, FileLimitParams, - FileTypeParams, FiltersAmountBetweenParams, FlightLayoverParams, FlightParams, @@ -665,12 +664,6 @@ const translations = { attachmentImageTooLarge: 'Deze afbeelding is te groot om te bekijken voordat deze wordt geüpload.', tooManyFiles: ({fileLimit}: FileLimitParams) => `U kunt maximaal ${fileLimit} bestanden tegelijk uploaden.`, sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `Bestanden overschrijden ${maxUploadSizeInMB} MB. Probeer het opnieuw.`, - someFilesCantBeUploaded: 'Sommige bestanden kunnen niet worden geüpload', - sizeLimitExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `Bestanden moeten kleiner zijn dan ${maxUploadSizeInMB} MB. Grotere bestanden worden niet geüpload.`, - maxFileLimitExceeded: "U kunt maximaal 30 bonnetjes tegelijk uploaden. Extra's worden niet geüpload.", - unsupportedFileType: ({fileType}: FileTypeParams) => `${fileType} bestanden worden niet ondersteund. Alleen ondersteunde bestandstypen worden geüpload.`, - learnMoreAboutSupportedFiles: 'Meer informatie over ondersteunde formaten.', - passwordProtected: "Met wachtwoord beveiligde PDF's worden niet ondersteund. Alleen ondersteunde bestanden worden geüpload.", }, dropzone: { addAttachments: 'Bijlagen toevoegen', diff --git a/src/languages/params.ts b/src/languages/params.ts index a59faeacc5ad..9a6666d53543 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -673,10 +673,6 @@ type FileLimitParams = { fileLimit: number; }; -type FileTypeParams = { - fileType: string; -}; - type CompanyCardBankName = { bankName: string; }; @@ -806,7 +802,6 @@ export type { AutoPayApprovedReportsLimitErrorParams, FeatureNameParams, FileLimitParams, - FileTypeParams, SpreadSheetColumnParams, SpreadFieldNameParams, AssignedCardParams, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index a112460924ff..ed8996e00ba5 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -99,7 +99,6 @@ import type { ExportIntegrationSelectedParams, FeatureNameParams, FileLimitParams, - FileTypeParams, FiltersAmountBetweenParams, FlightLayoverParams, FlightParams, @@ -666,12 +665,6 @@ const translations = { attachmentImageTooLarge: 'Ten obraz jest zbyt duży, aby wyświetlić podgląd przed przesłaniem.', tooManyFiles: ({fileLimit}: FileLimitParams) => `Możesz przesłać jednocześnie maksymalnie ${fileLimit} plików.`, sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `Plik przekracza ${maxUploadSizeInMB} MB. Proszę spróbować ponownie.`, - someFilesCantBeUploaded: 'Niektóre pliki nie mogą zostać przesłane', - sizeLimitExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `Pliki muszą mieć mniej niż ${maxUploadSizeInMB} MB. Większe pliki nie zostaną przesłane.`, - maxFileLimitExceeded: 'Możesz przesłać maksymalnie 30 paragonów naraz. Dodatkowe nie zostaną przesłane.', - unsupportedFileType: ({fileType}: FileTypeParams) => `Pliki ${fileType} nie są obsługiwane. Tylko obsługiwane typy plików zostaną przesłane.`, - learnMoreAboutSupportedFiles: 'Dowiedz się więcej o obsługiwanych formatach.', - passwordProtected: 'Pliki PDF chronione hasłem nie są obsługiwane. Tylko obsługiwane pliki zostaną przesłane.', }, dropzone: { addAttachments: 'Dodaj załączniki', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index f277b8edbc6f..f00c0039a247 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -99,7 +99,6 @@ import type { ExportIntegrationSelectedParams, FeatureNameParams, FileLimitParams, - FileTypeParams, FiltersAmountBetweenParams, FlightLayoverParams, FlightParams, @@ -665,12 +664,6 @@ const translations = { attachmentImageTooLarge: 'Esta imagem é muito grande para pré-visualizar antes de fazer o upload.', tooManyFiles: ({fileLimit}: FileLimitParams) => `Você pode enviar até ${fileLimit} arquivos de uma vez.`, sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `Os arquivos excedem ${maxUploadSizeInMB} MB. Por favor, tente novamente.`, - someFilesCantBeUploaded: 'Alguns arquivos não podem ser enviados', - sizeLimitExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `Os arquivos devem ter menos de ${maxUploadSizeInMB} MB. Arquivos maiores não serão enviados.`, - maxFileLimitExceeded: 'Você pode enviar até 30 recibos por vez. Os extras não serão enviados.', - unsupportedFileType: ({fileType}: FileTypeParams) => `Arquivos ${fileType} não são suportados. Apenas os tipos de arquivo suportados serão enviados.`, - learnMoreAboutSupportedFiles: 'Saiba mais sobre formatos suportados.', - passwordProtected: 'PDFs protegidos por senha não são suportados. Apenas arquivos suportados serão enviados.', }, dropzone: { addAttachments: 'Adicionar anexos', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 7ae877e2fc44..46567b9c4958 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -99,7 +99,6 @@ import type { ExportIntegrationSelectedParams, FeatureNameParams, FileLimitParams, - FileTypeParams, FiltersAmountBetweenParams, FlightLayoverParams, FlightParams, @@ -665,12 +664,6 @@ const translations = { attachmentImageTooLarge: '此图像太大,无法在上传前预览。', tooManyFiles: ({fileLimit}: FileLimitParams) => `您一次最多只能上传 ${fileLimit} 个文件。`, sizeExceededWithValue: ({maxUploadSizeInMB}: SizeExceededParams) => `文件超过 ${maxUploadSizeInMB} MB。请重试。`, - someFilesCantBeUploaded: '有些文件无法上传', - sizeLimitExceeded: ({maxUploadSizeInMB}: SizeExceededParams) => `文件必须小于${maxUploadSizeInMB} MB。较大的文件将不会被上传。`, - maxFileLimitExceeded: '您一次最多可上传30张收据。额外的将不会被上传。', - unsupportedFileType: ({fileType}: FileTypeParams) => `${fileType} 文件不受支持。只有受支持的文件类型才会被上传。`, - learnMoreAboutSupportedFiles: '了解更多关于支持的格式。', - passwordProtected: '不支持密码保护的PDF。只有受支持的文件才会被上传。', }, dropzone: { addAttachments: '添加附件', diff --git a/src/libs/fileDownload/FileUtils.ts b/src/libs/fileDownload/FileUtils.ts index 22369c1e6549..12e9a0c8243b 100644 --- a/src/libs/fileDownload/FileUtils.ts +++ b/src/libs/fileDownload/FileUtils.ts @@ -1,7 +1,7 @@ import {Str} from 'expensify-common'; import {Alert, Linking, Platform} from 'react-native'; import ImageSize from 'react-native-image-size'; -import type {TupleToUnion, ValueOf} from 'type-fest'; +import type {TupleToUnion} from 'type-fest'; import DateUtils from '@libs/DateUtils'; import getPlatform from '@libs/getPlatform'; import {translateLocal} from '@libs/Localize'; @@ -331,15 +331,7 @@ const resizeImageIfNeeded = (file: FileObject) => { if (!file || !Str.isImage(file.name ?? '') || (file?.size ?? 0) <= CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) { return Promise.resolve(file); } - return getImageDimensionsAfterResize(file).then(({width, height}) => - getImageManipulator({ - fileUri: file.uri ?? '', - width, - height, - fileName: file.name ?? '', - type: file.type, - }), - ); + return getImageDimensionsAfterResize(file).then(({width, height}) => getImageManipulator({fileUri: file.uri ?? '', width, height, fileName: file.name ?? '', type: file.type})); }; const createFile = (file: File): FileObject => { @@ -386,102 +378,14 @@ const validateReceipt = (file: FileObject, setUploadReceiptError: (isInvalid: bo }); }; -const isValidReceiptExtension = (file: FileObject) => { - const {fileExtension} = splitExtensionFromFileName(file?.name ?? ''); - return CONST.API_ATTACHMENT_VALIDATIONS.ALLOWED_RECEIPT_EXTENSIONS.includes( - fileExtension.toLowerCase() as TupleToUnion, - ); -}; - -const validateAttachment = (file: FileObject, isCheckingMultipleFiles?: boolean, isValidatingReceipt?: boolean) => { - const maxFileSize = isValidatingReceipt ? CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE : CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE; - if (!Str.isImage(file.name ?? '') && (file?.size ?? 0) > maxFileSize) { - return isCheckingMultipleFiles ? CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE_MULTIPLE : CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE; - } - - if ((file?.size ?? 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) { - return CONST.FILE_VALIDATION_ERRORS.FILE_TOO_SMALL; - } - - if (isValidatingReceipt && !isValidReceiptExtension(file)) { - return isCheckingMultipleFiles ? CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE_MULTIPLE : CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE; - } - return ''; -}; - -type TranslationAdditionalData = { - maxUploadSizeInMB?: number; - fileLimit?: number; - fileType?: string; -}; - -const getFileValidationErrorText = ( - validationError: ValueOf | null, - additionalData: TranslationAdditionalData = {}, -): { - title: string; - reason: string; -} => { - if (!validationError) { - return { - title: '', - reason: '', - }; +const getConfirmModalPrompt = (attachmentInvalidReason: TranslationPaths | undefined) => { + if (!attachmentInvalidReason) { + return ''; } - switch (validationError) { - case CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE: - return { - title: translateLocal('attachmentPicker.wrongFileType'), - reason: translateLocal('attachmentPicker.notAllowedExtension'), - }; - case CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE_MULTIPLE: - return { - title: translateLocal('attachmentPicker.someFilesCantBeUploaded'), - reason: translateLocal('attachmentPicker.unsupportedFileType', {fileType: additionalData.fileType ?? ''}), - }; - case CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE: - return { - title: translateLocal('attachmentPicker.attachmentTooLarge'), - reason: translateLocal('attachmentPicker.sizeExceeded'), - }; - case CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE_MULTIPLE: - return { - title: translateLocal('attachmentPicker.someFilesCantBeUploaded'), - reason: translateLocal('attachmentPicker.sizeLimitExceeded', { - maxUploadSizeInMB: additionalData.maxUploadSizeInMB ?? CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE / 1024 / 1024, - }), - }; - case CONST.FILE_VALIDATION_ERRORS.FILE_TOO_SMALL: - return { - title: translateLocal('attachmentPicker.attachmentTooSmall'), - reason: translateLocal('attachmentPicker.sizeNotMet'), - }; - case CONST.FILE_VALIDATION_ERRORS.FOLDER_NOT_ALLOWED: - return { - title: translateLocal('attachmentPicker.attachmentError'), - reason: translateLocal('attachmentPicker.folderNotAllowedMessage'), - }; - case CONST.FILE_VALIDATION_ERRORS.MAX_FILE_LIMIT_EXCEEDED: - return { - title: translateLocal('attachmentPicker.someFilesCantBeUploaded'), - reason: translateLocal('attachmentPicker.maxFileLimitExceeded'), - }; - case CONST.FILE_VALIDATION_ERRORS.FILE_CORRUPTED: - return { - title: translateLocal('attachmentPicker.attachmentError'), - reason: translateLocal('attachmentPicker.errorWhileSelectingCorruptedAttachment'), - }; - case CONST.FILE_VALIDATION_ERRORS.PROTECTED_FILE: - return { - title: translateLocal('attachmentPicker.attachmentError'), - reason: translateLocal('attachmentPicker.protectedPDFNotSupported'), - }; - default: - return { - title: translateLocal('attachmentPicker.attachmentError'), - reason: translateLocal('attachmentPicker.errorWhileSelectingCorruptedAttachment'), - }; + if (attachmentInvalidReason === 'attachmentPicker.sizeExceededWithLimit') { + return translateLocal(attachmentInvalidReason, {maxUploadSizeInMB: CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE / (1024 * 1024)}); } + return translateLocal(attachmentInvalidReason); }; export { @@ -506,7 +410,5 @@ export { resizeImageIfNeeded, createFile, validateReceipt, - validateAttachment, - isValidReceiptExtension, - getFileValidationErrorText, + getConfirmModalPrompt, }; diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index 8a810f32cdc6..1ac12c860ad8 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -38,7 +38,7 @@ import { search, unholdMoneyRequestOnSearch, } from '@libs/actions/Search'; -import {getFileValidationErrorText} from '@libs/fileDownload/FileUtils'; +import {getConfirmModalPrompt} from '@libs/fileDownload/FileUtils'; import {navigateToParticipantPage} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -74,7 +74,17 @@ function SearchPage({route}: SearchPageProps) { const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); const [isDeleteExpensesConfirmModalVisible, setIsDeleteExpensesConfirmModalVisible] = useState(false); const [isDownloadExportModalVisible, setIsDownloadExportModalVisible] = useState(false); - const {validateAndResizeFile, setIsAttachmentInvalid, isAttachmentInvalid, setUploadReceiptError, pdfFile, setPdfFile, isLoadingReceipt, fileError} = useFileValidation(); + const { + validateAndResizeFile, + setIsAttachmentInvalid, + isAttachmentInvalid, + attachmentInvalidReason, + attachmentInvalidReasonTitle, + setUploadReceiptError, + pdfFile, + setPdfFile, + isLoadingReceipt, + } = useFileValidation(); const {q} = route.params; @@ -388,6 +398,7 @@ function SearchPage({route}: SearchPageProps) { }); }; + // TODO: to be refactored in step 3 const hideReceiptModal = () => { setIsAttachmentInvalid(false); }; @@ -437,6 +448,7 @@ function SearchPage({route}: SearchPageProps) { const {resetVideoPlayerData} = usePlaybackContext(); const shouldShowOfflineIndicator = currentSearchResults?.data ?? lastNonEmptySearchResults; + // TODO: to be refactored in step 3 const PDFThumbnailView = pdfFile ? ( setUploadReceiptError(CONST.FILE_VALIDATION_ERRORS.PROTECTED_FILE)} - onLoadError={() => setUploadReceiptError(CONST.FILE_VALIDATION_ERRORS.FILE_CORRUPTED)} + onPassword={() => { + setUploadReceiptError(true, 'attachmentPicker.attachmentError', 'attachmentPicker.protectedPDFNotSupported'); + }} + onLoadError={() => { + setUploadReceiptError(true, 'attachmentPicker.attachmentError', 'attachmentPicker.errorWhileSelectingCorruptedAttachment'); + }} /> ) : null; @@ -500,11 +516,11 @@ function SearchPage({route}: SearchPageProps) { /> @@ -597,11 +613,11 @@ function SearchPage({route}: SearchPageProps) { diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 3e7453e50d73..b3a68e794885 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -50,9 +50,6 @@ type AttachmentPickerWithMenuItemsProps = { /** Callback to open the file in the modal */ displayFileInModal: (url: FileObject) => void; - /** Callback to open multiple files in the modal */ - displayMultipleFilesInModal: (files: FileObject[]) => void; - /** Whether or not the full size composer is available */ isFullComposerAvailable: boolean; @@ -110,7 +107,6 @@ function AttachmentPickerWithMenuItems({ currentUserPersonalDetails, reportParticipantIDs, displayFileInModal, - displayMultipleFilesInModal, isFullComposerAvailable, isComposerFullSize, reportID, @@ -313,24 +309,13 @@ function AttachmentPickerWithMenuItems({ // 4. And the Create button is at the bottom. const createButtonContainerStyles = [styles.flexGrow0, styles.flexShrink0]; - const isMultipleDragAndDropEnabled = isBetaEnabled(CONST.BETAS.NEWDOT_MULTI_FILES_DRAG_AND_DROP); - return ( - + {({openPicker}) => { const triggerAttachmentPicker = () => { onTriggerAttachmentPicker(); openPicker({ - onPicked: (data) => { - if (data.length > 1 && isMultipleDragAndDropEnabled) { - displayMultipleFilesInModal(data); - } else { - displayFileInModal(data.at(0) ?? {}); - } - }, + onPicked: (data) => displayFileInModal(data.at(0) ?? {}), onCanceled: onCanceledAttachmentPicker, }); }; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index e214a517c855..5cd2d43c792b 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -36,7 +36,7 @@ import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import DomUtils from '@libs/DomUtils'; import {getDraftComment} from '@libs/DraftCommentUtils'; -import {getFileValidationErrorText} from '@libs/fileDownload/FileUtils'; +import {getConfirmModalPrompt} from '@libs/fileDownload/FileUtils'; import getModalState from '@libs/getModalState'; import Performance from '@libs/Performance'; import { @@ -152,7 +152,8 @@ function ReportActionCompose({ // TODO: remove beta check after the feature is enabled const {isBetaEnabled} = usePermissions(); - const {validateAndResizeFile, setIsAttachmentInvalid, isAttachmentInvalid, setUploadReceiptError, pdfFile, setPdfFile, fileError} = useFileValidation(); + const {validateAndResizeFile, setIsAttachmentInvalid, isAttachmentInvalid, attachmentInvalidReason, attachmentInvalidReasonTitle, setUploadReceiptError, pdfFile, setPdfFile} = + useFileValidation(); /** * Updates the Highlight state of the composer @@ -290,9 +291,8 @@ function ReportActionCompose({ suggestionsRef.current.updateShouldShowSuggestionMenuToFalse(false); }, []); - const attachmentFileRef = useRef(null); - - const addAttachment = useCallback((file: FileObject | FileObject[]) => { + const attachmentFileRef = useRef(null); + const addAttachment = useCallback((file: FileObject) => { attachmentFileRef.current = file; const clear = composerRef.current?.clear; if (!clear) { @@ -318,15 +318,7 @@ function ReportActionCompose({ const newCommentTrimmed = newComment.trim(); if (attachmentFileRef.current) { - if (Array.isArray(attachmentFileRef.current)) { - // Handle multiple files - attachmentFileRef.current.forEach((file) => { - addAttachmentReportActions(reportID, file, newCommentTrimmed, true); - }); - } else { - // Handle single file - addAttachmentReportActions(reportID, attachmentFileRef.current, newCommentTrimmed, true); - } + addAttachmentReportActions(reportID, attachmentFileRef.current, newCommentTrimmed, true); attachmentFileRef.current = null; } else { Performance.markStart(CONST.TIMING.SEND_MESSAGE, {message: newCommentTrimmed}); @@ -522,8 +514,12 @@ function ReportActionCompose({ setPdfFile(null); setReceiptAndNavigate(pdfFile, true); }} - onPassword={() => setUploadReceiptError(CONST.FILE_VALIDATION_ERRORS.PROTECTED_FILE)} - onLoadError={() => setUploadReceiptError(CONST.FILE_VALIDATION_ERRORS.FILE_CORRUPTED)} + onPassword={() => { + setUploadReceiptError(true, 'attachmentPicker.attachmentError', 'attachmentPicker.protectedPDFNotSupported'); + }} + onLoadError={() => { + setUploadReceiptError(true, 'attachmentPicker.attachmentError', 'attachmentPicker.errorWhileSelectingCorruptedAttachment'); + }} /> ) : null; @@ -562,125 +558,121 @@ function ReportActionCompose({ reportID={reportID} shouldHandleNavigationBack > - {({displayFileInModal, displayMultipleFilesInModal}) => { - const handleAttachmentDrop = (event: DragEvent) => { - if (isAttachmentPreviewActive) { - return; - } - if (isBetaEnabled(CONST.BETAS.NEWDOT_MULTI_FILES_DRAG_AND_DROP) && event.dataTransfer?.files.length && event.dataTransfer?.files.length > 1) { - const files = Array.from(event.dataTransfer?.files).map((file) => { - // eslint-disable-next-line no-param-reassign - file.uri = URL.createObjectURL(file); - return file; - }); - displayMultipleFilesInModal(files); - return; - } - - const data = event.dataTransfer?.files[0]; - if (data) { - data.uri = URL.createObjectURL(data); - displayFileInModal(data); - } - }; - - return ( - <> - { - if (!shouldFocusInputOnScreenFocus) { + {({displayFileInModal}) => ( + <> + { + if (!shouldFocusInputOnScreenFocus) { + return; + } + focus(); + }} + actionButtonRef={actionButtonRef} + shouldDisableAttachmentItem={!!exceededMaxLength} + /> + { + composerRef.current = ref ?? undefined; + composerRefShared.set({ + clear: ref?.clear, + }); + }} + suggestionsRef={suggestionsRef} + isNextModalWillOpenRef={isNextModalWillOpenRef} + isScrollLikelyLayoutTriggered={isScrollLikelyLayoutTriggered} + raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLikelyLayoutTriggered} + reportID={reportID} + policyID={report?.policyID} + includeChronos={chatIncludesChronos(report)} + isGroupPolicyReport={isGroupPolicyReport} + lastReportAction={lastReportAction} + isMenuVisible={isMenuVisible} + inputPlaceholder={inputPlaceholder} + isComposerFullSize={isComposerFullSize} + setIsFullComposerAvailable={setIsFullComposerAvailable} + displayFileInModal={displayFileInModal} + onCleared={submitForm} + isBlockedFromConcierge={isBlockedFromConcierge} + disabled={disabled} + setIsCommentEmpty={setIsCommentEmpty} + handleSendMessage={handleSendMessage} + shouldShowComposeInput={shouldShowComposeInput} + onFocus={onFocus} + onBlur={onBlur} + measureParentContainer={measureContainer} + onValueChange={onValueChange} + didHideComposerInput={didHideComposerInput} + /> + {/* TODO: remove beta check after the feature is enabled */} + {isBetaEnabled(CONST.BETAS.NEWDOT_MULTI_FILES_DRAG_AND_DROP) && shouldDisplayDualDropZone && ( + { + if (isAttachmentPreviewActive) { return; } - focus(); + const data = event.dataTransfer?.files[0]; + if (data) { + data.uri = URL.createObjectURL(data); + displayFileInModal(data); + } }} - actionButtonRef={actionButtonRef} - shouldDisableAttachmentItem={!!exceededMaxLength} + onReceiptDrop={handleAddingReceipt} /> - { - composerRef.current = ref ?? undefined; - composerRefShared.set({ - clear: ref?.clear, - }); + )} + {isBetaEnabled(CONST.BETAS.NEWDOT_MULTI_FILES_DRAG_AND_DROP) && !shouldDisplayDualDropZone && ( + { + if (isAttachmentPreviewActive) { + return; + } + const data = event.dataTransfer?.files[0]; + if (data) { + data.uri = URL.createObjectURL(data); + displayFileInModal(data); + } }} - suggestionsRef={suggestionsRef} - isNextModalWillOpenRef={isNextModalWillOpenRef} - isScrollLikelyLayoutTriggered={isScrollLikelyLayoutTriggered} - raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLikelyLayoutTriggered} - reportID={reportID} - policyID={report?.policyID} - includeChronos={chatIncludesChronos(report)} - isGroupPolicyReport={isGroupPolicyReport} - lastReportAction={lastReportAction} - isMenuVisible={isMenuVisible} - inputPlaceholder={inputPlaceholder} - isComposerFullSize={isComposerFullSize} - setIsFullComposerAvailable={setIsFullComposerAvailable} - displayFileInModal={displayFileInModal} - onCleared={submitForm} - isBlockedFromConcierge={isBlockedFromConcierge} - disabled={disabled} - setIsCommentEmpty={setIsCommentEmpty} - handleSendMessage={handleSendMessage} - shouldShowComposeInput={shouldShowComposeInput} - onFocus={onFocus} - onBlur={onBlur} - measureParentContainer={measureContainer} - onValueChange={onValueChange} - didHideComposerInput={didHideComposerInput} - /> - {/* TODO: remove beta check after the feature is enabled */} - {isBetaEnabled(CONST.BETAS.NEWDOT_MULTI_FILES_DRAG_AND_DROP) && shouldDisplayDualDropZone && ( - - )} - {isBetaEnabled(CONST.BETAS.NEWDOT_MULTI_FILES_DRAG_AND_DROP) && !shouldDisplayDualDropZone && ( - - - - )} - {!isBetaEnabled(CONST.BETAS.NEWDOT_MULTI_FILES_DRAG_AND_DROP) && ( - { - if (isAttachmentPreviewActive) { - return; - } - const data = event.dataTransfer?.files[0]; - if (data) { - data.uri = URL.createObjectURL(data); - displayFileInModal(data); - } - }} + > + - )} - - ); - }} + + )} + {!isBetaEnabled(CONST.BETAS.NEWDOT_MULTI_FILES_DRAG_AND_DROP) && ( + { + if (isAttachmentPreviewActive) { + return; + } + const data = event.dataTransfer?.files[0]; + if (data) { + data.uri = URL.createObjectURL(data); + displayFileInModal(data); + } + }} + /> + )} + + )} {canUseTouchScreen() && isMediumScreenWidth ? null : (