Code cleanup and i18n compatibility
Replaced hardcoded text with i18n components Added title to confirmation button for a/b testing purposes Redirect the user to the beginning of the process on reload to avoid losing camera access, also removed unused code Update src/id-verification/IdVerificationContext.jsx, make children prop required Co-authored-by: Kyle McCormick <kmccormick@edx.org> Remove todo comment in ImageFileUpload.jsx Added comment to service.js and removed unused code from index.js
This commit is contained in:
@@ -1,101 +1,90 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import CameraPhoto, { FACING_MODES } from 'jslib-html5-camera-photo';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import shutter from './data/camera-shutter.base64.json'
|
||||
|
||||
const PHOTO_PROMTS = {
|
||||
TAKE_PHOTO: 'Take Photo',
|
||||
RETAKE_PHOTO: 'Retake Photo'
|
||||
}
|
||||
import shutter from './data/camera-shutter.base64.json';
|
||||
import messages from './IdVerification.messages';
|
||||
|
||||
class Camera extends React.Component {
|
||||
constructor (props, context) {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.cameraPhoto = null;
|
||||
this.videoRef = React.createRef();
|
||||
this.state = {
|
||||
trackedObject: null,
|
||||
dataUri: ''
|
||||
}
|
||||
dataUri: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
componentDidMount() {
|
||||
this.cameraPhoto = new CameraPhoto(this.videoRef.current);
|
||||
this.cameraPhoto.startCameraMaxResolution(FACING_MODES.USER)
|
||||
this.cameraPhoto.startCameraMaxResolution(FACING_MODES.USER);
|
||||
}
|
||||
|
||||
takePhoto () {
|
||||
takePhoto() {
|
||||
if (this.state.dataUri) {
|
||||
return this.reset()
|
||||
return this.reset();
|
||||
}
|
||||
const config = {
|
||||
sizeFactor: 1
|
||||
sizeFactor: 1,
|
||||
};
|
||||
|
||||
this.playShutterClick()
|
||||
let dataUri = this.cameraPhoto.getDataUri(config);
|
||||
|
||||
this.playShutterClick();
|
||||
const dataUri = this.cameraPhoto.getDataUri(config);
|
||||
this.setState({ dataUri });
|
||||
this.props.onImageCapture(dataUri)
|
||||
this.props.onImageCapture(dataUri);
|
||||
}
|
||||
|
||||
playShutterClick () {
|
||||
let audio = new Audio('data:audio/mp3;base64,' + shutter.base64);
|
||||
playShutterClick() {
|
||||
const audio = new Audio('data:audio/mp3;base64,' + shutter.base64);
|
||||
audio.play();
|
||||
}
|
||||
|
||||
reset () {
|
||||
this.setState({dataUri: ''})
|
||||
reset() {
|
||||
this.setState({ dataUri: '' });
|
||||
}
|
||||
|
||||
printTrackingInfo () {
|
||||
let trackedObject = this.state.trackedObject;
|
||||
if (!trackedObject) {
|
||||
trackedObject = {
|
||||
x: 'N/A',
|
||||
y: 'N/A',
|
||||
width: 'N/A',
|
||||
height: 'N/A',
|
||||
}
|
||||
}
|
||||
var res = `
|
||||
x: ${trackedObject.x}
|
||||
y: ${trackedObject.y}
|
||||
width: ${trackedObject.width}
|
||||
height: ${trackedObject.height}
|
||||
`
|
||||
return res
|
||||
}
|
||||
|
||||
render () {
|
||||
let cameraFlashClass = this.state.dataUri ? 'do-transition camera-flash' : 'camera-flash';
|
||||
render() {
|
||||
const cameraFlashClass = this.state.dataUri
|
||||
? 'do-transition camera-flash'
|
||||
: 'camera-flash';
|
||||
return (
|
||||
<div className="camera-outer-wrapper shadow">
|
||||
<div className="camera-wrapper">
|
||||
<div className={ cameraFlashClass }></div>
|
||||
<div className='camera-outer-wrapper shadow'>
|
||||
<div className='camera-wrapper'>
|
||||
<div className={cameraFlashClass} />
|
||||
<video
|
||||
ref={this.videoRef}
|
||||
autoPlay={true}
|
||||
className="camera-video"
|
||||
className='camera-video'
|
||||
style={{ display: this.state.dataUri ? 'none' : 'block' }}
|
||||
/>
|
||||
<img
|
||||
alt="imgCamera"
|
||||
alt='imgCamera'
|
||||
src={this.state.dataUri}
|
||||
className="camera-video"
|
||||
className='camera-video'
|
||||
style={{ display: this.state.dataUri ? 'block' : 'none' }}
|
||||
/>
|
||||
</div>
|
||||
<button className="btn btn-primary camera-btn" accessKey="c" onClick={ () => {
|
||||
this.takePhoto();
|
||||
}}> {this.state.dataUri ? PHOTO_PROMTS.RETAKE_PHOTO : PHOTO_PROMTS.TAKE_PHOTO} </button>
|
||||
<button
|
||||
className='btn btn-primary camera-btn'
|
||||
accessKey='c'
|
||||
onClick={() => {
|
||||
this.takePhoto();
|
||||
}}
|
||||
>
|
||||
{this.state.dataUri
|
||||
? this.props.intl.formatMessage(messages['id.verification.photo.retake'])
|
||||
: this.props.intl.formatMessage(messages['id.verification.photo.take'])}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Camera.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onImageCapture: PropTypes.func.isRequired,
|
||||
}
|
||||
};
|
||||
|
||||
export default Camera;
|
||||
export default injectIntl(Camera);
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
export default function CameraHelp() {
|
||||
import messages from './IdVerification.messages';
|
||||
|
||||
function CameraHelp(props) {
|
||||
return (
|
||||
<div>
|
||||
<h6>What if I can't see the camera image or if I can't see my photo to determine which side is visible</h6>
|
||||
<p>
|
||||
You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts
|
||||
to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for
|
||||
a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen.
|
||||
If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle.
|
||||
The most common reason for rejection is in ability to read the text on the ID card.
|
||||
</p>
|
||||
<h6>What if I have difficulty holding my head in position relative to the camera?</h6>
|
||||
<p>If you require assistance with taking a photo for submission, contact edX support for additional suggestions.</p>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
{props.intl.formatMessage(messages['id.verification.camera.help.sight.question'])}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.camera.help.sight.answer'])}
|
||||
</p>
|
||||
<h6>
|
||||
{props.intl.formatMessage(messages['id.verification.camera.help.head.question'])}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.camera.help.head.answer'])}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CameraHelp.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CameraHelp);
|
||||
|
||||
28
src/id-verification/ExistingRequest.jsx
Normal file
28
src/id-verification/ExistingRequest.jsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './IdVerification.messages';
|
||||
|
||||
function ExistingRequest(props) {
|
||||
return (
|
||||
<div>
|
||||
<h3 aria-level="1" tabIndex="-1">
|
||||
{props.intl.formatMessage(messages['id.verification.existing.request.title'])}
|
||||
</h3>
|
||||
{props.status === 'pending' || props.status == 'approved'
|
||||
? <p>{props.intl.formatMessage(messages['id.verification.existing.request.pending.text'])}</p>
|
||||
: <p>{props.intl.formatMessage(messages['id.verification.existing.request.denied.text'])}</p>
|
||||
}
|
||||
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||
{props.intl.formatMessage(messages['id.verification.return'])}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ExistingRequest.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ExistingRequest);
|
||||
346
src/id-verification/IdVerification.messages.js
Normal file
346
src/id-verification/IdVerification.messages.js
Normal file
@@ -0,0 +1,346 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'id.verification.next': {
|
||||
id: 'id.verification.next',
|
||||
defaultMessage: 'Next',
|
||||
description: 'Next button.',
|
||||
},
|
||||
'id.verification.requirements.title': {
|
||||
id: 'id.verification.requirements.title',
|
||||
defaultMessage: 'Photo Verification Requirements',
|
||||
description: 'Title for the Photo Verification Requirements page.',
|
||||
},
|
||||
'id.verification.requirements.description': {
|
||||
id: 'id.verification.requirements.description',
|
||||
defaultMessage: 'In order to complete Photo Verification online, you will need the following:',
|
||||
description: 'Description for the Photo Verification Requirements page.',
|
||||
},
|
||||
'id.verification.requirements.card.device.title': {
|
||||
id: 'id.verification.requirements.card.device.title',
|
||||
defaultMessage: 'Device with Camera',
|
||||
description: 'Title for the Device with Camera card.',
|
||||
},
|
||||
'id.verification.requirements.card.device.allow': {
|
||||
id: 'id.verification.requirements.card.device.allow',
|
||||
defaultMessage: 'Allow',
|
||||
description: 'Bold text emphasizing that the user needs to click "allow" in order to enable the camera.',
|
||||
},
|
||||
'id.verification.requirements.card.id.title': {
|
||||
id: 'id.verification.requirements.card.id.title',
|
||||
defaultMessage: 'Photo Identification',
|
||||
description: 'Title for the Photo Identification requirement card.',
|
||||
},
|
||||
'id.verification.requirements.card.id.text': {
|
||||
id: 'id.verification.requirements.card.id.text',
|
||||
defaultMessage: 'You need a valid ID that contains your full name and photo.',
|
||||
description: 'Text that explains that the user needs a photo ID.',
|
||||
},
|
||||
'id.verification.privacy.title': {
|
||||
id: 'id.verification.privacy.title',
|
||||
defaultMessage: 'Privacy Information',
|
||||
description: 'Title for Privacy Information.',
|
||||
},
|
||||
'id.verification.privacy.need.photo.question': {
|
||||
id: 'id.verification.privacy.need.photo.question',
|
||||
defaultMessage: 'Why does edX need my photo?',
|
||||
description: 'Question about why edX needs a verification photo.',
|
||||
},
|
||||
'id.verification.privacy.need.photo.answer': {
|
||||
id: 'id.verification.privacy.need.photo.answer',
|
||||
defaultMessage: 'We use your verification photos to confirm your identity and ensure the validity of your certificate.',
|
||||
description: 'Answering why edX needs a verification photo.',
|
||||
},
|
||||
'id.verification.privacy.do.with.photo.question': {
|
||||
id: 'id.verification.privacy.do.with.photo.question',
|
||||
defaultMessage: 'What does edX do with this photo?',
|
||||
description: 'Question about what edX does with the verification photo.',
|
||||
},
|
||||
'id.verification.privacy.do.with.photo.answer': {
|
||||
id: 'id.verification.privacy.do.with.photo.answer',
|
||||
defaultMessage: 'We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.',
|
||||
description: 'Answering what edX does with the verification photo.',
|
||||
},
|
||||
'id.verification.existing.request.title': {
|
||||
id: 'id.verification.existing.request.title',
|
||||
defaultMessage: 'Identity Verification',
|
||||
description: 'Title for text that displays when user has already made a request.',
|
||||
},
|
||||
'id.verification.existing.request.pending.text': {
|
||||
id: 'id.verification.existing.request.pending.text',
|
||||
defaultMessage: 'You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).',
|
||||
description: 'Text that displays when user has a pending or approved request.',
|
||||
},
|
||||
'id.verification.existing.request.denied.text': {
|
||||
id: 'id.verification.existing.request.denied.text',
|
||||
defaultMessage: 'You cannot verify your identity at this time.',
|
||||
description: 'Text that displays when user is denied from making a request.',
|
||||
},
|
||||
'id.verification.photo.take': {
|
||||
id: 'id.verification.photo.take',
|
||||
defaultMessage: 'Take Photo',
|
||||
description: 'Button to take photo.',
|
||||
},
|
||||
'id.verification.photo.retake': {
|
||||
id: 'id.verification.photo.retake',
|
||||
defaultMessage: 'Retake Photo',
|
||||
description: 'Button to retake photo.',
|
||||
},
|
||||
'id.verification.camera.access.title': {
|
||||
id: 'id.verification.camera.access.title',
|
||||
defaultMessage: 'Camera Permissions',
|
||||
description: 'Title for the Camera Access page.',
|
||||
},
|
||||
'id.verification.camera.access.click.allow': {
|
||||
id: 'id.verification.camera.access.click.allow',
|
||||
defaultMessage: 'Please make sure to click "Allow"',
|
||||
description: 'Instruction to allow camera access.',
|
||||
},
|
||||
'id.verification.camera.access.enable': {
|
||||
id: 'id.verification.camera.access.enable',
|
||||
defaultMessage: 'Enable Camera',
|
||||
description: 'Text to enable camera.',
|
||||
},
|
||||
'id.verification.camera.access.problems': {
|
||||
id: 'id.verification.camera.access.problems',
|
||||
defaultMessage: 'Having problems?',
|
||||
description: 'Text for when the user is having problems enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.skip': {
|
||||
id: 'id.verification.camera.access.skip',
|
||||
defaultMessage: 'Skip and upload image files instead',
|
||||
description: 'Text to skip camera access and enable image uploading.',
|
||||
},
|
||||
'id.verification.camera.access.success': {
|
||||
id: 'id.verification.camera.access.success',
|
||||
defaultMessage: 'Looks like your camera is working and ready.',
|
||||
description: 'Text to confirm that camera is working.',
|
||||
},
|
||||
'id.verification.camera.access.failure': {
|
||||
id: 'id.verification.camera.access.failure',
|
||||
defaultMessage: 'It looks like we\'re unable to access your camera. You will need to upload image files of you and your photo id.',
|
||||
description: 'Text indicating that the camera could not be accessed and image upload will be enabled.',
|
||||
},
|
||||
'id.verification.photo.tips.title': {
|
||||
id: 'id.verification.photo.tips.title',
|
||||
defaultMessage: 'Helpful Photo Tips',
|
||||
description: 'Title for the Photo Tips page.',
|
||||
},
|
||||
'id.verification.photo.tips.description': {
|
||||
id: 'id.verification.photo.tips.description',
|
||||
defaultMessage: 'Next, we\'ll need you to take a photo of your face. Please review the helpful tips below.',
|
||||
description: 'Description for the photo tips page.',
|
||||
},
|
||||
'id.verification.photo.tips.list.title': {
|
||||
id: 'id.verification.photo.tips.list.title',
|
||||
defaultMessage: 'Photo Tips',
|
||||
description: 'Title for the list of photo tips.',
|
||||
},
|
||||
'id.verification.photo.tips.list.description': {
|
||||
id: 'id.verification.photo.tips.list.description',
|
||||
defaultMessage: 'To take a successful photo, make sure that:',
|
||||
description: 'Description for the list of photo tips.',
|
||||
},
|
||||
'id.verification.photo.tips.list.well.lit': {
|
||||
id: 'id.verification.photo.tips.list.well.lit',
|
||||
defaultMessage: 'Your face is well-lit.',
|
||||
description: 'Tip to make sure the user\'s face is well lit.',
|
||||
},
|
||||
'id.verification.photo.tips.list.inside.frame': {
|
||||
id: 'id.verification.photo.tips.list.inside.frame',
|
||||
defaultMessage: 'Your entire face fits inside the frame.',
|
||||
description: 'Tip to make sure the user\'s face fits inside the frame.',
|
||||
},
|
||||
'id.verification.portrait.photo.title.camera': {
|
||||
id: 'id.verification.portrait.photo.title.camera',
|
||||
defaultMessage: 'Take Your Photo',
|
||||
description: 'Title for the Portrait Photo page if camera access is enabled.',
|
||||
},
|
||||
'id.verification.portrait.photo.title.upload': {
|
||||
id: 'id.verification.portrait.photo.title.upload',
|
||||
defaultMessage: 'Upload Your Portrait Photo',
|
||||
description: 'Title for the Portrait Photo page if camera access is disabled.',
|
||||
},
|
||||
'id.verification.portrait.photo.preview.alt': {
|
||||
id: 'id.verification.portrait.photo.preview.alt',
|
||||
defaultMessage: 'Preview of photo of user\'s face.',
|
||||
description: 'Alt text for the portrait photo preview.',
|
||||
},
|
||||
'id.verification.portrait.photo.instructions.camera': {
|
||||
id: 'id.verification.portrait.photo.instructions.camera',
|
||||
defaultMessage: 'When your face is in position, use the Take Photo button below to take your photo.',
|
||||
description: 'Instructions to use the camera to take a portrait photo..',
|
||||
},
|
||||
'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)',
|
||||
description: 'Instructions for portrait photo upload.',
|
||||
},
|
||||
'id.verification.camera.help.sight.question': {
|
||||
id: 'id.verification.camera.help.sight.question',
|
||||
defaultMessage: 'What if I can\'t see the camera image or if I can\'t see my photo to determine which side is visible?',
|
||||
description: 'Question on what to do if the user cannot see the camera image or photo during verification.',
|
||||
},
|
||||
'id.verification.camera.help.sight.answer': {
|
||||
id: 'id.verification.camera.help.sight.answer',
|
||||
defaultMessage: 'You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.',
|
||||
description: 'Confirming what to do if the camera image or photo cannot be seen during verification.',
|
||||
},
|
||||
'id.verification.camera.help.head.question': {
|
||||
id: 'id.verification.camera.help.head.question',
|
||||
defaultMessage: 'What if I have difficulty holding my head in position relative to the camera?',
|
||||
description: 'Question on what to do if the user has difficulty holding their head relative to the camera.',
|
||||
},
|
||||
'id.verification.camera.help.head.answer': {
|
||||
id: 'id.verification.camera.help.head.answer',
|
||||
defaultMessage: 'If you require assistance with taking a photo for submission, contact edX support for additional suggestions.',
|
||||
description: 'Confirming what to do if the user has difficult holding their head relative to the camera.',
|
||||
},
|
||||
'id.verification.id.tips.title': {
|
||||
id: 'id.verification.id.tips.title',
|
||||
defaultMessage: 'Helpful ID Tips',
|
||||
description: 'Title for the ID Tips page.',
|
||||
},
|
||||
'id.verification.id.tips.description': {
|
||||
id: 'id.verification.id.tips.description',
|
||||
defaultMessage: 'Next you\'ll need an eligible ID photo, make sure that:',
|
||||
description: 'Description for the ID Tips page.',
|
||||
},
|
||||
'id.verification.id.tips.list.well.lit': {
|
||||
id: 'id.verification.id.tips.list.well.lit',
|
||||
defaultMessage: 'Your ID is well-lit.',
|
||||
description: 'Tip to ensure ID is well lit.',
|
||||
},
|
||||
'id.verification.id.tips.list.clear': {
|
||||
id: 'id.verification.id.tips.list.clear',
|
||||
defaultMessage: 'Ensure that you can see your photo and clearly read your name.',
|
||||
description: 'Tip to ensure ID and name can be seen clearly.',
|
||||
},
|
||||
'id.verification.id.photo.title.camera': {
|
||||
id: 'id.verification.id.photo.title.camera',
|
||||
defaultMessage: 'Take ID Photo',
|
||||
description: 'Title for the ID Photo page if camera access is enabled.',
|
||||
},
|
||||
'id.verification.id.photo.title.upload': {
|
||||
id: 'id.verification.id.photo.title.upload',
|
||||
defaultMessage: 'Upload Your ID Photo',
|
||||
description: 'Title for the ID Photo page if camera access is disabled.',
|
||||
},
|
||||
'id.verification.id.photo.preview.alt': {
|
||||
id: 'id.verification.id.photo.preview.alt',
|
||||
defaultMessage: 'Preview of photo ID.',
|
||||
description: 'Alt text for the ID photo preview.',
|
||||
},
|
||||
'id.verification.id.photo.instructions.camera': {
|
||||
id: 'id.verification.id.photo.instructions.camera',
|
||||
defaultMessage: 'When your ID is in position, use the Take Photo button below to take your photo.',
|
||||
description: 'Instructions to use the camera to take an ID photo.',
|
||||
},
|
||||
'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. (Supported formats: .jpg, .jpeg, .png)',
|
||||
description: 'Instructions for ID photo upload.',
|
||||
},
|
||||
'id.verification.account.name.title': {
|
||||
id: 'id.verification.account.name.title',
|
||||
defaultMessage: 'Account Name Check',
|
||||
description: 'Title for the Account Name Check page.',
|
||||
},
|
||||
'id.verification.account.name.instructions': {
|
||||
id: 'id.verification.account.name.instructions',
|
||||
defaultMessage: 'Please check the Account Name below to ensure it matches the name on your ID. If not, click "Edit".',
|
||||
description: 'Text to verify that the account name matches the name on the ID photo.',
|
||||
},
|
||||
'id.verification.account.name.warning.prefix': {
|
||||
id: 'id.verification.account.name.warning.prefix',
|
||||
defaultMessage: 'Please Note:',
|
||||
description: 'Prefix to the warning that any change to the account name will be saved to the account.',
|
||||
},
|
||||
'id.verification.account.name.settings': {
|
||||
id: 'id.verification.account.name.settings',
|
||||
defaultMessage: 'Account Settings',
|
||||
description: 'Link to Account Settings.',
|
||||
},
|
||||
'id.verification.account.name.label': {
|
||||
id: 'id.verification.account.name.label',
|
||||
defaultMessage: 'Name on ID',
|
||||
description: 'Label for account name input.',
|
||||
},
|
||||
'id.verification.account.name.edit': {
|
||||
id: 'id.verification.account.name.edit',
|
||||
defaultMessage: 'Edit',
|
||||
description: 'Button to edit account name.',
|
||||
},
|
||||
'id.verification.account.name.photo.alt': {
|
||||
id: 'id.verification.account.name.photo.alt',
|
||||
defaultMessage: 'Photo of your ID to be submitted.',
|
||||
description: 'Alt text for the photo of the user\'s ID.',
|
||||
},
|
||||
'id.verification.account.name.save': {
|
||||
id: 'id.verification.account.name.save',
|
||||
defaultMessage: 'Save',
|
||||
description: 'Button to save the account name.',
|
||||
},
|
||||
'id.verification.review.title': {
|
||||
id: 'id.verification.review.title',
|
||||
defaultMessage: 'Review Your Photos',
|
||||
description: 'Title for the review your photos page.',
|
||||
},
|
||||
'id.verification.review.description': {
|
||||
id: 'id.verification.review.description',
|
||||
defaultMessage: 'Make sure we can verify your identity with the photos and information you have provided.',
|
||||
description: 'Description for the review your photos page.',
|
||||
},
|
||||
'id.verification.review.portrait.label': {
|
||||
id: 'id.verification.review.portrait.label',
|
||||
defaultMessage: 'Your Portrait',
|
||||
description: 'Label for the portrait card.',
|
||||
},
|
||||
'id.verification.review.portrait.alt': {
|
||||
id: 'id.verification.review.portrait.alt',
|
||||
defaultMessage: 'Photo of your face to be submitted.',
|
||||
description: 'Alt text for the portrait photo.',
|
||||
},
|
||||
'id.verification.review.portrait.retake': {
|
||||
id: 'id.verification.review.portrait.retake',
|
||||
defaultMessage: 'Retake Portrait Photo',
|
||||
description: 'Button to retake the portrait photo.',
|
||||
},
|
||||
'id.verification.review.id.label': {
|
||||
id: 'id.verification.review.id.label',
|
||||
defaultMessage: 'Your Photo ID',
|
||||
description: 'Label for the Photo ID card.',
|
||||
},
|
||||
'id.verification.review.id.alt': {
|
||||
id: 'id.verification.review.id.alt',
|
||||
defaultMessage: 'Photo of your ID to be submitted.',
|
||||
description: 'Alt text for the ID photo.',
|
||||
},
|
||||
'id.verification.review.id.retake': {
|
||||
id: 'id.verification.review.id.retake',
|
||||
defaultMessage: 'Retake ID Photo',
|
||||
description: 'Button to retake the ID photo.',
|
||||
},
|
||||
'id.verification.review.confirm': {
|
||||
id: 'id.verification.review.confirm',
|
||||
defaultMessage: 'Confirm',
|
||||
description: 'Button to confirm all information is correct.',
|
||||
},
|
||||
'id.verification.submitted.title': {
|
||||
id: 'id.verification.submitted.title',
|
||||
defaultMessage: 'Identity Verification in Progress',
|
||||
description: 'Title for the submitted page.',
|
||||
},
|
||||
'id.verification.submitted.text': {
|
||||
id: 'id.verification.submitted.text',
|
||||
defaultMessage: 'We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content.',
|
||||
description: 'Text confirming that ID verification request has been received.',
|
||||
},
|
||||
'id.verification.return': {
|
||||
id: 'id.verification.submitted.return',
|
||||
defaultMessage: 'Return to Your Dashboard',
|
||||
description: 'Button to return to the dashboard.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { hasGetUserMediaSupport } from './getUserMediaShim';
|
||||
import { getExistingIdVerification } from './data/service';
|
||||
import PageLoading from '../account-settings/PageLoading'
|
||||
import PageLoading from '../account-settings/PageLoading';
|
||||
import ExistingRequest from './ExistingRequest';
|
||||
|
||||
const IdVerificationContext = React.createContext({});
|
||||
|
||||
@@ -54,35 +54,22 @@ function IdVerificationContextProvider({ children }) {
|
||||
};
|
||||
|
||||
// Call verification status endpoint to check whether we can verify.
|
||||
useEffect(() => {(async () => {
|
||||
const existingIdV = await getExistingIdVerification();
|
||||
setExistingIdVerification(existingIdV);
|
||||
})()}, []);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const existingIdV = await getExistingIdVerification();
|
||||
setExistingIdVerification(existingIdV);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// If we are waiting for verification status endpoint, show spinner.
|
||||
if (!existingIdVerification) {
|
||||
return <PageLoading srMessage='Loading verification status' />;
|
||||
return <PageLoading srMessage="Loading verification status" />;
|
||||
}
|
||||
|
||||
if (!existingIdVerification.canVerify) {
|
||||
const status = existingIdVerification.status;
|
||||
const { status } = existingIdVerification;
|
||||
return (
|
||||
<div>
|
||||
<h3 aria-level="1" tabIndex="-1">Identity Verification</h3>
|
||||
{status === 'pending' || status == 'approved'
|
||||
? <p>
|
||||
You have already submitted your verification information.
|
||||
You will see a message on your dashboard when the verification process
|
||||
is complete (usually within 1-2 days).
|
||||
</p>
|
||||
: <p>
|
||||
You cannot verify your identity at this time.
|
||||
</p>
|
||||
}
|
||||
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||
Return to Your Dashboard
|
||||
</a>
|
||||
</div>
|
||||
<ExistingRequest status={status} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -93,10 +80,7 @@ function IdVerificationContextProvider({ children }) {
|
||||
);
|
||||
}
|
||||
IdVerificationContextProvider.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
IdVerificationContextProvider.defaultProps = {
|
||||
children: undefined,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Route, Switch, Redirect, useRouteMatch } from 'react-router-dom';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Modal, Button } from '@edx/paragon';
|
||||
import { idVerificationSelector } from './data/selectors';
|
||||
import './getUserMediaShim';
|
||||
@@ -17,57 +17,68 @@ import TakeIdPhotoPanel from './panels/TakeIdPhotoPanel';
|
||||
import SummaryPanel from './panels/SummaryPanel';
|
||||
import SubmittedPanel from './panels/SubmittedPanel';
|
||||
|
||||
import messages from './IdVerification.messages';
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
function IdVerificationPage() {
|
||||
function IdVerificationPage(props) {
|
||||
const { path } = useRouteMatch();
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="page__id-verification container-fluid py-5">
|
||||
<div className="row">
|
||||
<div className="col-lg-6 col-md-8">
|
||||
<IdVerificationContextProvider>
|
||||
<Switch>
|
||||
<Route exact path={path}>
|
||||
<Redirect to={`${path}/review-requirements`} />
|
||||
</Route>
|
||||
<Route path={`${path}/review-requirements`} component={ReviewRequirementsPanel} />
|
||||
<Route path={`${path}/request-camera-access`} component={RequestCameraAccessPanel} />
|
||||
<Route path={`${path}/portrait-photo-context`} component={PortraitPhotoContextPanel} />
|
||||
<Route path={`${path}/take-portrait-photo`} component={TakePortraitPhotoPanel} />
|
||||
<Route path={`${path}/id-context`} component={IdContextPanel} />
|
||||
<Route path={`${path}/get-name-id`} component={GetNameIdPanel} />
|
||||
<Route path={`${path}/take-id-photo`} component={TakeIdPhotoPanel} />
|
||||
<Route path={`${path}/summary`} component={SummaryPanel} />
|
||||
<Route path={`${path}/submitted`} component={SubmittedPanel} />
|
||||
</Switch>
|
||||
</IdVerificationContextProvider>
|
||||
</div>
|
||||
<div className="col-lg-6 col-md-4 pt-md-0 pt-4 text-right">
|
||||
<Button className="btn-link px-0" onClick={() => setIsModalOpen(true)}>
|
||||
Privacy Information
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<Modal
|
||||
open={isModalOpen}
|
||||
title="Privacy Information"
|
||||
body={(
|
||||
<div>
|
||||
<h6>Why does edX need my photo?</h6>
|
||||
<p>We use your verification photos to confirm your identity and ensure the validity of your certificate.</p>
|
||||
<h6>What does edX do with this photo?</h6>
|
||||
<p>We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.</p>
|
||||
<>
|
||||
{/* If user reloads, redirect to the beginning of the process */}
|
||||
<Redirect to={`${path}/review-requirements`} />
|
||||
<div className="page__id-verification container-fluid py-5">
|
||||
<div className="row">
|
||||
<div className="col-lg-6 col-md-8">
|
||||
<IdVerificationContextProvider>
|
||||
<Switch>
|
||||
<Route exact path={path}>
|
||||
<Redirect to={`${path}/review-requirements`} />
|
||||
</Route>
|
||||
<Route path={`${path}/review-requirements`} component={ReviewRequirementsPanel} />
|
||||
<Route path={`${path}/request-camera-access`} component={RequestCameraAccessPanel} />
|
||||
<Route path={`${path}/portrait-photo-context`} component={PortraitPhotoContextPanel} />
|
||||
<Route path={`${path}/take-portrait-photo`} component={TakePortraitPhotoPanel} />
|
||||
<Route path={`${path}/id-context`} component={IdContextPanel} />
|
||||
<Route path={`${path}/get-name-id`} component={GetNameIdPanel} />
|
||||
<Route path={`${path}/take-id-photo`} component={TakeIdPhotoPanel} />
|
||||
<Route path={`${path}/summary`} component={SummaryPanel} />
|
||||
<Route path={`${path}/submitted`} component={SubmittedPanel} />
|
||||
</Switch>
|
||||
</IdVerificationContextProvider>
|
||||
</div>
|
||||
)}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
<div className="col-lg-6 col-md-4 pt-md-0 pt-4 text-right">
|
||||
<Button className="btn-link px-0" onClick={() => setIsModalOpen(true)}>
|
||||
Privacy Information
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={isModalOpen}
|
||||
title={props.intl.formatMessage(messages['id.verification.privacy.title'])}
|
||||
body={(
|
||||
<div>
|
||||
<h6>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.question'])}</h6>
|
||||
<p>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}</p>
|
||||
<h6>{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.question'])}</h6>
|
||||
<p>{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.answer'])}</p>
|
||||
</div>
|
||||
)}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
IdVerificationPage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default connect(idVerificationSelector, {
|
||||
})(injectIntl(IdVerificationPage));
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
|
||||
export default function ImageFileUpload({ onFileChange }) {
|
||||
const handleChange = useCallback((e) => {
|
||||
if (e.target.files.length === 0) {
|
||||
// do something else
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,12 +40,13 @@ export async function getExistingIdVerification() {
|
||||
*
|
||||
* Returns { success: Boolean, message: String|null }
|
||||
*/
|
||||
export async function submitIdVerfication(verificationData) {
|
||||
export async function submitIdVerification(verificationData) {
|
||||
const keyMap = {
|
||||
facePhotoFile: 'face_image',
|
||||
idPhotoFile: 'photo_id_image',
|
||||
idPhotoName: 'full_name',
|
||||
courseRunKey: 'course_id',
|
||||
// Currently does not support a redirect back to the original course. See MST-282: https://openedx.atlassian.net/browse/MST-282
|
||||
};
|
||||
const postData = {};
|
||||
// Don't include blank/null/undefined values.
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
export { default } from './IdVerificationPage';
|
||||
// export { default as reducer } from './data/reducers';
|
||||
// export { default as saga } from './data/sagas';
|
||||
// export { storeName } from './data/selectors';
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React, { useContext, useState, useEffect, useRef } from 'react';
|
||||
import { Input, Button } from '@edx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import { IdVerificationContext } from '../IdVerificationContext';
|
||||
import ImagePreview from '../ImagePreview';
|
||||
|
||||
export default function GetNameIdPanel() {
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function GetNameIdPanel(props) {
|
||||
const panelSlug = 'get-name-id';
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const nameInputRef = useRef();
|
||||
@@ -26,14 +29,26 @@ export default function GetNameIdPanel() {
|
||||
name={panelSlug}
|
||||
title="Account Name Check"
|
||||
>
|
||||
<p>Please check the Account Name below to ensure it matches the name on your ID. If not, click "Edit".</p>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.instructions'])}
|
||||
</p>
|
||||
|
||||
<div className="alert alert-warning">
|
||||
<strong>Please Note:</strong> any edit to your name will be saved to your account and can be reviewed on <Link to="/">Account Settings</Link>.
|
||||
<FormattedMessage
|
||||
id="id.verification.account.name.warning"
|
||||
defaultMessage="{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}."
|
||||
description="Warning that any edit to the user's name will be saved to the account."
|
||||
values={{
|
||||
prefix: <strong>{props.intl.formatMessage(messages['id.verification.account.name.warning.prefix'])}</strong>,
|
||||
accountSettings: <Link to="/">{props.intl.formatMessage(messages['id.verification.account.name.settings'])}</Link>,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label htmlFor="photo-id-name">Name</label>
|
||||
<label htmlFor="photo-id-name">
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.label'])}
|
||||
</label>
|
||||
<div className="d-flex">
|
||||
<Input
|
||||
id="photo-id-name"
|
||||
@@ -49,7 +64,7 @@ export default function GetNameIdPanel() {
|
||||
className="btn-link px-0 ml-3"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
Edit
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.edit'])}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -57,14 +72,20 @@ export default function GetNameIdPanel() {
|
||||
<ImagePreview
|
||||
id="photo-of-id"
|
||||
src={idPhotoFile}
|
||||
alt="Photo of your ID to be submitted."
|
||||
alt={props.intl.formatMessage(messages['id.verification.account.name.photo.alt'])}
|
||||
/>
|
||||
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary">
|
||||
{isEditing ? 'Save' : 'Next'}
|
||||
{isEditing ? props.intl.formatMessage(messages['id.verification.account.name.save']) : props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
GetNameIdPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GetNameIdPanel);
|
||||
|
||||
@@ -1,33 +1,50 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
|
||||
export default function IdContextPanel() {
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function IdContextPanel(props) {
|
||||
const panelSlug = 'id-context';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title="Helpful ID Tips"
|
||||
title={props.intl.formatMessage(messages['id.verification.id.tips.title'])}
|
||||
>
|
||||
<p>Next you'll need an eligible photo, make sure that:</p>
|
||||
<p>{props.intl.formatMessage(messages['id.verification.id.tips.description'])}</p>
|
||||
<div className="card mb-4 shadow">
|
||||
<div className="card-body">
|
||||
<h6>Photo Tips</h6>
|
||||
<p>To take a successful photo, make sure that:</p>
|
||||
<h6>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.list.title'])}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.list.description'])}
|
||||
</p>
|
||||
<ul className="mb-0">
|
||||
<li>Your ID is well-lit.</li>
|
||||
<li>Ensure that you can see your photo and clearly read your name.</li>
|
||||
<li>
|
||||
{props.intl.formatMessage(messages['id.verification.id.tips.list.well.lit'])}
|
||||
</li>
|
||||
<li>
|
||||
{props.intl.formatMessage(messages['id.verification.id.tips.list.clear'])}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary">
|
||||
Next
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
IdContextPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(IdContextPanel);
|
||||
|
||||
@@ -1,33 +1,52 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
|
||||
export default function PortraitPhotoContextPanel() {
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function PortraitPhotoContextPanel(props) {
|
||||
const panelSlug = 'portrait-photo-context';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title="Helpful Photo Tips"
|
||||
title={props.intl.formatMessage(messages['id.verification.photo.tips.title'])}
|
||||
>
|
||||
<p>Next, we'll need you to take a photo of your face. Please review the helpful tips below.</p>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.description'])}
|
||||
</p>
|
||||
<div className="card mb-4 shadow">
|
||||
<div className="card-body">
|
||||
<h6>Photo Tips</h6>
|
||||
<p>To take a successful photo, make sure that:</p>
|
||||
<h6>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.list.title'])}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.list.description'])}
|
||||
</p>
|
||||
<ul className="mb-0">
|
||||
<li>Your face is well-lit.</li>
|
||||
<li>Your entire face fits inside the frame.</li>
|
||||
<li>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.list.well.lit'])}
|
||||
</li>
|
||||
<li>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.list.inside.frame'])}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary">
|
||||
Next
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
PortraitPhotoContextPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(PortraitPhotoContextPanel);
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Collapsible } from '@edx/paragon';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import { IdVerificationContext, MEDIA_ACCESS } from '../IdVerificationContext';
|
||||
|
||||
export default function RequestCameraAccessPanel() {
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function RequestCameraAccessPanel(props) {
|
||||
const panelSlug = 'request-camera-access';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
const { tryGetUserMedia, mediaAccess } = useContext(IdVerificationContext);
|
||||
@@ -14,25 +17,33 @@ export default function RequestCameraAccessPanel() {
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title="Camera Permissions"
|
||||
title={props.intl.formatMessage(messages['id.verification.camera.access.title'])}
|
||||
>
|
||||
{mediaAccess === MEDIA_ACCESS.PENDING && (
|
||||
<div>
|
||||
<p>In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. <strong>Please make sure to click "Allow"</strong></p>
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="id.verification.request.camera.access.instructions"
|
||||
defaultMessage="In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}"
|
||||
description="Instructions to enable camera access."
|
||||
values={{
|
||||
clickAllow: <strong>{props.intl.formatMessage(messages['id.verification.camera.access.click.allow'])}</strong>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<div className="action-row">
|
||||
<button className="btn btn-primary" onClick={tryGetUserMedia}>
|
||||
Enable Camera
|
||||
{props.intl.formatMessage(messages['id.verification.camera.access.enable'])}
|
||||
</button>
|
||||
<Collapsible.Advanced className="mr-auto">
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Collapsible.Trigger tag="button" className="btn btn-link px-0">
|
||||
Having problems?
|
||||
{props.intl.formatMessage(messages['id.verification.camera.access.problems'])}
|
||||
</Collapsible.Trigger>
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Body>
|
||||
<Link to={nextPanelSlug} className="btn btn-link">
|
||||
Skip and upload image files instead
|
||||
{props.intl.formatMessage(messages['id.verification.camera.access.skip'])}
|
||||
</Link>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
@@ -43,11 +54,11 @@ export default function RequestCameraAccessPanel() {
|
||||
{mediaAccess === MEDIA_ACCESS.GRANTED && (
|
||||
<div>
|
||||
<p>
|
||||
Looks like your camera is working and ready.
|
||||
{props.intl.formatMessage(messages['id.verification.camera.access.success'])}
|
||||
</p>
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary">
|
||||
Next
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -56,12 +67,11 @@ export default function RequestCameraAccessPanel() {
|
||||
{[MEDIA_ACCESS.UNSUPPORTED, MEDIA_ACCESS.DENIED].includes(mediaAccess) && (
|
||||
<div>
|
||||
<p>
|
||||
It looks like we're unable to access your camera. You will need to upload
|
||||
image files of you and your photo id.
|
||||
{props.intl.formatMessage(messages['id.verification.camera.access.failure'])}
|
||||
</p>
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary">
|
||||
Next
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,3 +80,9 @@ export default function RequestCameraAccessPanel() {
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
RequestCameraAccessPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(RequestCameraAccessPanel);
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
|
||||
export default function ReviewRequirementsPanel() {
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function ReviewRequirementsPanel(props) {
|
||||
const panelSlug = 'review-requirements';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
return (
|
||||
@@ -14,31 +17,62 @@ export default function ReviewRequirementsPanel() {
|
||||
focusOnMount={false}
|
||||
>
|
||||
<p>
|
||||
In order to complete Photo Verification online, you will need the following
|
||||
{props.intl.formatMessage(messages['id.verification.requirements.description'])}
|
||||
</p>
|
||||
<div className="card mb-4 shadow">
|
||||
<div className="card-body">
|
||||
<h6>Device with Camera</h6>
|
||||
<p className="mb-0">You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click <strong>Allow</strong>.</p>
|
||||
<h6>
|
||||
{props.intl.formatMessage(messages['id.verification.requirements.card.device.title'])}
|
||||
</h6>
|
||||
<p className="mb-0">
|
||||
<FormattedMessage
|
||||
id="id.verification.requirements.card.device.text"
|
||||
defaultMessage="You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}."
|
||||
description="Text explaining that the user needs access to a camera."
|
||||
values={{
|
||||
allow: <strong>{props.intl.formatMessage(messages['id.verification.requirements.card.device.allow'])}</strong>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card mb-4 shadow">
|
||||
<div className="card-body">
|
||||
<h6>Photo Identification</h6>
|
||||
<p className="mb-0">You need a valid ID that contains your full name and photo.</p>
|
||||
<h6>
|
||||
{props.intl.formatMessage(messages['id.verification.requirements.card.id.title'])}
|
||||
</h6>
|
||||
<p className="mb-0">
|
||||
{props.intl.formatMessage(messages['id.verification.requirements.card.id.text'])}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="mb-3">Privacy Information</h4>
|
||||
<h6>Why does edX need my photo?</h6>
|
||||
<p>We use your verification photos to confirm your identity and ensure the validity of your certificate.</p>
|
||||
<h6>What does edX do with this photo?</h6>
|
||||
<p>We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.</p>
|
||||
<h4 className="mb-3">
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.title'])}
|
||||
</h4>
|
||||
<h6>
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.need.photo.question'])}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}
|
||||
</p>
|
||||
<h6>
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.question'])}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.answer'])}
|
||||
</p>
|
||||
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary">
|
||||
Next
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
ReviewRequirementsPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ReviewRequirementsPanel);
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
|
||||
export default function SubmittedPanel() {
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function SubmittedPanel(props) {
|
||||
const panelSlug = 'submitted';
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title="Identity Verification in Progress"
|
||||
title={props.intl.formatMessage(messages['id.verification.submitted.title'])}
|
||||
>
|
||||
<p>We have received you information and are verifiying your identity. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days). In the meantime, you can still access all available course content.</p>
|
||||
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/dashboard`}>Return to Your Dashboard</a>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.submitted.text'])}
|
||||
</p>
|
||||
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||
{props.intl.formatMessage(messages['id.verification.return'])}
|
||||
</a>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
SubmittedPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SubmittedPanel);
|
||||
|
||||
@@ -2,14 +2,16 @@ import React, { useContext } from 'react';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { Input, Button } from '@edx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import { submitIdVerfication } from '../data/service';
|
||||
import BasePanel from './BasePanel';
|
||||
import { IdVerificationContext } from '../IdVerificationContext';
|
||||
import ImagePreview from '../ImagePreview';
|
||||
|
||||
export default function SummaryPanel() {
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function SummaryPanel(props) {
|
||||
const panelSlug = 'summary';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
const {
|
||||
@@ -19,22 +21,15 @@ export default function SummaryPanel() {
|
||||
idPhotoName,
|
||||
} = useContext(IdVerificationContext);
|
||||
const nameToBeUsed = idPhotoName || nameOnAccount || '';
|
||||
const courseRunKey = null; // TODO: Implement course run key
|
||||
// TODO: Implement course run key
|
||||
|
||||
function SubmitButton() {
|
||||
function handleClick(e) {
|
||||
const verificationData = {
|
||||
facePhotoFile,
|
||||
idPhotoFile,
|
||||
idPhotoName,
|
||||
courseRunKey,
|
||||
};
|
||||
const { success, message } = submitIdVerfication(verificationData);
|
||||
function handleClick() {
|
||||
history.push(nextPanelSlug);
|
||||
}
|
||||
return (
|
||||
<Button className="btn btn-primary" onClick={handleClick}>
|
||||
Confirm
|
||||
<Button className="btn btn-primary" title="Confirmation" onClick={handleClick}>
|
||||
{props.intl.formatMessage(messages['id.verification.review.confirm'])}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -42,16 +37,20 @@ export default function SummaryPanel() {
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title="Review Your Photos"
|
||||
title={props.intl.formatMessage(messages['id.verification.review.title'])}
|
||||
>
|
||||
<p>Make sure we can verify your identity with the photos and information you have provided.</p>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.review.description'])}
|
||||
</p>
|
||||
<div className="row mb-4">
|
||||
<div className="col-6">
|
||||
<label htmlFor="photo-of-face">Your Portrait</label>
|
||||
<label htmlFor="photo-of-face">
|
||||
{props.intl.formatMessage(messages['id.verification.review.portrait.label'])}
|
||||
</label>
|
||||
<ImagePreview
|
||||
id="photo-of-face"
|
||||
src={facePhotoFile}
|
||||
alt="Photo of your face to be submitted."
|
||||
alt={props.intl.formatMessage(messages['id.verification.review.portrait.alt'])}
|
||||
/>
|
||||
<Link
|
||||
className="btn btn-inverse-primary shadow"
|
||||
@@ -60,15 +59,17 @@ export default function SummaryPanel() {
|
||||
state: { fromSummary: true },
|
||||
}}
|
||||
>
|
||||
Retake Portrait Photo
|
||||
{props.intl.formatMessage(messages['id.verification.review.portrait.retake'])}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<label htmlFor="photo-of-id/edit">Your Photo ID</label>
|
||||
<label htmlFor="photo-of-id/edit">
|
||||
{props.intl.formatMessage(messages['id.verification.review.id.label'])}
|
||||
</label>
|
||||
<ImagePreview
|
||||
id="photo-of-id"
|
||||
src={idPhotoFile}
|
||||
alt="Photo of your ID to be submitted."
|
||||
alt={props.intl.formatMessage(messages['id.verification.review.id.alt'])}
|
||||
/>
|
||||
<Link
|
||||
className="btn btn-inverse-primary shadow"
|
||||
@@ -77,12 +78,14 @@ export default function SummaryPanel() {
|
||||
state: { fromSummary: true },
|
||||
}}
|
||||
>
|
||||
Retake ID Photo
|
||||
{props.intl.formatMessage(messages['id.verification.review.id.retake'])}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="name-to-be-used">Name on ID</label>
|
||||
<label htmlFor="name-to-be-used">
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.label'])}
|
||||
</label>
|
||||
<div className="d-flex">
|
||||
<Input
|
||||
id="name-to-be-used"
|
||||
@@ -99,7 +102,7 @@ export default function SummaryPanel() {
|
||||
state: { fromSummary: true },
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.edit'])}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,3 +110,9 @@ export default function SummaryPanel() {
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
SummaryPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SummaryPanel);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
@@ -9,7 +10,9 @@ import Camera from '../Camera';
|
||||
import CameraHelp from '../CameraHelp';
|
||||
import { IdVerificationContext, MEDIA_ACCESS } from '../IdVerificationContext';
|
||||
|
||||
export default function TakeIdPhotoPanel() {
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function TakeIdPhotoPanel(props) {
|
||||
const panelSlug = 'take-id-photo';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
const { setIdPhotoFile, idPhotoFile, mediaAccess } = useContext(IdVerificationContext);
|
||||
@@ -17,29 +20,39 @@ export default function TakeIdPhotoPanel() {
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={shouldUseCamera ? 'Take ID Photo' : 'Upload Your ID Photo'}
|
||||
title={shouldUseCamera ? props.intl.formatMessage(messages['id.verification.id.photo.title.camera']) : props.intl.formatMessage(messages['id.verification.id.photo.title.upload'])}
|
||||
>
|
||||
<div>
|
||||
{idPhotoFile && !shouldUseCamera && <ImagePreview src={idPhotoFile} alt="Preview of photo of ID." />}
|
||||
{idPhotoFile && !shouldUseCamera && <ImagePreview src={idPhotoFile} alt={props.intl.formatMessage(messages['id.verification.id.photo.preview.alt'])} />}
|
||||
|
||||
{shouldUseCamera ? (
|
||||
<div>
|
||||
<p>When your ID is in position, use the Take Photo button below to take your photo.</p>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.camera'])}
|
||||
</p>
|
||||
<Camera onImageCapture={setIdPhotoFile} />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p>Please upload a ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)</p>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
|
||||
</p>
|
||||
<ImageFileUpload onFileChange={setIdPhotoFile} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="action-row" style={{ visibility: idPhotoFile ? 'unset' : 'hidden' }}>
|
||||
<Link to={nextPanelSlug} className="btn btn-primary">
|
||||
Next
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
{shouldUseCamera && <CameraHelp/>}
|
||||
{shouldUseCamera && <CameraHelp />}
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
TakeIdPhotoPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(TakeIdPhotoPanel);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
@@ -9,7 +10,9 @@ import Camera from '../Camera';
|
||||
import CameraHelp from '../CameraHelp';
|
||||
import { IdVerificationContext, MEDIA_ACCESS } from '../IdVerificationContext';
|
||||
|
||||
export default function TakePortraitPhotoPanel() {
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function TakePortraitPhotoPanel(props) {
|
||||
const panelSlug = 'take-portrait-photo';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
const { setFacePhotoFile, facePhotoFile, mediaAccess } = useContext(IdVerificationContext);
|
||||
@@ -18,29 +21,39 @@ export default function TakePortraitPhotoPanel() {
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={shouldUseCamera ? 'Take Your Photo' : 'Upload Your Portrait Photo'}
|
||||
title={shouldUseCamera ? props.intl.formatMessage(messages['id.verification.portrait.photo.title.camera']) : props.intl.formatMessage(messages['id.verification.portrait.photo.title.upload'])}
|
||||
>
|
||||
<div>
|
||||
{facePhotoFile && !shouldUseCamera && <ImagePreview src={facePhotoFile} alt="Preview of photo of user's face." />}
|
||||
{facePhotoFile && !shouldUseCamera && <ImagePreview src={facePhotoFile} alt={props.intl.formatMessage(messages['id.verification.portrait.photo.preview.alt'])} />}
|
||||
|
||||
{shouldUseCamera ? (
|
||||
<div>
|
||||
<p>When your face is in position, use the Take Photo button below to take your photo.</p>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.portrait.photo.instructions.camera'])}
|
||||
</p>
|
||||
<Camera onImageCapture={setFacePhotoFile} />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<p>Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)</p>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.portrait.photo.instructions.upload'])}
|
||||
</p>
|
||||
<ImageFileUpload onFileChange={setFacePhotoFile} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="action-row" style={{ visibility: facePhotoFile ? 'unset' : 'hidden' }}>
|
||||
<Link to={nextPanelSlug} className="btn btn-primary">
|
||||
Next
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
{shouldUseCamera && <CameraHelp/>}
|
||||
{shouldUseCamera && <CameraHelp />}
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
TakePortraitPhotoPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(TakePortraitPhotoPanel);
|
||||
|
||||
@@ -30,10 +30,6 @@ export const useNextPanelSlug = (originSlug) => {
|
||||
// check if the user is too far into the flow and if so, return the slug of the
|
||||
// furthest panel they are allow to be.
|
||||
export const useVerificationRedirectSlug = (slug) => {
|
||||
// TODO: remove this short-circuit after development is done
|
||||
return null;
|
||||
|
||||
// eslint-disable-next-line no-unreachable
|
||||
const { facePhotoFile, idPhotoFile } = useContext(IdVerificationContext);
|
||||
const indexOfCurrentPanel = panelSteps.indexOf(slug);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user