Skip to content

Commit

Permalink
Merge pull request #28399 from hoangzinh/df/23228
Browse files Browse the repository at this point in the history
New Feature: Support file uploads on mobile (e.g. PDFs, docx, etc.)
  • Loading branch information
johnmlee101 authored Oct 11, 2023
2 parents 3a45cfd + 71d3ade commit b39ff5f
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 128 deletions.
29 changes: 18 additions & 11 deletions src/components/AttachmentPicker/index.native.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import _ from 'underscore';
import React, {useState, useRef, useCallback, useMemo} from 'react';
import PropTypes from 'prop-types';
import {View, Alert, Linking} from 'react-native';
import RNDocumentPicker from 'react-native-document-picker';
import RNFetchBlob from 'react-native-blob-util';
import lodashCompact from 'lodash/compact';
import {launchImageLibrary} from 'react-native-image-picker';
import {propTypes as basePropTypes, defaultProps} from './attachmentPickerPropTypes';
import {propTypes as basePropTypes, defaultProps as baseDefaultProps} from './attachmentPickerPropTypes';
import CONST from '../../CONST';
import * as FileUtils from '../../libs/fileDownload/FileUtils';
import * as Expensicons from '../Icon/Expensicons';
Expand All @@ -19,6 +21,14 @@ import useArrowKeyFocusManager from '../../hooks/useArrowKeyFocusManager';

const propTypes = {
...basePropTypes,

/** If this value is true, then we exclude Camera option. */
shouldHideCameraOption: PropTypes.bool,
};

const defaultProps = {
...baseDefaultProps,
shouldHideCameraOption: false,
};

/**
Expand Down Expand Up @@ -90,7 +100,7 @@ const getDataForUpload = (fileData) => {
* @param {propTypes} props
* @returns {JSX.Element}
*/
function AttachmentPicker({type, children}) {
function AttachmentPicker({type, children, shouldHideCameraOption}) {
const [isVisible, setIsVisible] = useState(false);

const completeAttachmentSelection = useRef();
Expand Down Expand Up @@ -180,8 +190,8 @@ function AttachmentPicker({type, children}) {
);

const menuItemData = useMemo(() => {
const data = [
{
const data = lodashCompact([
!shouldHideCameraOption && {
icon: Expensicons.Camera,
textTranslationKey: 'attachmentPicker.takePhoto',
pickAttachment: () => showImagePicker(launchCamera),
Expand All @@ -191,18 +201,15 @@ function AttachmentPicker({type, children}) {
textTranslationKey: 'attachmentPicker.chooseFromGallery',
pickAttachment: () => showImagePicker(launchImageLibrary),
},
];

if (type !== CONST.ATTACHMENT_PICKER_TYPE.IMAGE) {
data.push({
type !== CONST.ATTACHMENT_PICKER_TYPE.IMAGE && {
icon: Expensicons.Paperclip,
textTranslationKey: 'attachmentPicker.chooseDocument',
pickAttachment: showDocumentPicker,
});
}
},
]);

return data;
}, [showDocumentPicker, showImagePicker, type]);
}, [showDocumentPicker, showImagePicker, type, shouldHideCameraOption]);

const [focusedIndex, setFocusedIndex] = useArrowKeyFocusManager({initialFocusedIndex: -1, maxIndex: menuItemData.length - 1, isActive: isVisible});

Expand Down
150 changes: 33 additions & 117 deletions src/pages/iou/ReceiptSelector/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useCameraDevices} from 'react-native-vision-camera';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import {launchImageLibrary} from 'react-native-image-picker';
import {withOnyx} from 'react-native-onyx';
import {RESULTS} from 'react-native-permissions';
import PressableWithFeedback from '../../../components/Pressable/PressableWithFeedback';
import Icon from '../../../components/Icon';
import * as Expensicons from '../../../components/Icon/Expensicons';
import AttachmentPicker from '../../../components/AttachmentPicker';
import styles from '../../../styles/styles';
import Shutter from '../../../../assets/images/shutter.svg';
import Hand from '../../../../assets/images/hand.svg';
Expand Down Expand Up @@ -63,31 +63,6 @@ const defaultProps = {
isInTabNavigator: true,
};

/**
* See https://github.com/react-native-image-picker/react-native-image-picker/#options
* for ImagePicker configuration options
*/
const imagePickerOptions = {
includeBase64: false,
saveToPhotos: false,
selectionLimit: 1,
includeExtra: false,
};

/**
* Return imagePickerOptions based on the type
* @param {String} type
* @returns {Object}
*/
function getImagePickerOptions(type) {
// mediaType property is one of the ImagePicker configuration to restrict types'
const mediaType = type === CONST.ATTACHMENT_PICKER_TYPE.IMAGE ? 'photo' : 'mixed';
return {
mediaType,
...imagePickerOptions,
};
}

function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator}) {
const devices = useCameraDevices('wide-angle-camera');
const device = devices.back;
Expand Down Expand Up @@ -127,35 +102,6 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator})
};
}, []);

/**
* Inform the users when they need to grant camera access and guide them to settings
*/
const showPermissionsAlert = () => {
Alert.alert(
translate('attachmentPicker.cameraPermissionRequired'),
translate('attachmentPicker.expensifyDoesntHaveAccessToCamera'),
[
{
text: translate('common.cancel'),
style: 'cancel',
},
{
text: translate('common.settings'),
onPress: () => Linking.openSettings(),
},
],
{cancelable: false},
);
};

/**
* A generic handling when we don't know the exact reason for an error
*
*/
const showGeneralAlert = () => {
Alert.alert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingAttachment'));
};

const askForPermissions = () => {
// There's no way we can check for the BLOCKED status without requesting the permission first
// https://github.com/zoontek/react-native-permissions/blob/a836e114ce3a180b2b23916292c79841a267d828/README.md?plain=1#L670
Expand All @@ -172,36 +118,6 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator})
});
};

/**
* Common image picker handling
*
* @param {function} imagePickerFunc - RNImagePicker.launchCamera or RNImagePicker.launchImageLibrary
* @returns {Promise}
*/
const showImagePicker = (imagePickerFunc) =>
new Promise((resolve, reject) => {
imagePickerFunc(getImagePickerOptions(CONST.ATTACHMENT_PICKER_TYPE.IMAGE), (response) => {
if (response.didCancel) {
// When the user cancelled resolve with no attachment
return resolve();
}
if (response.errorCode) {
switch (response.errorCode) {
case 'permission':
showPermissionsAlert();
return resolve();
default:
showGeneralAlert();
break;
}

return reject(new Error(`Error during attachment selection: ${response.errorMessage}`));
}

return resolve(response.assets);
});
});

const takePhoto = useCallback(() => {
const showCameraAlert = () => {
Alert.alert(translate('receipt.cameraErrorTitle'), translate('receipt.cameraErrorMessage'));
Expand Down Expand Up @@ -284,38 +200,38 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator})
/>
)}
<View style={[styles.flexRow, styles.justifyContentAround, styles.alignItemsCenter, styles.pv3]}>
<PressableWithFeedback
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
accessibilityLabel={translate('receipt.gallery')}
style={[styles.alignItemsStart]}
onPress={() => {
showImagePicker(launchImageLibrary)
.then((receiptImage) => {
const filePath = receiptImage[0].uri;
IOU.setMoneyRequestReceipt(filePath, receiptImage[0].fileName);

if (transactionID) {
FileUtils.readFileAsync(filePath, receiptImage[0].fileName).then((receipt) => {
IOU.replaceReceipt(transactionID, receipt, filePath);
});
Navigation.dismissModal();
return;
}

IOU.navigateToNextPage(iou, iouType, report, route.path);
})
.catch(() => {
Log.info('User did not select an image from gallery');
});
}}
>
<Icon
height={32}
width={32}
src={Expensicons.Gallery}
fill={themeColors.textSupporting}
/>
</PressableWithFeedback>
<AttachmentPicker shouldHideCameraOption>
{({openPicker}) => (
<PressableWithFeedback
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
accessibilityLabel={translate('receipt.gallery')}
style={[styles.alignItemsStart]}
onPress={() => {
openPicker({
onPicked: (file) => {
const filePath = file.uri;
IOU.setMoneyRequestReceipt(filePath, file.name);

if (transactionID) {
IOU.replaceReceipt(transactionID, file, filePath);
Navigation.dismissModal();
return;
}

IOU.navigateToNextPage(iou, iouType, report, route.path);
},
});
}}
>
<Icon
height={32}
width={32}
src={Expensicons.Gallery}
fill={themeColors.textSupporting}
/>
</PressableWithFeedback>
)}
</AttachmentPicker>
<PressableWithFeedback
accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON}
accessibilityLabel={translate('receipt.shutter')}
Expand Down

0 comments on commit b39ff5f

Please sign in to comment.