Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
a058146
feat: improve attachment validation
chrispader Sep 17, 2025
6739696
remove;: unused import
chrispader Sep 18, 2025
542334d
Merge branch 'main' into pr/70740
chrispader Feb 18, 2026
0c4a513
refactor: rename CONST variable block
chrispader Feb 18, 2026
76ee521
fix: add missing CONST values
chrispader Feb 18, 2026
0079017
fix: legacy CONST key
chrispader Feb 18, 2026
ff9c18c
fix: incorrect usage of `useFilesValidation` hook
chrispader Feb 18, 2026
6035f9a
fix: update usage of validation functions
chrispader Feb 18, 2026
33e2bfc
Merge branch 'main' into pr/70740
chrispader Feb 19, 2026
3be89c0
fix: `getFileValidationErrorText` type error
chrispader Feb 19, 2026
4f9692c
fix: use `Log` instaed of `console`
chrispader Feb 19, 2026
662e189
fix: pass down validation options to validator function
chrispader Feb 19, 2026
34063ca
fix: pass file objects instead of wrapper struct
chrispader Feb 19, 2026
f014e7f
refactor: remove unused `getConfirmModalPrompt` function
chrispader Feb 19, 2026
33cd2be
refactor: refactor `useFilesValidation` hook for more readability
chrispader Feb 19, 2026
843acd4
test: fix test cases
chrispader Feb 19, 2026
02e9868
test: fix file error cases with multiple files
chrispader Feb 19, 2026
24019c5
test: add more test cases for attachment file validation
chrispader Feb 19, 2026
5e585aa
fix: failing tests
chrispader Feb 19, 2026
fade3fd
fix: invalid options param
chrispader Feb 19, 2026
662308c
refactor: convert to async functions
chrispader Feb 19, 2026
9b5a71c
fix: remove unnecessary array vs. single instance checks
chrispader Feb 19, 2026
b6993e7
fix: replace `console.error`
chrispader Feb 19, 2026
67425eb
Merge branch 'main' into pr/70740
chrispader Feb 26, 2026
b341077
fix: prettier
chrispader Feb 26, 2026
d07538e
Merge branch 'main' into pr/70740
chrispader Feb 26, 2026
89479bb
refactor: AttachmentValidation functions
chrispader Feb 27, 2026
3f73a1e
refactor: further simplify validation logic
chrispader Feb 27, 2026
667393f
fix: reset on valid validation
chrispader Feb 27, 2026
2d12c2e
Merge branch 'main' into pr/70740
chrispader Mar 2, 2026
77ea218
fix: prettier
chrispader Mar 2, 2026
27e0b96
Merge branch 'main' into pr/70740
chrispader Mar 6, 2026
431f5be
fix: undo invalid change
chrispader Mar 6, 2026
68bbd95
Merge branch 'main' into pr/70740
chrispader Mar 9, 2026
c192b3d
Merge branch 'main' into pr/70740
chrispader Mar 12, 2026
696c820
fix: validate corrupted files
chrispader Mar 12, 2026
8410652
refactor: merge validation error constants
chrispader Mar 12, 2026
f8fdeea
refactor: improve `useFilesValidation` code and make use of async-await
chrispader Mar 12, 2026
a8ae217
fix: only set validate multiple files state if more than 1
chrispader Mar 12, 2026
5811936
refactor: simplify attachment validation logic
chrispader Mar 13, 2026
893f03a
refactor: extract back `validateAttachmentFile` function
chrispader Mar 13, 2026
2cab9ce
chore: update tests
chrispader Mar 13, 2026
60a8d05
chore: update tests
chrispader Mar 13, 2026
592e71b
fix: attachment validation in `ReportAddAttachmentModalContent`
chrispader Mar 13, 2026
92e4dd5
test: update tests to match new `validateAttachmentFile` implementation
chrispader Mar 13, 2026
3729607
refactor: rename reset callback
chrispader Mar 13, 2026
acee6c8
Merge branch 'main' into pr/70740
chrispader Mar 13, 2026
73b5ca3
refactor: file validation logic
chrispader Mar 13, 2026
a1fb241
test: fix failing test
chrispader Mar 13, 2026
ae0deea
fix: always resolve file conversion promise
chrispader Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2325,16 +2325,16 @@ const CONST = {
},

FILE_VALIDATION_ERRORS: {
FILE_INVALID: 'fileInvalid',
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',
HEIC_OR_HEIF_IMAGE: 'heicOrHeifImage',
IMAGE_DIMENSIONS_TOO_LARGE: 'imageDimensionsTooLarge',
FOLDER_NOT_ALLOWED: 'folderNotAllowed',
MAX_FILE_LIMIT_EXCEEDED: 'maxFileLimitExceeded',
},

IOS_CAMERA_ROLL_ACCESS_ERROR: 'Access to photo library was denied',
Expand Down
302 changes: 141 additions & 161 deletions src/hooks/useFilesValidation.tsx

Large diffs are not rendered by default.

106 changes: 0 additions & 106 deletions src/libs/AttachmentUtils.ts

This file was deleted.

133 changes: 52 additions & 81 deletions src/libs/fileDownload/FileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -644,58 +644,28 @@ const hasHeicOrHeifExtension = (file: FileObject) => {
* Otherwise, it attempts to fetch the file via its URI and reconstruct a File
* with full metadata (name, size, type).
*/
const normalizeFileObject = (file: FileObject): Promise<FileObject> => {
const normalizeFileObject = async (file: FileObject): Promise<FileObject> => {
if (file instanceof File || file instanceof Blob) {
return Promise.resolve(file);
return file;
}

const isAndroidNative = getPlatform() === CONST.PLATFORM.ANDROID;
const isIOSNative = getPlatform() === CONST.PLATFORM.IOS;
const isNativePlatform = isAndroidNative || isIOSNative;

if (!isNativePlatform || 'size' in file) {
return Promise.resolve(file);
return file;
}

if (typeof file.uri !== 'string') {
return Promise.resolve(file);
return file;
}

return fetch(file.uri)
.then((response) => response.blob())
.then((blob) => {
const name = file.name ?? 'unknown';
const type = file.type ?? blob.type ?? 'application/octet-stream';
const normalizedFile = new File([blob], name, {type});
return normalizedFile;
})
.catch((error) => {
return Promise.reject(error);
});
};

type ValidateAttachmentOptions = {
isValidatingReceipts?: boolean;
isValidatingMultipleFiles?: boolean;
};

const validateAttachment = (file: FileObject, validationOptions?: ValidateAttachmentOptions) => {
const maxFileSize = validationOptions?.isValidatingReceipts ? CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE : CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE;

if (validationOptions?.isValidatingReceipts && !isValidReceiptExtension(file)) {
return validationOptions?.isValidatingMultipleFiles ? CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE_MULTIPLE : CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE;
}

// Images are exempt from file size check since they will be resized
if (!Str.isImage(file.name ?? '') && !hasHeicOrHeifExtension(file) && (file?.size ?? 0) > maxFileSize) {
return validationOptions?.isValidatingMultipleFiles ? CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE_MULTIPLE : CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE;
}

if (validationOptions?.isValidatingReceipts && (file?.size ?? 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
return CONST.FILE_VALIDATION_ERRORS.FILE_TOO_SMALL;
}

return '';
const response = await fetch(file.uri);
const blob = await response.blob();
const name = file.name ?? 'unknown';
const type = file.type ?? blob.type ?? 'application/octet-stream';
return new File([blob], name, {type});
};

type TranslationAdditionalData = {
Expand All @@ -704,11 +674,16 @@ type TranslationAdditionalData = {
fileType?: string;
};

type GetFileValidationErrorTextOptions = {
isValidatingReceipt?: boolean;
isValidatingMultipleFiles?: boolean;
};

const getFileValidationErrorText = (
translate: LocalizedTranslate,
validationError: ValueOf<typeof CONST.FILE_VALIDATION_ERRORS> | null,
additionalData: TranslationAdditionalData = {},
isValidatingReceipt = false,
options: GetFileValidationErrorTextOptions = {},
): {
title: string;
reason: string;
Expand All @@ -719,49 +694,57 @@ const getFileValidationErrorText = (
reason: '',
};
}
const maxSize = isValidatingReceipt ? CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE : CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE;
const maxSize = options.isValidatingReceipt ? CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE : CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE;

if (options.isValidatingMultipleFiles) {
switch (validationError) {
case CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE:
return {
title: translate('attachmentPicker.someFilesCantBeUploaded'),
reason: translate('attachmentPicker.unsupportedFileType', additionalData.fileType ?? ''),
};
case CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE:
return {
title: translate('attachmentPicker.someFilesCantBeUploaded'),
reason: translate('attachmentPicker.sizeLimitExceeded', {
maxUploadSizeInMB: additionalData.maxUploadSizeInMB ?? maxSize / 1024 / 1024,
}),
};
case CONST.FILE_VALIDATION_ERRORS.FOLDER_NOT_ALLOWED:
return {
title: translate('attachmentPicker.attachmentError'),
reason: translate('attachmentPicker.folderNotAllowedMessage'),
};
case CONST.FILE_VALIDATION_ERRORS.MAX_FILE_LIMIT_EXCEEDED:
return {
title: translate('attachmentPicker.someFilesCantBeUploaded'),
reason: translate('attachmentPicker.maxFileLimitExceeded'),
};
default:
break;
}
}

switch (validationError) {
case CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE:
return {
title: translate('attachmentPicker.wrongFileType'),
reason: translate('attachmentPicker.notAllowedExtension'),
};
case CONST.FILE_VALIDATION_ERRORS.WRONG_FILE_TYPE_MULTIPLE:
return {
title: translate('attachmentPicker.someFilesCantBeUploaded'),
reason: translate('attachmentPicker.unsupportedFileType', additionalData.fileType ?? ''),
};
case CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE:
return {
title: translate('attachmentPicker.attachmentTooLarge'),
reason: isValidatingReceipt
reason: options.isValidatingReceipt
? translate('attachmentPicker.sizeExceededWithLimit', {
maxUploadSizeInMB: additionalData.maxUploadSizeInMB ?? CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE / 1024 / 1024,
})
: translate('attachmentPicker.sizeExceeded'),
};
case CONST.FILE_VALIDATION_ERRORS.FILE_TOO_LARGE_MULTIPLE:
return {
title: translate('attachmentPicker.someFilesCantBeUploaded'),
reason: translate('attachmentPicker.sizeLimitExceeded', {
maxUploadSizeInMB: additionalData.maxUploadSizeInMB ?? maxSize / 1024 / 1024,
}),
};
case CONST.FILE_VALIDATION_ERRORS.FILE_TOO_SMALL:
return {
title: translate('attachmentPicker.attachmentTooSmall'),
reason: translate('attachmentPicker.sizeNotMet'),
};
case CONST.FILE_VALIDATION_ERRORS.FOLDER_NOT_ALLOWED:
return {
title: translate('attachmentPicker.attachmentError'),
reason: translate('attachmentPicker.folderNotAllowedMessage'),
};
case CONST.FILE_VALIDATION_ERRORS.MAX_FILE_LIMIT_EXCEEDED:
return {
title: translate('attachmentPicker.someFilesCantBeUploaded'),
reason: translate('attachmentPicker.maxFileLimitExceeded'),
};
case CONST.FILE_VALIDATION_ERRORS.FILE_CORRUPTED:
return {
title: translate('attachmentPicker.attachmentError'),
Expand All @@ -778,21 +761,13 @@ const getFileValidationErrorText = (
reason: translate('attachmentPicker.imageDimensionsTooLarge'),
};
default:
return {
title: translate('attachmentPicker.attachmentError'),
reason: translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'),
};
break;
}
};

const getConfirmModalPrompt = (translate: LocalizedTranslate, attachmentInvalidReason: TranslationPaths | undefined) => {
if (!attachmentInvalidReason) {
return '';
}
if (attachmentInvalidReason === 'attachmentPicker.sizeExceededWithLimit') {
return translate(attachmentInvalidReason, {maxUploadSizeInMB: CONST.API_ATTACHMENT_VALIDATIONS.RECEIPT_MAX_SIZE / (1024 * 1024)});
}
return translate(attachmentInvalidReason);
return {
title: translate('attachmentPicker.attachmentError'),
reason: translate('attachmentPicker.errorWhileSelectingCorruptedAttachment'),
};
};

const MAX_CANVAS_SIZE = 4096;
Expand Down Expand Up @@ -906,16 +881,12 @@ export {
resizeImageIfNeeded,
createFile,
validateReceipt,
validateAttachment,
normalizeFileObject,
isValidReceiptExtension,
getFileValidationErrorText,
hasHeicOrHeifExtension,
getConfirmModalPrompt,
canvasFallback,
getFilesFromClipboardEvent,
cleanFileObject,
cleanFileObjectName,
};

export type {ValidateAttachmentOptions};
Loading
Loading