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
34 changes: 31 additions & 3 deletions app/javascript/packages/document-capture/context/acuant.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createContext, useContext, useMemo, useEffect, useState } from 'react';
import DeviceContext from './device';
import AnalyticsContext from './analytics';

/** @typedef {import('react').ReactNode} ReactNode */

Expand All @@ -9,15 +10,25 @@ import DeviceContext from './device';
* @prop {boolean} isCameraSupported Whether camera is supported.
*/

/**
* @typedef {1|2|400|401|403} AcuantInitializeCode Acuant initialization callback code.
*
* @see https://github.com/Acuant/JavascriptWebSDKV11/blob/11.4.4/SimpleHTMLApp/webSdk/dist/AcuantJavascriptWebSdk.js#L1327-L1353
*/

/**
* @typedef AcuantCallbackOptions
*
* @prop {()=>void} onSuccess Success callback.
* @prop {()=>void} onFail Failure callback.
* @prop {(code: AcuantInitializeCode, description: string)=>void} onFail Failure callback.
*/

/**
* @typedef {(credentials:string?,endpoint:string?,AcuantCallbackOptions)=>void} AcuantInitialize
* @typedef {(
* credentials: string?,
* endpoint: string?,
* callback: AcuantCallbackOptions,
* )=>void} AcuantInitialize
*/

/**
Expand Down Expand Up @@ -66,6 +77,7 @@ function AcuantContextProvider({
children,
}) {
const { isMobile } = useContext(DeviceContext);
const { addPageAction } = useContext(AnalyticsContext);
// Only mobile devices should load the Acuant SDK. Consider immediately ready otherwise.
const [isReady, setIsReady] = useState(!isMobile);
const [isAcuantLoaded, setIsAcuantLoaded] = useState(false);
Expand Down Expand Up @@ -94,13 +106,29 @@ function AcuantContextProvider({
endpoint,
{
onSuccess: () => {
addPageAction({
label: 'IdV: Acuant SDK loaded',
payload: { success: true },
});

setIsCameraSupported(
/** @type {AcuantGlobal} */ (window).AcuantCamera.isCameraSupported,
);
setIsReady(true);
setIsAcuantLoaded(true);
},
onFail: () => setIsError(true),
onFail(code, description) {
addPageAction({
label: 'IdV: Acuant SDK loaded',
payload: {
success: false,
code,
description,
},
});

setIsError(true);
},
},
);
};
Expand Down
48 changes: 24 additions & 24 deletions app/javascript/packs/document-capture.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,34 +141,34 @@ loadPolyfills(['fetch', 'crypto']).then(async () => {

render(
<DeviceContext.Provider value={device}>
<AcuantContextProvider
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.

These changes only move up the analytics provider. It's easier to see using "Hide whitespace changes" setting in GitHub:

image

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.

Related: Following a prior similar comment about moving to some flattened, "composed" provider implementation, I explored this a bit. I think it may help improve diffs in review, though I think the general readability does suffer a bit, at least in the explored implementation.

function ComposeComponents({ components, children }) {
  return components.reduceRight(
    (result, [Provider, props]) => (
      // eslint-disable-next-line react/jsx-props-no-spreading
      <Provider {...props}>{result}</Provider>
    ),
    children
  );
}
render(
  <ComposeComponents
    components={[
      [DeviceContext.Provider, { value: device }],
      [AnalyticsContext.Provider, { value: { addPageAction, noticeError } }],
      [
        AcuantContextProvider,
        {
          credentials: getMetaContent("acuant-sdk-initialization-creds"),
          endpoint: getMetaContent("acuant-sdk-initialization-endpoint"),
        },
      ],
      [
        UploadContextProvider,
        {
          endpoint: /** @type {string} */ (appRoot.getAttribute(
            "data-endpoint"
          )),
          statusEndpoint: /** @type {string} */ (appRoot.getAttribute(
            "data-status-endpoint"
          )),
          statusPollInterval:
            Number(appRoot.getAttribute("data-status-poll-interval-ms")) ||
            undefined,
          method: isAsyncForm ? "PUT" : "POST",
          csrf,
          isMockClient,
          backgroundUploadURLs,
          backgroundUploadEncryptKey,
          formData,
        },
      ],
      [I18nContext.Provider, { value: i18n.strings }],
      [ServiceProviderContext.Provider, { value: getServiceProvider() }],
      [AssetContext.Provider, { value: assets }],
    ]}
  >
    <DocumentCapture isAsyncForm={isAsyncForm} onStepChange={keepAlive} />
  </ComposeComponents>,
  appRoot
);

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.

I agree -- a little diff noise here and there seems easier than trying to mentally unpack the loop

credentials={getMetaContent('acuant-sdk-initialization-creds')}
endpoint={getMetaContent('acuant-sdk-initialization-endpoint')}
>
<UploadContextProvider
endpoint={/** @type {string} */ (appRoot.getAttribute('data-endpoint'))}
statusEndpoint={/** @type {string} */ (appRoot.getAttribute('data-status-endpoint'))}
statusPollInterval={
Number(appRoot.getAttribute('data-status-poll-interval-ms')) || undefined
}
method={isAsyncForm ? 'PUT' : 'POST'}
csrf={csrf}
isMockClient={isMockClient}
backgroundUploadURLs={backgroundUploadURLs}
backgroundUploadEncryptKey={backgroundUploadEncryptKey}
formData={formData}
<AnalyticsContext.Provider value={{ addPageAction, noticeError }}>
<AcuantContextProvider
credentials={getMetaContent('acuant-sdk-initialization-creds')}
endpoint={getMetaContent('acuant-sdk-initialization-endpoint')}
>
<I18nContext.Provider value={i18n.strings}>
<ServiceProviderContext.Provider value={getServiceProvider()}>
<AnalyticsContext.Provider value={{ addPageAction, noticeError }}>
<UploadContextProvider
endpoint={/** @type {string} */ (appRoot.getAttribute('data-endpoint'))}
statusEndpoint={/** @type {string} */ (appRoot.getAttribute('data-status-endpoint'))}
statusPollInterval={
Number(appRoot.getAttribute('data-status-poll-interval-ms')) || undefined
}
method={isAsyncForm ? 'PUT' : 'POST'}
csrf={csrf}
isMockClient={isMockClient}
backgroundUploadURLs={backgroundUploadURLs}
backgroundUploadEncryptKey={backgroundUploadEncryptKey}
formData={formData}
>
<I18nContext.Provider value={i18n.strings}>
<ServiceProviderContext.Provider value={getServiceProvider()}>
<AssetContext.Provider value={assets}>
<DocumentCapture isAsyncForm={isAsyncForm} onStepChange={keepAlive} />
</AssetContext.Provider>
</AnalyticsContext.Provider>
</ServiceProviderContext.Provider>
</I18nContext.Provider>
</UploadContextProvider>
</AcuantContextProvider>
</ServiceProviderContext.Provider>
</I18nContext.Provider>
</UploadContextProvider>
</AcuantContextProvider>
</AnalyticsContext.Provider>
</DeviceContext.Provider>,
appRoot,
);
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"@testing-library/user-event": "^12.6.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"@types/sinon": "^9.0.11",
"chai": "^4.2.0",
"dirty-chai": "^2.0.1",
"eslint": "^7.16.0",
Expand Down
128 changes: 85 additions & 43 deletions spec/javascripts/packages/document-capture/context/acuant-spec.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sinon from 'sinon';
import { useContext } from 'react';
import { renderHook } from '@testing-library/react-hooks';
import { DeviceContext } from '@18f/identity-document-capture';
import { DeviceContext, AnalyticsContext } from '@18f/identity-document-capture';
import AcuantContext, {
Provider as AcuantContextProvider,
} from '@18f/identity-document-capture/context/acuant';
Expand Down Expand Up @@ -92,50 +93,97 @@ describe('document-capture/context/acuant', () => {
});
});

it('provides ready context when successfully loaded', () => {
const { result } = renderHook(() => useContext(AcuantContext), {
wrapper: ({ children }) => (
<DeviceContext.Provider value={{ isMobile: true }}>
<AcuantContextProvider sdkSrc="about:blank">{children}</AcuantContextProvider>
</DeviceContext.Provider>
),
context('successful initialization', () => {
let result;
let addPageAction;

beforeEach(() => {
addPageAction = sinon.spy();
({ result } = renderHook(() => useContext(AcuantContext), {
wrapper: ({ children }) => (
<AnalyticsContext.Provider value={{ addPageAction }}>
<DeviceContext.Provider value={{ isMobile: true }}>
<AcuantContextProvider sdkSrc="about:blank">{children}</AcuantContextProvider>
</DeviceContext.Provider>
</AnalyticsContext.Provider>
),
}));

window.AcuantJavascriptWebSdk = {
initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(),
};
window.AcuantCamera = { isCameraSupported: true };
window.onAcuantSdkLoaded();
});

window.AcuantJavascriptWebSdk = {
initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(),
};
window.AcuantCamera = { isCameraSupported: true };
window.onAcuantSdkLoaded();
it('provides ready context', () => {
expect(result.current).to.eql({
isReady: true,
isAcuantLoaded: true,
isError: false,
isCameraSupported: true,
credentials: null,
endpoint: null,
});
});

expect(result.current).to.eql({
isReady: true,
isAcuantLoaded: true,
isError: false,
isCameraSupported: true,
credentials: null,
endpoint: null,
it('logs', () => {
expect(addPageAction).to.have.been.calledWith({
label: 'IdV: Acuant SDK loaded',
payload: {
success: true,
},
});
});
});

it('has camera availability at time of ready', () => {
const { result } = renderHook(() => useContext(AcuantContext), {
wrapper: ({ children }) => (
<DeviceContext.Provider value={{ isMobile: true }}>
<AcuantContextProvider sdkSrc="about:blank">{children}</AcuantContextProvider>
</DeviceContext.Provider>
),
context('failed initialization', () => {
let result;
let addPageAction;

beforeEach(() => {
addPageAction = sinon.spy();
({ result } = renderHook(() => useContext(AcuantContext), {
wrapper: ({ children }) => (
<AnalyticsContext.Provider value={{ addPageAction }}>
<DeviceContext.Provider value={{ isMobile: true }}>
<AcuantContextProvider sdkSrc="about:blank">{children}</AcuantContextProvider>
</DeviceContext.Provider>
</AnalyticsContext.Provider>
),
}));

window.AcuantJavascriptWebSdk = {
initialize: (_credentials, _endpoint, { onFail }) =>
onFail(401, 'Server returned a 401 (missing credentials).'),
};
window.onAcuantSdkLoaded();
});

window.AcuantJavascriptWebSdk = {
initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(),
};
window.AcuantCamera = { isCameraSupported: true };
window.onAcuantSdkLoaded();
it('provides error context', () => {
expect(result.current).to.eql({
isReady: false,
isAcuantLoaded: false,
isError: true,
isCameraSupported: null,
credentials: null,
endpoint: null,
});
});

expect(result.current.isCameraSupported).to.be.true();
it('logs', () => {
expect(addPageAction).to.have.been.calledWith({
label: 'IdV: Acuant SDK loaded',
payload: {
success: false,
code: sinon.match.number,
description: sinon.match.string,
},
});
});
});

it('provides error context when failed to loaded', () => {
it('has camera availability at time of ready', () => {
const { result } = renderHook(() => useContext(AcuantContext), {
wrapper: ({ children }) => (
<DeviceContext.Provider value={{ isMobile: true }}>
Expand All @@ -145,18 +193,12 @@ describe('document-capture/context/acuant', () => {
});

window.AcuantJavascriptWebSdk = {
initialize: (_credentials, _endpoint, { onFail }) => onFail(),
initialize: (_credentials, _endpoint, { onSuccess }) => onSuccess(),
};
window.AcuantCamera = { isCameraSupported: true };
window.onAcuantSdkLoaded();

expect(result.current).to.eql({
isReady: false,
isAcuantLoaded: false,
isError: true,
isCameraSupported: null,
credentials: null,
endpoint: null,
});
expect(result.current.isCameraSupported).to.be.true();
});

it('cleans up after itself on unmount', () => {
Expand Down
12 changes: 12 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1222,6 +1222,18 @@
"@types/prop-types" "*"
csstype "^3.0.2"

"@types/sinon@^9.0.11":
version "9.0.11"
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-9.0.11.tgz#7af202dda5253a847b511c929d8b6dda170562eb"
integrity sha512-PwP4UY33SeeVKodNE37ZlOsR9cReypbMJOhZ7BVE0lB+Hix3efCOxiJWiE5Ia+yL9Cn2Ch72EjFTRze8RZsNtg==
dependencies:
"@types/sinonjs__fake-timers" "*"

"@types/sinonjs__fake-timers@*":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-6.0.2.tgz#3a84cf5ec3249439015e14049bd3161419bf9eae"
integrity sha512-dIPoZ3g5gcx9zZEszaxLSVTvMReD3xxyyDnQUjA6IYDG9Ba2AV0otMPs+77sG9ojB4Qr2N2Vk5RnKeuA0X/0bg==

"@types/testing-library__react-hooks@^3.4.0":
version "3.4.1"
resolved "https://registry.yarnpkg.com/@types/testing-library__react-hooks/-/testing-library__react-hooks-3.4.1.tgz#b8d7311c6c1f7db3103e94095fe901f8fef6e433"
Expand Down