@@ -14,7 +14,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
1414import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL' ;
1515import attachmentModalHandler from '@libs/AttachmentModalHandler' ;
1616import fileDownload from '@libs/fileDownload' ;
17- import { cleanFileName , getFileName , validateImageForCorruption } from '@libs/fileDownload/FileUtils' ;
17+ import { cleanFileName , getFileName , getFileValidationErrorText , validateAttachment , validateImageForCorruption } from '@libs/fileDownload/FileUtils' ;
1818import Navigation from '@libs/Navigation/Navigation' ;
1919import { getOriginalMessage , getReportAction , isMoneyRequestAction } from '@libs/ReportActionsUtils' ;
2020import { hasEReceipt , hasMissingSmartscanFields , hasReceipt , hasReceiptSource , isReceiptBeingScanned } from '@libs/TransactionUtils' ;
@@ -23,7 +23,6 @@ import variables from '@styles/variables';
2323import { detachReceipt } from '@userActions/IOU' ;
2424import type { IOUAction , IOUType } from '@src/CONST' ;
2525import CONST from '@src/CONST' ;
26- import type { TranslationPaths } from '@src/languages/types' ;
2726import ONYXKEYS from '@src/ONYXKEYS' ;
2827import ROUTES from '@src/ROUTES' ;
2928import type * as OnyxTypes from '@src/types/onyx' ;
@@ -64,6 +63,7 @@ type FileObject = Partial<File | ImagePickerResponse>;
6463
6564type ChildrenProps = {
6665 displayFileInModal : ( data : FileObject ) => void ;
66+ displayMultipleFilesInModal : ( data : FileObject [ ] ) => void ;
6767 show : ( ) => void ;
6868} ;
6969
@@ -75,7 +75,7 @@ type AttachmentModalProps = {
7575 attachmentID ?: string ;
7676
7777 /** Optional callback to fire when we want to preview an image and approve it for use. */
78- onConfirm ?: ( ( file : FileObject ) => void ) | null ;
78+ onConfirm ?: ( ( file : FileObject | FileObject [ ] ) => void ) | null ;
7979
8080 /** Whether the modal should be open by default */
8181 defaultOpen ?: boolean ;
@@ -196,11 +196,10 @@ function AttachmentModal({
196196 const styles = useThemeStyles ( ) ;
197197 const [ isModalOpen , setIsModalOpen ] = useState ( defaultOpen ) ;
198198 const [ shouldLoadAttachment , setShouldLoadAttachment ] = useState ( false ) ;
199- const [ isAttachmentInvalid , setIsAttachmentInvalid ] = useState ( false ) ;
199+ const [ fileError , setFileError ] = useState < ValueOf < typeof CONST . FILE_VALIDATION_ERRORS > | null > ( null ) ;
200+ const [ isFileErrorModalVisible , setIsFileErrorModalVisible ] = useState ( false ) ;
200201 const [ isDeleteReceiptConfirmModalVisible , setIsDeleteReceiptConfirmModalVisible ] = useState ( false ) ;
201202 const [ isAuthTokenRequiredState , setIsAuthTokenRequiredState ] = useState ( isAuthTokenRequired ) ;
202- const [ attachmentInvalidReasonTitle , setAttachmentInvalidReasonTitle ] = useState < TranslationPaths | null > ( null ) ;
203- const [ attachmentInvalidReason , setAttachmentInvalidReason ] = useState < TranslationPaths | null > ( null ) ;
204203 const [ sourceState , setSourceState ] = useState < AvatarSource > ( ( ) => source ) ;
205204 const [ modalType , setModalType ] = useState < ModalType > ( CONST . MODAL . MODAL_TYPE . CENTERED_UNSWIPEABLE ) ;
206205 const [ isConfirmButtonDisabled , setIsConfirmButtonDisabled ] = useState ( false ) ;
@@ -210,13 +209,14 @@ function AttachmentModal({
210209 const { windowWidth} = useWindowDimensions ( ) ;
211210 const { shouldUseNarrowLayout} = useResponsiveLayout ( ) ;
212211 const nope = useSharedValue ( false ) ;
213- const isOverlayModalVisible = ( isReceiptAttachment && isDeleteReceiptConfirmModalVisible ) || ( ! isReceiptAttachment && isAttachmentInvalid ) ;
212+ const isOverlayModalVisible = ( isReceiptAttachment && isDeleteReceiptConfirmModalVisible ) || ( ! isReceiptAttachment && fileError ) ;
214213 const iouType = useMemo ( ( ) => iouTypeProp ?? ( isTrackExpenseAction ? CONST . IOU . TYPE . TRACK : CONST . IOU . TYPE . SUBMIT ) , [ isTrackExpenseAction , iouTypeProp ] ) ;
215214 const parentReportAction = getReportAction ( report ?. parentReportID , report ?. parentReportActionID ) ;
216215 // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
217216 const transactionID = ( isMoneyRequestAction ( parentReportAction ) && getOriginalMessage ( parentReportAction ) ?. IOUTransactionID ) || CONST . DEFAULT_NUMBER_ID ;
218217 const [ transaction ] = useOnyx ( `${ ONYXKEYS . COLLECTION . TRANSACTION } ${ transactionID } ` , { canBeMissing : true } ) ;
219218 const [ currentAttachmentLink , setCurrentAttachmentLink ] = useState ( attachmentLink ) ;
219+ const [ validFilesToUpload , setValidFilesToUpload ] = useState < FileObject [ ] > ( [ ] ) ;
220220 const { setAttachmentError, isErrorInAttachment, clearAttachmentErrors} = useAttachmentErrors ( ) ;
221221
222222 const [ file , setFile ] = useState < FileObject | undefined > (
@@ -300,7 +300,11 @@ function AttachmentModal({
300300 }
301301
302302 if ( onConfirm ) {
303- onConfirm ( Object . assign ( file ?? { } , { source : sourceState } as FileObject ) ) ;
303+ if ( validFilesToUpload . length ) {
304+ onConfirm ( validFilesToUpload ) ;
305+ } else {
306+ onConfirm ( Object . assign ( file ?? { } , { source : sourceState } as FileObject ) ) ;
307+ }
304308 }
305309
306310 setIsModalOpen ( false ) ;
@@ -311,7 +315,7 @@ function AttachmentModal({
311315 * Close the confirm modals.
312316 */
313317 const closeConfirmModal = useCallback ( ( ) => {
314- setIsAttachmentInvalid ( false ) ;
318+ setIsFileErrorModalVisible ( false ) ;
315319 setIsDeleteReceiptConfirmModalVisible ( false ) ;
316320 } , [ ] ) ;
317321
@@ -325,44 +329,101 @@ function AttachmentModal({
325329 } , [ transaction ] ) ;
326330
327331 const isValidFile = useCallback (
328- ( fileObject : FileObject ) =>
332+ ( fileObject : FileObject , isCheckingMultipleFiles ?: boolean ) =>
329333 validateImageForCorruption ( fileObject )
330334 . then ( ( ) => {
331- if ( fileObject . size && fileObject . size > CONST . API_ATTACHMENT_VALIDATIONS . MAX_SIZE ) {
332- setIsAttachmentInvalid ( true ) ;
333- setAttachmentInvalidReasonTitle ( 'attachmentPicker.attachmentTooLarge' ) ;
334- setAttachmentInvalidReason ( 'attachmentPicker.sizeExceeded' ) ;
335- return false ;
336- }
337-
338- if ( fileObject . size && fileObject . size < CONST . API_ATTACHMENT_VALIDATIONS . MIN_SIZE ) {
339- setIsAttachmentInvalid ( true ) ;
340- setAttachmentInvalidReasonTitle ( 'attachmentPicker.attachmentTooSmall' ) ;
341- setAttachmentInvalidReason ( 'attachmentPicker.sizeNotMet' ) ;
335+ const error = validateAttachment ( fileObject , isCheckingMultipleFiles ) ;
336+ if ( error ) {
337+ setFileError ( error ) ;
338+ setIsFileErrorModalVisible ( true ) ;
342339 return false ;
343340 }
344-
345341 return true ;
346342 } )
347343 . catch ( ( ) => {
348- setIsAttachmentInvalid ( true ) ;
349- setAttachmentInvalidReasonTitle ( 'attachmentPicker.attachmentError' ) ;
350- setAttachmentInvalidReason ( 'attachmentPicker.errorWhileSelectingCorruptedAttachment' ) ;
344+ setFileError ( CONST . FILE_VALIDATION_ERRORS . FILE_CORRUPTED ) ;
345+ setIsFileErrorModalVisible ( true ) ;
351346 return false ;
352347 } ) ,
353348 [ ] ,
354349 ) ;
355350
356351 const isDirectoryCheck = useCallback ( ( data : FileObject ) => {
357352 if ( 'webkitGetAsEntry' in data && ( data as DataTransferItem ) . webkitGetAsEntry ( ) ?. isDirectory ) {
358- setIsAttachmentInvalid ( true ) ;
359- setAttachmentInvalidReasonTitle ( 'attachmentPicker.attachmentError' ) ;
360- setAttachmentInvalidReason ( 'attachmentPicker.folderNotAllowedMessage' ) ;
353+ setFileError ( CONST . FILE_VALIDATION_ERRORS . FOLDER_NOT_ALLOWED ) ;
354+ setIsFileErrorModalVisible ( true ) ;
361355 return false ;
362356 }
363357 return true ;
364358 } , [ ] ) ;
365359
360+ const handleOpenModal = useCallback (
361+ ( inputSource : string , fileObject : FileObject ) => {
362+ const inputModalType = getModalType ( inputSource , fileObject ) ;
363+ setIsModalOpen ( true ) ;
364+ setSourceState ( inputSource ) ;
365+ setFile ( fileObject ) ;
366+ setModalType ( inputModalType ) ;
367+ } ,
368+ [ getModalType , setSourceState , setFile , setModalType ] ,
369+ ) ;
370+
371+ useEffect ( ( ) => {
372+ if ( ! validFilesToUpload . length ) {
373+ return ;
374+ }
375+
376+ if ( validFilesToUpload . length > 0 ) {
377+ if ( fileError ) {
378+ return ;
379+ }
380+ const fileToDisplay = validFilesToUpload . at ( 0 ) ;
381+ if ( fileToDisplay ) {
382+ const inputSource = fileToDisplay . uri ?? '' ;
383+ handleOpenModal ( inputSource , fileToDisplay ) ;
384+ }
385+ }
386+ } , [ fileError , handleOpenModal , validFilesToUpload ] ) ;
387+
388+ const validateFiles = useCallback (
389+ ( data : FileObject [ ] ) => {
390+ let validFiles : FileObject [ ] = [ ] ;
391+
392+ Promise . all ( data . map ( ( fileToUpload ) => isValidFile ( fileToUpload , true ) . then ( ( isValid ) => ( isValid ? fileToUpload : null ) ) ) ) . then ( ( results ) => {
393+ validFiles = results . filter ( ( validFile ) : validFile is FileObject => validFile !== null ) ;
394+ setValidFilesToUpload ( validFiles ) ;
395+ } ) ;
396+ } ,
397+ [ isValidFile ] ,
398+ ) ;
399+
400+ const confirmAndContinue = ( ) => {
401+ if ( fileError === CONST . FILE_VALIDATION_ERRORS . MAX_FILE_LIMIT_EXCEEDED ) {
402+ validateFiles ( validFilesToUpload ) ;
403+ }
404+ setIsFileErrorModalVisible ( false ) ;
405+ InteractionManager . runAfterInteractions ( ( ) => {
406+ setFileError ( null ) ;
407+ } ) ;
408+ } ;
409+
410+ const validateAndDisplayMultipleFilesToUpload = useCallback (
411+ ( data : FileObject [ ] ) => {
412+ if ( ! data ?. length || data . some ( ( fileObject ) => ! isDirectoryCheck ( fileObject ) ) ) {
413+ return ;
414+ }
415+ if ( data . length > CONST . API_ATTACHMENT_VALIDATIONS . MAX_FILE_LIMIT ) {
416+ const validFiles = data . slice ( 0 , CONST . API_ATTACHMENT_VALIDATIONS . MAX_FILE_LIMIT ) ;
417+ setValidFilesToUpload ( validFiles ) ;
418+ setFileError ( CONST . FILE_VALIDATION_ERRORS . MAX_FILE_LIMIT_EXCEEDED ) ;
419+ setIsFileErrorModalVisible ( true ) ;
420+ return ;
421+ }
422+ validateFiles ( data ) ;
423+ } ,
424+ [ isDirectoryCheck , validateFiles ] ,
425+ ) ;
426+
366427 const validateAndDisplayFileToUpload = useCallback (
367428 ( data : FileObject ) => {
368429 if ( ! data || ! isDirectoryCheck ( data ) ) {
@@ -392,21 +453,13 @@ function AttachmentModal({
392453 }
393454 const inputSource = URL . createObjectURL ( updatedFile ) ;
394455 updatedFile . uri = inputSource ;
395- const inputModalType = getModalType ( inputSource , updatedFile ) ;
396- setIsModalOpen ( true ) ;
397- setSourceState ( inputSource ) ;
398- setFile ( updatedFile ) ;
399- setModalType ( inputModalType ) ;
456+ handleOpenModal ( inputSource , updatedFile ) ;
400457 } else if ( fileObject . uri ) {
401- const inputModalType = getModalType ( fileObject . uri , fileObject ) ;
402- setIsModalOpen ( true ) ;
403- setSourceState ( fileObject . uri ) ;
404- setFile ( fileObject ) ;
405- setModalType ( inputModalType ) ;
458+ handleOpenModal ( fileObject . uri , fileObject ) ;
406459 }
407460 } ) ;
408461 } ,
409- [ isValidFile , getModalType , isDirectoryCheck ] ,
462+ [ isDirectoryCheck , isValidFile , handleOpenModal ] ,
410463 ) ;
411464
412465 /**
@@ -433,6 +486,15 @@ function AttachmentModal({
433486 [ onModalClose ] ,
434487 ) ;
435488
489+ const closeAndResetModal = useCallback ( ( ) => {
490+ closeConfirmModal ( ) ;
491+ closeModal ( ) ;
492+ InteractionManager . runAfterInteractions ( ( ) => {
493+ setFileError ( null ) ;
494+ setValidFilesToUpload ( [ ] ) ;
495+ } ) ;
496+ } , [ closeConfirmModal , closeModal ] ) ;
497+
436498 /**
437499 * open the modal
438500 */
@@ -531,10 +593,10 @@ function AttachmentModal({
531593 }
532594 setShouldLoadAttachment ( false ) ;
533595 clearAttachmentErrors ( ) ;
596+ setValidFilesToUpload ( [ ] ) ;
534597 if ( isPDFLoadError . current ) {
535- setIsAttachmentInvalid ( true ) ;
536- setAttachmentInvalidReasonTitle ( 'attachmentPicker.attachmentError' ) ;
537- setAttachmentInvalidReason ( 'attachmentPicker.errorWhileSelectingCorruptedAttachment' ) ;
598+ setFileError ( CONST . FILE_VALIDATION_ERRORS . FILE_CORRUPTED ) ;
599+ setIsFileErrorModalVisible ( true ) ;
538600 return ;
539601 }
540602
@@ -682,13 +744,14 @@ function AttachmentModal({
682744 </ Modal >
683745 { ! isReceiptAttachment && (
684746 < ConfirmModal
685- title = { attachmentInvalidReasonTitle ? translate ( attachmentInvalidReasonTitle ) : '' }
686- onConfirm = { closeConfirmModal }
687- onCancel = { closeConfirmModal }
688- isVisible = { isAttachmentInvalid }
689- prompt = { attachmentInvalidReason ? translate ( attachmentInvalidReason ) : '' }
690- confirmText = { translate ( 'common.close' ) }
691- shouldShowCancelButton = { false }
747+ title = { getFileValidationErrorText ( fileError ) . title }
748+ onConfirm = { confirmAndContinue }
749+ onCancel = { closeAndResetModal }
750+ isVisible = { isFileErrorModalVisible }
751+ prompt = { getFileValidationErrorText ( fileError ) . reason }
752+ confirmText = { translate ( validFilesToUpload . length ? 'common.continue' : 'common.close' ) }
753+ shouldShowCancelButton = { ! ! validFilesToUpload . length }
754+ cancelText = { translate ( 'common.cancel' ) }
692755 onModalHide = { ( ) => {
693756 if ( ! isPDFLoadError . current ) {
694757 return ;
@@ -701,6 +764,7 @@ function AttachmentModal({
701764
702765 { children ?.( {
703766 displayFileInModal : validateAndDisplayFileToUpload ,
767+ displayMultipleFilesInModal : validateAndDisplayMultipleFilesToUpload ,
704768 show : openModal ,
705769 } ) }
706770 </ >
0 commit comments