Skip to content

Commit 04055d8

Browse files
committed
Merge branch 'main' into feature/useOnyx-wrapper
2 parents 0795027 + a8246a8 commit 04055d8

21 files changed

Lines changed: 601 additions & 320 deletions

File tree

Mobile-Expensify

src/CONST/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,8 @@ const CONST = {
250250

251251
// Allowed extensions for receipts
252252
ALLOWED_RECEIPT_EXTENSIONS: ['heif', 'heic', 'jpg', 'jpeg', 'gif', 'png', 'pdf', 'htm', 'html', 'text', 'rtf', 'doc', 'tif', 'tiff', 'msword', 'zip', 'xml', 'message'],
253+
254+
MAX_FILE_LIMIT: 30,
253255
},
254256

255257
// Allowed extensions for spreadsheets import
@@ -1874,6 +1876,19 @@ const CONST = {
18741876
// Video MimeTypes allowed by iOS photos app.
18751877
VIDEO: /\.(mov|mp4)$/,
18761878
},
1879+
1880+
FILE_VALIDATION_ERRORS: {
1881+
WRONG_FILE_TYPE: 'wrongFileType',
1882+
WRONG_FILE_TYPE_MULTIPLE: 'wrongFileTypeMultiple',
1883+
FILE_TOO_LARGE: 'fileTooLarge',
1884+
FILE_TOO_LARGE_MULTIPLE: 'fileTooLargeMultiple',
1885+
FILE_TOO_SMALL: 'fileTooSmall',
1886+
FILE_CORRUPTED: 'fileCorrupted',
1887+
FOLDER_NOT_ALLOWED: 'folderNotAllowed',
1888+
MAX_FILE_LIMIT_EXCEEDED: 'fileLimitExceeded',
1889+
PROTECTED_FILE: 'protectedFile',
1890+
},
1891+
18771892
IOS_CAMERA_ROLL_ACCESS_ERROR: 'Access to photo library was denied',
18781893
ADD_PAYMENT_MENU_POSITION_Y: 226,
18791894
ADD_PAYMENT_MENU_POSITION_X: 356,

src/components/AttachmentModal.tsx

Lines changed: 113 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
1414
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
1515
import attachmentModalHandler from '@libs/AttachmentModalHandler';
1616
import fileDownload from '@libs/fileDownload';
17-
import {cleanFileName, getFileName, validateImageForCorruption} from '@libs/fileDownload/FileUtils';
17+
import {cleanFileName, getFileName, getFileValidationErrorText, validateAttachment, validateImageForCorruption} from '@libs/fileDownload/FileUtils';
1818
import Navigation from '@libs/Navigation/Navigation';
1919
import {getOriginalMessage, getReportAction, isMoneyRequestAction} from '@libs/ReportActionsUtils';
2020
import {hasEReceipt, hasMissingSmartscanFields, hasReceipt, hasReceiptSource, isReceiptBeingScanned} from '@libs/TransactionUtils';
@@ -23,7 +23,6 @@ import variables from '@styles/variables';
2323
import {detachReceipt} from '@userActions/IOU';
2424
import type {IOUAction, IOUType} from '@src/CONST';
2525
import CONST from '@src/CONST';
26-
import type {TranslationPaths} from '@src/languages/types';
2726
import ONYXKEYS from '@src/ONYXKEYS';
2827
import ROUTES from '@src/ROUTES';
2928
import type * as OnyxTypes from '@src/types/onyx';
@@ -64,6 +63,7 @@ type FileObject = Partial<File | ImagePickerResponse>;
6463

6564
type 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

Comments
 (0)