diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index 777e180c..51690697 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -3,12 +3,16 @@ on:
schedule:
- cron: "30 16 * * *"
+permissions:
+ issues: write
+ pull-requests: write
+
jobs:
stale:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- - uses: actions/stale@v7
+ - uses: actions/stale@v9
with:
stale-issue-message: "This issue is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 7 days."
stale-pr-message: "This PR is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 7 days."
diff --git a/packages/components/document-capture/document-instructions/src/DocumentInstruction.js b/packages/components/document-capture/document-instructions/src/DocumentInstruction.js
new file mode 100644
index 00000000..3df99f1a
--- /dev/null
+++ b/packages/components/document-capture/document-instructions/src/DocumentInstruction.js
@@ -0,0 +1,404 @@
+"use strict";
+import SmartFileUpload from "../../../domain/SmartFileUpload";
+import styles from "../../../styles";
+
+function templateString() {
+ return `
+ ${styles}
+
+
+ ${this.showNavigation
+ ? `
+
+
+
+
+
+
+
+ Close SmileIdentity Verification frame
+
+
`
+ : ""
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Make sure you are in a well-lit environment where your face is
+ clear and visible
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hold your phone steady so the selfie is clear and sharp. Don't
+ take blurry images.
+
+
+
+
+
+
+
+
+
+ `;
+}
+
+class DocumentInstruction extends HTMLElement {
+ constructor() {
+ super();
+ this.templateString = templateString.bind(this);
+ this.render = () => {
+ return this.templateString();
+ };
+
+ this.attachShadow({ mode: "open" });
+ }
+
+ connectedCallback() {
+ const template = document.createElement("template");
+ template.innerHTML = this.render();
+
+ this.shadowRoot.appendChild(template.content.cloneNode(true));
+
+ this.backButton = this.shadowRoot.querySelector("#back-button");
+ this.takeDocumentPhotoButton = this.shadowRoot.querySelector('#take-photo');
+ this.uploadDocumentPhotoButton = this.shadowRoot.querySelector('#upload-photo');
+
+ const CloseIframeButtons = this.shadowRoot.querySelectorAll(".close-iframe");
+
+ this.backButton && this.backButton.addEventListener("click", (e) => {
+ this.handleBackEvents(e);
+ });
+
+ CloseIframeButtons.forEach((button) => {
+ button.addEventListener(
+ "click",
+ () => {
+ this.closeWindow();
+ },
+ false
+ );
+ });
+
+ if (this.takeDocumentPhotoButton) this.takeDocumentPhotoButton.addEventListener('click', () => {
+ this.dispatchEvent(
+ new CustomEvent("DocumentInstruction::StartCamera", {
+ detail: {},
+ }),
+ );
+ });
+
+ if (this.uploadDocumentPhotoButton) this.uploadDocumentPhotoButton.addEventListener('change', async (event) => {
+ this.shadowRoot.querySelector('#error').innerHTML = '';
+ try {
+ const { files } = event.target;
+
+ // validate file, and convert file to data url
+ const fileData = await SmartFileUpload.retrieve(files);
+
+ this.dispatchEvent(
+ new CustomEvent("DocumentInstruction::DocumentChange", {
+ detail: { image: fileData },
+ }),
+ );
+ } catch (error) {
+ this.shadowRoot.querySelector('#error').innerHTML = error.message;
+ }
+ });
+ }
+
+ get hideBack() {
+ return this.hasAttribute("hide-back-to-host");
+ }
+
+ get showNavigation() {
+ return this.hasAttribute('show-navigation');
+ }
+
+ get themeColor() {
+ return this.getAttribute("theme-color") || "#043C93";
+ }
+
+ get hideAttribution() {
+ return this.hasAttribute("hide-attribution");
+ }
+
+ get documentCaptureModes() {
+ return this.getAttribute("document-capture-modes") || "camera";
+ }
+
+ get supportBothCaptureModes() {
+ const value = this.documentCaptureModes;
+ return value.includes("camera") && value.includes("upload");
+ }
+ get title() {
+ return this.getAttribute('title') || 'Submit Front of ID';
+ }
+
+ handleBackEvents() {
+ this.dispatchEvent(new CustomEvent("SmileIdentity::Exit"));
+ }
+
+ closeWindow() {
+ const referenceWindow = window.parent;
+ referenceWindow.postMessage("SmileIdentity::Close", "*");
+ }
+}
+
+if ("customElements" in window && !customElements.get("document-instruction")) {
+ window.customElements.define("document-instruction", DocumentInstruction);
+}
+
+export { DocumentInstruction };
diff --git a/packages/components/document-capture/document-instructions/src/DocumentInstruction.stories.js b/packages/components/document-capture/document-instructions/src/DocumentInstruction.stories.js
new file mode 100644
index 00000000..fb2f0c86
--- /dev/null
+++ b/packages/components/document-capture/document-instructions/src/DocumentInstruction.stories.js
@@ -0,0 +1,17 @@
+import "./index";
+
+const meta = {
+ component: "document-instruction",
+};
+
+export default meta;
+
+export const DocumentInstruction = {
+ render: () => `
+
+
+ `,
+}
\ No newline at end of file
diff --git a/packages/components/document-capture/document-instructions/src/index.js b/packages/components/document-capture/document-instructions/src/index.js
new file mode 100644
index 00000000..f539c7a5
--- /dev/null
+++ b/packages/components/document-capture/document-instructions/src/index.js
@@ -0,0 +1,3 @@
+export {
+ DocumentInstruction
+} from './DocumentInstruction';
diff --git a/packages/components/domain/Constants.js b/packages/components/domain/Constants.js
new file mode 100644
index 00000000..14233f3f
--- /dev/null
+++ b/packages/components/domain/Constants.js
@@ -0,0 +1,23 @@
+/**
+ * The type of image submitted in the job request
+ * @readonly
+ * @enum {number}
+ */
+export const IMAGE_TYPE = {
+ /** SELFIE_IMAGE_FILE Selfie image in .png or .jpg file format */
+ SELFIE_IMAGE_FILE: 0,
+ /** ID_CARD_IMAGE_FILE ID card image in .png or .jpg file format */
+ ID_CARD_IMAGE_FILE: 1,
+ /** SELFIE_IMAGE_BASE64 Base64 encoded selfie image (.png or .jpg) */
+ SELFIE_IMAGE_BASE64: 2,
+ /** ID_CARD_IMAGE_BASE64 Base64 encoded ID card image (.png or .jpg) */
+ ID_CARD_IMAGE_BASE64: 3,
+ /** LIVENESS_IMAGE_FILE Liveness image in .png or .jpg file format */
+ LIVENESS_IMAGE_FILE: 4,
+ /** ID_CARD_BACK_IMAGE_FILE Back of ID card image in .png or .jpg file format */
+ ID_CARD_BACK_IMAGE_FILE: 5,
+ /** LIVENESS_IMAGE_BASE64 Base64 encoded liveness image (.jpg or .png) */
+ LIVENESS_IMAGE_BASE64: 6,
+ /** ID_CARD_BACK_IMAGE_BASE64 Base64 encoded back of ID card image (.jpg or .png) */
+ ID_CARD_BACK_IMAGE_BASE64: 7,
+};
\ No newline at end of file
diff --git a/packages/components/domain/SmartFileUpload.js b/packages/components/domain/SmartFileUpload.js
new file mode 100644
index 00000000..bda0e9cf
--- /dev/null
+++ b/packages/components/domain/SmartFileUpload.js
@@ -0,0 +1,66 @@
+class SmartFileUpload {
+ static memoryLimit = 10240000;
+
+ static supportedTypes = ['image/jpeg', 'image/png'];
+
+ static getHumanSize(numberOfBytes) {
+ // Approximate to the closest prefixed unit
+ const units = [
+ 'B',
+ 'kB',
+ 'MB',
+ 'GB',
+ 'TB',
+ 'PB',
+ 'EB',
+ 'ZB',
+ 'YB',
+ ];
+ const exponent = Math.min(
+ Math.floor(Math.log(numberOfBytes) / Math.log(1024)),
+ units.length - 1,
+ );
+ const approx = numberOfBytes / 1024 ** exponent;
+ const output = exponent === 0
+ ? `${numberOfBytes} bytes`
+ : `${approx.toFixed(0)} ${units[exponent]}`;
+
+ return output;
+ }
+
+ static getData(file) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+
+ reader.onload = (e) => {
+ resolve(e.target.result);
+ };
+ reader.onerror = () => {
+ reject(new Error('An error occurred reading the file. Please check the file, and try again'));
+ };
+ reader.readAsDataURL(file);
+ });
+ }
+
+ static async retrieve(files) {
+ if (files.length > 1) {
+ throw new Error('Only one file upload is permitted at a time');
+ }
+
+ const file = files[0];
+
+ if (!SmartFileUpload.supportedTypes.includes(file.type)) {
+ throw new Error('Unsupported file format. Please ensure that you are providing a JPG or PNG image');
+ }
+
+ if (file.size > SmartFileUpload.memoryLimit) {
+ throw new Error(`${file.name} is too large. Please ensure that the file is less than ${SmartFileUpload.getHumanSize(SmartFileUpload.memoryLimit)}.`);
+ }
+
+ const imageAsDataUrl = await SmartFileUpload.getData(file);
+
+ return imageAsDataUrl;
+ }
+}
+
+export default SmartFileUpload;
diff --git a/packages/components/domain/camera/SmartCamera.js b/packages/components/domain/camera/SmartCamera.js
new file mode 100644
index 00000000..01828dd9
--- /dev/null
+++ b/packages/components/domain/camera/SmartCamera.js
@@ -0,0 +1,76 @@
+class SmartCamera {
+ static stream = null;
+
+ static async getMedia(constraints) {
+ try {
+ SmartCamera.stream = await navigator.mediaDevices.getUserMedia({
+ ...constraints,
+ video: {
+ ...constraints.video,
+ // NOTE: Special case for multi-camera Samsung devices (learnt from Acuant)
+ // "We found out that some triple camera Samsung devices (S10, S20, Note 20, etc) capture images blurry at edges.
+ // Zooming to 2X, matching the telephoto lens, doesn't solve it completely but mitigates it."
+ zoom: SmartCamera.isSamsungMultiCameraDevice() ? 2.0 : 1.0,
+ }
+ });
+ return SmartCamera.stream;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ static stopMedia() {
+ if (SmartCamera.stream) {
+ SmartCamera.stream.getTracks().forEach(track => track.stop());
+ SmartCamera.stream = null;
+ }
+ }
+
+ static isSamsungMultiCameraDevice() {
+ const matchedModelNumber = navigator.userAgent.match(/SM-[N|G]\d{3}/);
+ if (!matchedModelNumber) {
+ return false;
+ }
+
+ const modelNumber = parseInt(matchedModelNumber[0].match(/\d{3}/)[0], 10);
+ const smallerModelNumber = 970; // S10e
+ return !isNaN(modelNumber) && modelNumber >= smallerModelNumber;
+ }
+
+ static handleCameraError(e) {
+ switch (e.name) {
+ case 'NotAllowedError':
+ case 'SecurityError':
+ return `
+ Looks like camera access was not granted, or was blocked by a browser
+ level setting / extension. Please follow the prompt from the URL bar,
+ or extensions, and enable access.
+ You may need to refresh to start all over again
+ `;
+ case 'AbortError':
+ return `
+ Oops! Something happened, and we lost access to your stream.
+ Please refresh to start all over again
+ `;
+ case 'NotReadableError':
+ return `
+ There seems to be a problem with your device's camera, or its connection.
+ Please check this, and when resolved, try again. Or try another device.
+ `;
+ case 'NotFoundError':
+ return `
+ We are unable to find a video stream.
+ You may need to refresh to start all over again
+ `;
+ case 'TypeError':
+ return `
+ This site is insecure, and as such cannot have access to your camera.
+ Try to navigate to a secure version of this page, or contact the owner.
+ `;
+ default:
+ return e.message;
+ }
+ }
+}
+
+export { SmartCamera };
\ No newline at end of file
diff --git a/packages/components/styles.js b/packages/components/styles.js
new file mode 100644
index 00000000..3e8c2e2f
--- /dev/null
+++ b/packages/components/styles.js
@@ -0,0 +1,353 @@
+import typography from "./typography";
+const styles = `
+
+`
+
+export default styles;
\ No newline at end of file
diff --git a/packages/components/typography.js b/packages/components/typography.js
new file mode 100644
index 00000000..90569318
--- /dev/null
+++ b/packages/components/typography.js
@@ -0,0 +1,52 @@
+const typography = `
+ .text-xs {
+ font-size: 0.75rem;
+ line-height: 1rem;
+ }
+ .text-sm {
+ font-size: 0.875rem;
+ line-height: 1.125rem;
+ }
+ .text-base {
+ font-size: 1rem;
+ line-height: 1rem;
+ }
+ .text-lg {
+ font-size: 1.125rem;
+ line-height: 1.75rem;
+ }
+ .text-xl {
+ font-size: 1.25rem;
+ line-height: 1.75rem;
+ }
+ .text-2xl {
+ font-size: 1.5rem;
+ line-height: 2rem;
+ }
+ .text-3xl {
+ font-size: 1.875rem;
+ line-height: 2.25rem;
+ }
+ .text-4xl {
+ font-size: 2rem;
+ line-height: 2.5rem;
+ }
+ .text-5xl {
+ font-size: 2.25rem;
+ line-height: 2.5rem;
+ }
+ .font-bold {
+ font-weight: 700;
+ }
+ .font-semibold {
+ font-weight: 600;
+ }
+ .font-medium {
+ font-weight: 500;
+ }
+ .font-normal {
+ font-weight: 400;
+ }
+`
+
+export default typography;