Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
101 commits
Select commit Hold shift + click to select a range
8b72a83
feat: allow multiple files
koko57 Jun 2, 2025
cbaf882
fix: resolve conflicts
koko57 Jun 3, 2025
19fa998
feat: create a new function for multiple files
koko57 Jun 3, 2025
bcd9dd1
feat: create validation utils
koko57 Jun 3, 2025
d0190be
feat: exctract isValidReceiptExtension
koko57 May 15, 2025
18fdcae
fix: minor fix
koko57 Jun 3, 2025
2d963f5
feat: refactor error handling for the attachment modal
koko57 May 15, 2025
7fdda7e
fix: resolve naming conflict
koko57 May 16, 2025
186a45f
feat: add translations
koko57 Jun 3, 2025
3e9b09a
feat: show error modal when max file limit exceeded
koko57 Jun 3, 2025
18e7db1
fix: minor translation improvement
koko57 Jun 3, 2025
0e039f6
fix: resolve conflicts
koko57 Jun 4, 2025
9b55b87
feat: validate multiple files
koko57 Jun 4, 2025
18fc660
feat: show error modal
koko57 Jun 4, 2025
5e76042
feat: add other error cases
koko57 Jun 5, 2025
f762127
fix: resolve conflicts
koko57 Jun 5, 2025
fcb66ec
fix: minor fix
koko57 Jun 6, 2025
da2224c
Merge branch 'main' into feat/59443-multi-files-attachments
koko57 Jun 6, 2025
eebdcbe
fix: resolve conflicts
koko57 Jun 6, 2025
3cd6658
Merge branch 'feat/59442-split-screen-dropzone' into feat/59443-multi…
koko57 Jun 8, 2025
92a0837
feat: add protected file validation error
koko57 Jun 9, 2025
b64b63b
feat: handle PDF validation inside a hook
koko57 Jun 9, 2025
a74199c
fix: resolve conflicts
koko57 Jun 11, 2025
4a449c2
feat: file validation for mixed files
koko57 Jun 11, 2025
4039bf8
Merge branch 'main' into feat/59443-multi-files-attachments
koko57 Jun 12, 2025
817d8c9
feat: move confirmation error modal to the hook
koko57 Jun 12, 2025
5ac67a6
refactor: change getFileValidationErrorText to return translated paths
koko57 Jun 12, 2025
69716b9
feat: display invalid file type
koko57 Jun 12, 2025
a41837d
fix: show link
koko57 Jun 12, 2025
8567d25
fix: resolve conflicts
koko57 Jun 16, 2025
ebd4a38
feat: handle max file limit error
koko57 Jun 16, 2025
6ac2a50
feat: accept multiple files on mobile
koko57 Jun 16, 2025
ddb8922
feat: handle multiple different errors
koko57 Jun 16, 2025
e377d4f
fix: resolve conflicts
koko57 Jun 18, 2025
fae680b
Merge branch 'main' into feat/59443-multi-files-attachments
koko57 Jun 18, 2025
4db6682
feat: move function to be reused
koko57 Jun 18, 2025
9856876
feat: support multiple files
koko57 Jun 20, 2025
1592e97
fix: resolve conflicts
koko57 Jun 20, 2025
8b32ab1
Merge branch 'main' into feat/59443-multi-files-attachments
koko57 Jun 23, 2025
aeba168
feat: enable dropping multiple files on the Reports Page
koko57 Jun 23, 2025
587b369
feat: enable multiple receipts in chats
koko57 Jun 23, 2025
74870a4
fix: minor fix
koko57 Jun 23, 2025
06e3b14
fix: minor fix
koko57 Jun 23, 2025
31fd6ee
feat: use new hook in Confirmation step
koko57 Jun 23, 2025
3350d70
feat: useFilesValidation
koko57 Jun 24, 2025
44c085c
feat: create new util function
koko57 Jun 24, 2025
45caac5
feat: convert heic to jpeg
koko57 Jun 24, 2025
ec94399
feat: convert and resize files when validating
koko57 Jun 25, 2025
c000f83
feat: use unique errors
koko57 Jun 25, 2025
b2285e0
feat: enable picking multiple files on mobile
koko57 Jun 25, 2025
0b02361
fix: remove unnecessary line
koko57 Jun 25, 2025
98d70ed
fix: revert new hook for mobile
koko57 Jun 25, 2025
d7cc845
Merge branch 'main' into feat/59441-multiple-receipts
koko57 Jun 25, 2025
768c23b
fix: use fullscreen loader from the hook
koko57 Jun 25, 2025
6771552
fix: create new transactions when more files dropped
koko57 Jun 25, 2025
4454498
fix: run prettier
koko57 Jun 25, 2025
6609222
fix: remove isLoadingReceipt
koko57 Jun 25, 2025
e7747a9
feat: use useFilesValidation hook in IOU Scan for mobile
koko57 Jun 25, 2025
00fb9c0
fix: remove duplicate
koko57 Jun 25, 2025
ffa15d4
fix: eslint
koko57 Jun 26, 2025
3995db3
Merge branch 'main' into feat/59441-multiple-receipts
koko57 Jun 26, 2025
4a8307c
fix: prettier
koko57 Jun 26, 2025
cb668d8
fix: lint
koko57 Jun 26, 2025
006ce21
fix: lint
koko57 Jun 26, 2025
3f8f786
fix: revert change
koko57 Jun 26, 2025
efd102a
fix: typecheck
koko57 Jun 26, 2025
63c0929
fix: resolve conflicts
koko57 Jul 1, 2025
d49c88b
fix: typecheck
koko57 Jul 1, 2025
a0e1eb0
fix: resolve conflicts
koko57 Jul 1, 2025
270bb21
fix: revert unnecessary changes
koko57 Jul 1, 2025
54f2a01
fix: typecheck
koko57 Jul 1, 2025
a784403
fix: add a new receipt when one file already dropped
koko57 Jul 1, 2025
c1a587d
feat: add copies for multi files
koko57 Jul 1, 2025
7adb81c
fix: remove unnecessary imports
koko57 Jul 1, 2025
f3788eb
Merge branch 'main' into feat/59441-multiple-receipts
koko57 Jul 2, 2025
bdbb665
fix: skip participants page when having active policy
koko57 Jul 2, 2025
1f80d4f
fix: add check for closed reports
koko57 Jul 2, 2025
90afce1
fix: show dual dropzone only when it's possible to create money request
koko57 Jul 2, 2025
5474f5c
fix: remove unnecessary checks
koko57 Jul 2, 2025
f02938d
fix: remove unnecessary imports
koko57 Jul 2, 2025
ee15d57
fix: revert permissions
koko57 Jul 2, 2025
322c8a4
fix: revert changes
koko57 Jul 2, 2025
41b2713
fix: resolve conflicts
koko57 Jul 3, 2025
f434769
fix: add missing translations
koko57 Jul 3, 2025
a9afcc8
fix: revert changes in ReportActionCompose
koko57 Jul 3, 2025
4e86dc5
fix: run prettier
koko57 Jul 3, 2025
0095c23
fix: add missing translation
koko57 Jul 3, 2025
57658a9
fix: track expense when self dm
koko57 Jul 3, 2025
0968db2
fix: resolve conflicts
koko57 Jul 3, 2025
4343a36
fix: apply requested changes
koko57 Jul 3, 2025
2460c91
fix: accept single file when adding a receipt to a manual request
koko57 Jul 3, 2025
8950cad
Merge branch 'main' into feat/59441-multiple-receipts
koko57 Jul 3, 2025
417be2d
fix: add suggested condition
koko57 Jul 3, 2025
0c1cee6
fix: change copies to singular
koko57 Jul 3, 2025
e6977fd
fix: confirmation drop title
koko57 Jul 3, 2025
961884e
fix: restore reverted code
koko57 Jul 3, 2025
eab67d1
fix: restore native picker changes
koko57 Jul 3, 2025
7e7f50a
Merge branch 'main' into feat/59441-multiple-receipts
koko57 Jul 4, 2025
2177bb7
fix: preserve file when cloning request
koko57 Jul 4, 2025
e10e824
fix: prettier
koko57 Jul 4, 2025
dda0173
fix: prevent from adding more receipts when going back to scan when b…
koko57 Jul 4, 2025
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
15 changes: 15 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ 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
Expand Down Expand Up @@ -1879,6 +1881,19 @@ 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,
Expand Down
167 changes: 89 additions & 78 deletions src/components/AttachmentPicker/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,45 +153,68 @@ function AttachmentPicker({
return reject(new Error(`Error during attachment selection: ${response.errorMessage}`));
}

const targetAsset = response.assets?.[0];
const targetAssetUri = targetAsset?.uri;

if (!targetAssetUri) {
const assets = response.assets;
if (!assets || assets.length === 0) {
return resolve();
}

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);
}
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();
}
});
});
}),
[fileLimit, showGeneralAlert, type],
Expand Down Expand Up @@ -287,42 +310,20 @@ 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<void[]> | undefined => {
(attachments: Asset[] | LocalCopy[] | void = []): Promise<void> | undefined => {
if (!attachments || attachments.length === 0) {
onCanceled.current();
return Promise.resolve([]);
return Promise.resolve();
}

const filesToProcess = attachments.map((fileData) => {
if (!fileData) {
onCanceled.current();
return Promise.resolve();
return Promise.resolve(null);
}

/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
Expand All @@ -345,20 +346,10 @@ function AttachmentPicker({
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;
});
})
.then((file) => getDataForUpload(file))
.catch(() => {
showImageCorruptionAlert();
return null;
});
}

Expand All @@ -370,21 +361,41 @@ function AttachmentPicker({

if (fileDataObject.width <= 0 || fileDataObject.height <= 0) {
showImageCorruptionAlert();
return Promise.resolve(); // Skip processing this corrupted file
return null;
}

return validateAndCompleteAttachmentSelection(fileDataObject);
return getDataForUpload(fileDataObject);
})
.catch(() => {
showImageCorruptionAlert();
return null;
});
}
return validateAndCompleteAttachmentSelection(fileDataObject);

return getDataForUpload(fileDataObject).catch((error: Error) => {
showGeneralAlert(error.message);
return null;
});
});

return Promise.all(filesToProcess);
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');
}
});
},
[shouldValidateImage, validateAndCompleteAttachmentSelection, showGeneralAlert, showImageCorruptionAlert],
[shouldValidateImage, showGeneralAlert, showImageCorruptionAlert],
);

/**
Expand Down
12 changes: 9 additions & 3 deletions src/components/AttachmentPicker/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,15 @@ function AttachmentPicker({children, type = CONST.ATTACHMENT_PICKER_TYPE.FILE, a
return;
}

const file = e.target.files[0];

if (file) {
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];
file.uri = URL.createObjectURL(file);
onPicked.current([file]);
}
Expand Down
62 changes: 0 additions & 62 deletions src/hooks/useFileValidation.ts

This file was deleted.

Loading
Loading