diff --git a/src/id-verification/CameraHelpWithUpload.jsx b/src/id-verification/CameraHelpWithUpload.jsx index bc27dc6..d1cb8fa 100644 --- a/src/id-verification/CameraHelpWithUpload.jsx +++ b/src/id-verification/CameraHelpWithUpload.jsx @@ -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 && }

{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])} +

diff --git a/src/id-verification/IdVerification.messages.js b/src/id-verification/IdVerification.messages.js index 0a42a02..59caddc 100644 --- a/src/id-verification/IdVerification.messages.js +++ b/src/id-verification/IdVerification.messages.js @@ -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', diff --git a/src/id-verification/ImageFileUpload.jsx b/src/id-verification/ImageFileUpload.jsx index 2f966a4..4419719 100644 --- a/src/id-verification/ImageFileUpload.jsx +++ b/src/id-verification/ImageFileUpload.jsx @@ -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 }) { <> - {fileTooLargeError && ( + {error && ( - {intl.formatMessage(messages['id.verification.id.photo.instructions.upload.error'])} + {intl.formatMessage(messages[`id.verification.id.photo.instructions.upload.error.${error}`])} + )} diff --git a/src/id-verification/SupportedMediaTypes.jsx b/src/id-verification/SupportedMediaTypes.jsx new file mode 100644 index 0000000..b2c6f18 --- /dev/null +++ b/src/id-verification/SupportedMediaTypes.jsx @@ -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 {getSupportedTypes()}; +} diff --git a/src/id-verification/panels/SummaryPanel.jsx b/src/id-verification/panels/SummaryPanel.jsx index ee8d245..a11b466 100644 --- a/src/id-verification/panels/SummaryPanel.jsx +++ b/src/id-verification/panels/SummaryPanel.jsx @@ -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'])} + + + ); + } } return (

{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])} +

diff --git a/src/id-verification/panels/TakePortraitPhotoPanel.jsx b/src/id-verification/panels/TakePortraitPhotoPanel.jsx index f1d073a..b9656a5 100644 --- a/src/id-verification/panels/TakePortraitPhotoPanel.jsx +++ b/src/id-verification/panels/TakePortraitPhotoPanel.jsx @@ -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) {

{props.intl.formatMessage(messages['id.verification.portrait.photo.instructions.upload'])} +

diff --git a/src/id-verification/tests/panels/SummaryPanel.test.jsx b/src/id-verification/tests/panels/SummaryPanel.test.jsx index b63ac2e..e7a9627 100644 --- a/src/id-verification/tests/panels/SummaryPanel.test.jsx +++ b/src/id-verification/tests/panels/SummaryPanel.test.jsx @@ -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');