diff --git a/package-lock.json b/package-lock.json
index fe0ed04..a883d01 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11287,6 +11287,11 @@
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true
},
+ "jslib-html5-camera-photo": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/jslib-html5-camera-photo/-/jslib-html5-camera-photo-3.1.3.tgz",
+ "integrity": "sha512-8vJwX0a6o82+AJur7KntGvGLYhfxN5h7HRNd77zuoLOi3/Rd+5L47gL9MwTHdtWu3J3VC3a3MuC4KiW8l8HXzA=="
+ },
"json-buffer": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz",
@@ -14728,10 +14733,9 @@
"dev": true
},
"qs": {
- "version": "6.5.2",
- "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
- "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
- "dev": true
+ "version": "6.9.3",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.3.tgz",
+ "integrity": "sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw=="
},
"query-string": {
"version": "5.1.1",
@@ -15582,6 +15586,12 @@
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=",
"dev": true
},
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==",
+ "dev": true
+ },
"tough-cookie": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
diff --git a/package.json b/package.json
index 4235419..3229f80 100755
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"form-urlencoded": "4.0.1",
"formdata-polyfill": "3.0.19",
"history": "4.10.1",
+ "jslib-html5-camera-photo": "^3.1.3",
"lodash.camelcase": "4.3.0",
"lodash.debounce": "4.0.8",
"lodash.findindex": "4.6.0",
@@ -56,6 +57,7 @@
"memoize-one": "5.1.1",
"newrelic": "5.13.1",
"prop-types": "15.7.2",
+ "qs": "6.9.3",
"react": "16.10.2",
"react-dom": "16.10.2",
"react-redux": "7.1.3",
diff --git a/src/id-verification/Camera.jsx b/src/id-verification/Camera.jsx
new file mode 100644
index 0000000..360148a
--- /dev/null
+++ b/src/id-verification/Camera.jsx
@@ -0,0 +1,101 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import CameraPhoto, { FACING_MODES } from 'jslib-html5-camera-photo';
+
+import shutter from './data/camera-shutter.base64.json'
+
+const PHOTO_PROMTS = {
+ TAKE_PHOTO: 'Take Photo',
+ RETAKE_PHOTO: 'Retake Photo'
+}
+
+class Camera extends React.Component {
+ constructor (props, context) {
+ super(props, context);
+ this.cameraPhoto = null;
+ this.videoRef = React.createRef();
+ this.state = {
+ trackedObject: null,
+ dataUri: ''
+ }
+ }
+
+ componentDidMount () {
+ this.cameraPhoto = new CameraPhoto(this.videoRef.current);
+ this.cameraPhoto.startCameraMaxResolution(FACING_MODES.USER)
+ }
+
+ takePhoto () {
+ if (this.state.dataUri) {
+ return this.reset()
+ }
+ const config = {
+ sizeFactor: 1
+ };
+
+ this.playShutterClick()
+ let dataUri = this.cameraPhoto.getDataUri(config);
+ this.setState({ dataUri });
+ this.props.onImageCapture(dataUri)
+ }
+
+ playShutterClick () {
+ let audio = new Audio('data:audio/mp3;base64,' + shutter.base64);
+ audio.play();
+ }
+
+ 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';
+ return (
+
+
+
+
+
+
+
{
+ this.takePhoto();
+ }}> {this.state.dataUri ? PHOTO_PROMTS.RETAKE_PHOTO : PHOTO_PROMTS.TAKE_PHOTO}
+
+ );
+ }
+}
+
+Camera.propTypes = {
+ onImageCapture: PropTypes.func.isRequired,
+}
+
+export default Camera;
\ No newline at end of file
diff --git a/src/id-verification/CameraHelp.jsx b/src/id-verification/CameraHelp.jsx
new file mode 100644
index 0000000..d02f409
--- /dev/null
+++ b/src/id-verification/CameraHelp.jsx
@@ -0,0 +1,19 @@
+
+import React from 'react';
+
+export default function CameraHelp() {
+ return (
+
+
What if I can't see the camera image or if I can't see my photo to determine which side is visible
+
+ 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.
+
+
What if I have difficulty holding my head in position relative to the camera?
+
If you require assistance with taking a photo for submission, contact edX support for additional suggestions.
+
+ );
+}
diff --git a/src/id-verification/IdVerificationContext.jsx b/src/id-verification/IdVerificationContext.jsx
new file mode 100644
index 0000000..187ae1a
--- /dev/null
+++ b/src/id-verification/IdVerificationContext.jsx
@@ -0,0 +1,106 @@
+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'
+
+const IdVerificationContext = React.createContext({});
+
+const MEDIA_ACCESS = {
+ PENDING: 'pending',
+ UNSUPPORTED: 'unsupported',
+ DENIED: 'denied',
+ GRANTED: 'granted',
+};
+
+function IdVerificationContextProvider({ children }) {
+ const [existingIdVerification, setExistingIdVerification] = useState(null);
+ const [facePhotoFile, setFacePhotoFile] = useState(null);
+ const [idPhotoFile, setIdPhotoFile] = useState(null);
+ const [idPhotoName, setIdPhotoName] = useState(null);
+ const [mediaStream, setMediaStream] = useState(null);
+ const [mediaAccess, setMediaAccess] = useState(hasGetUserMediaSupport ?
+ MEDIA_ACCESS.PENDING :
+ MEDIA_ACCESS.UNSUPPORTED);
+ const { authenticatedUser } = useContext(AppContext);
+
+ const contextValue = {
+ existingIdVerification,
+ facePhotoFile,
+ idPhotoFile,
+ idPhotoName,
+ mediaStream,
+ mediaAccess,
+ nameOnAccount: authenticatedUser.name,
+ setExistingIdVerification,
+ setFacePhotoFile,
+ setIdPhotoFile,
+ setIdPhotoName,
+ tryGetUserMedia: async () => {
+ try {
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
+ setMediaAccess(MEDIA_ACCESS.GRANTED);
+ setMediaStream(stream);
+ // If we would like to stop the stream immediately. I guess we can leave it open
+ // const tracks = stream.getTracks();
+ // tracks.forEach(track => track.stop());
+ } catch (err) {
+ setMediaAccess(MEDIA_ACCESS.DENIED);
+ }
+ },
+ };
+
+ // Call verification status endpoint to check whether we can verify.
+ useEffect(() => {(async () => {
+ const existingIdV = await getExistingIdVerification();
+ setExistingIdVerification(existingIdV);
+ })()}, []);
+
+ // If we are waiting for verification status endpoint, show spinner.
+ if (!existingIdVerification) {
+ return ;
+ }
+
+ if (!existingIdVerification.canVerify) {
+ const status = existingIdVerification.status;
+ return (
+
+
Identity Verification
+ {status === 'pending' || status == 'approved'
+ ?
+ 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).
+
+ :
+ You cannot verify your identity at this time.
+
+ }
+
+ Return to Your Dashboard
+
+
+ );
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+IdVerificationContextProvider.propTypes = {
+ children: PropTypes.node,
+};
+IdVerificationContextProvider.defaultProps = {
+ children: undefined,
+};
+
+export {
+ IdVerificationContext,
+ IdVerificationContextProvider,
+ MEDIA_ACCESS,
+};
diff --git a/src/id-verification/IdVerificationPage.jsx b/src/id-verification/IdVerificationPage.jsx
new file mode 100644
index 0000000..77e4850
--- /dev/null
+++ b/src/id-verification/IdVerificationPage.jsx
@@ -0,0 +1,73 @@
+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 { Modal, Button } from '@edx/paragon';
+import { idVerificationSelector } from './data/selectors';
+import './getUserMediaShim';
+
+import { IdVerificationContextProvider } from './IdVerificationContext';
+import ReviewRequirementsPanel from './panels/ReviewRequirementsPanel';
+import RequestCameraAccessPanel from './panels/RequestCameraAccessPanel';
+import PortraitPhotoContextPanel from './panels/PortraitPhotoContextPanel';
+import TakePortraitPhotoPanel from './panels/TakePortraitPhotoPanel';
+import IdContextPanel from './panels/IdContextPanel';
+import GetNameIdPanel from './panels/GetNameIdPanel';
+import TakeIdPhotoPanel from './panels/TakeIdPhotoPanel';
+import SummaryPanel from './panels/SummaryPanel';
+import SubmittedPanel from './panels/SubmittedPanel';
+
+// eslint-disable-next-line react/prefer-stateless-function
+function IdVerificationPage() {
+ const { path } = useRouteMatch();
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ setIsModalOpen(true)}>
+ Privacy Information
+
+
+
+
+
+
+ Why does edX need my photo?
+ We use your verification photos to confirm your identity and ensure the validity of your certificate.
+ What does edX do with this photo?
+ 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.
+
+ )}
+ onClose={() => setIsModalOpen(false)}
+ />
+
+
+ );
+}
+export default connect(idVerificationSelector, {
+})(injectIntl(IdVerificationPage));
+
diff --git a/src/id-verification/ImageFileUpload.jsx b/src/id-verification/ImageFileUpload.jsx
new file mode 100644
index 0000000..00d7941
--- /dev/null
+++ b/src/id-verification/ImageFileUpload.jsx
@@ -0,0 +1,28 @@
+import React, { useCallback } from 'react';
+import PropTypes from 'prop-types';
+
+export default function ImageFileUpload({ onFileChange }) {
+ const handleChange = useCallback((e) => {
+ if (e.target.files.length === 0) {
+ // do something else
+ return;
+ }
+
+ const fileObject = e.target.files[0];
+ const fileReader = new FileReader();
+ fileReader.addEventListener('load', () => onFileChange(fileReader.result));
+ fileReader.readAsDataURL(fileObject);
+ }, []);
+
+ return (
+
+ );
+}
+
+ImageFileUpload.propTypes = {
+ onFileChange: PropTypes.func.isRequired,
+};
diff --git a/src/id-verification/ImagePreview.jsx b/src/id-verification/ImagePreview.jsx
new file mode 100644
index 0000000..ad9d6f8
--- /dev/null
+++ b/src/id-verification/ImagePreview.jsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export default function ImagePreview({ src, alt, id }) {
+ return (
+
+
+
+
+
+ );
+}
+
+ImagePreview.propTypes = {
+ src: PropTypes.string.isRequired,
+ alt: PropTypes.string.isRequired,
+ id: PropTypes.string,
+};
diff --git a/src/id-verification/_id-verification.scss b/src/id-verification/_id-verification.scss
new file mode 100644
index 0000000..b5d7f88
--- /dev/null
+++ b/src/id-verification/_id-verification.scss
@@ -0,0 +1,62 @@
+.page__id-verification {
+ .verification-panel {
+ .card {
+ border-top: solid 4px theme-color('warning');
+ }
+ .image-preview {
+ margin-bottom: 1rem;;
+ max-width: 20rem;
+ img {
+ display: block;
+ max-width: 100%;
+ max-height: 10rem;
+ }
+ }
+ }
+ .action-row {
+ display: flex;
+ flex-direction: row-reverse;
+ align-items: flex-start;
+ .btn-primary {
+ min-width: 8rem;
+ margin-left: 1rem;
+ }
+ .btn-link {
+ padding-left: 0;
+ padding-right: 0;
+ text-decoration: underline;
+ align-self: center;
+ }
+ }
+ .camera-outer-wrapper {
+ margin-bottom: 15px;
+ text-align: center;
+ border-radius: 0.3rem;
+ }
+ .camera-wrapper {
+ position: relative;
+ width: 100%;
+ height: 100%
+ }
+
+ .camera-video {
+ width: 100%;
+ }
+
+ .camera-btn {
+ margin: 10px;
+ }
+
+ .camera-flash {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ opacity :1;
+ transition:opacity .9s ease-out;
+ }
+
+ .camera-flash.do-transition {
+ opacity: 0;
+ background: white;
+ }
+}
\ No newline at end of file
diff --git a/src/id-verification/data/camera-shutter.base64.json b/src/id-verification/data/camera-shutter.base64.json
new file mode 100644
index 0000000..50112c1
--- /dev/null
+++ b/src/id-verification/data/camera-shutter.base64.json
@@ -0,0 +1,3 @@
+{
+ "base64": "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Oau1+DczWWohH8L0eh/nWTbo8/OvyZ/ccZuEf/+Rn/l/L/M//+ZEqKpH+ZFToqJ+iqi+i/yoi+ip9yhgYMh27OZUWpgoIEd1ITFaxUllRQWcDAaskNCYwCgJAlEQaBsJgsBQSlREGgbCYLBUBAUSiINB2dKnUZK3kSJ1948Y9LNCn/I9c763Wf1AVct8rUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5REtg5yOQ+/ACAYIkLiWBAEIy5G6UjKQIBcAPWAH5ARjShVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVACCU025+ZGfywyOy3//ksyP+XMmstIyNWCggQNHI1ls////WWRyMmVgYME6GTLJZ///2WWxyNWWSoZGrWyzL/9WVqCBOhkyhgYMI6GrBQUqFpoVFP9bDILCzOLiv/rFRYk//qF+LCrDKm/4o2sVFtbP6hdmLCsyoXUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5Rkjg/ygl8ruCAaQiiABWAAAAAAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVX/+5RkUY/wAABpAAAACAAADSAAAAEAAAGkAAAAIAAANIAAAARVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVU="
+}
\ No newline at end of file
diff --git a/src/id-verification/data/selectors.js b/src/id-verification/data/selectors.js
new file mode 100644
index 0000000..9c94d05
--- /dev/null
+++ b/src/id-verification/data/selectors.js
@@ -0,0 +1,3 @@
+export const storeName = 'idVerifiction';
+
+export const idVerificationSelector = state => ({ ...state[storeName] });
diff --git a/src/id-verification/data/service.js b/src/id-verification/data/service.js
new file mode 100644
index 0000000..41dec84
--- /dev/null
+++ b/src/id-verification/data/service.js
@@ -0,0 +1,71 @@
+import qs from 'qs';
+
+import { getConfig } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+
+/**
+ * Get ID verification status from LMS.
+ *
+ * Returns {
+ * status: String,
+ * expires: String|null,
+ * canVerify: Boolean,
+ * }
+ */
+export async function getExistingIdVerification() {
+ const url = `${getConfig().LMS_BASE_URL}/verify_student/status/`;
+ const requestConfig = {
+ headers: { Accept: 'application/json' },
+ };
+ try {
+ const response = await getAuthenticatedHttpClient().get(url, requestConfig);
+ return {
+ status: response.data.status || null,
+ expires: response.data.expires || null,
+ canVerify: response.data.can_verify || false,
+ };
+ } catch (e) {
+ return { status: null, expires: null, canVerify: false };
+ }
+}
+
+/**
+ * Submit ID verifiction to LMS.
+ *
+ * verificationData should take the shape of:
+ * - facePhotoFile (String): Base64-encoded image.
+ * - idPhotoFile (String|null): Optional Base64-encoded image
+ * - idPhotoName (String|null): Optional string to change the user's name to.
+ * - courseRunKey (String|null): Optional course run to redirect to.
+ *
+ * Returns { success: Boolean, message: String|null }
+ */
+export async function submitIdVerfication(verificationData) {
+ const keyMap = {
+ facePhotoFile: 'face_image',
+ idPhotoFile: 'photo_id_image',
+ idPhotoName: 'full_name',
+ courseRunKey: 'course_id',
+ };
+ const postData = {};
+ // Don't include blank/null/undefined values.
+ // Note that this will also drop the value `false`.
+ Object.keys(keyMap).forEach((jsKey) => {
+ const apiKey = keyMap[jsKey];
+ if (verificationData[jsKey]) {
+ postData[apiKey] = verificationData[jsKey];
+ }
+ });
+
+ const url = `${getConfig().LMS_BASE_URL}/verify_student/submit-photos/`;
+ const urlEncodedPostData = qs.stringify(postData);
+ const requestConfig = {
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ };
+ try {
+ await getAuthenticatedHttpClient().post(url, urlEncodedPostData, requestConfig);
+ return { success: true, message: null };
+ } catch (e) {
+ return { success: false, message: String(e) }; // TODO: is String(e) right?
+ }
+}
diff --git a/src/id-verification/getUserMediaShim.js b/src/id-verification/getUserMediaShim.js
new file mode 100644
index 0000000..2cc0169
--- /dev/null
+++ b/src/id-verification/getUserMediaShim.js
@@ -0,0 +1,59 @@
+/**
+ * This polyfill is from MDN:
+ * https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
+ *
+ * Their description:
+ * "Here's an example of using navigator.mediaDevices.getUserMedia(), with a
+ * polyfill to cope with older browsers. Note that this polyfill does not
+ * correct for legacy differences in constraints syntax, which means constraints
+ * won't work well across browsers. It is recommended to use the adapter.js
+ * polyfill instead, which does handle constraints."
+ *
+ * Despite the lack of support for differences in constraints we'll use this
+ * since it's small and simple and we don't have a need for constraints at the
+ * moment. I've added an export hasGetUserMediaSupport before the polyfill to
+ * help us understand support before making calls to getUserMedia.
+ */
+
+// IIFE to check getUserMedia support. Must be run before the polyfill.
+const hasGetUserMediaSupport = (() => {
+ // Modern API
+ if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
+ return true;
+ }
+ // Deprecated APIs
+ if (navigator.webkitGetUserMedia || navigator.mozGetUserMedia) {
+ return true;
+ }
+ return false;
+})();
+
+// Older browsers might not implement mediaDevices at all, so we set an empty object first
+if (navigator.mediaDevices === undefined) {
+ navigator.mediaDevices = {};
+}
+
+// Some browsers partially implement mediaDevices. We can't just assign an object
+// with getUserMedia as it would overwrite existing properties.
+// Here, we will just add the getUserMedia property if it's missing.
+if (navigator.mediaDevices.getUserMedia === undefined) {
+ // eslint-disable-next-line func-names
+ navigator.mediaDevices.getUserMedia = function (constraints) {
+ // First get ahold of the legacy getUserMedia, if present
+ const getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+
+ // Some browsers just don't implement it - return a rejected promise with an error
+ // to keep a consistent interface
+ if (!getUserMedia) {
+ return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
+ }
+
+ // Otherwise, wrap the call to the old navigator.getUserMedia with a Promise
+ return new Promise(((resolve, reject) => {
+ getUserMedia.call(navigator, constraints, resolve, reject);
+ }));
+ };
+}
+
+// eslint-disable-next-line import/prefer-default-export
+export { hasGetUserMediaSupport };
diff --git a/src/id-verification/index.js b/src/id-verification/index.js
new file mode 100644
index 0000000..fea8a5d
--- /dev/null
+++ b/src/id-verification/index.js
@@ -0,0 +1,4 @@
+export { default } from './IdVerificationPage';
+// export { default as reducer } from './data/reducers';
+// export { default as saga } from './data/sagas';
+// export { storeName } from './data/selectors';
diff --git a/src/id-verification/panels/BasePanel.jsx b/src/id-verification/panels/BasePanel.jsx
new file mode 100644
index 0000000..713003f
--- /dev/null
+++ b/src/id-verification/panels/BasePanel.jsx
@@ -0,0 +1,43 @@
+import React, { useRef, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { Redirect } from 'react-router';
+import { useVerificationRedirectSlug } from '../routing-utilities';
+
+export default function BasePanel({
+ children,
+ focusOnMount,
+ name,
+ title,
+}) {
+ const headingRef = useRef();
+
+ // focus heading element on mount
+ useEffect(() => {
+ if (focusOnMount && headingRef.current) {
+ headingRef.current.focus();
+ }
+ }, []);
+
+ const redirectSlug = useVerificationRedirectSlug(name);
+ if (redirectSlug) {
+ return ;
+ }
+
+ return (
+
+
{title}
+ {children}
+
+ );
+}
+
+BasePanel.propTypes = {
+ children: PropTypes.node.isRequired,
+ focusOnMount: PropTypes.bool,
+ name: PropTypes.string.isRequired,
+ title: PropTypes.node.isRequired,
+};
+
+BasePanel.defaultProps = {
+ focusOnMount: true,
+};
diff --git a/src/id-verification/panels/GetNameIdPanel.jsx b/src/id-verification/panels/GetNameIdPanel.jsx
new file mode 100644
index 0000000..9516f4a
--- /dev/null
+++ b/src/id-verification/panels/GetNameIdPanel.jsx
@@ -0,0 +1,70 @@
+import React, { useContext, useState, useEffect, useRef } from 'react';
+import { Input, Button } from '@edx/paragon';
+import { Link } from 'react-router-dom';
+
+import { useNextPanelSlug } from '../routing-utilities';
+import BasePanel from './BasePanel';
+import { IdVerificationContext } from '../IdVerificationContext';
+import ImagePreview from '../ImagePreview';
+
+export default function GetNameIdPanel() {
+ const panelSlug = 'get-name-id';
+ const [isEditing, setIsEditing] = useState(false);
+ const nameInputRef = useRef();
+ const nextPanelSlug = useNextPanelSlug(panelSlug);
+ useEffect(() => {
+ if (isEditing && nameInputRef.current) {
+ nameInputRef.current.focus();
+ }
+ }, [isEditing]);
+ const {
+ nameOnAccount, idPhotoName, setIdPhotoName, idPhotoFile,
+ } = useContext(IdVerificationContext);
+ const nameOnAccountValue = nameOnAccount || '';
+ return (
+
+ Please check the Account Name below to ensure it matches the name on your ID. If not, click "Edit".
+
+
+ Please Note: any edit to your name will be saved to your account and can be reviewed on Account Settings.
+
+
+
+
Name
+
+ setIdPhotoName(e.target.value)}
+ />
+ {!isEditing && (
+ setIsEditing(true)}
+ >
+ Edit
+
+ )}
+
+
+
+
+
+
+ {isEditing ? 'Save' : 'Next'}
+
+
+
+ );
+}
diff --git a/src/id-verification/panels/IdContextPanel.jsx b/src/id-verification/panels/IdContextPanel.jsx
new file mode 100644
index 0000000..efce411
--- /dev/null
+++ b/src/id-verification/panels/IdContextPanel.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+import { useNextPanelSlug } from '../routing-utilities';
+import BasePanel from './BasePanel';
+
+export default function IdContextPanel() {
+ const panelSlug = 'id-context';
+ const nextPanelSlug = useNextPanelSlug(panelSlug);
+ return (
+
+ Next you'll need an eligible photo, make sure that:
+
+
+
Photo Tips
+
To take a successful photo, make sure that:
+
+ Your ID is well-lit.
+ Ensure that you can see your photo and clearly read your name.
+
+
+
+
+
+ Next
+
+
+
+ );
+}
diff --git a/src/id-verification/panels/PortraitPhotoContextPanel.jsx b/src/id-verification/panels/PortraitPhotoContextPanel.jsx
new file mode 100644
index 0000000..9d243e7
--- /dev/null
+++ b/src/id-verification/panels/PortraitPhotoContextPanel.jsx
@@ -0,0 +1,33 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+import { useNextPanelSlug } from '../routing-utilities';
+import BasePanel from './BasePanel';
+
+export default function PortraitPhotoContextPanel() {
+ const panelSlug = 'portrait-photo-context';
+ const nextPanelSlug = useNextPanelSlug(panelSlug);
+ return (
+
+ Next, we'll need you to take a photo of your face. Please review the helpful tips below.
+
+
+
Photo Tips
+
To take a successful photo, make sure that:
+
+ Your face is well-lit.
+ Your entire face fits inside the frame.
+
+
+
+
+
+ Next
+
+
+
+ );
+}
diff --git a/src/id-verification/panels/RequestCameraAccessPanel.jsx b/src/id-verification/panels/RequestCameraAccessPanel.jsx
new file mode 100644
index 0000000..ae811fc
--- /dev/null
+++ b/src/id-verification/panels/RequestCameraAccessPanel.jsx
@@ -0,0 +1,72 @@
+import React, { useContext } from 'react';
+import { Link } from 'react-router-dom';
+import { Collapsible } from '@edx/paragon';
+
+import { useNextPanelSlug } from '../routing-utilities';
+import BasePanel from './BasePanel';
+import { IdVerificationContext, MEDIA_ACCESS } from '../IdVerificationContext';
+
+export default function RequestCameraAccessPanel() {
+ const panelSlug = 'request-camera-access';
+ const nextPanelSlug = useNextPanelSlug(panelSlug);
+ const { tryGetUserMedia, mediaAccess } = useContext(IdVerificationContext);
+
+ return (
+
+ {mediaAccess === MEDIA_ACCESS.PENDING && (
+
+
In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. Please make sure to click "Allow"
+
+
+
+ Enable Camera
+
+
+
+
+ Having problems?
+
+
+
+
+ Skip and upload image files instead
+
+
+
+
+
+ )}
+
+ {mediaAccess === MEDIA_ACCESS.GRANTED && (
+
+
+ Looks like your camera is working and ready.
+
+
+
+ Next
+
+
+
+ )}
+
+ {[MEDIA_ACCESS.UNSUPPORTED, MEDIA_ACCESS.DENIED].includes(mediaAccess) && (
+
+
+ It looks like we're unable to access your camera. You will need to upload
+ image files of you and your photo id.
+
+
+
+ Next
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/id-verification/panels/ReviewRequirementsPanel.jsx b/src/id-verification/panels/ReviewRequirementsPanel.jsx
new file mode 100644
index 0000000..1a8512a
--- /dev/null
+++ b/src/id-verification/panels/ReviewRequirementsPanel.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+
+import { useNextPanelSlug } from '../routing-utilities';
+import BasePanel from './BasePanel';
+
+export default function ReviewRequirementsPanel() {
+ const panelSlug = 'review-requirements';
+ const nextPanelSlug = useNextPanelSlug(panelSlug);
+ return (
+
+
+ In order to complete Photo Verification online, you will need the following
+
+
+
+
Device with Camera
+
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 .
+
+
+
+
+
Photo Identification
+
You need a valid ID that contains your full name and photo.
+
+
+ Privacy Information
+ Why does edX need my photo?
+ We use your verification photos to confirm your identity and ensure the validity of your certificate.
+ What does edX do with this photo?
+ 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.
+
+
+
+ Next
+
+
+
+ );
+}
diff --git a/src/id-verification/panels/SubmittedPanel.jsx b/src/id-verification/panels/SubmittedPanel.jsx
new file mode 100644
index 0000000..634d9ba
--- /dev/null
+++ b/src/id-verification/panels/SubmittedPanel.jsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { getConfig } from '@edx/frontend-platform';
+
+import { useNextPanelSlug } from '../routing-utilities';
+import BasePanel from './BasePanel';
+
+export default function SubmittedPanel() {
+ const panelSlug = 'submitted';
+ return (
+
+ 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.
+ Return to Your Dashboard
+
+ );
+}
diff --git a/src/id-verification/panels/SummaryPanel.jsx b/src/id-verification/panels/SummaryPanel.jsx
new file mode 100644
index 0000000..3ff8476
--- /dev/null
+++ b/src/id-verification/panels/SummaryPanel.jsx
@@ -0,0 +1,109 @@
+import React, { useContext } from 'react';
+import { history } from '@edx/frontend-platform';
+import { Input, Button } from '@edx/paragon';
+import { Link } from 'react-router-dom';
+
+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() {
+ const panelSlug = 'summary';
+ const nextPanelSlug = useNextPanelSlug(panelSlug);
+ const {
+ facePhotoFile,
+ idPhotoFile,
+ nameOnAccount,
+ idPhotoName,
+ } = useContext(IdVerificationContext);
+ const nameToBeUsed = idPhotoName || nameOnAccount || '';
+ const courseRunKey = null; // TODO: Implement course run key
+
+ function SubmitButton() {
+ function handleClick(e) {
+ const verificationData = {
+ facePhotoFile,
+ idPhotoFile,
+ idPhotoName,
+ courseRunKey,
+ };
+ const { success, message } = submitIdVerfication(verificationData);
+ history.push(nextPanelSlug);
+ }
+ return (
+
+ Confirm
+
+ );
+ }
+
+ return (
+
+ Make sure we can verify your identity with the photos and information you have provided.
+
+
+ Your Portrait
+
+
+ Retake Portrait Photo
+
+
+
+ Your Photo ID
+
+
+ Retake ID Photo
+
+
+
+
+
Name on ID
+
+ {}}
+ />
+
+
+ Edit
+
+
+
+
+
+ );
+}
diff --git a/src/id-verification/panels/TakeIdPhotoPanel.jsx b/src/id-verification/panels/TakeIdPhotoPanel.jsx
new file mode 100644
index 0000000..3932e0c
--- /dev/null
+++ b/src/id-verification/panels/TakeIdPhotoPanel.jsx
@@ -0,0 +1,45 @@
+import React, { useContext } from 'react';
+import { Link } from 'react-router-dom';
+
+import { useNextPanelSlug } from '../routing-utilities';
+import BasePanel from './BasePanel';
+import ImageFileUpload from '../ImageFileUpload';
+import ImagePreview from '../ImagePreview';
+import Camera from '../Camera';
+import CameraHelp from '../CameraHelp';
+import { IdVerificationContext, MEDIA_ACCESS } from '../IdVerificationContext';
+
+export default function TakeIdPhotoPanel() {
+ const panelSlug = 'take-id-photo';
+ const nextPanelSlug = useNextPanelSlug(panelSlug);
+ const { setIdPhotoFile, idPhotoFile, mediaAccess } = useContext(IdVerificationContext);
+ const shouldUseCamera = mediaAccess === MEDIA_ACCESS.GRANTED;
+ return (
+
+
+ {idPhotoFile && !shouldUseCamera &&
}
+
+ {shouldUseCamera ? (
+
+
When your ID is in position, use the Take Photo button below to take your photo.
+
+
+ ) : (
+
+
Please upload a ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)
+
+
+ )}
+
+
+
+ Next
+
+
+ {shouldUseCamera && }
+
+ );
+}
diff --git a/src/id-verification/panels/TakePortraitPhotoPanel.jsx b/src/id-verification/panels/TakePortraitPhotoPanel.jsx
new file mode 100644
index 0000000..f5c6846
--- /dev/null
+++ b/src/id-verification/panels/TakePortraitPhotoPanel.jsx
@@ -0,0 +1,46 @@
+import React, { useContext } from 'react';
+import { Link } from 'react-router-dom';
+
+import { useNextPanelSlug } from '../routing-utilities';
+import BasePanel from './BasePanel';
+import ImageFileUpload from '../ImageFileUpload';
+import ImagePreview from '../ImagePreview';
+import Camera from '../Camera';
+import CameraHelp from '../CameraHelp';
+import { IdVerificationContext, MEDIA_ACCESS } from '../IdVerificationContext';
+
+export default function TakePortraitPhotoPanel() {
+ const panelSlug = 'take-portrait-photo';
+ const nextPanelSlug = useNextPanelSlug(panelSlug);
+ const { setFacePhotoFile, facePhotoFile, mediaAccess } = useContext(IdVerificationContext);
+ const shouldUseCamera = mediaAccess === MEDIA_ACCESS.GRANTED;
+
+ return (
+
+
+ {facePhotoFile && !shouldUseCamera &&
}
+
+ {shouldUseCamera ? (
+
+
When your face is in position, use the Take Photo button below to take your photo.
+
+
+ ) : (
+
+
Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)
+
+
+ )}
+
+
+
+ Next
+
+
+ {shouldUseCamera && }
+
+ );
+}
diff --git a/src/id-verification/routing-utilities.js b/src/id-verification/routing-utilities.js
new file mode 100644
index 0000000..96da242
--- /dev/null
+++ b/src/id-verification/routing-utilities.js
@@ -0,0 +1,52 @@
+import { useContext } from 'react';
+import { useLocation } from 'react-router-dom';
+import { IdVerificationContext } from './IdVerificationContext';
+
+const panelSteps = [
+ 'review-requirements',
+ 'request-camera-access',
+ 'portrait-photo-context',
+ 'take-portrait-photo',
+ 'id-context',
+ 'take-id-photo',
+ 'get-name-id',
+ 'summary',
+ 'submitted',
+];
+
+// eslint-disable-next-line import/prefer-default-export
+export const useNextPanelSlug = (originSlug) => {
+ // Go back to the summary view if that's where they came from
+ const location = useLocation();
+ const isFromSummary = location.state && location.state.fromSummary;
+ if (isFromSummary) {
+ return 'summary';
+ }
+
+ const nextIndex = panelSteps.indexOf(originSlug) + 1;
+ return nextIndex < panelSteps.length ? panelSteps[nextIndex] : null;
+};
+
+// 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);
+
+ if (!facePhotoFile) {
+ if (indexOfCurrentPanel > panelSteps.indexOf('take-portrait-photo')) {
+ return 'portrait-photo-context';
+ }
+ } else if (!idPhotoFile) {
+ if (indexOfCurrentPanel > panelSteps.indexOf('take-id-photo')) {
+ return 'id-context';
+ }
+ }
+
+ // The user has satisfied requirements to view the panel they're on.
+ return null;
+};
diff --git a/src/index.jsx b/src/index.jsx
index f1803a9..42dda62 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -11,6 +11,7 @@ import Footer, { messages as footerMessages } from '@edx/frontend-component-foot
import configureStore from './data/configureStore';
import AccountSettingsPage, { NotFoundPage } from './account-settings';
+import IdVerificationPage from './id-verification';
import CoachingConsent from './account-settings/coaching/CoachingConsent';
import appMessages from './i18n';
@@ -18,11 +19,11 @@ import './index.scss';
import './assets/favicon.ico';
const HeaderFooterLayout = ({ children }) => (
-
+
-
- {children}
-
+
+ {children}
+
);
@@ -33,9 +34,12 @@ subscribe(APP_READY, () => {
-
-
-
+
+
+
+
+
+
,
diff --git a/src/index.scss b/src/index.scss
index 29bfa7e..165d90a 100755
--- a/src/index.scss
+++ b/src/index.scss
@@ -8,6 +8,7 @@ $fa-font-path: "~font-awesome/fonts";
@import "~@edx/frontend-component-footer/dist/footer";
@import "./account-settings/style";
+@import "./id-verification/id-verification";
.word-break-all {
word-break: break-all !important;