diff --git a/app/javascript/packages/document-capture/components/acuant-camera.tsx b/app/javascript/packages/document-capture/components/acuant-camera.tsx
index 6b857187b11..5b17401d942 100644
--- a/app/javascript/packages/document-capture/components/acuant-camera.tsx
+++ b/app/javascript/packages/document-capture/components/acuant-camera.tsx
@@ -1,13 +1,16 @@
-import { useContext, useEffect } from 'react';
+import React, { useContext, useEffect } from 'react';
import type { ReactNode } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import { useImmutableCallback } from '@18f/identity-react-hooks';
import AcuantContext from '../context/acuant';
declare let AcuantCameraUI: AcuantCameraUIInterface;
+declare let AcuantPassiveLiveness: AcuantPassiveLivenessInterface;
+
declare global {
interface Window {
AcuantCameraUI: AcuantCameraUIInterface;
+ AcuantPassiveLiveness: AcuantPassiveLivenessInterface;
}
}
@@ -16,6 +19,7 @@ declare global {
*/
type AcuantGlobals = {
AcuantCameraUI: AcuantCameraUIInterface;
+ AcuantPassiveLiveness: AcuantPassiveLivenessInterface;
AcuantCamera: AcuantCameraInterface;
};
export type AcuantGlobal = Window & AcuantGlobals;
@@ -138,6 +142,8 @@ type AcuantCameraUIStart = (
options?: AcuantCameraUIOptions,
) => void;
+type AcuantPassiveLivenessStart = () => void;
+
interface AcuantCameraUIInterface {
/**
* Start capture
@@ -148,6 +154,16 @@ interface AcuantCameraUIInterface {
*/
end: () => void;
}
+interface AcuantPassiveLivenessInterface {
+ /**
+ * Start capture
+ */
+ start: AcuantPassiveLivenessStart;
+ /**
+ * End capture
+ */
+ end: () => void;
+}
type AcuantCameraStart = (
callback: (response: AcuantImage) => void,
@@ -263,6 +279,10 @@ interface AcuantCameraContextProps {
* Crop started callback, invoked after capture is made and before image has been evaluated
*/
onCropStart: () => void;
+ /**
+ * Whether this camera is for selfie mode (other option is captureing an id)
+ */
+ selfieMode: boolean;
/**
* React children node
*/
@@ -289,10 +309,22 @@ const getActualAcuantCameraUI = (): AcuantCameraUIInterface => {
return AcuantCameraUI;
};
+const getActualAcuantPassiveLiveness = (): AcuantPassiveLivenessInterface => {
+ if (window.AcuantPassiveLiveness) {
+ return window.AcuantPassiveLiveness;
+ }
+ if (typeof AcuantPassiveLiveness === 'undefined') {
+ // eslint-disable-next-line no-console
+ console.error('AcuantCameraUI is not defined in the global scope');
+ }
+ return AcuantPassiveLiveness;
+};
+
function AcuantCamera({
onImageCaptureSuccess = () => {},
onImageCaptureFailure = () => {},
onCropStart = () => {},
+ selfieMode = false,
children,
}: AcuantCameraContextProps) {
const { isReady, setIsActive } = useContext(AcuantContext);
@@ -307,6 +339,45 @@ function AcuantCamera({
},
[onImageCaptureSuccess],
);
+ const faceCaptureCallback = {
+ onDetectorInitialized: () => {
+ console.log('onDetectorInitialized');
+ // This callback is triggered when the face detector is ready.
+ // Until then, no actions are executed and the user sees only the camera stream.
+ // You can opt to display an alert before the callback is triggered.
+ },
+ onDetection: (text) => {
+ console.log('onDetection', text);
+ // Triggered when the face does not pass the scan. The UI element
+ // should be updated here to provide guidence to the user
+ },
+ onOpened: () => {
+ // Camera has opened
+ console.log('onOpened');
+ },
+ onClosed: () => {
+ // Camera has closed
+ console.log('onClosed');
+ },
+ onError: (error) => {
+ // Error occurred. Camera permission not granted will
+ // manifest here with 1 as error code. Unexpected errors will have 2 as error code.
+ console.log('onError', error);
+ },
+ onPhotoTaken: () => {
+ // The photo has been taken and it's showing a preview with a button to accept or retake the image.
+ console.log('onPhotoTaken');
+ },
+ onPhotoRetake: () => {
+ // Triggered when retake button is tapped
+ console.log('onPhotoRetake');
+ },
+ onCaptured: (base64Image) => {
+ // Triggered when accept button is tapped
+ console.log('onCaptured');
+ //onImageCaptureSuccess({image: base64Image});
+ },
+ };
useEffect(() => {
const textOptions = {
@@ -319,7 +390,24 @@ function AcuantCamera({
TAP_TO_CAPTURE: t('doc_auth.info.capture_status_tap_to_capture'),
},
};
- if (isReady) {
+ const faceDetectionStates = {
+ FACE_NOT_FOUND: 'FACE NOT FOUND',
+ TOO_MANY_FACES: 'TOO MANY FACES',
+ FACE_ANGLE_TOO_LARGE: 'FACE ANGLE TOO LARGE',
+ PROBABILITY_TOO_SMALL: 'PROBABILITY TOO SMALL',
+ FACE_TOO_SMALL: 'FACE TOO SMALL',
+ FACE_CLOSE_TO_BORDER: 'TOO CLOSE TO THE FRAME',
+ };
+
+ const cleanupCamera = () => {
+ window.AcuantCameraUI.end();
+ setIsActive(false);
+ };
+ const cleanupSelfieCamera = () => {
+ window.AcuantPassiveLiveness.end();
+ setIsActive(false);
+ };
+ const startCamera = () => {
const onFailureCallbackWithOptions = (...args) => onImageCaptureFailure(...args);
Object.keys(textOptions).forEach((key) => {
onFailureCallbackWithOptions[key] = textOptions[key];
@@ -336,12 +424,21 @@ function AcuantCamera({
textOptions,
);
setIsActive(true);
- }
+ };
+ const startSelfieCamera = () => {
+ window.AcuantPassiveLiveness = getActualAcuantPassiveLiveness();
+ // This opens the native camera, but TODO callbacks
+ //window.AcuantPassiveLiveness.startManualCapture((image) => console.log('image', image));
+ window.AcuantPassiveLiveness.start(faceCaptureCallback, faceDetectionStates);
+ setIsActive(true);
+ };
+ if (isReady) {
+ selfieMode ? startSelfieCamera() : startCamera();
+ }
return () => {
if (isReady) {
- window.AcuantCameraUI.end();
- setIsActive(false);
+ selfieMode ? cleanupSelfieCamera() : cleanupCamera();
}
};
}, [isReady]);
diff --git a/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx b/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx
index f21eb626abb..a84cd9ef45c 100644
--- a/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx
+++ b/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx
@@ -67,6 +67,7 @@ function AcuantCaptureCanvas() {
)}
+