Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,15 @@ interface AcuantCameraUICallbacks {
* Optional frame available callback
*/
onFrameAvailable?: (response: AcuantDetectedResult) => void;
/**
* Callback that occurs when there is a failure.
*/
onFailure: (error?: AcuantCaptureFailureError, code?: string) => void;
}

type AcuantCameraUIStart = (
callbacks: AcuantCameraUICallbacks,
onFailure: AcuantFailureCallback,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it still work for the older version to remove this? Same on line 308 below.

Copy link
Copy Markdown
Contributor Author

@eileen-nava eileen-nava Aug 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I addressed this in my latest commits, with lots of help from Matt! 🙏🏻

onFailureCallbackWithOptions: AcuantFailureCallback,
options?: AcuantCameraUIOptions,
) => void;

Expand Down Expand Up @@ -210,9 +214,9 @@ export interface AcuantSuccessResponse {
*/
image: AcuantImage;
/**
* Document type
* Document type for Acuant SDK 11.9.1
*/
cardtype: AcuantDocumentType;
cardType: AcuantDocumentType;
/**
* Detected image glare
*/
Expand All @@ -235,6 +239,13 @@ export interface AcuantSuccessResponse {
dpi: number;
}

export type LegacyAcuantSuccessResponse = Omit<AcuantSuccessResponse, 'cardType'> & {
/**
* Document type for Acuant SDK 11.8.2
*/
cardtype: AcuantDocumentType;
};

type AcuantSuccessCallback = (response: AcuantSuccessResponse) => void;

type AcuantFailureCallback = (error?: AcuantCaptureFailureError, code?: string) => void;
Expand Down Expand Up @@ -298,24 +309,31 @@ function AcuantCamera({
);

useEffect(() => {
const textOptions = {
text: {
NONE: t('doc_auth.info.capture_status_none'),
SMALL_DOCUMENT: t('doc_auth.info.capture_status_small_document'),
BIG_DOCUMENT: t('doc_auth.info.capture_status_big_document'),
GOOD_DOCUMENT: null,
CAPTURING: t('doc_auth.info.capture_status_capturing'),
TAP_TO_CAPTURE: t('doc_auth.info.capture_status_tap_to_capture'),
},
};
if (isReady) {
const onFailureCallbackWithOptions = (...args) => onImageCaptureFailure(...args);
Object.keys(textOptions).forEach((key) => {
onFailureCallbackWithOptions[key] = textOptions[key];
});

window.AcuantCameraUI = getActualAcuantCameraUI();
window.AcuantCameraUI.start(
{
onCaptured: onCropStart,
onCropped,
onFailure: onImageCaptureFailure,
},
onImageCaptureFailure,
{
text: {
NONE: t('doc_auth.info.capture_status_none'),
SMALL_DOCUMENT: t('doc_auth.info.capture_status_small_document'),
BIG_DOCUMENT: t('doc_auth.info.capture_status_big_document'),
GOOD_DOCUMENT: null,
CAPTURING: t('doc_auth.info.capture_status_capturing'),
TAP_TO_CAPTURE: t('doc_auth.info.capture_status_tap_to_capture'),
},
},
onFailureCallbackWithOptions,
textOptions,
);
setIsActive(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import type { FullScreenRefHandle } from '@18f/identity-components';
import { useDidUpdateEffect } from '@18f/identity-react-hooks';
import { useI18n } from '@18f/identity-react-i18n';
import AcuantCamera, { AcuantDocumentType } from './acuant-camera';
import type { AcuantCaptureFailureError, AcuantSuccessResponse } from './acuant-camera';
import type {
AcuantCaptureFailureError,
AcuantSuccessResponse,
LegacyAcuantSuccessResponse,
} from './acuant-camera';
import AcuantCaptureCanvas from './acuant-capture-canvas';
import AcuantContext, { AcuantCaptureMode } from '../context/acuant';
import AnalyticsContext from '../context/analytics';
Expand Down Expand Up @@ -426,11 +430,15 @@ function AcuantCapture(
}
}

function onAcuantImageCaptureSuccess(nextCapture: AcuantSuccessResponse) {
const { image, cardtype, dpi, moire, glare, sharpness } = nextCapture;
function onAcuantImageCaptureSuccess(
nextCapture: AcuantSuccessResponse | LegacyAcuantSuccessResponse,
) {
const { image, dpi, moire, glare, sharpness } = nextCapture;
const cardType = 'cardType' in nextCapture ? nextCapture.cardType : nextCapture.cardtype;

const isAssessedAsGlare = glare < glareThreshold;
const isAssessedAsBlurry = sharpness < sharpnessThreshold;
const isAssessedAsUnsupported = cardtype !== AcuantDocumentType.ID;
const isAssessedAsUnsupported = cardType !== AcuantDocumentType.ID;
const { width, height, data } = image;

let assessment: AcuantImageAssessment;
Expand All @@ -453,7 +461,7 @@ function AcuantCapture(
mimeType: 'image/jpeg', // Acuant Web SDK currently encodes all images as JPEG
source: 'acuant',
isAssessedAsUnsupported,
documentType: getDocumentTypeLabel(cardtype),
documentType: getDocumentTypeLabel(cardType),
dpi,
moire,
glare,
Expand Down
26 changes: 18 additions & 8 deletions app/javascript/packages/document-capture/context/acuant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ declare let AcuantCamera: AcuantCameraInterface;
declare global {
interface AcuantJavascriptWebSdkInterface {
initialize: AcuantInitialize;
startWorkers: AcuantWorkersInitialize;
START_FAIL_CODE: string;
REPEAT_FAIL_CODE: string;
SEQUENCE_BREAK_CODE: string;
start?: AcuantWorkersInitialize;
startWorkers?: AcuantWorkersInitialize;
}
}

Expand Down Expand Up @@ -161,14 +162,23 @@ AcuantContext.displayName = 'AcuantContext';
/**
* Returns a found AcuantJavascriptWebSdk
* object, if one is available.
* This function normalizes differences between
* the 11.5.0 and 11.7.0 SDKs. The former attached
* the object to the global window, while the latter
* sets the object in the global (but non-window)
* scope.
* Depending on the SDK version,
* will use either startWorkers (11.8.2) or start (11.9.1)
*/
const getActualAcuantJavascriptWebSdk = (): AcuantJavascriptWebSdkInterface => {
if (window.AcuantJavascriptWebSdk) {
if (
window.AcuantJavascriptWebSdk &&
typeof window.AcuantJavascriptWebSdk.startWorkers === 'function' &&
typeof window.AcuantJavascriptWebSdk.start !== 'function'
) {
return {
...window.AcuantJavascriptWebSdk,
start(...args) {
window.AcuantJavascriptWebSdk.startWorkers?.(...args);
},
};
}
if (window.AcuantJavascriptWebSdk && typeof window.AcuantJavascriptWebSdk.start === 'function') {
return window.AcuantJavascriptWebSdk;
}
if (typeof AcuantJavascriptWebSdk === 'undefined') {
Expand Down Expand Up @@ -257,7 +267,7 @@ function AcuantContextProvider({
window.AcuantJavascriptWebSdk = getActualAcuantJavascriptWebSdk();
window.AcuantJavascriptWebSdk.initialize(credentials, endpoint, {
onSuccess: () => {
window.AcuantJavascriptWebSdk.startWorkers(() => {
window.AcuantJavascriptWebSdk.start?.(() => {
window.AcuantCamera = getActualAcuantCamera();
const { isCameraSupported: nextIsCameraSupported } = window.AcuantCamera;
trackEvent('IdV: Acuant SDK loaded', {
Expand Down
1 change: 1 addition & 0 deletions public/acuant/11.9.1/AcuantCamera.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions public/acuant/11.9.1/AcuantImageService.min.js

Large diffs are not rendered by default.

Binary file added public/acuant/11.9.1/AcuantImageService.wasm
Binary file not shown.
1 change: 1 addition & 0 deletions public/acuant/11.9.1/AcuantImageWorker.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/acuant/11.9.1/AcuantInitializerService.min.js

Large diffs are not rendered by default.

Binary file not shown.
1 change: 1 addition & 0 deletions public/acuant/11.9.1/AcuantInitializerWorker.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/acuant/11.9.1/AcuantJavascriptWebSdk.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions public/acuant/11.9.1/AcuantMetricsService.min.js

Large diffs are not rendered by default.

Binary file added public/acuant/11.9.1/AcuantMetricsService.wasm
Binary file not shown.
1 change: 1 addition & 0 deletions public/acuant/11.9.1/AcuantMetricsWorker.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

261 changes: 261 additions & 0 deletions public/acuant/11.9.1/AcuantPassiveLiveness.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions public/acuant/11.9.1/html5-qrcode.min.js

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions public/acuant/11.9.1/opencv.min.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const ACUANT_CAPTURE_SUCCESS_RESULT = {
width: 1748,
height: 1104,
},
cardtype: AcuantDocumentType.ID,
cardType: AcuantDocumentType.ID,
dpi: 519,
moire: 99,
moireraw: 99,
Expand Down Expand Up @@ -259,7 +259,7 @@ describe('document-capture/components/acuant-capture', () => {
expect(window.AcuantCameraUI.end.called).to.be.false();
});

it('shows error if capture fails', async () => {
it('shows error if capture fails: legacy version of Acuant SDK', async () => {
const trackEvent = sinon.spy();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you clarify how this is selecting the legacy version? I'm not seeing version number differences in the two specs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we look at line 275, we can see that the second argument for the start function is the onFailure callback. If this were the more recent version of Acuant SDK, the onFailure callback would be included in the first argument for the start function, which are the AcuantCameraUICallbacks.

To give a more general answer, I decided to test that both version 11.8.2 and version 11.9.1 are supported by stubbing the start function as it would be called for the two different versions. I didn't put any direct references to version number in the specs.

const { container, getByLabelText, findByText } = render(
<AnalyticsContext.Provider value={{ trackEvent }}>
Expand Down Expand Up @@ -288,7 +288,40 @@ describe('document-capture/components/acuant-capture', () => {
expect(document.activeElement).to.equal(button);
});

it('shows sequence break error', async () => {
it('shows error if capture fails: latest version of Acuant SDK', async () => {
const trackEvent = sinon.spy();
const { container, getByLabelText, findByText } = render(
<AnalyticsContext.Provider value={{ trackEvent }}>
<DeviceContext.Provider value={{ isMobile: true }}>
<AcuantContextProvider sdkSrc="about:blank" cameraSrc="about:blank">
<AcuantCapture label="Image" name="test" />
</AcuantContextProvider>
</DeviceContext.Provider>
</AnalyticsContext.Provider>,
);

const start = async ({ onFailure }) => {
await onFailure('Camera not supported.', 'start-fail-code');
};

initialize({
start,
});

const button = getByLabelText('Image');
await userEvent.click(button);

await findByText('doc_auth.errors.camera.failed');
expect(window.AcuantCameraUI.end).to.have.been.calledOnce();
expect(container.querySelector('.full-screen')).to.be.null();
expect(trackEvent).to.have.been.calledWith('IdV: Image capture failed', {
field: 'test',
error: 'Camera not supported',
});
expect(document.activeElement).to.equal(button);
});

it('shows sequence break error: legacy version of SDK', async () => {
const trackEvent = sinon.spy();
const { container, getByLabelText, findByText } = render(
<AnalyticsContext.Provider value={{ trackEvent }}>
Expand All @@ -306,6 +339,48 @@ describe('document-capture/components/acuant-capture', () => {
const code = 'sequence-break-code';
document.cookie = `AcuantCameraHasFailed=${code}`;
onError('iOS 15 sequence break', code);
});
}),
});

const button = getByLabelText('Image');
await userEvent.click(button);

await findByText('doc_auth.errors.upload_error errors.messages.try_again');
expect(window.AcuantCameraUI.end).to.have.been.calledOnce();
expect(container.querySelector('.full-screen')).to.be.null();
expect(trackEvent).to.have.been.calledWith('IdV: Image capture failed', {
field: 'test',
error: 'iOS 15 GPU Highwater failure (SEQUENCE_BREAK_CODE)',
});
await waitFor(() => document.activeElement === button);

const defaultPrevented = !fireEvent.click(button);

window.AcuantCameraUI.start.resetHistory();
expect(defaultPrevented).to.be.false();
expect(window.AcuantCameraUI.start.called).to.be.false();
});

it('shows sequence break error: latest version of SDK', async () => {
const trackEvent = sinon.spy();
const { container, getByLabelText, findByText } = render(
<AnalyticsContext.Provider value={{ trackEvent }}>
<DeviceContext.Provider value={{ isMobile: true }}>
<AcuantContextProvider sdkSrc="about:blank" cameraSrc="about:blank">
<AcuantCapture label="Image" name="test" />
</AcuantContextProvider>
</DeviceContext.Provider>
</AnalyticsContext.Provider>,
);

initialize({
start: sinon.stub().callsFake((callbacks) => {
const { onFailure } = callbacks;
setTimeout(() => {
const code = 'sequence-break-code';
document.cookie = `AcuantCameraHasFailed=${code}`;
onFailure('iOS 15 sequence break', code);
}, 0);
}),
});
Expand All @@ -329,7 +404,7 @@ describe('document-capture/components/acuant-capture', () => {
expect(window.AcuantCameraUI.start.called).to.be.false();
});

it('calls onCameraAccessDeclined if camera access is declined', async () => {
it('calls onCameraAccessDeclined if camera access is declined: legacy version of SDK', async () => {
const trackEvent = sinon.spy();
const onCameraAccessDeclined = sinon.stub();
const { container, getByLabelText } = render(
Expand Down Expand Up @@ -365,6 +440,46 @@ describe('document-capture/components/acuant-capture', () => {
expect(document.activeElement).to.equal(button);
});

it('calls onCameraAccessDeclined if camera access is declined: latest version of SDK', async () => {
const trackEvent = sinon.spy();
const onCameraAccessDeclined = sinon.stub();
const { container, getByLabelText } = render(
<AnalyticsContext.Provider value={{ trackEvent }}>
<DeviceContext.Provider value={{ isMobile: true }}>
<AcuantContextProvider sdkSrc="about:blank" cameraSrc="about:blank">
<AcuantCapture
label="Image"
name="test"
onCameraAccessDeclined={onCameraAccessDeclined}
/>
</AcuantContextProvider>
</DeviceContext.Provider>
</AnalyticsContext.Provider>,
);

const start = async ({ onFailure }) => {
await onFailure(new Error());
};

initialize({
start,
});

const button = getByLabelText('Image');
await userEvent.click(button);

await Promise.all([
expect(onCameraAccessDeclined).to.eventually.be.called(),
expect(window.AcuantCameraUI.end).to.eventually.be.called(),
]);
expect(container.querySelector('.full-screen')).to.be.null();
expect(trackEvent).to.have.been.calledWith('IdV: Image capture failed', {
field: 'test',
error: 'User or system denied camera access',
});
expect(document.activeElement).to.equal(button);
});

it('blocks focus trap default focus return behavior if focus transitions during error', async () => {
let outsideInput;
const onCameraAccessDeclined = sinon.stub().callsFake(() => {
Expand All @@ -384,8 +499,12 @@ describe('document-capture/components/acuant-capture', () => {
);
outsideInput = getByTestId('outside-input');

const start = async ({ onFailure }) => {
await onFailure(new Error());
};

initialize({
start: sinon.stub().callsArgWithAsync(1, new Error()),
start,
});

const button = getByLabelText('Image');
Expand Down Expand Up @@ -567,7 +686,7 @@ describe('document-capture/components/acuant-capture', () => {
await Promise.resolve();
callbacks.onCropped({
...ACUANT_CAPTURE_SUCCESS_RESULT,
cardtype: AcuantDocumentType.PASSPORT,
cardType: AcuantDocumentType.PASSPORT,
});
}),
});
Expand Down Expand Up @@ -822,7 +941,7 @@ describe('document-capture/components/acuant-capture', () => {
await Promise.resolve();
callbacks.onCaptured();
await Promise.resolve();
callbacks.onCropped({ ...ACUANT_CAPTURE_SUCCESS_RESULT, cardtype: incorrectCardType });
callbacks.onCropped({ ...ACUANT_CAPTURE_SUCCESS_RESULT, cardType: incorrectCardType });
}),
});

Expand Down
Loading