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() {

)}
+