fix: add extra validation to IDV image upload

This commit is contained in:
Bianca Severino
2021-03-30 11:26:29 -04:00
parent 613229bbd1
commit b2c08193f7
8 changed files with 75 additions and 12 deletions

View File

@@ -8,6 +8,7 @@ import messages from './IdVerification.messages';
import ImageFileUpload from './ImageFileUpload';
import IdVerificationContext from './IdVerificationContext';
import ImagePreview from './ImagePreview';
import SupportedMediaTypes from './SupportedMediaTypes';
function CameraHelpWithUpload(props) {
const { setIdPhotoFile, idPhotoFile, userId } = useContext(IdVerificationContext);
@@ -34,6 +35,7 @@ function CameraHelpWithUpload(props) {
{idPhotoFile && hasUploadedImage && <ImagePreview src={idPhotoFile} alt={props.intl.formatMessage(messages['id.verification.id.photo.preview.alt'])} />}
<p>
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
<SupportedMediaTypes />
</p>
<ImageFileUpload onFileChange={setAndTrackIdPhotoFile} intl={props.intl} />
</Collapsible>

View File

@@ -413,7 +413,7 @@ const messages = defineMessages({
},
'id.verification.portrait.photo.instructions.upload': {
id: 'id.verification.portrait.photo.instructions.upload',
defaultMessage: 'Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)',
defaultMessage: 'Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ',
description: 'Instructions for portrait photo upload.',
},
'id.verification.camera.help.sight.question': {
@@ -493,11 +493,16 @@ const messages = defineMessages({
},
'id.verification.id.photo.instructions.upload': {
id: 'id.verification.id.photo.instructions.upload',
defaultMessage: 'Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. (Supported formats: .jpg, .jpeg, .png)',
defaultMessage: 'Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ',
description: 'Instructions for ID photo upload.',
},
'id.verification.id.photo.instructions.upload.error': {
id: 'id.verification.id.photo.instructions.upload.error',
'id.verification.id.photo.instructions.upload.error.invalidFileType': {
id: 'id.verification.id.photo.instructions.upload.error.invalidFileType',
defaultMessage: 'The file you have selected is not a supported image type. Please choose from the following formats: ',
description: 'Error message for file upload that is not a supported image type.',
},
'id.verification.id.photo.instructions.upload.error.fileTooLarge': {
id: 'id.verification.id.photo.instructions.upload.error.fileTooLarge',
defaultMessage: 'The file you have selected is too large. Please try again with a file less than 10MB.',
description: 'Error message for file upload that is larger than 10MB.',
},
@@ -616,6 +621,11 @@ const messages = defineMessages({
defaultMessage: 'A valid account name is required. Please update your account name to match the name on your ID.',
description: 'Error message displayed when the user\'s account name is missing.',
},
'id.verification.submission.alert.error.unsupported': {
id: 'id.verification.submission.alert.error.unsupported',
defaultMessage: 'One or more of the files you have uploaded is in an unsupported format. Please choose from the following: ',
description: 'Error message displayed when the user uploads an unsupported file type.',
},
'id.verification.review.error': {
id: 'id.verification.review.error',
defaultMessage: 'edX Support Page',

View File

@@ -3,9 +3,14 @@ import { intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Alert } from '@edx/paragon';
import messages from './IdVerification.messages';
import SupportedMediaTypes from './SupportedMediaTypes';
export default function ImageFileUpload({ onFileChange, intl }) {
const [fileTooLargeError, setFileTooLargeError] = useState(false);
const [error, setError] = useState(null);
const errorTypes = {
invalidFileType: 'invalidFileType',
fileTooLarge: 'fileTooLarge',
};
const maxFileSize = 10000000;
const handleChange = useCallback((e) => {
@@ -14,12 +19,15 @@ export default function ImageFileUpload({ onFileChange, intl }) {
}
const fileObject = e.target.files[0];
if (fileObject.size < maxFileSize) {
if (!fileObject.type.startsWith('image')) {
setError(errorTypes.invalidFileType);
} else if (fileObject.size >= maxFileSize) {
setError(errorTypes.fileTooLarge);
} else {
setError(null);
const fileReader = new FileReader();
fileReader.addEventListener('load', () => onFileChange(fileReader.result));
fileReader.readAsDataURL(fileObject);
} else {
setFileTooLargeError(true);
}
}, []);
@@ -27,18 +35,19 @@ export default function ImageFileUpload({ onFileChange, intl }) {
<>
<input
type="file"
accept=".png, .jpg, .jpeg"
accept="image/*"
data-testid="fileUpload"
onChange={handleChange}
/>
{fileTooLargeError && (
{error && (
<Alert
id="fileTooLargeError"
id="fileError"
variant="danger"
tabIndex="-1"
style={{ marginTop: '1rem' }}
>
{intl.formatMessage(messages['id.verification.id.photo.instructions.upload.error'])}
{intl.formatMessage(messages[`id.verification.id.photo.instructions.upload.error.${error}`])}
<SupportedMediaTypes />
</Alert>
)}
</>

View File

@@ -0,0 +1,14 @@
import React from 'react';
export default function SupportedMediaTypes() {
const SUPPORTED_TYPES = ['.png', '.jpeg', '.jpg', '.bmp', '.webp', '.tiff'];
const getSupportedTypes = () => SUPPORTED_TYPES.map((type, index) => {
if (index === SUPPORTED_TYPES.length - 1) {
return type;
}
return `${type}, `;
});
return <span>{getSupportedTypes()}</span>;
}

View File

@@ -14,6 +14,7 @@ import ImagePreview from '../ImagePreview';
import messages from '../IdVerification.messages';
import CameraHelpWithUpload from '../CameraHelpWithUpload';
import SupportedMediaTypes from '../SupportedMediaTypes';
function SummaryPanel(props) {
const panelSlug = 'summary';
@@ -103,6 +104,14 @@ function SummaryPanel(props) {
if (submissionError.message.includes('Name')) {
return props.intl.formatMessage(messages['id.verification.submission.alert.error.name']);
}
if (submissionError.message.includes('unsupported format')) {
return (
<>
{props.intl.formatMessage(messages['id.verification.submission.alert.error.unsupported'])}
<SupportedMediaTypes />
</>
);
}
}
return (
<FormattedMessage

View File

@@ -12,6 +12,7 @@ import CameraHelp from '../CameraHelp';
import ImagePreview from '../ImagePreview';
import ImageFileUpload from '../ImageFileUpload';
import CollapsibleImageHelp from '../CollapsibleImageHelp';
import SupportedMediaTypes from '../SupportedMediaTypes';
function TakeIdPhotoPanel(props) {
const panelSlug = 'take-id-photo';
@@ -39,6 +40,7 @@ function TakeIdPhotoPanel(props) {
<div style={{ marginBottom: '1.25rem' }}>
<p data-testid="upload-text">
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
<SupportedMediaTypes />
</p>
<ImageFileUpload onFileChange={setIdPhotoFile} intl={props.intl} />
</div>

View File

@@ -12,6 +12,7 @@ import IdVerificationContext from '../IdVerificationContext';
import messages from '../IdVerification.messages';
import CollapsibleImageHelp from '../CollapsibleImageHelp';
import SupportedMediaTypes from '../SupportedMediaTypes';
function TakePortraitPhotoPanel(props) {
const panelSlug = 'take-portrait-photo';
@@ -39,6 +40,7 @@ function TakePortraitPhotoPanel(props) {
<div style={{ marginBottom: '1.25rem' }}>
<p data-testid="upload-text">
{props.intl.formatMessage(messages['id.verification.portrait.photo.instructions.upload'])}
<SupportedMediaTypes />
</p>
<ImageFileUpload onFileChange={setFacePhotoFile} intl={props.intl} />
</div>

View File

@@ -179,6 +179,21 @@ describe('SummaryPanel', () => {
);
});
it('displays correct error for unsupported file type', async () => {
dataService.submitIdVerification = jest.fn().mockReturnValue({
success: false,
status: 400,
message: 'Image data is in an unsupported format.',
});
await getPanel();
const button = await screen.findByTestId('submit-button');
await act(async () => fireEvent.click(button));
const error = await screen.getByTestId('submission-error');
expect(error).toHaveTextContent(
'One or more of the files you have uploaded is in an unsupported format. Please choose from the following:',
);
});
it('does not show ID upload option if user is in experiment', async () => {
await getPanel();
const collapsible = await screen.queryByTestId('collapsible');