diff --git a/app/javascript/packages/components/package.json b/app/javascript/packages/components/package.json
index c3d276925b1..400ac567672 100644
--- a/app/javascript/packages/components/package.json
+++ b/app/javascript/packages/components/package.json
@@ -3,6 +3,6 @@
"private": true,
"version": "1.0.0",
"dependencies": {
- "react": "^17.0.1"
+ "react": "^17.0.2"
}
}
diff --git a/app/javascript/packages/compose-components/README.md b/app/javascript/packages/compose-components/README.md
new file mode 100644
index 00000000000..fec7f76731e
--- /dev/null
+++ b/app/javascript/packages/compose-components/README.md
@@ -0,0 +1,17 @@
+# `@18f/identity-compose-components`
+
+A utility function to compose a set of React components and their props to a single component.
+
+Convenient for flattening a deeply-nested arrangement of context providers, for example.
+
+## Example
+
+```jsx
+const App = composeComponents(
+ [FirstContext.Provider, { value: 1 }],
+ [SecondContext.Provider, { value: 2 }],
+ AppRoot,
+);
+
+render(App, document.getElementById('app-root'));
+```
diff --git a/app/javascript/packages/compose-components/index.js b/app/javascript/packages/compose-components/index.js
new file mode 100644
index 00000000000..d152e389d7f
--- /dev/null
+++ b/app/javascript/packages/compose-components/index.js
@@ -0,0 +1,51 @@
+import { createElement } from 'react';
+
+/** @typedef {import('react').ComponentType
} ComponentType @template P */
+
+/**
+ * @typedef {[ComponentType
, P]} NormalizedComponentPair
+ *
+ * @template P
+ */
+
+/**
+ * @typedef {[ComponentType
, P]|[ComponentType
]|ComponentType
} ComponentPair
+ *
+ * @template P
+ */
+
+/**
+ * A utility function to compose a set of React components and their props to a single component.
+ *
+ * Convenient for flattening a deeply-nested arrangement of context providers, for example.
+ *
+ * @example
+ * ```jsx
+ * const App = composeComponents(
+ * [FirstContext.Provider, { value: 1 }],
+ * [SecondContext.Provider, { value: 2 }],
+ * AppRoot,
+ * );
+ *
+ * render(App, document.getElementById('app-root'));
+ * ```
+ *
+ * @param {...ComponentPair<*>} components
+ *
+ * @return {ComponentType<*>}
+ */
+export function composeComponents(...components) {
+ return function ComposedComponent() {
+ /** @type {JSX.Element?} */
+ let element = null;
+ for (let i = components.length - 1; i >= 0; i--) {
+ const componentPair = /** @type {NormalizedComponentPair<*>} */ (Array.isArray(components[i])
+ ? components[i]
+ : [components[i]]);
+ const [ComponentType, props] = componentPair;
+ element = createElement(ComponentType, props, element);
+ }
+
+ return element;
+ };
+}
diff --git a/app/javascript/packages/compose-components/index.spec.jsx b/app/javascript/packages/compose-components/index.spec.jsx
new file mode 100644
index 00000000000..7e1e296e7c0
--- /dev/null
+++ b/app/javascript/packages/compose-components/index.spec.jsx
@@ -0,0 +1,27 @@
+import { createContext, useContext } from 'react';
+import { render } from '@testing-library/react';
+import { composeComponents } from './index.js';
+
+describe('composeComponents', () => {
+ it('composes components', () => {
+ const FirstContext = createContext(null);
+ const SecondContext = createContext(null);
+ const AppRoot = () => (
+ <>
+ {useContext(FirstContext)}
+ {useContext(SecondContext)}
+ >
+ );
+
+ const ComposedComponent = composeComponents(
+ [FirstContext.Provider, { value: 1 }],
+ [SecondContext.Provider, { value: 2 }],
+ [({ children }) => <>{children}3>],
+ AppRoot,
+ );
+
+ const { getByText } = render();
+
+ expect(getByText('123')).to.be.ok();
+ });
+});
diff --git a/app/javascript/packages/compose-components/package.json b/app/javascript/packages/compose-components/package.json
new file mode 100644
index 00000000000..8ec82d11652
--- /dev/null
+++ b/app/javascript/packages/compose-components/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@18f/identity-compose-components",
+ "private": true,
+ "version": "1.0.0",
+ "dependencies": {
+ "react": "^17.0.2"
+ }
+}
diff --git a/app/javascript/packages/document-capture/package.json b/app/javascript/packages/document-capture/package.json
index 7c522b3f53a..393b64ed7ed 100644
--- a/app/javascript/packages/document-capture/package.json
+++ b/app/javascript/packages/document-capture/package.json
@@ -4,7 +4,7 @@
"version": "1.0.0",
"dependencies": {
"focus-trap": "^6.2.3",
- "react": "^17.0.1",
- "react-dom": "^17.0.1"
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2"
}
}
diff --git a/app/javascript/packages/react-i18n/package.json b/app/javascript/packages/react-i18n/package.json
index 0906ab47a9f..75371b668c6 100644
--- a/app/javascript/packages/react-i18n/package.json
+++ b/app/javascript/packages/react-i18n/package.json
@@ -3,6 +3,6 @@
"private": true,
"version": "1.0.0",
"dependencies": {
- "react": "^17.0.1"
+ "react": "^17.0.2"
}
}
diff --git a/app/javascript/packs/document-capture.jsx b/app/javascript/packs/document-capture.jsx
index d2d16028432..31fb9617b91 100644
--- a/app/javascript/packs/document-capture.jsx
+++ b/app/javascript/packs/document-capture.jsx
@@ -1,4 +1,5 @@
import { render } from 'react-dom';
+import { composeComponents } from '@18f/identity-compose-components';
import {
AppContext,
DocumentCapture,
@@ -138,41 +139,39 @@ loadPolyfills(['fetch', 'crypto', 'url']).then(async () => {
appName: /** @type string */ (appRoot.dataset.appName),
};
- render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ,
- appRoot,
+ const App = composeComponents(
+ [AppContext.Provider, { value: appContext }],
+ [DeviceContext.Provider, { value: device }],
+ [AnalyticsContext.Provider, { value: { addPageAction, noticeError } }],
+ [
+ AcuantContextProvider,
+ {
+ credentials: getMetaContent('acuant-sdk-initialization-creds'),
+ endpoint: getMetaContent('acuant-sdk-initialization-endpoint'),
+ glareThreshold,
+ sharpnessThreshold,
+ },
+ ],
+ [
+ UploadContextProvider,
+ {
+ endpoint: String(appRoot.getAttribute('data-endpoint')),
+ statusEndpoint: 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 }],
+ [ServiceProviderContextProvider, { value: getServiceProvider() }],
+ [AssetContext.Provider, { value: assets }],
+ [DocumentCapture, { isAsyncForm, onStepChange: keepAlive }],
);
+
+ render(, appRoot);
});
diff --git a/package.json b/package.json
index 09a647e654e..29095cb14e8 100644
--- a/package.json
+++ b/package.json
@@ -32,8 +32,8 @@
"identity-style-guide": "^6.2.0",
"intl-tel-input": "^17.0.8",
"postcss-clean": "^1.1.0",
- "react": "^17.0.1",
- "react-dom": "^17.0.1",
+ "react": "^17.0.2",
+ "react-dom": "^17.0.2",
"source-map-loader": "^1.1.3",
"zxcvbn": "4.4.2"
},
@@ -57,7 +57,7 @@
"mocha": "^8.2.1",
"mq-polyfill": "^1.1.8",
"prettier": "^2.2.1",
- "react-test-renderer": "^17.0.1",
+ "react-test-renderer": "^17.0.2",
"sinon": "^9.2.2",
"sinon-chai": "^3.5.0",
"svgo": "^1.3.2",
diff --git a/yarn.lock b/yarn.lock
index e2424b4ee88..80049cda8ee 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -7642,19 +7642,19 @@ raw-body@2.4.0:
iconv-lite "0.4.24"
unpipe "1.0.0"
-react-dom@^17.0.1:
- version "17.0.1"
- resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.1.tgz#1de2560474ec9f0e334285662ede52dbc5426fc6"
- integrity sha512-6eV150oJZ9U2t9svnsspTMrWNyHc6chX0KzDeAOXftRa8bNeOKTTfCJ7KorIwenkHd2xqVTBTCZd79yk/lx/Ug==
+react-dom@^17.0.2:
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"
+ integrity sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
- scheduler "^0.20.1"
+ scheduler "^0.20.2"
-"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1:
- version "17.0.1"
- resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
- integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
+"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2:
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
+ integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
react-is@^16.8.1:
version "16.13.1"
@@ -7669,20 +7669,20 @@ react-shallow-renderer@^16.13.1:
object-assign "^4.1.1"
react-is "^16.12.0 || ^17.0.0"
-react-test-renderer@^17.0.1:
- version "17.0.1"
- resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3187e636c3063e6ae498aedf21ecf972721574c7"
- integrity sha512-/dRae3mj6aObwkjCcxZPlxDFh73XZLgvwhhyON2haZGUEhiaY5EjfAdw+d/rQmlcFwdTpMXCSGVk374QbCTlrA==
+react-test-renderer@^17.0.2:
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c"
+ integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==
dependencies:
object-assign "^4.1.1"
- react-is "^17.0.1"
+ react-is "^17.0.2"
react-shallow-renderer "^16.13.1"
- scheduler "^0.20.1"
+ scheduler "^0.20.2"
-react@^17.0.1:
- version "17.0.1"
- resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
- integrity sha512-lG9c9UuMHdcAexXtigOZLX8exLWkW0Ku29qPRU8uhF2R9BN96dLCt0psvzPLlHc5OWkgymP3qwTRgbnw5BKx3w==
+react@^17.0.2:
+ version "17.0.2"
+ resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
+ integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
@@ -8067,10 +8067,10 @@ saxes@^5.0.0:
dependencies:
xmlchars "^2.2.0"
-scheduler@^0.20.1:
- version "0.20.1"
- resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.1.tgz#da0b907e24026b01181ecbc75efdc7f27b5a000c"
- integrity sha512-LKTe+2xNJBNxu/QhHvDR14wUXHRQbVY5ZOYpOGWRzhydZUqrLb2JBvLPY7cAqFmqrWuDED0Mjk7013SZiOz6Bw==
+scheduler@^0.20.2:
+ version "0.20.2"
+ resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.20.2.tgz#4baee39436e34aa93b4874bddcbf0fe8b8b50e91"
+ integrity sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"