Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added validation for image in reciept scan #27960

Closed
3 changes: 3 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ const CONST = {

// An arbitrary size, but the same minimum as in the PHP layer
MIN_SIZE: 240,

// File extensions that are not allowed to be uploaded
UNALLOWED_EXTENSIONS: [],
},

AUTO_AUTH_STATE: {
Expand Down
89 changes: 89 additions & 0 deletions src/hooks/useReceiptValidation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {useState, useCallback} from 'react';
import {Alert, Platform} from 'react-native';
import lodashGet from 'lodash/get';
import _ from 'underscore';
import useLocalize from './useLocalize';
import * as FileUtils from '../libs/fileDownload/FileUtils';
import CONST from '../CONST';

export default function useReceiptValidation() {
const {translate} = useLocalize();
const [receiptValidation, setReceiptValidation] = useState({
isReceiptInvalid: false,
title: '',
reason: '',
});

const resetValidationErrors = useCallback(() => {
setReceiptValidation({
...receiptValidation,
title: '',
reason: '',
});
}, [receiptValidation]);

const resetValidation = useCallback(() => {
setReceiptValidation({
...receiptValidation,
isReceiptInvalid: false,
});
}, [receiptValidation]);

const showImageCorruptionAlert = useCallback(() => {
Alert.alert(translate('attachmentPicker.attachmentError'), translate('attachmentPicker.errorWhileSelectingCorruptedImage'));
}, [translate]);

const setUploadReceiptError = useCallback((isInvalid, title, reason) => {
if (Platform.OS !== 'ios') {
setReceiptValidation({
isReceiptInvalid: isInvalid,
title,
reason,
});
return;
}

setTimeout(() => {
setReceiptValidation({
isReceiptInvalid: isInvalid,
title,
reason,
});
}, CONST.ANIMATED_TRANSITION);
}, []);

const validateReceipt = useCallback(
(file) => {
const fileName = lodashGet(file, 'fileName', '') || lodashGet(file, 'name', '');
const {fileExtension} = FileUtils.splitExtensionFromFileName(fileName);

if (_.contains(CONST.API_ATTACHMENT_VALIDATIONS.UNALLOWED_EXTENSIONS, fileExtension.toLowerCase())) {
setUploadReceiptError(true, 'attachmentPicker.wrongFileType', 'attachmentPicker.notAllowedExtension');
return false;
}

const isFileCorrupted = (file.width !== undefined && file.width <= 0) || (file.height !== undefined && file.height <= 0);

if (isFileCorrupted) {
showImageCorruptionAlert();
return false;
}

const fileSize = lodashGet(file, 'fileSize', 0) || lodashGet(file, 'size', 0);
if (fileSize > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
setUploadReceiptError(true, 'attachmentPicker.attachmentTooLarge', 'attachmentPicker.sizeExceeded');
return false;
}

if (fileSize < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
setUploadReceiptError(true, 'attachmentPicker.attachmentTooSmall', 'attachmentPicker.sizeNotMet');
return false;
}

return true;
},
[setUploadReceiptError, showImageCorruptionAlert],
);

return {resetValidation, resetValidationErrors, validateReceipt, receiptValidation};
}
54 changes: 8 additions & 46 deletions src/pages/iou/ReceiptSelector/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {View, Text, PanResponder, PixelRatio} from 'react-native';
import React, {useContext, useRef, useState} from 'react';
import lodashGet from 'lodash/get';
import _ from 'underscore';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import * as IOU from '../../../libs/actions/IOU';
Expand All @@ -19,8 +18,8 @@ import useWindowDimensions from '../../../hooks/useWindowDimensions';
import useLocalize from '../../../hooks/useLocalize';
import {DragAndDropContext} from '../../../components/DragAndDrop/Provider';
import {iouPropTypes, iouDefaultProps} from '../propTypes';
import * as FileUtils from '../../../libs/fileDownload/FileUtils';
import Navigation from '../../../libs/Navigation/Navigation';
import useReceiptValidation from '../../../hooks/useReceiptValidation';

const propTypes = {
/** The report on which the request is initiated on */
Expand Down Expand Up @@ -61,49 +60,11 @@ const defaultProps = {

function ReceiptSelector(props) {
const iouType = lodashGet(props.route, 'params.iouType', '');
const [isAttachmentInvalid, setIsAttachmentInvalid] = useState(false);
const [attachmentInvalidReasonTitle, setAttachmentInvalidReasonTitle] = useState('');
const [attachmentInvalidReason, setAttachmentValidReason] = useState('');
const [receiptImageTopPosition, setReceiptImageTopPosition] = useState(0);
const {isSmallScreenWidth} = useWindowDimensions();
const {translate} = useLocalize();
const {isDraggingOver} = useContext(DragAndDropContext);

const hideReciptModal = () => {
setIsAttachmentInvalid(false);
};

/**
* Sets the upload receipt error modal content when an invalid receipt is uploaded
* @param {*} isInvalid
* @param {*} title
* @param {*} reason
*/
const setUploadReceiptError = (isInvalid, title, reason) => {
setIsAttachmentInvalid(isInvalid);
setAttachmentInvalidReasonTitle(title);
setAttachmentValidReason(reason);
};

function validateReceipt(file) {
const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(file, 'name', ''));
if (_.contains(CONST.API_ATTACHMENT_VALIDATIONS.UNALLOWED_EXTENSIONS, fileExtension.toLowerCase())) {
setUploadReceiptError(true, 'attachmentPicker.wrongFileType', 'attachmentPicker.notAllowedExtension');
return false;
}

if (lodashGet(file, 'size', 0) > CONST.API_ATTACHMENT_VALIDATIONS.MAX_SIZE) {
setUploadReceiptError(true, 'attachmentPicker.attachmentTooLarge', 'attachmentPicker.sizeExceeded');
return false;
}

if (lodashGet(file, 'size', 0) < CONST.API_ATTACHMENT_VALIDATIONS.MIN_SIZE) {
setUploadReceiptError(true, 'attachmentPicker.attachmentTooSmall', 'attachmentPicker.sizeNotMet');
return false;
}

return true;
}
const {resetValidation, resetValidationErrors, validateReceipt, receiptValidation} = useReceiptValidation();

/**
* Sets the Receipt objects and navigates the user to the next page
Expand Down Expand Up @@ -192,11 +153,12 @@ function ReceiptSelector(props) {
receiptImageTopPosition={receiptImageTopPosition}
/>
<ConfirmModal
title={attachmentInvalidReasonTitle ? translate(attachmentInvalidReasonTitle) : ''}
onConfirm={hideReciptModal}
onCancel={hideReciptModal}
isVisible={isAttachmentInvalid}
prompt={attachmentInvalidReason ? translate(attachmentInvalidReason) : ''}
title={receiptValidation.title ? translate(receiptValidation.title) : ''}
onConfirm={resetValidation}
onCancel={resetValidation}
isVisible={receiptValidation.isReceiptInvalid}
onModalHide={resetValidationErrors}
prompt={receiptValidation.reason ? translate(receiptValidation.reason) : ''}
confirmText={translate('common.close')}
shouldShowCancelButton={false}
/>
Expand Down
19 changes: 17 additions & 2 deletions src/pages/iou/ReceiptSelector/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ import Button from '../../../components/Button';
import useLocalize from '../../../hooks/useLocalize';
import ONYXKEYS from '../../../ONYXKEYS';
import Log from '../../../libs/Log';
import ConfirmModal from '../../../components/ConfirmModal';
import * as CameraPermission from './CameraPermission';
import {iouPropTypes, iouDefaultProps} from '../propTypes';
import NavigationAwareCamera from './NavigationAwareCamera';
import Navigation from '../../../libs/Navigation/Navigation';
import * as FileUtils from '../../../libs/fileDownload/FileUtils';
import TabNavigationAwareCamera from './TabNavigationAwareCamera';
import useReceiptValidation from '../../../hooks/useReceiptValidation';

const propTypes = {
/** React Navigation route */
Expand Down Expand Up @@ -97,12 +99,11 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator})
const [permissions, setPermissions] = useState('granted');
const isAndroidBlockedPermissionRef = useRef(false);
const appState = useRef(AppState.currentState);

const iouType = lodashGet(route, 'params.iouType', '');
const reportID = lodashGet(route, 'params.reportID', '');
const pageIndex = lodashGet(route, 'params.pageIndex', 1);

const {translate} = useLocalize();
const {resetValidation, resetValidationErrors, validateReceipt, receiptValidation} = useReceiptValidation();

const CameraComponent = isInTabNavigator ? TabNavigationAwareCamera : NavigationAwareCamera;

Expand Down Expand Up @@ -283,6 +284,10 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator})
onPress={() => {
showImagePicker(launchImageLibrary)
.then((receiptImage) => {
if (!validateReceipt(receiptImage[0])) {
return;
}

const filePath = receiptImage[0].uri;
IOU.setMoneyRequestReceipt(filePath, receiptImage[0].fileName);

Expand Down Expand Up @@ -333,6 +338,16 @@ function ReceiptSelector({route, report, iou, transactionID, isInTabNavigator})
/>
</PressableWithFeedback>
</View>
<ConfirmModal
title={receiptValidation.title ? translate(receiptValidation.title) : ''}
onConfirm={resetValidation}
onCancel={resetValidation}
isVisible={receiptValidation.isReceiptInvalid}
onModalHide={resetValidationErrors}
prompt={receiptValidation.reason ? translate(receiptValidation.reason) : ''}
confirmText={translate('common.close')}
shouldShowCancelButton={false}
/>
</View>
);
}
Expand Down
Loading