Compare commits
24 Commits
alangsto/c
...
alangsto/u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bd6c06f37 | ||
|
|
dcc0496ace | ||
|
|
30e25b96bb | ||
|
|
1d01abc7da | ||
|
|
917152df22 | ||
|
|
961c0feb78 | ||
|
|
7d57d86729 | ||
|
|
34c5de1340 | ||
|
|
81d604d046 | ||
|
|
e6f7e83cf5 | ||
|
|
a970e17070 | ||
|
|
f471ae0aa7 | ||
|
|
b9efe6faee | ||
|
|
2dbccec1f1 | ||
|
|
9f38b975d9 | ||
|
|
ae355cefcf | ||
|
|
d63dfc929f | ||
|
|
64be9edeac | ||
|
|
5f4f82eae1 | ||
|
|
c8c7352549 | ||
|
|
88206e4282 | ||
|
|
d8e23b1a02 | ||
|
|
5db21d2483 | ||
|
|
526d6114f2 |
54
package-lock.json
generated
54
package-lock.json
generated
@@ -4879,6 +4879,35 @@
|
||||
"loader-utils": "^1.2.3"
|
||||
}
|
||||
},
|
||||
"@tensorflow-models/blazeface": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "git+https://github.com/alangsto/blazeface.git#ae2ae0f29538ede33c404e2059608df4c1416bba"
|
||||
},
|
||||
"@tensorflow/tfjs-converter": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-1.6.1.tgz",
|
||||
"integrity": "sha512-9czv3o+5JNT1hXhXmcZ8xYLCrWwtuhX9j262sNQF7wJnnVUOjaCUmjXeHkmWik9jh60TcSYbVD7PcsdR9Blzuw=="
|
||||
},
|
||||
"@tensorflow/tfjs-core": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-1.6.1.tgz",
|
||||
"integrity": "sha512-BLWWjOUCvFjuX4ezKQKn5LSnkilLT5mshwhE8Qb/ZaHWN0HhTMiYy7vBmQVO7JXEPGaIVh2gzh8bpaJyjlTuyg==",
|
||||
"requires": {
|
||||
"@types/offscreencanvas": "~2019.3.0",
|
||||
"@types/seedrandom": "2.4.27",
|
||||
"@types/webgl-ext": "0.0.30",
|
||||
"@types/webgl2": "0.0.4",
|
||||
"node-fetch": "~2.1.2",
|
||||
"seedrandom": "2.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-fetch": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz",
|
||||
"integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@testing-library/dom": {
|
||||
"version": "7.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-7.21.5.tgz",
|
||||
@@ -5548,6 +5577,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/object-assign/-/object-assign-4.0.30.tgz",
|
||||
"integrity": "sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI="
|
||||
},
|
||||
"@types/offscreencanvas": {
|
||||
"version": "2019.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz",
|
||||
"integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q=="
|
||||
},
|
||||
"@types/prettier": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.0.2.tgz",
|
||||
@@ -5589,6 +5623,11 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/seedrandom": {
|
||||
"version": "2.4.27",
|
||||
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.27.tgz",
|
||||
"integrity": "sha1-nbVjk33YaRX2kJK8QyWdL0hXjkE="
|
||||
},
|
||||
"@types/stack-utils": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz",
|
||||
@@ -5609,6 +5648,16 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.0.tgz",
|
||||
"integrity": "sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI="
|
||||
},
|
||||
"@types/webgl-ext": {
|
||||
"version": "0.0.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz",
|
||||
"integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg=="
|
||||
},
|
||||
"@types/webgl2": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.4.tgz",
|
||||
"integrity": "sha512-PACt1xdErJbMUOUweSrbVM7gSIYm1vTncW2hF6Os/EeWi6TXYAYMPp+8v6rzHmypE5gHrxaxZNXgMkJVIdZpHw=="
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "13.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.7.tgz",
|
||||
@@ -24537,6 +24586,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"seedrandom": {
|
||||
"version": "2.4.3",
|
||||
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-2.4.3.tgz",
|
||||
"integrity": "sha1-JDhQTa0zkXMUv/GKxNeU8W1qrsw="
|
||||
},
|
||||
"seek-bzip": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz",
|
||||
|
||||
@@ -38,6 +38,9 @@
|
||||
"@fortawesome/free-regular-svg-icons": "5.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "5.8.2",
|
||||
"@fortawesome/react-fontawesome": "0.1.11",
|
||||
"@tensorflow-models/blazeface": "0.0.5",
|
||||
"@tensorflow/tfjs-converter": "1.6.1",
|
||||
"@tensorflow/tfjs-core": "1.6.1",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"bowser": "^2.10.0",
|
||||
"classnames": "2.2.6",
|
||||
|
||||
@@ -182,6 +182,9 @@
|
||||
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).",
|
||||
"id.verification.photo.take": "Take Photo",
|
||||
"id.verification.photo.retake": "Retake Photo",
|
||||
"id.verification.photo.enable.detection": "Enable Face Detection",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.",
|
||||
"id.verification.photo.enable.detection.id.help.text": "If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.",
|
||||
"id.verification.camera.access.title": "Camera Permissions",
|
||||
"id.verification.camera.access.title.success": "Camera Access Enabled",
|
||||
"id.verification.camera.access.title.failed": "Camera Access Failed",
|
||||
@@ -243,7 +246,8 @@
|
||||
"id.verification.id.photo.title.upload": "Upload a Photo of Your ID",
|
||||
"id.verification.id.photo.preview.alt": "Preview of photo ID.",
|
||||
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. (Supported formats: .jpg, .jpeg, .png)",
|
||||
"id.verification.id.photo.instructions.upload.error": "The file you have selected is too large. Please try again with a file less than 10MB.",
|
||||
"id.verification.account.name.title": "Account Name Check",
|
||||
"id.verification.account.name.instructions": "The name on your account and the name on your ID must be an exact match. If not, please click \"No\" to update your account name.",
|
||||
"id.verification.account.name.radio.label": "Does the name on your ID match the Account Name below?",
|
||||
@@ -264,11 +268,13 @@
|
||||
"id.verification.review.id.alt": "Photo of your ID to be submitted.",
|
||||
"id.verification.review.id.retake": "Retake ID Photo",
|
||||
"id.verification.review.confirm": "Submit",
|
||||
"id.verification.review.error": "edX Support Page",
|
||||
"id.verification.submitted.title": "Identity Verification in Progress",
|
||||
"id.verification.submitted.text": "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 5 days). In the meantime, you can still access all available course content.",
|
||||
"id.verification.return.dashboard": "Return to Your Dashboard",
|
||||
"id.verification.return.course": "Return to Course",
|
||||
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
|
||||
"id.verification.requirements.card.device.text": "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}.",
|
||||
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists,\n please go to {support_link} for help.\n ",
|
||||
"id.verification.account.name.edit": "Edit{sr}"
|
||||
}
|
||||
@@ -182,6 +182,9 @@
|
||||
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).",
|
||||
"id.verification.photo.take": "Take Photo",
|
||||
"id.verification.photo.retake": "Retake Photo",
|
||||
"id.verification.photo.enable.detection": "Enable Face Detection",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.",
|
||||
"id.verification.photo.enable.detection.id.help.text": "If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.",
|
||||
"id.verification.camera.access.title": "Camera Permissions",
|
||||
"id.verification.camera.access.title.success": "Camera Access Enabled",
|
||||
"id.verification.camera.access.title.failed": "Camera Access Failed",
|
||||
@@ -243,7 +246,8 @@
|
||||
"id.verification.id.photo.title.upload": "Upload a Photo of Your ID",
|
||||
"id.verification.id.photo.preview.alt": "Preview of photo ID.",
|
||||
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. (Supported formats: .jpg, .jpeg, .png)",
|
||||
"id.verification.id.photo.instructions.upload.error": "The file you have selected is too large. Please try again with a file less than 10MB.",
|
||||
"id.verification.account.name.title": "Account Name Check",
|
||||
"id.verification.account.name.instructions": "The name on your account and the name on your ID must be an exact match. If not, please click \"No\" to update your account name.",
|
||||
"id.verification.account.name.radio.label": "Does the name on your ID match the Account Name below?",
|
||||
@@ -264,11 +268,13 @@
|
||||
"id.verification.review.id.alt": "Photo of your ID to be submitted.",
|
||||
"id.verification.review.id.retake": "Retake ID Photo",
|
||||
"id.verification.review.confirm": "Submit",
|
||||
"id.verification.review.error": "edX Support Page",
|
||||
"id.verification.submitted.title": "Identity Verification in Progress",
|
||||
"id.verification.submitted.text": "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 5 days). In the meantime, you can still access all available course content.",
|
||||
"id.verification.return.dashboard": "Return to Your Dashboard",
|
||||
"id.verification.return.course": "Return to Course",
|
||||
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
|
||||
"id.verification.requirements.card.device.text": "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}.",
|
||||
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists,\n please go to {support_link} for help.\n ",
|
||||
"id.verification.account.name.edit": "Edit{sr}"
|
||||
}
|
||||
@@ -182,6 +182,9 @@
|
||||
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).",
|
||||
"id.verification.photo.take": "Take Photo",
|
||||
"id.verification.photo.retake": "Retake Photo",
|
||||
"id.verification.photo.enable.detection": "Enable Face Detection",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.",
|
||||
"id.verification.photo.enable.detection.id.help.text": "If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.",
|
||||
"id.verification.camera.access.title": "Camera Permissions",
|
||||
"id.verification.camera.access.title.success": "Camera Access Enabled",
|
||||
"id.verification.camera.access.title.failed": "Camera Access Failed",
|
||||
@@ -243,7 +246,8 @@
|
||||
"id.verification.id.photo.title.upload": "Upload a Photo of Your ID",
|
||||
"id.verification.id.photo.preview.alt": "Preview of photo ID.",
|
||||
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. (Supported formats: .jpg, .jpeg, .png)",
|
||||
"id.verification.id.photo.instructions.upload.error": "The file you have selected is too large. Please try again with a file less than 10MB.",
|
||||
"id.verification.account.name.title": "Account Name Check",
|
||||
"id.verification.account.name.instructions": "The name on your account and the name on your ID must be an exact match. If not, please click \"No\" to update your account name.",
|
||||
"id.verification.account.name.radio.label": "Does the name on your ID match the Account Name below?",
|
||||
@@ -264,11 +268,13 @@
|
||||
"id.verification.review.id.alt": "Photo of your ID to be submitted.",
|
||||
"id.verification.review.id.retake": "Retake ID Photo",
|
||||
"id.verification.review.confirm": "Submit",
|
||||
"id.verification.review.error": "edX Support Page",
|
||||
"id.verification.submitted.title": "Identity Verification in Progress",
|
||||
"id.verification.submitted.text": "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 5 days). In the meantime, you can still access all available course content.",
|
||||
"id.verification.return.dashboard": "Return to Your Dashboard",
|
||||
"id.verification.return.course": "Return to Course",
|
||||
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
|
||||
"id.verification.requirements.card.device.text": "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}.",
|
||||
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists,\n please go to {support_link} for help.\n ",
|
||||
"id.verification.account.name.edit": "Edit{sr}"
|
||||
}
|
||||
@@ -182,6 +182,9 @@
|
||||
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).",
|
||||
"id.verification.photo.take": "Take Photo",
|
||||
"id.verification.photo.retake": "Retake Photo",
|
||||
"id.verification.photo.enable.detection": "Enable Face Detection",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.",
|
||||
"id.verification.photo.enable.detection.id.help.text": "If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.",
|
||||
"id.verification.camera.access.title": "Camera Permissions",
|
||||
"id.verification.camera.access.title.success": "Camera Access Enabled",
|
||||
"id.verification.camera.access.title.failed": "Camera Access Failed",
|
||||
@@ -243,7 +246,8 @@
|
||||
"id.verification.id.photo.title.upload": "Upload a Photo of Your ID",
|
||||
"id.verification.id.photo.preview.alt": "Preview of photo ID.",
|
||||
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. (Supported formats: .jpg, .jpeg, .png)",
|
||||
"id.verification.id.photo.instructions.upload.error": "The file you have selected is too large. Please try again with a file less than 10MB.",
|
||||
"id.verification.account.name.title": "Account Name Check",
|
||||
"id.verification.account.name.instructions": "The name on your account and the name on your ID must be an exact match. If not, please click \"No\" to update your account name.",
|
||||
"id.verification.account.name.radio.label": "Does the name on your ID match the Account Name below?",
|
||||
@@ -264,11 +268,13 @@
|
||||
"id.verification.review.id.alt": "Photo of your ID to be submitted.",
|
||||
"id.verification.review.id.retake": "Retake ID Photo",
|
||||
"id.verification.review.confirm": "Submit",
|
||||
"id.verification.review.error": "edX Support Page",
|
||||
"id.verification.submitted.title": "Identity Verification in Progress",
|
||||
"id.verification.submitted.text": "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 5 days). In the meantime, you can still access all available course content.",
|
||||
"id.verification.return.dashboard": "Return to Your Dashboard",
|
||||
"id.verification.return.course": "Return to Course",
|
||||
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
|
||||
"id.verification.requirements.card.device.text": "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}.",
|
||||
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists,\n please go to {support_link} for help.\n ",
|
||||
"id.verification.account.name.edit": "Edit{sr}"
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as blazeface from '@tensorflow-models/blazeface';
|
||||
import CameraPhoto, { FACING_MODES } from 'jslib-html5-camera-photo';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Spinner } from '@edx/paragon';
|
||||
|
||||
import shutter from './data/camera-shutter.base64.json';
|
||||
import messages from './IdVerification.messages';
|
||||
@@ -11,27 +13,204 @@ class Camera extends React.Component {
|
||||
super(props, context);
|
||||
this.cameraPhoto = null;
|
||||
this.videoRef = React.createRef();
|
||||
this.canvasRef = React.createRef();
|
||||
this.setDetection = this.setDetection.bind(this);
|
||||
this.state = {
|
||||
trackedObject: null,
|
||||
dataUri: '',
|
||||
videoHasLoaded: false,
|
||||
shouldDetect: false,
|
||||
isFinishedLoadingDetection: true,
|
||||
shouldGiveFeedback: true,
|
||||
feedback: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.cameraPhoto = new CameraPhoto(this.videoRef.current);
|
||||
this.cameraPhoto.startCamera(FACING_MODES.USER, { width: 1280 });
|
||||
this.cameraPhoto.startCamera(
|
||||
this.props.isPortrait ? FACING_MODES.USER : FACING_MODES.ENVIRONMENT,
|
||||
{ width: 640, height: 480 },
|
||||
);
|
||||
}
|
||||
|
||||
async componentWillUnmount() {
|
||||
this.cameraPhoto.stopCamera();
|
||||
}
|
||||
|
||||
setDetection() {
|
||||
this.setState(
|
||||
{ shouldDetect: !this.state.shouldDetect },
|
||||
() => {
|
||||
if (this.state.shouldDetect) {
|
||||
this.setState({ isFinishedLoadingDetection: false });
|
||||
this.startDetection();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
startDetection() {
|
||||
setTimeout(() => {
|
||||
if (this.state.videoHasLoaded) {
|
||||
const loadModelPromise = blazeface.load();
|
||||
Promise.all([loadModelPromise])
|
||||
.then((values) => {
|
||||
this.setState({ isFinishedLoadingDetection: true });
|
||||
this.detectFromVideoFrame(values[0], this.videoRef.current);
|
||||
});
|
||||
} else {
|
||||
this.setState({ isFinishedLoadingDetection: true });
|
||||
this.setState({ shouldDetect: false });
|
||||
// TODO: add error message
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
detectFromVideoFrame = (model, video) => {
|
||||
model.estimateFaces(video).then((predictions) => {
|
||||
if (this.state.shouldDetect && !this.state.dataUri) {
|
||||
this.showDetections(predictions);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.detectFromVideoFrame(model, video);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
showDetections = (predictions) => {
|
||||
let canvasContext;
|
||||
if (predictions.length > 0) {
|
||||
canvasContext = this.canvasRef.current.getContext('2d');
|
||||
canvasContext.clearRect(0, 0, canvasContext.canvas.width, canvasContext.canvas.height);
|
||||
}
|
||||
// predictions is an array of objects describing each detected face
|
||||
predictions.forEach((prediction) => {
|
||||
const start = [prediction.topLeft[0], prediction.topLeft[1]];
|
||||
const end = [prediction.bottomRight[0], prediction.bottomRight[1]];
|
||||
const size = [end[0] - start[0], end[1] - start[1]];
|
||||
|
||||
// landmarks is an array of points representing each facial landmark (i.e. right eye, left eye, nose, etc.)
|
||||
const features = prediction.landmarks;
|
||||
let isInPosition = true;
|
||||
|
||||
// for each of the landmarks, determine if it is in position
|
||||
for (let j = 0; j < features.length; j++) {
|
||||
const x = features[j][0];
|
||||
const y = features[j][1];
|
||||
|
||||
let isInRange;
|
||||
if (this.props.isPortrait) {
|
||||
isInRange = this.isInRangeForPortrait(x, y);
|
||||
} else {
|
||||
isInRange = this.isInRangeForID(x, y);
|
||||
}
|
||||
// if it is not in range, give feedback depending on which feature is out of range
|
||||
isInPosition = isInPosition && isInRange;
|
||||
}
|
||||
|
||||
// draw a box depending on if all landmarks are in position
|
||||
if (isInPosition) {
|
||||
canvasContext.strokeStyle = '#00ffff';
|
||||
canvasContext.lineWidth = 6;
|
||||
canvasContext.strokeRect(start[0], start[1], size[0], size[1]);
|
||||
// give positive feedback here if user is in correct position
|
||||
this.giveFeedback(predictions.length, [], true);
|
||||
} else {
|
||||
canvasContext.fillStyle = 'rgba(255, 51, 0, 0.75)';
|
||||
canvasContext.fillRect(start[0], start[1], size[0], size[1]);
|
||||
this.giveFeedback(predictions.length, features[0], false);
|
||||
}
|
||||
});
|
||||
|
||||
if (predictions.length === 0) {
|
||||
this.giveFeedback(predictions.length, [], false);
|
||||
}
|
||||
}
|
||||
|
||||
giveFeedback(numFaces, rightEye, isCorrect) {
|
||||
// testing blazeface
|
||||
if (this.state.shouldGiveFeedback) {
|
||||
const currentFeedback = this.state.feedback;
|
||||
let newFeedback = '';
|
||||
if (numFaces === 1) {
|
||||
// only give feedback if one face is detected otherwise
|
||||
// it would be difficult to tell a user which face to move
|
||||
if (isCorrect) {
|
||||
newFeedback = this.props.intl.formatMessage(messages['id.verification.photo.feedback.correct']);
|
||||
} else {
|
||||
// give feedback based on where user is
|
||||
newFeedback = this.props.intl.formatMessage(messages[this.getGridPosition(rightEye)]);
|
||||
}
|
||||
} else if (numFaces > 1) {
|
||||
newFeedback = this.props.intl.formatMessage(messages['id.verification.photo.feedback.two.faces']);
|
||||
} else {
|
||||
newFeedback = this.props.intl.formatMessage(messages['id.verification.photo.feedback.no.faces']);
|
||||
}
|
||||
if (currentFeedback !== newFeedback) {
|
||||
// only update status if it is different, so we don't overload the user with status updates
|
||||
this.setState({ feedback: newFeedback });
|
||||
}
|
||||
// turn off feedback for one to ensure that instructions aren't disruptive/interrupting
|
||||
this.setState({ shouldGiveFeedback: false });
|
||||
setTimeout(() => {
|
||||
this.setState({ shouldGiveFeedback: true });
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
getGridPosition(coordinates) {
|
||||
// Used to determine where a face is (i.e. top-left, center-right, bottom-center, etc.)
|
||||
|
||||
const x = coordinates[0];
|
||||
const y = coordinates[1];
|
||||
|
||||
let messageBase = 'id.verification.photo.feedback';
|
||||
|
||||
const heightUpperLimit = 320;
|
||||
const heightMiddleLimit = 160;
|
||||
|
||||
if (y < heightMiddleLimit) {
|
||||
messageBase += '.top';
|
||||
} else if (y < heightUpperLimit && y >= heightMiddleLimit) {
|
||||
messageBase += '.center';
|
||||
} else {
|
||||
messageBase += '.bottom';
|
||||
}
|
||||
|
||||
const widthRightLimit = 213;
|
||||
const widthMiddleLimit = 427;
|
||||
|
||||
if (x < widthRightLimit) {
|
||||
messageBase += '.right';
|
||||
} else if (x >= widthRightLimit && x < widthMiddleLimit) {
|
||||
messageBase += '.center';
|
||||
} else {
|
||||
messageBase += '.left';
|
||||
}
|
||||
|
||||
return messageBase;
|
||||
}
|
||||
|
||||
isInRangeForPortrait(x, y) {
|
||||
return x > 47 && x < 570 && y > 100 && y < 410;
|
||||
}
|
||||
|
||||
isInRangeForID(x, y) {
|
||||
return x > 120 && x < 470 && y > 120 && y < 350;
|
||||
}
|
||||
|
||||
setVideoHasLoaded() {
|
||||
this.setState({ videoHasLoaded: 'true' });
|
||||
}
|
||||
|
||||
takePhoto() {
|
||||
if (this.state.dataUri) {
|
||||
return this.reset();
|
||||
}
|
||||
|
||||
const config = {
|
||||
sizeFactor: 1,
|
||||
sizeFactor: this.getSizeFactor(),
|
||||
};
|
||||
|
||||
this.playShutterClick();
|
||||
@@ -40,13 +219,37 @@ class Camera extends React.Component {
|
||||
this.props.onImageCapture(dataUri);
|
||||
}
|
||||
|
||||
getSizeFactor() {
|
||||
let sizeFactor = 1;
|
||||
const settings = this.cameraPhoto.getCameraSettings();
|
||||
if (settings) {
|
||||
const videoWidth = settings.width;
|
||||
const videoHeight = settings.height;
|
||||
// need to multiply by 3 because each pixel contains 3 bytes
|
||||
const currentSize = videoWidth * videoHeight * 3;
|
||||
// chose a limit of 9,999,999 (bytes) so that result will
|
||||
// always be less than 10MB
|
||||
const ratio = 9999999 / currentSize;
|
||||
|
||||
// if the current resolution creates an image larger than 10 MB, adjust sizeFactor (resolution)
|
||||
// to ensure that image will have a file size of less than 10 MB.
|
||||
if (ratio < 1) {
|
||||
sizeFactor = ratio;
|
||||
}
|
||||
}
|
||||
return sizeFactor;
|
||||
}
|
||||
|
||||
playShutterClick() {
|
||||
const audio = new Audio('data:audio/mp3;base64,' + shutter.base64);
|
||||
const audio = new Audio(`data:audio/mp3;base64,${shutter.base64}`);
|
||||
audio.play();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setState({ dataUri: '' });
|
||||
if (this.state.shouldDetect) {
|
||||
this.startDetection();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -54,21 +257,58 @@ class Camera extends React.Component {
|
||||
? 'do-transition camera-flash'
|
||||
: 'camera-flash';
|
||||
return (
|
||||
<div className='camera-outer-wrapper shadow'>
|
||||
<div className='camera-wrapper'>
|
||||
<div className="camera-outer-wrapper shadow">
|
||||
<Form.Group style={{ textAlign: 'left', padding: '0.5rem', marginBottom: '0.5rem' }} >
|
||||
<Form.Check
|
||||
id="videoDetection"
|
||||
name="videoDetection"
|
||||
label={this.props.intl.formatMessage(messages['id.verification.photo.enable.detection'])}
|
||||
aria-describedby="videoDetectionHelpText"
|
||||
checked={this.state.shouldDetect}
|
||||
onChange={this.setDetection}
|
||||
style={{ padding: '0rem', marginLeft: '1.25rem', float: this.state.isFinishedLoadingDetection ? 'none' : 'left' }}
|
||||
/>
|
||||
{!this.state.isFinishedLoadingDetection && <Spinner animation="border" variant="primary" style={{ marginLeft: '0.5rem' }} data-testid="spinner" />}
|
||||
<Form.Text id="videoDetectionHelpText" data-testid="videoDetectionHelpText">
|
||||
{this.props.isPortrait
|
||||
? this.props.intl.formatMessage(messages['id.verification.photo.enable.detection.portrait.help.text'])
|
||||
: this.props.intl.formatMessage(messages['id.verification.photo.enable.detection.id.help.text'])}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<div className="camera-wrapper">
|
||||
<div className={cameraFlashClass} />
|
||||
<video
|
||||
ref={this.videoRef}
|
||||
autoPlay={true}
|
||||
className='camera-video'
|
||||
style={{ display: this.state.dataUri ? 'none' : 'block' }}
|
||||
data-testid="video"
|
||||
autoPlay
|
||||
className="camera-video"
|
||||
onLoadedData={() => { this.setVideoHasLoaded(); }}
|
||||
style={{
|
||||
display: this.state.dataUri ? 'none' : 'block',
|
||||
WebkitTransform: 'scaleX(-1)',
|
||||
transform: 'scaleX(-1)',
|
||||
}}
|
||||
playsInline
|
||||
/>
|
||||
<canvas
|
||||
ref={this.canvasRef}
|
||||
data-testid="detection-canvas"
|
||||
className="canvas-video"
|
||||
style={{
|
||||
display: !this.state.shouldDetect || this.state.dataUri ? 'none' : 'block',
|
||||
WebkitTransform: 'scaleX(-1)',
|
||||
transform: 'scaleX(-1)',
|
||||
}}
|
||||
width="640"
|
||||
height="480"
|
||||
/>
|
||||
<img
|
||||
alt='imgCamera'
|
||||
alt="imgCamera"
|
||||
src={this.state.dataUri}
|
||||
className='camera-video'
|
||||
className="camera-video"
|
||||
style={{ display: this.state.dataUri ? 'block' : 'none' }}
|
||||
/>
|
||||
<div role="status" className="sr-only">{this.state.feedback}</div>
|
||||
</div>
|
||||
<button
|
||||
className={`btn camera-btn ${
|
||||
@@ -76,7 +316,7 @@ class Camera extends React.Component {
|
||||
'btn-outline-primary'
|
||||
: 'btn-primary'
|
||||
}`}
|
||||
accessKey='c'
|
||||
accessKey="c"
|
||||
onClick={() => {
|
||||
this.takePhoto();
|
||||
}}
|
||||
@@ -93,6 +333,7 @@ class Camera extends React.Component {
|
||||
Camera.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onImageCapture: PropTypes.func.isRequired,
|
||||
isPortrait: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(Camera);
|
||||
|
||||
@@ -35,7 +35,7 @@ function CameraHelpWithUpload(props) {
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
|
||||
</p>
|
||||
<ImageFileUpload onFileChange={setAndTrackIdPhotoFile} />
|
||||
<ImageFileUpload onFileChange={setAndTrackIdPhotoFile} intl={props.intl} />
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -81,6 +81,81 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Retake Photo',
|
||||
description: 'Button to retake photo.',
|
||||
},
|
||||
'id.verification.photo.enable.detection': {
|
||||
id: 'id.verification.photo.enable.detection',
|
||||
defaultMessage: 'Enable Face Detection',
|
||||
description: 'Text label for the checkbox to enable face detection.',
|
||||
},
|
||||
'id.verification.photo.enable.detection.portrait.help.text': {
|
||||
id: 'id.verification.photo.enable.detection.portrait.help.text',
|
||||
defaultMessage: 'If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.',
|
||||
description: 'Help text that appears for enabling face detection on the portrait photo panel.',
|
||||
},
|
||||
'id.verification.photo.enable.detection.id.help.text': {
|
||||
id: 'id.verification.photo.enable.detection.id.help.text',
|
||||
defaultMessage: 'If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.',
|
||||
description: 'Help text that appears for enabling face detection on the portrait photo panel.',
|
||||
},
|
||||
'id.verification.photo.feedback.correct': {
|
||||
id: 'id.verification.photo.feedback.correct',
|
||||
defaultMessage: 'Face is in a good position.',
|
||||
description: 'Text for screen reader when user\'s face is in a good position.',
|
||||
},
|
||||
'id.verification.photo.feedback.two.faces': {
|
||||
id: 'id.verification.photo.feedback.two.faces',
|
||||
defaultMessage: 'More than one face detected.',
|
||||
description: 'Text for screen reader when more than one face detected.',
|
||||
},
|
||||
'id.verification.photo.feedback.no.faces': {
|
||||
id: 'id.verification.photo.feedback.no.faces',
|
||||
defaultMessage: 'No face detected.',
|
||||
description: 'Text for screen reader when no face detected.',
|
||||
},
|
||||
'id.verification.photo.feedback.top.left': {
|
||||
id: 'id.verification.photo.feedback.top.left',
|
||||
defaultMessage: 'Incorrect position. Top left.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.top.center': {
|
||||
id: 'id.verification.photo.feedback.top.center',
|
||||
defaultMessage: 'Incorrect position. Top center.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.top.right': {
|
||||
id: 'id.verification.photo.feedback.top.right',
|
||||
defaultMessage: 'Incorrect position. Top right.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.center.left': {
|
||||
id: 'id.verification.photo.feedback.center.left',
|
||||
defaultMessage: 'Incorrect position. Center left.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.center.center': {
|
||||
id: 'id.verification.photo.feedback.center.center',
|
||||
defaultMessage: 'Incorrect position. Too close to camera.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.center.right': {
|
||||
id: 'id.verification.photo.feedback.center.right',
|
||||
defaultMessage: 'Incorrect position. Center right.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.bottom.left': {
|
||||
id: 'id.verification.photo.feedback.bottom.left',
|
||||
defaultMessage: 'Incorrect position. Bottom left.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.bottom.center': {
|
||||
id: 'id.verification.photo.feedback.bottom.center',
|
||||
defaultMessage: 'Incorrect position. Bottom center.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.bottom.right': {
|
||||
id: 'id.verification.photo.feedback.bottom.right',
|
||||
defaultMessage: 'Incorrect position. Bottom right.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.camera.access.title': {
|
||||
id: 'id.verification.camera.access.title',
|
||||
defaultMessage: 'Camera Permissions',
|
||||
@@ -388,9 +463,14 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.id.photo.instructions.upload': {
|
||||
id: 'id.verification.id.photo.instructions.upload',
|
||||
defaultMessage: 'Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)',
|
||||
defaultMessage: 'Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. (Supported formats: .jpg, .jpeg, .png)',
|
||||
description: 'Instructions for ID photo upload.',
|
||||
},
|
||||
'id.verification.id.photo.instructions.upload.error': {
|
||||
id: 'id.verification.id.photo.instructions.upload.error',
|
||||
defaultMessage: 'The file you have selected is too large. Please try again with a file less than 10MB.',
|
||||
description: 'Error message for file upload that is larger than 10MB.',
|
||||
},
|
||||
'id.verification.account.name.title': {
|
||||
id: 'id.verification.account.name.title',
|
||||
defaultMessage: 'Account Name Check',
|
||||
@@ -491,6 +571,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Submit',
|
||||
description: 'Button to confirm all information is correct and submit.',
|
||||
},
|
||||
'id.verification.review.error': {
|
||||
id: 'id.verification.review.error',
|
||||
defaultMessage: 'edX Support Page',
|
||||
description: 'Text linking to the support page.',
|
||||
},
|
||||
'id.verification.submitted.title': {
|
||||
id: 'id.verification.submitted.title',
|
||||
defaultMessage: 'Identity Verification in Progress',
|
||||
|
||||
@@ -58,7 +58,7 @@ function IdVerificationContextProvider({ children }) {
|
||||
tracks.forEach(track => track.stop());
|
||||
setMediaStream(null);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Call verification status endpoint to check whether we can verify.
|
||||
|
||||
@@ -1,28 +1,52 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import messages from './IdVerification.messages';
|
||||
|
||||
|
||||
export default function ImageFileUpload({ onFileChange, intl }) {
|
||||
const [fileTooLargeError, setFileTooLargeError] = useState(false);
|
||||
const maxFileSize = 10000000;
|
||||
|
||||
export default function ImageFileUpload({ onFileChange }) {
|
||||
const handleChange = useCallback((e) => {
|
||||
if (e.target.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileObject = e.target.files[0];
|
||||
const fileReader = new FileReader();
|
||||
fileReader.addEventListener('load', () => onFileChange(fileReader.result));
|
||||
fileReader.readAsDataURL(fileObject);
|
||||
if (fileObject.size < maxFileSize) {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.addEventListener('load', () => onFileChange(fileReader.result));
|
||||
fileReader.readAsDataURL(fileObject);
|
||||
} else {
|
||||
setFileTooLargeError(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
data-testid="fileUpload"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
data-testid="fileUpload"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{fileTooLargeError && (
|
||||
<Alert
|
||||
id="fileTooLargeError"
|
||||
variant="danger"
|
||||
tabIndex="-1"
|
||||
style={{ marginTop: '1rem' }}
|
||||
>
|
||||
{intl.formatMessage(messages['id.verification.id.photo.instructions.upload.error'])}
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ImageFileUpload.propTypes = {
|
||||
onFileChange: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
}
|
||||
.form-check {
|
||||
padding: 0.5rem 0.5rem 1rem;
|
||||
padding: 0.5rem 0.5rem 1rem;
|
||||
.form-check-label {
|
||||
margin-left: 0.5rem;
|
||||
padding-top: 0.2rem;
|
||||
@@ -52,6 +52,14 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.canvas-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.camera-btn {
|
||||
margin: 10px;
|
||||
}
|
||||
@@ -68,4 +76,4 @@
|
||||
opacity: 0;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import { history } from '@edx/frontend-platform';
|
||||
import { Input, Button, Spinner } from '@edx/paragon';
|
||||
import { Input, Button, Spinner, Alert } from '@edx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
@@ -25,6 +25,7 @@ function SummaryPanel(props) {
|
||||
} = useContext(IdVerificationContext);
|
||||
const nameToBeUsed = idPhotoName || nameOnAccount || '';
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submissionError, setSubmissionError] = useState(false);
|
||||
|
||||
function SubmitButton() {
|
||||
async function handleClick() {
|
||||
@@ -39,6 +40,10 @@ function SummaryPanel(props) {
|
||||
if (result.success) {
|
||||
stopUserMedia();
|
||||
history.push(nextPanelSlug);
|
||||
} else {
|
||||
stopUserMedia();
|
||||
setIsSubmitting(false);
|
||||
setSubmissionError(true);
|
||||
}
|
||||
}
|
||||
return (
|
||||
@@ -59,6 +64,24 @@ function SummaryPanel(props) {
|
||||
name={panelSlug}
|
||||
title={props.intl.formatMessage(messages['id.verification.review.title'])}
|
||||
>
|
||||
{submissionError &&
|
||||
<Alert
|
||||
variant="danger"
|
||||
data-testid="submission-error"
|
||||
dismissible
|
||||
onClose={() => setSubmissionError(false)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="idv.submission.alert.error"
|
||||
defaultMessage={`
|
||||
We encountered a technical error while trying to submit ID verification.
|
||||
This might be a temporary issue, so please try again in a few minutes.
|
||||
If the problem persists,
|
||||
please go to {support_link} for help.
|
||||
`}
|
||||
values={{ support_link: <Alert.Link href="https://support.edx.org/hc/en-us">{props.intl.formatMessage(messages['id.verification.review.error'])}</Alert.Link> }}
|
||||
/>
|
||||
</Alert>}
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.review.description'])}
|
||||
</p>
|
||||
|
||||
@@ -24,7 +24,7 @@ function TakeIdPhotoPanel(props) {
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.camera'])}
|
||||
</p>
|
||||
<Camera onImageCapture={setIdPhotoFile} />
|
||||
<Camera onImageCapture={setIdPhotoFile} isPortrait={false} />
|
||||
</div>
|
||||
<CameraHelp />
|
||||
<div className="action-row" style={{ visibility: idPhotoFile ? 'unset' : 'hidden' }}>
|
||||
|
||||
@@ -33,7 +33,7 @@ function TakePortraitPhotoPanel(props) {
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.portrait.photo.instructions.camera'])}
|
||||
</p>
|
||||
<Camera onImageCapture={setFacePhotoFile} />
|
||||
<Camera onImageCapture={setFacePhotoFile} isPortrait />
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
|
||||
@@ -4,10 +4,12 @@ import { createMemoryHistory } from 'history';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { render, cleanup, screen, act, fireEvent } from '@testing-library/react';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import * as blazeface from '@tensorflow-models/blazeface';
|
||||
import { IdVerificationContext } from '../IdVerificationContext';
|
||||
import Camera from '../Camera';
|
||||
|
||||
jest.mock('jslib-html5-camera-photo');
|
||||
jest.mock('@tensorflow-models/blazeface');
|
||||
|
||||
window.HTMLMediaElement.prototype.play = () => {};
|
||||
|
||||
@@ -19,6 +21,13 @@ describe('SubmittedPanel', () => {
|
||||
const defaultProps = {
|
||||
intl: {},
|
||||
onImageCapture: jest.fn(),
|
||||
isPortrait: true,
|
||||
};
|
||||
|
||||
const idProps = {
|
||||
intl: {},
|
||||
onImageCapture: jest.fn(),
|
||||
isPortrait: false,
|
||||
};
|
||||
|
||||
const contextValue = {};
|
||||
@@ -42,4 +51,88 @@ describe('SubmittedPanel', () => {
|
||||
fireEvent.click(button);
|
||||
expect(defaultProps.onImageCapture).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows correct help text for portrait photo capture', async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...defaultProps} />
|
||||
</IdVerificationContext.Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
)));
|
||||
const helpText = screen.getByTestId('videoDetectionHelpText');
|
||||
expect(helpText.textContent).toEqual(expect.stringContaining('Your face can be seen clearly'));
|
||||
});
|
||||
|
||||
it('shows correct help text for id photo capture', async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...idProps} />
|
||||
</IdVerificationContext.Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
)));
|
||||
const helpText = screen.getByTestId('videoDetectionHelpText');
|
||||
expect(helpText.textContent).toEqual(expect.stringContaining('The face can be seen clearly'));
|
||||
});
|
||||
|
||||
it('shows spinner when loading face detection', async () => {
|
||||
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...defaultProps} />
|
||||
</IdVerificationContext.Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
)));
|
||||
|
||||
await fireEvent.loadedData(screen.queryByTestId('video'));
|
||||
const checkbox = await screen.findByLabelText('Enable Face Detection');
|
||||
fireEvent.click(checkbox);
|
||||
expect(screen.queryByTestId('spinner')).toBeDefined();
|
||||
});
|
||||
|
||||
it('canvas is visible when detection is enabled', async () => {
|
||||
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...defaultProps} />
|
||||
</IdVerificationContext.Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
)));
|
||||
|
||||
await fireEvent.loadedData(screen.queryByTestId('video'));
|
||||
expect(screen.queryByTestId('detection-canvas')).toHaveStyle('display:none');
|
||||
const checkbox = await screen.findByLabelText('Enable Face Detection');
|
||||
await fireEvent.click(checkbox);
|
||||
expect(screen.queryByTestId('detection-canvas')).toHaveStyle('display:block');
|
||||
});
|
||||
|
||||
it('blazeface is called when detection is enabled', async () => {
|
||||
blazeface.load = jest.fn().mockResolvedValue({ estimateFaces: jest.fn().mockResolvedValue([]) });
|
||||
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlCamera {...defaultProps} />
|
||||
</IdVerificationContext.Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
)));
|
||||
|
||||
await fireEvent.loadedData(screen.queryByTestId('video'));
|
||||
const checkbox = await screen.findByLabelText('Enable Face Detection');
|
||||
await fireEvent.click(checkbox);
|
||||
setTimeout(() => { expect(blazeface.load).toHaveBeenCalled(); }, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,7 @@ import { render, cleanup, act, screen, fireEvent, waitFor } from '@testing-libra
|
||||
import '@edx/frontend-platform/analytics';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { submitIdVerification } from '../../data/service';
|
||||
import * as dataService from '../../data/service';
|
||||
import { IdVerificationContext } from '../../IdVerificationContext';
|
||||
import SummaryPanel from '../../panels/SummaryPanel';
|
||||
|
||||
@@ -13,9 +13,8 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendTrackEvent: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../data/service', () => ({
|
||||
submitIdVerification: jest.fn(() => ({ success: true, message: null })),
|
||||
}));
|
||||
jest.mock('../../data/service');
|
||||
dataService.submitIdVerification = jest.fn().mockReturnValue({ success: true });
|
||||
|
||||
const IntlSummaryPanel = injectIntl(SummaryPanel);
|
||||
|
||||
@@ -74,7 +73,26 @@ describe('SummaryPanel', () => {
|
||||
it('submits', async () => {
|
||||
const button = await screen.findByTestId('submit-button');
|
||||
fireEvent.click(button);
|
||||
expect(submitIdVerification).toHaveBeenCalled();
|
||||
await waitFor(() => expect(contextValue.stopUserMedia).toHaveBeenCalled())
|
||||
expect(dataService.submitIdVerification).toHaveBeenCalled();
|
||||
await waitFor(() => expect(contextValue.stopUserMedia).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('shows error when cannot submit', async () => {
|
||||
await cleanup();
|
||||
dataService.submitIdVerification = jest.fn().mockReturnValue({ success: false });
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlSummaryPanel {...defaultProps} />
|
||||
</IdVerificationContext.Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
)));
|
||||
const button = await screen.findByTestId('submit-button');
|
||||
await act(async () => fireEvent.click(button));
|
||||
expect(dataService.submitIdVerification).toHaveBeenCalled();
|
||||
const error = await screen.getByTestId('submission-error');
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user