Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a15df4f
FullScreen: Support background color customization
aduth Nov 8, 2021
db7fb96
initial upgrade to acuant v11.5.0
aduth Nov 8, 2021
ad5328a
move fullscreen into acuantcapturecanvas
aduth Nov 8, 2021
070c4f6
Revert "move fullscreen into acuantcapturecanvas"
aduth Nov 8, 2021
81d06d5
implement usecookie hook
aduth Nov 8, 2021
3681a91
handle acuant sequence break failure as failed to start
aduth Nov 8, 2021
c09fca7
remove element remove override
aduth Nov 8, 2021
12b233b
Move up acuant access failure check
aduth Nov 8, 2021
ef37d9e
skip worker start if camera not supported
aduth Nov 8, 2021
39c3446
Remove unused accessible status labels
aduth Nov 8, 2021
0fc1e0c
move inline styles to stylesheet
aduth Nov 8, 2021
d40ebb1
add specs for fullscreen bgcolor prop
aduth Nov 8, 2021
22345c8
Revert "skip worker start if camera not supported"
aduth Nov 8, 2021
83b8cb7
Add inline documentation for useCookie
aduth Nov 8, 2021
ac52311
Add useCookie specs
aduth Nov 8, 2021
14a3cbc
Clean-up acuantcameracreated callback in useEffect
aduth Nov 9, 2021
7e85286
Update Acuant canvas button to explicitly click canvas
aduth Nov 9, 2021
9fc6476
Restore cropping failure handling
aduth Nov 9, 2021
bb447e9
Fix JavaScript specs
aduth Nov 9, 2021
04c45d8
Fix type error for useCookie setter
aduth Nov 9, 2021
c8060fa
Resolve lint error on CSS property order
aduth Nov 9, 2021
ca224f9
Rearrange shouldStartAcuantCaptpure logic
aduth Nov 9, 2021
9825cf3
Restore script_src unsafe-eval for document capture CSP
aduth Nov 9, 2021
33347fb
Avoid parsing full cookie string, find value by name
aduth Nov 9, 2021
8ffc46e
Remove bgColor feature for FullScreen
aduth Nov 9, 2021
b3b49ca
Center acuant canvas in landscape orientation
aduth Nov 9, 2021
fcf8415
Disable CodeClimate complex-logic
aduth Nov 9, 2021
f5db1c0
Customize iOS 15 sequence break error message
aduth Nov 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ checks:
config:
threshold: 4
complex-logic:
config:
threshold: 4
enabled: false
file-lines:
enabled: false
method-complexity:
Expand Down
2 changes: 2 additions & 0 deletions app/controllers/concerns/idv/document_capture_concern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ def override_document_capture_step_csp
request,
# required to run wasm until wasm-eval is available
script_src: ['\'unsafe-eval\''],
# required because acuant styles its own elements with inline style attributes
style_src: ['\'unsafe-inline\''],
# required for retrieving image dimensions from uploaded images
img_src: ['blob:'],
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
import { useContext, useMemo, useEffect, useRef, useState } from 'react';
import { useContext, useEffect, useRef, useState } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import AcuantContext from '../context/acuant';
import useAsset from '../hooks/use-asset';
import useInstanceId from '../hooks/use-instance-id';
import useImmutableCallback from '../hooks/use-immutable-callback';
import './acuant-capture-canvas.scss';

/**
* @enum {string}
*/
const CaptureStatus = {
ALIGN: 'ALIGN',
MOVE_CLOSER: 'MOVE_CLOSER',
TAP_TO_CAPTURE: 'TAP_TO_CAPTURE',
CAPTURING: 'CAPTURING',
};
/** @typedef {import('../context/acuant').AcuantJavaScriptWebSDK} AcuantJavaScriptWebSDK */

/**
* @enum {number}
Expand Down Expand Up @@ -92,13 +84,14 @@ export function defineObservableProperty(object, property, onChangeCallback) {

/**
* @typedef {(
* | null // Cropping failure (SDK v11.4.3, L753)
* | undefined // Cropping failure (SDK v11.4.3, L960)
* | 'Camera not supported.' // Camera not supported (SDK v11.4.3, L74, L798)
* | 'already started.' // Capture already started (SDK v11.4.3, L565)
* | 'already started' // Capture already started (SDK v11.4.3, L580)
* | 'Missing HTML elements.' // Required page elements are not available (SDK v11.4.3, L568)
* | MediaStreamError // User or system denied camera access (SDK v11.4.3, L544)
* | undefined // Cropping failure (SDK v11.5.0, L1171)
* | 'Camera not supported.' // Camera not supported (SDK v11.5.0, L978)
* | 'already started.' // Capture already started (SDK v11.5.0, L724)
* | 'Missing HTML elements.' // Required page elements are not available (SDK v11.5.0, L727)
* | Error // User or system denied camera access (SDK v11.5.0, L673)
* | "Expected div with 'acuant-camera' id" // Failure to setup due to missing element (SDK v11.5.0, L706)
* | 'Live capture has previously failed and was called again. User was sent to manual capture.' // Previous failure (SDK v11.5.0, L698)
* | 'sequence-break' // iOS 15 sequence break (SDK v11.5.0, L1327)
* )} AcuantCaptureFailureError
*/

Expand Down Expand Up @@ -127,6 +120,7 @@ export function defineObservableProperty(object, property, onChangeCallback) {
* @typedef AcuantCamera
*
* @prop {(callback: (response: AcuantImage)=>void, errorCallback: function)=>void} start
* @prop {(callback: AcuantCameraUICallbacks)=>void} startManualCapture
* @prop {(callback: (response: AcuantImage)=>void)=>void} triggerCapture
* @prop {(
* data: string,
Expand All @@ -142,6 +136,7 @@ export function defineObservableProperty(object, property, onChangeCallback) {
*
* @prop {AcuantCameraUI} AcuantCameraUI Acuant camera UI API.
* @prop {AcuantCamera} AcuantCamera Acuant camera API.
* @prop {AcuantJavaScriptWebSDK} AcuantJavascriptWebSdk Acuant web SDK.
*/

/**
Expand Down Expand Up @@ -189,7 +184,7 @@ export function defineObservableProperty(object, property, onChangeCallback) {
*/

/**
* @typedef {(error:AcuantCaptureFailureError)=>void} AcuantFailureCallback
* @typedef {(error?: AcuantCaptureFailureError, code?: string) => void} AcuantFailureCallback
*/

/**
Expand All @@ -199,55 +194,6 @@ export function defineObservableProperty(object, property, onChangeCallback) {
* @prop {AcuantFailureCallback} onImageCaptureFailure Failure callback.
*/

/**
* Returns the computed capture status based on current capture type and frame state.
*
* @param {AcuantCaptureType} captureType Current capture type.
* @param {AcuantFrameState} frameState Current frame state.
*
* @return {CaptureStatus}
*/
function getCaptureStatus(captureType, frameState) {
// Acuant internally updates UI state to "Tap to Capture" but does _not_ invoke the
// `onFrameAvailable` callback, so we have to track `captureType` separately.
if (captureType === 'TAP' || frameState === AcuantUIState.TAP_TO_CAPTURE) {
return CaptureStatus.TAP_TO_CAPTURE;
}

switch (frameState) {
case AcuantDocumentState.GOOD_DOCUMENT:
return CaptureStatus.CAPTURING;
case AcuantDocumentState.SMALL_DOCUMENT:
return CaptureStatus.MOVE_CLOSER;
default:
return CaptureStatus.ALIGN;
}
}

/**
* Returns the translation key to use for the status text based on current capture status.
*
* @param {CaptureStatus} captureStatus
*
* @return {string}
*/
function getStatusLabelKey(captureStatus) {
switch (captureStatus) {
case CaptureStatus.CAPTURING:
// i18n-tasks-use t('doc_auth.accessible_labels.status_capturing')
return 'doc_auth.accessible_labels.status_capturing';
case CaptureStatus.MOVE_CLOSER:
// i18n-tasks-use t('doc_auth.accessible_labels.status_move_closer')
return 'doc_auth.accessible_labels.status_move_closer';
case CaptureStatus.TAP_TO_CAPTURE:
// i18n-tasks-use t('doc_auth.accessible_labels.status_tap_to_capture')
return 'doc_auth.accessible_labels.status_tap_to_capture';
default:
// i18n-tasks-use t('doc_auth.accessible_labels.status_align')
return 'doc_auth.accessible_labels.status_align';
}
}

/**
* @param {AcuantCaptureCanvasProps} props Component props.
*/
Expand All @@ -258,49 +204,40 @@ function AcuantCaptureCanvas({
const { isReady } = useContext(AcuantContext);
const { getAssetPath } = useAsset();
const { t } = useI18n();
const instanceId = useInstanceId();
const [hasCaptured, setHasCaptured] = useState(false);
const canvasRef = useRef(/** @type {(HTMLCanvasElement & {callback: function})?} */ (null));
const cameraRef = useRef(/** @type {HTMLDivElement?} */ (null));
const onCropped = useImmutableCallback(
(response) => {
if (response) {
onImageCaptureSuccess(response);
} else {
onImageCaptureFailure(response);
onImageCaptureFailure();
}
},
[onImageCaptureSuccess, onImageCaptureFailure],
[onImageCaptureSuccess],
);
const [captureType, setCaptureType] = useState(/** @type {AcuantCaptureType} */ ('AUTO'));
const [frameState, setFrameState] = useState(
/** @type {AcuantFrameState} */ (AcuantDocumentState.NO_DOCUMENT),
);
const captureStatus = useMemo(() => getCaptureStatus(captureType, frameState), [
captureType,
frameState,
]);

useEffect(() => {
if (canvasRef.current) {
function onAcuantCameraCreated() {
const canvas = document.getElementById('acuant-ui-canvas');
// Acuant SDK assigns a callback property to the canvas when it switches to its "Tap to
// Capture" mode (Acuant SDK v11.4.4, L158). Infer capture type by presence of the property.
defineObservableProperty(canvasRef.current, 'callback', (callback) => {
defineObservableProperty(canvas, 'callback', (callback) => {
setCaptureType(callback ? 'TAP' : 'AUTO');
});
}

cameraRef.current?.addEventListener('acuantcameracreated', onAcuantCameraCreated);
return () => {
cameraRef.current?.removeEventListener('acuantcameracreated', onAcuantCameraCreated);
};
}, []);

useEffect(() => {
if (isReady) {
setHasCaptured(false);
/** @type {AcuantGlobal} */ (window).AcuantCameraUI.start(
{
onFrameAvailable(result) {
setFrameState(result.state);
},
onCaptured() {
setHasCaptured(true);
},
onCaptured() {},
onCropped,
},
onImageCaptureFailure,
Expand All @@ -323,8 +260,7 @@ function AcuantCaptureCanvas({
};
}, [isReady]);

// The video element is never visible to the user, but it needs to be present
// in the DOM for the Acuant SDK to capture the feed from the camera.
const clickCanvas = () => document.getElementById('acuant-ui-canvas')?.click();

return (
<>
Expand All @@ -338,47 +274,24 @@ function AcuantCaptureCanvas({
alt=""
width="144"
height="144"
style={{
position: 'absolute',
left: '50%',
top: '50%',
transform: 'translate(-72px, -72px)',
}}
className="acuant-capture-canvas__spinner"
/>
)}
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video id="acuant-player" controls autoPlay playsInline style={{ display: 'none' }} />
<div id="acuant-sdk-capture-view">
<canvas
id="acuant-video-canvas"
ref={canvasRef}
tabIndex={0}
aria-labelledby={`acuant-sdk-heading-${instanceId}`}
aria-describedby={`acuant-sdk-instructions-${instanceId}`}
style={{
width: '100%',
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
}}
>
<h2 key="label" id={`acuant-sdk-heading-${instanceId}`}>
{t('doc_auth.accessible_labels.camera_video_capture_label')}
</h2>
{captureType !== 'TAP' && (
<p key="description" id={`acuant-sdk-instructions-${instanceId}`}>
{t('doc_auth.accessible_labels.camera_video_capture_instructions')}
</p>
)}
<button key="button" type="button" disabled={captureType !== 'TAP'}>
{t('doc_auth.buttons.take_picture')}
</button>
</canvas>
<div role="status" aria-live="polite" className="usa-sr-only">
{isReady && !hasCaptured ? t(getStatusLabelKey(captureStatus)) : null}
</div>
</div>
<h2 className="usa-sr-only">{t('doc_auth.accessible_labels.camera_video_capture_label')}</h2>
{captureType !== 'TAP' && (
<p className="usa-sr-only">
{t('doc_auth.accessible_labels.camera_video_capture_instructions')}
</p>
)}
<div id="acuant-camera" ref={cameraRef} className="acuant-capture-canvas__camera" />
<button
type="button"
onClick={clickCanvas}
disabled={captureType !== 'TAP'}
className="usa-sr-only"
>
{t('doc_auth.buttons.take_picture')}
</button>
</>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.acuant-capture-canvas__spinner {
left: 50%;
position: absolute;
top: 50%;
transform: translate(-72px, -72px);
}

.acuant-capture-canvas__camera {
left: 50%;
max-width: 100%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
width: 100%;

@media (orientation: landscape) {
width: fit-content;
}
}
Loading