fix: add extra validation to IDV image upload
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
14
src/id-verification/SupportedMediaTypes.jsx
Normal file
14
src/id-verification/SupportedMediaTypes.jsx
Normal 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>;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user