Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions __mocks__/@react-native-documents/picker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type {FileToCopy} from '@react-native-documents/picker';

const keepLocalCopy = jest.fn();
const pick = jest.fn();
const types = Object.freeze({
allFiles: 'public.item',
audio: 'public.audio',
csv: 'public.comma-separated-values-text',
doc: 'com.microsoft.word.doc',
docx: 'org.openxmlformats.wordprocessingml.document',
images: 'public.image',
json: 'public.json',
pdf: 'com.adobe.pdf',
plainText: 'public.plain-text',
ppt: 'com.microsoft.powerpoint.ppt',
pptx: 'org.openxmlformats.presentationml.presentation',
video: 'public.movie',
xls: 'com.microsoft.excel.xls',
xlsx: 'org.openxmlformats.spreadsheetml.sheet',
zip: 'public.zip-archive',
});

export type {FileToCopy};
export {keepLocalCopy, pick, types};
33 changes: 0 additions & 33 deletions __mocks__/react-native-document-picker.ts

This file was deleted.

8 changes: 4 additions & 4 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1714,7 +1714,7 @@ PODS:
- ReactCommon/turbomodule/bridging
- ReactCommon/turbomodule/core
- Yoga
- react-native-document-picker (9.3.1):
- react-native-document-picker (10.1.1):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -2991,7 +2991,7 @@ DEPENDENCIES:
- react-native-blob-util (from `../node_modules/react-native-blob-util`)
- "react-native-cameraroll (from `../node_modules/@react-native-camera-roll/camera-roll`)"
- react-native-config (from `../node_modules/react-native-config`)
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- "react-native-document-picker (from `../node_modules/@react-native-documents/picker`)"
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-key-command (from `../node_modules/react-native-key-command`)
Expand Down Expand Up @@ -3227,7 +3227,7 @@ EXTERNAL SOURCES:
react-native-config:
:path: "../node_modules/react-native-config"
react-native-document-picker:
:path: "../node_modules/react-native-document-picker"
:path: "../node_modules/@react-native-documents/picker"
react-native-geolocation:
:path: "../node_modules/@react-native-community/geolocation"
react-native-image-picker:
Expand Down Expand Up @@ -3469,7 +3469,7 @@ SPEC CHECKSUMS:
react-native-blob-util: d65692a2acce17b7a836805114828142a3634033
react-native-cameraroll: 78710a5d35b0c6f41899a193e714564d8fd648b0
react-native-config: 5b9e180ca7beb5748ba473b257bb9a7d10e2a957
react-native-document-picker: da64a39fd71a84a9d3f7f58c8b0623c393e9991c
react-native-document-picker: 5cb1c6615796389f4f1b7fe2e4f103e38e4d6398
react-native-geolocation: cd91e4fd914de585933c836fd71f6bdb3c97a410
react-native-image-picker: 9e419813377d42566b0168121ab9483614488db5
react-native-key-command: 96c9dfb09bc89ecd1d58348c0d3ed84d0255648b
Expand Down
30 changes: 11 additions & 19 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"@react-native-clipboard/clipboard": "^1.15.0",
"@react-native-community/geolocation": "3.3.0",
"@react-native-community/netinfo": "11.2.1",
"@react-native-documents/picker": "^10.1.1",

@rayane-d rayane-d May 29, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upgrading @react-native-documents/picker to 10.1.1 caused this bug: #60600

"@react-native-firebase/analytics": "^12.3.0",
"@react-native-firebase/app": "^12.3.0",
"@react-native-firebase/crashlytics": "^12.3.0",
Expand Down Expand Up @@ -159,7 +160,6 @@
"react-native-collapsible": "^1.6.2",
"react-native-config": "1.5.3",
"react-native-device-info": "10.3.1",
"react-native-document-picker": "^9.3.1",
"react-native-draggable-flatlist": "^4.0.1",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "2.22.0",
Expand Down
106 changes: 59 additions & 47 deletions src/components/AttachmentPicker/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type {FileToCopy} from '@react-native-documents/picker';
import {keepLocalCopy, pick, types} from '@react-native-documents/picker';
import {Str} from 'expensify-common';
import {manipulateAsync, SaveFormat} from 'expo-image-manipulator';
import {ImageManipulator, SaveFormat} from 'expo-image-manipulator';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {Alert, View} from 'react-native';
import RNFetchBlob from 'react-native-blob-util';
import RNDocumentPicker from 'react-native-document-picker';
import type {DocumentPickerOptions, DocumentPickerResponse} from 'react-native-document-picker';
import {launchImageLibrary} from 'react-native-image-picker';
import type {Asset, Callback, CameraOptions, ImageLibraryOptions, ImagePickerResponse} from 'react-native-image-picker';
import ImageSize from 'react-native-image-size';
Expand All @@ -19,20 +19,27 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import {cleanFileName, showCameraPermissionsAlert, verifyFileFormat} from '@libs/fileDownload/FileUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type IconAsset from '@src/types/utils/IconAsset';
import launchCamera from './launchCamera/launchCamera';
import type AttachmentPickerProps from './types';

type LocalCopy = {
name: string | null;
uri: string;
size: number | null;
type: string | null;
};

type Item = {
/** The icon associated with the item. */
icon: IconAsset;
/** The key in the translations file to use for the title */
textTranslationKey: TranslationPaths;
/** Function to call when the user clicks the item */
pickAttachment: () => Promise<Asset[] | void | DocumentPickerResponse[]>;
pickAttachment: () => Promise<Asset[] | void | LocalCopy[]>;
};

/**
Expand All @@ -56,36 +63,14 @@ const getImagePickerOptions = (type: string, fileLimit: number): CameraOptions |
};
};

/**
* Return documentPickerOptions based on the type
* @param {String} type
* @param {Number} fileLimit
* @returns {Object}
*/

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,
};
};

/**
* The data returned from `show` is different on web and mobile, so use this function to ensure the data we
* send to the xhr will be handled properly.
*/
const getDataForUpload = (fileData: FileResponse): Promise<FileObject> => {
const fileName = fileData.name || 'chat_attachment';
const fileResult: FileObject = {
name: FileUtils.cleanFileName(fileName),
name: cleanFileName(fileName),
type: fileData.type,
width: fileData.width,
height: fileData.height,
Expand Down Expand Up @@ -158,7 +143,7 @@ function AttachmentPicker({
if (response.errorCode) {
switch (response.errorCode) {
case 'permission':
FileUtils.showCameraPermissionsAlert();
showCameraPermissionsAlert();
return resolve();
default:
showGeneralAlert();
Expand All @@ -176,11 +161,13 @@ function AttachmentPicker({
}

if (targetAsset?.type?.startsWith('image')) {
FileUtils.verifyFileFormat({fileUri: targetAssetUri, formatSignatures: CONST.HEIC_SIGNATURES})
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) {
manipulateAsync(targetAssetUri, [], {format: SaveFormat.JPEG})
ImageManipulator.manipulate(targetAssetUri)
.renderAsync()
.then((manipulatedImage) => manipulatedImage.saveAsync({format: SaveFormat.JPEG}))
.then((manipResult) => {
const uri = manipResult.uri;
const convertedAsset = {
Expand Down Expand Up @@ -211,21 +198,38 @@ function AttachmentPicker({
);
/**
* Launch the DocumentPicker. Results are in the same format as ImagePicker
*
* @returns {Promise<DocumentPickerResponse[] | void>}
*/
const showDocumentPicker = useCallback(
(): Promise<DocumentPickerResponse[] | void> =>
RNDocumentPicker.pick(getDocumentPickerOptions(type, fileLimit)).catch((error: Error) => {
if (RNDocumentPicker.isCancel(error)) {
return;
}
// eslint-disable-next-line @lwc/lwc/no-async-await
const showDocumentPicker = useCallback(async (): Promise<LocalCopy[]> => {
const pickedFiles = await pick({
type: [type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE ? types.images : types.allFiles],
allowMultiSelection: fileLimit !== 1,
});

showGeneralAlert(error.message);
throw error;
}),
[fileLimit, showGeneralAlert, type],
);
const localCopies = await keepLocalCopy({
files: pickedFiles.map((file) => {
return {
uri: file.uri,
fileName: file.name ?? '',
};
}) as [FileToCopy, ...FileToCopy[]],
destination: 'cachesDirectory',
});

return pickedFiles.map((file, index) => {
const localCopy = localCopies[index];
if (localCopy.status !== 'success') {
throw new Error("Couldn't create local file copy");
}

return {
name: file.name,
uri: localCopy.localUri,
size: file.size,
type: file.type,
};
});
}, [fileLimit, type]);

const menuItemData: Item[] = useMemo(() => {
const data: Item[] = [
Expand Down Expand Up @@ -309,7 +313,7 @@ function AttachmentPicker({
* sends the selected attachment to the caller (parent component)
*/
const pickAttachment = useCallback(
(attachments: Asset[] | DocumentPickerResponse[] | void = []): Promise<void[]> | undefined => {
(attachments: Asset[] | LocalCopy[] | void = []): Promise<void[]> | undefined => {
if (!attachments || attachments.length === 0) {
onCanceled.current();
return Promise.resolve([]);
Expand All @@ -323,7 +327,7 @@ function AttachmentPicker({

/* 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 fileDataUri = ('uri' in fileData && fileData.uri) || '';

const fileDataObject: FileResponse = {
name: fileDataName ?? '',
Expand Down Expand Up @@ -397,6 +401,14 @@ function AttachmentPicker({
onModalHide.current = () => {
setTimeout(() => {
item.pickAttachment()
.catch((error: Error) => {
if (JSON.stringify(error).includes('OPERATION_CANCELED')) {
return;
}

showGeneralAlert(error.message);
throw error;
})
.then((result) => pickAttachment(result))
.catch(console.error)
.finally(() => {
Expand All @@ -407,7 +419,7 @@ function AttachmentPicker({
};
close();
},
[pickAttachment, onOpenPicker],
[onOpenPicker, pickAttachment, showGeneralAlert],
);

useKeyboardShortcut(
Expand Down
Loading