Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,19 @@ import { useContext, useEffect, useRef } from 'react';
import { getAssetPath } from '@18f/identity-assets';
import { useI18n } from '@18f/identity-react-i18n';
import AcuantContext from '../context/acuant';

/**
* Defines a property on the given object, calling the change callback when that property is set to
* a new value.
*
* @param {any} object Object on which to define property.
* @param {string} property Property name to observe.
* @param {(nextValue: any) => void} onChangeCallback Callback to trigger on change.
*/
export function defineObservableProperty(object, property, onChangeCallback) {
let currentValue;

Object.defineProperty(object, property, {
get() {
return currentValue;
},
set(nextValue) {
currentValue = nextValue;
onChangeCallback(nextValue);
},
configurable: true,
});
}

/**
* Resets a property on the given object, applying the originalDescriptor, if provided,
* or deleting the property entirely if not.
*
* @param {any} object Object on which to define property.
* @param {string} property Property name to observe.
* @param {any} originalDescriptor The descriptor to reset the property with.
*/
export function resetObservableProperty(object, property, originalDescriptor) {
if (object === undefined) {
return;
}

if (originalDescriptor !== undefined) {
Object.defineProperty(object, property, originalDescriptor);
} else {
delete object[property];
}
}
import {
defineObservableProperty,
stopObservingProperty,
} from '../higher-order/observable-property';

function AcuantCaptureCanvas() {
const { isReady, acuantCaptureMode, setAcuantCaptureMode } = useContext(AcuantContext);
const { t } = useI18n();
const cameraRef = useRef(/** @type {HTMLDivElement?} */ (null));

useEffect(() => {
let canvas;
let originalDescriptor;

function onAcuantCameraCreated() {
canvas = document.getElementById('acuant-ui-canvas');
if (originalDescriptor === undefined) {
originalDescriptor = Object.getOwnPropertyDescriptor(canvas, 'callback');
}

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) => {
Expand All @@ -71,8 +25,12 @@ function AcuantCaptureCanvas() {

cameraRef.current?.addEventListener('acuantcameracreated', onAcuantCameraCreated);
return () => {
const canvas = document.getElementById('acuant-ui-canvas');
if (canvas) {
stopObservingProperty(canvas, 'callback');
}

cameraRef.current?.removeEventListener('acuantcameracreated', onAcuantCameraCreated);
resetObservableProperty(canvas, 'callback', originalDescriptor);
};
}, []);

Expand Down
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'd expect this folder to contain higher-order React components, which this isn't. I might have suggested this be implemented as a hook instead, like useDefinedProperty.

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.

ok! I can clean it up to be a hook, I was just guessing what to call it

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.

Cleanup PR: #10838

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* 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 });
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,12 @@
import sinon from 'sinon';
import userEvent from '@testing-library/user-event';
import { AcuantContextProvider, DeviceContext } from '@18f/identity-document-capture';
import AcuantCaptureCanvas, {
defineObservableProperty,
} from '@18f/identity-document-capture/components/acuant-capture-canvas';
import AcuantCaptureCanvas from '@18f/identity-document-capture/components/acuant-capture-canvas';
import { render, useAcuant } from '../../../support/document-capture';

describe('document-capture/components/acuant-capture-canvas', () => {
const { initialize } = useAcuant();

describe('defineObservableProperty', () => {
it('behaves like an object', () => {
const object = {};
defineObservableProperty(object, 'key', () => {});
object.key = 'value';

expect(object.key).to.equal('value');
});

it('calls the callback on changes, with the changed value', () => {
const callback = sinon.spy();
const object = {};
defineObservableProperty(object, 'key', callback);
object.key = 'value';

expect(callback).to.have.been.calledOnceWithExactly('value');
});
});

it('renders a "take photo" button', async () => {
const { getByRole, container } = render(
<DeviceContext.Provider value={{ isMobile: true }}>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import sinon from 'sinon';
import {
defineObservableProperty,
stopObservingProperty,
} from '@18f/identity-document-capture/higher-order/observable-property';

describe('document-capture/higher-order/observable-property', () => {
describe('defineObservableProperty', () => {
it('behaves like an object', () => {
const object = {} as { key?: string };
defineObservableProperty(object, 'key', () => {});
object.key = 'value';

expect(object.key).to.equal('value');
});

it('calls the callback on changes, with the changed value', () => {
const callback = sinon.spy();
const object = {} as { key?: string };
defineObservableProperty(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', () => {
const object = {} as { key?: string };
const callback = sinon.spy();
defineObservableProperty(object, 'key', callback);

object.key = 'value';

stopObservingProperty(object, 'key');
expect(object.key).to.equal('value');

object.key = 'second_value';
expect(object.key).to.equal('second_value');

expect(callback).to.have.been.calledOnceWithExactly('value');
});
});
});