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 78b560e8d92..c6d94ba397c 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx +++ b/app/javascript/packages/document-capture/components/acuant-capture-canvas.jsx @@ -1,38 +1,33 @@ -import { useContext, useEffect, useRef } from 'react'; +import { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { getAssetPath } from '@18f/identity-assets'; import { useI18n } from '@18f/identity-react-i18n'; import AcuantContext from '../context/acuant'; -import { - defineObservableProperty, - stopObservingProperty, -} from '../higher-order/observable-property'; +import { useObservableProperty } from '../hooks/use-observable-property'; function AcuantCaptureCanvas() { const { isReady, acuantCaptureMode, setAcuantCaptureMode } = useContext(AcuantContext); const { t } = useI18n(); const cameraRef = useRef(/** @type {HTMLDivElement?} */ (null)); + const [canvas, setCanvas] = useState(/** @type {HTMLElement? } */ (null)); useEffect(() => { - 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(canvas, 'callback', (callback) => { - setAcuantCaptureMode(callback ? 'TAP' : 'AUTO'); - }); - } - + const onAcuantCameraCreated = () => setCanvas(document.getElementById('acuant-ui-canvas')); cameraRef.current?.addEventListener('acuantcameracreated', onAcuantCameraCreated); - return () => { - const canvas = document.getElementById('acuant-ui-canvas'); - if (canvas) { - stopObservingProperty(canvas, 'callback'); - } - + return () => cameraRef.current?.removeEventListener('acuantcameracreated', onAcuantCameraCreated); - }; - }, []); + }, [cameraRef.current]); + + const onCallback = useCallback( + (callback) => { + setAcuantCaptureMode(callback ? 'TAP' : 'AUTO'); + }, + [setAcuantCaptureMode], + ); + + // 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. + useObservableProperty(canvas, 'callback', onCallback); const clickCanvas = () => document.getElementById('acuant-ui-canvas')?.click(); diff --git a/app/javascript/packages/document-capture/higher-order/observable-property.tsx b/app/javascript/packages/document-capture/higher-order/observable-property.tsx deleted file mode 100644 index c6ed08b349f..00000000000 --- a/app/javascript/packages/document-capture/higher-order/observable-property.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Defines a property on the given object, calling the change callback when that property is set to - * a new value. - * - * @param object Object on which to define property. - * @param property Property name to observe. - * @param onChangeCallback Callback to trigger on change. - */ -export function defineObservableProperty( - object: any, - property: string, - onChangeCallback: (nextValue: any) => void, -) { - let currentValue: any; - - Object.defineProperty(object, property, { - get() { - return currentValue; - }, - set(nextValue) { - currentValue = nextValue; - onChangeCallback(nextValue); - }, - configurable: true, - }); -} - -/** - * Removes an observable property by removing the defined getter/setter methods - * and replaces the value with the most recent value. - * - * @param object Object on which to remove defined property. - * @param property Property name to remove observer for - */ -export function stopObservingProperty(object: any, property: string) { - const currentValue = object[property]; - - Object.defineProperty(object, property, { value: currentValue, writable: true }); -} diff --git a/app/javascript/packages/document-capture/hooks/use-observable-property.tsx b/app/javascript/packages/document-capture/hooks/use-observable-property.tsx new file mode 100644 index 00000000000..9ef6c45d971 --- /dev/null +++ b/app/javascript/packages/document-capture/hooks/use-observable-property.tsx @@ -0,0 +1,43 @@ +import { useEffect } from 'react'; + +/** + * Defines a property on the given object as an effect, + * It will call the change callback when that property is set to + * a new value. + * + * No-ops if object is not present. + * + * @param object Object on which to define property. + * @param property Property name to observe. + * @param onChangeCallback Callback to trigger on change. + */ +export function useObservableProperty( + object: any, + property: string, + onChangeCallback: (nextValue: any) => void, +) { + useEffect(() => { + if (!object) { + return; + } + + let currentValue: any; + + Object.defineProperty(object, property, { + get() { + return currentValue; + }, + set(nextValue) { + currentValue = nextValue; + onChangeCallback(nextValue); + }, + configurable: true, + }); + + return () => { + const value = object[property]; + + Object.defineProperty(object, property, { value, writable: true }); + }; + }, [object, property, onChangeCallback]); +} diff --git a/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx b/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx index 3eaa8b4f8e9..d50fa33d75d 100644 --- a/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx +++ b/spec/javascript/packages/document-capture/components/acuant-capture-canvas-spec.jsx @@ -1,5 +1,6 @@ import sinon from 'sinon'; import userEvent from '@testing-library/user-event'; +import { act } from '@testing-library/react'; import { AcuantContextProvider, DeviceContext } from '@18f/identity-document-capture'; import AcuantCaptureCanvas from '@18f/identity-document-capture/components/acuant-capture-canvas'; import { render, useAcuant } from '../../../support/document-capture'; @@ -16,9 +17,10 @@ describe('document-capture/components/acuant-capture-canvas', () => { , ); - initialize(); - window.AcuantCameraUI.start(); - + act(() => { + initialize(); + window.AcuantCameraUI.start(); + }); const button = getByRole('button', { name: 'doc_auth.buttons.take_picture' }); expect(button.disabled).to.be.true(); diff --git a/spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx b/spec/javascript/packages/document-capture/hooks/use-observable-property-spec.tsx similarity index 56% rename from spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx rename to spec/javascript/packages/document-capture/hooks/use-observable-property-spec.tsx index 0e0f66a56e6..c07f5e19fda 100644 --- a/spec/javascript/packages/document-capture/higher-order/observable-property-spec.tsx +++ b/spec/javascript/packages/document-capture/hooks/use-observable-property-spec.tsx @@ -1,14 +1,14 @@ import sinon from 'sinon'; -import { - defineObservableProperty, - stopObservingProperty, -} from '@18f/identity-document-capture/higher-order/observable-property'; +import { useObservableProperty } from '@18f/identity-document-capture/hooks/use-observable-property'; +import { renderHook } from '@testing-library/react-hooks'; -describe('document-capture/higher-order/observable-property', () => { - describe('defineObservableProperty', () => { +describe('document-capture/hooks/use-observable-property', () => { + describe('useObservableProperty', () => { it('behaves like an object', () => { const object = {} as { key?: string }; - defineObservableProperty(object, 'key', () => {}); + + renderHook(() => useObservableProperty(object, 'key', () => {})); + object.key = 'value'; expect(object.key).to.equal('value'); @@ -17,22 +17,21 @@ describe('document-capture/higher-order/observable-property', () => { it('calls the callback on changes, with the changed value', () => { const callback = sinon.spy(); const object = {} as { key?: string }; - defineObservableProperty(object, 'key', callback); + + renderHook(() => useObservableProperty(object, 'key', callback)); object.key = 'value'; expect(callback).to.have.been.calledOnceWithExactly('value'); }); - }); - describe('stopObservingProperty', () => { - it('removes the defined property and set the last value as a plain value', () => { + it('returns a cleanup function that removes the observer', () => { const object = {} as { key?: string }; const callback = sinon.spy(); - defineObservableProperty(object, 'key', callback); + const { unmount } = renderHook(() => useObservableProperty(object, 'key', callback)); object.key = 'value'; - stopObservingProperty(object, 'key'); + unmount(); expect(object.key).to.equal('value'); object.key = 'second_value';