diff --git a/src/CONST.ts b/src/CONST.ts index e448a323ef7a..b1f25163997e 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -475,6 +475,9 @@ const CONST = { }, }, NON_USD_BANK_ACCOUNT: { + ALLOWED_FILE_TYPES: ['pdf', 'jpg', 'jpeg', 'png'], + FILE_LIMIT: 10, + TOTAL_FILES_SIZE_LIMIT: 5242880, STEP: { COUNTRY: 'CountryStep', BANK_INFO: 'BankInfoStep', diff --git a/src/components/AttachmentPicker/index.native.tsx b/src/components/AttachmentPicker/index.native.tsx index d3a51c7fc0f0..bc4467e82f01 100644 --- a/src/components/AttachmentPicker/index.native.tsx +++ b/src/components/AttachmentPicker/index.native.tsx @@ -33,46 +33,46 @@ type Item = { pickAttachment: () => Promise; }; -/** - * See https://github.com/react-native-image-picker/react-native-image-picker/#options - * for ImagePicker configuration options - */ -const imagePickerOptions: Partial = { - includeBase64: false, - saveToPhotos: false, - selectionLimit: 1, - includeExtra: false, - assetRepresentationMode: 'current', -}; - /** * Return imagePickerOptions based on the type */ -const getImagePickerOptions = (type: string): CameraOptions => { +const getImagePickerOptions = (type: string, fileLimit: number): CameraOptions | ImageLibraryOptions => { // mediaType property is one of the ImagePicker configuration to restrict types' const mediaType = type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE ? 'photo' : 'mixed'; + + /** + * See https://github.com/react-native-image-picker/react-native-image-picker/#options + * for ImagePicker configuration options + */ return { mediaType, - ...imagePickerOptions, + includeBase64: false, + saveToPhotos: false, + includeExtra: false, + assetRepresentationMode: 'current', + selectionLimit: fileLimit, }; }; /** * Return documentPickerOptions based on the type * @param {String} type + * @param {Number} fileLimit * @returns {Object} */ -const getDocumentPickerOptions = (type: string): DocumentPickerOptions => { +const getDocumentPickerOptions = (type: string, fileLimit: number): DocumentPickerOptions => { if (type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE) { return { type: [RNDocumentPicker.types.images], copyTo: 'cachesDirectory', + allowMultiSelection: fileLimit !== 1, }; } return { type: [RNDocumentPicker.types.allFiles], copyTo: 'cachesDirectory', + allowMultiSelection: fileLimit !== 1, }; }; @@ -111,13 +111,14 @@ function AttachmentPicker({ type = CONST.ATTACHMENT_PICKER_TYPE.FILE, children, shouldHideCameraOption = false, - shouldHideGalleryOption = false, shouldValidateImage = true, + shouldHideGalleryOption = false, + fileLimit = 1, }: AttachmentPickerProps) { const styles = useThemeStyles(); const [isVisible, setIsVisible] = useState(false); - const completeAttachmentSelection = useRef<(data: FileObject) => void>(() => {}); + const completeAttachmentSelection = useRef<(data: FileObject[]) => void>(() => {}); const onModalHide = useRef<() => void>(); const onCanceled = useRef<() => void>(() => {}); const popoverRef = useRef(null); @@ -143,7 +144,7 @@ function AttachmentPicker({ const showImagePicker = useCallback( (imagePickerFunc: (options: CameraOptions, callback: Callback) => Promise): Promise => new Promise((resolve, reject) => { - imagePickerFunc(getImagePickerOptions(type), (response: ImagePickerResponse) => { + imagePickerFunc(getImagePickerOptions(type, fileLimit), (response: ImagePickerResponse) => { if (response.didCancel) { // When the user cancelled resolve with no attachment return resolve(); @@ -200,7 +201,7 @@ function AttachmentPicker({ } }); }), - [showGeneralAlert, type], + [fileLimit, showGeneralAlert, type], ); /** * Launch the DocumentPicker. Results are in the same format as ImagePicker @@ -209,7 +210,7 @@ function AttachmentPicker({ */ const showDocumentPicker = useCallback( (): Promise => - RNDocumentPicker.pick(getDocumentPickerOptions(type)).catch((error: Error) => { + RNDocumentPicker.pick(getDocumentPickerOptions(type, fileLimit)).catch((error: Error) => { if (RNDocumentPicker.isCancel(error)) { return; } @@ -217,7 +218,7 @@ function AttachmentPicker({ showGeneralAlert(error.message); throw error; }), - [showGeneralAlert, type], + [fileLimit, showGeneralAlert, type], ); const menuItemData: Item[] = useMemo(() => { @@ -261,7 +262,7 @@ function AttachmentPicker({ * @param onPickedHandler A callback that will be called with the selected attachment * @param onCanceledHandler A callback that will be called without a selected attachment */ - const open = (onPickedHandler: (file: FileObject) => void, onCanceledHandler: () => void = () => {}) => { + const open = (onPickedHandler: (files: FileObject[]) => void, onCanceledHandler: () => void = () => {}) => { // eslint-disable-next-line react-compiler/react-compiler completeAttachmentSelection.current = onPickedHandler; onCanceled.current = onCanceledHandler; @@ -286,7 +287,7 @@ function AttachmentPicker({ } return getDataForUpload(fileData) .then((result) => { - completeAttachmentSelection.current(result); + completeAttachmentSelection.current([result]); }) .catch((error: Error) => { showGeneralAlert(error.message); @@ -301,63 +302,78 @@ function AttachmentPicker({ * sends the selected attachment to the caller (parent component) */ const pickAttachment = useCallback( - (attachments: Asset[] | DocumentPickerResponse[] | void = []): Promise | undefined => { + (attachments: Asset[] | DocumentPickerResponse[] | void = []): Promise | undefined => { if (!attachments || attachments.length === 0) { onCanceled.current(); - return Promise.resolve(); + return Promise.resolve([]); } - const fileData = attachments[0]; - if (!fileData) { - onCanceled.current(); - return Promise.resolve(); - } - /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ - const fileDataName = ('fileName' in fileData && fileData.fileName) || ('name' in fileData && fileData.name) || ''; - const fileDataUri = ('fileCopyUri' in fileData && fileData.fileCopyUri) || ('uri' in fileData && fileData.uri) || ''; - - const fileDataObject: FileResponse = { - name: fileDataName ?? '', - uri: fileDataUri, - size: ('size' in fileData && fileData.size) || ('fileSize' in fileData && fileData.fileSize) || null, - type: fileData.type ?? '', - width: ('width' in fileData && fileData.width) || undefined, - height: ('height' in fileData && fileData.height) || undefined, - }; + const filesToProcess = attachments.map((fileData) => { + if (!fileData) { + onCanceled.current(); + return Promise.resolve(); + } - if (!shouldValidateImage && fileDataName && Str.isImage(fileDataName)) { - ImageSize.getSize(fileDataUri) - .then(({width, height}) => { - fileDataObject.width = width; - fileDataObject.height = height; - return fileDataObject; - }) - .then((file) => { - getDataForUpload(file) - .then((result) => { - completeAttachmentSelection.current(result); - }) - .catch((error: Error) => { - showGeneralAlert(error.message); - throw error; - }); - }); - return; - } - /* eslint-enable @typescript-eslint/prefer-nullish-coalescing */ - if (fileDataName && Str.isImage(fileDataName)) { - ImageSize.getSize(fileDataUri) - .then(({width, height}) => { - fileDataObject.width = width; - fileDataObject.height = height; - validateAndCompleteAttachmentSelection(fileDataObject); - }) - .catch(() => showImageCorruptionAlert()); - } else { + /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ + const fileDataName = ('fileName' in fileData && fileData.fileName) || ('name' in fileData && fileData.name) || ''; + const fileDataUri = ('fileCopyUri' in fileData && fileData.fileCopyUri) || ('uri' in fileData && fileData.uri) || ''; + + const fileDataObject: FileResponse = { + name: fileDataName ?? '', + uri: fileDataUri, + size: ('size' in fileData && fileData.size) || ('fileSize' in fileData && fileData.fileSize) || null, + type: fileData.type ?? '', + width: ('width' in fileData && fileData.width) || undefined, + height: ('height' in fileData && fileData.height) || undefined, + }; + + if (!shouldValidateImage && fileDataName && Str.isImage(fileDataName)) { + return ImageSize.getSize(fileDataUri) + .then(({width, height}) => { + fileDataObject.width = width; + fileDataObject.height = height; + return fileDataObject; + }) + .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(); + }); + } + + if (fileDataName && Str.isImage(fileDataName)) { + return ImageSize.getSize(fileDataUri) + .then(({width, height}) => { + fileDataObject.width = width; + fileDataObject.height = height; + + if (fileDataObject.width <= 0 || fileDataObject.height <= 0) { + showImageCorruptionAlert(); + return Promise.resolve(); // Skip processing this corrupted file + } + + return validateAndCompleteAttachmentSelection(fileDataObject); + }) + .catch(() => { + showImageCorruptionAlert(); + }); + } return validateAndCompleteAttachmentSelection(fileDataObject); - } + }); + + return Promise.all(filesToProcess); }, - [validateAndCompleteAttachmentSelection, showImageCorruptionAlert, shouldValidateImage, showGeneralAlert], + [shouldValidateImage, validateAndCompleteAttachmentSelection, showGeneralAlert, showImageCorruptionAlert], ); /** diff --git a/src/components/AttachmentPicker/index.tsx b/src/components/AttachmentPicker/index.tsx index f3c880fcb835..2484198d3916 100644 --- a/src/components/AttachmentPicker/index.tsx +++ b/src/components/AttachmentPicker/index.tsx @@ -1,5 +1,6 @@ import React, {useRef} from 'react'; import type {ValueOf} from 'type-fest'; +import type {FileObject} from '@components/AttachmentModal'; import * as Browser from '@libs/Browser'; import Visibility from '@libs/Visibility'; import CONST from '@src/CONST'; @@ -42,9 +43,9 @@ function getAcceptableFileTypesFromAList(fileTypes: Array