diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index be141f9340b..d60736db56a 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,2 +1,3 @@
//= require i18n-strings
+//= require assets
//= require local-time
diff --git a/app/assets/javascripts/assets.js.erb b/app/assets/javascripts/assets.js.erb
new file mode 100644
index 00000000000..f4c170af449
--- /dev/null
+++ b/app/assets/javascripts/assets.js.erb
@@ -0,0 +1,13 @@
+window.LoginGov = window.LoginGov || {};
+window.LoginGov.assets = {};
+
+<% keys = [
+ 'state-id-sample-front.jpg',
+ 'plus.svg',
+ 'minus.svg',
+ 'up-carat-thin.svg'
+] %>
+
+<% keys.each do |key| %>
+ window.LoginGov.assets['<%= ActionController::Base.helpers.j key %>'] = '<%= ActionController::Base.helpers.j ActionController::Base.helpers.asset_path key %>';
+<% end %>
diff --git a/app/assets/javascripts/i18n-strings.js.erb b/app/assets/javascripts/i18n-strings.js.erb
index fefe0ae54a8..f19f3e3e55d 100644
--- a/app/assets/javascripts/i18n-strings.js.erb
+++ b/app/assets/javascripts/i18n-strings.js.erb
@@ -47,7 +47,20 @@ window.LoginGov = window.LoginGov || {};
'zxcvbn.feedback.for_a_stronger_password_use_a_few_words_separated_by_spaces_but_avoid_common_phrases',
'zxcvbn.feedback.use_a_longer_keyboard_pattern_with_more_turns',
'doc_auth.buttons.take_picture',
- 'doc_auth.headings.welcome'
+ 'doc_auth.headings.welcome',
+ 'image_description.accordian_plus_buttom',
+ 'image_description.accordian_minus_buttom',
+ 'users.personal_key.close',
+ 'doc_auth.tips.title',
+ 'doc_auth.tips.title_more',
+ 'doc_auth.tips.header_text',
+ 'doc_auth.tips.text1',
+ 'doc_auth.tips.text2',
+ 'doc_auth.tips.text3',
+ 'doc_auth.tips.text4',
+ 'doc_auth.tips.text5',
+ 'doc_auth.tips.text6',
+ 'doc_auth.tips.text7'
] %>
window.LoginGov.I18n = {
diff --git a/app/javascript/app/components/accordion.js b/app/javascript/app/components/accordion.js
index 14f5da6239d..186ddd19985 100644
--- a/app/javascript/app/components/accordion.js
+++ b/app/javascript/app/components/accordion.js
@@ -17,8 +17,10 @@ class Accordion extends Events {
}
setup() {
- this.bindEvents();
- this.onInitialize();
+ if (!this.isInitialized()) {
+ this.bindEvents();
+ this.onInitialize();
+ }
}
bindEvents() {
@@ -41,6 +43,11 @@ class Accordion extends Events {
onInitialize() {
this.setExpanded(false);
this.collapsedIcon.classList.remove('display-none');
+ this.el.setAttribute('data-initialized', '');
+ }
+
+ isInitialized() {
+ return this.el.hasAttribute('data-initialized');
}
handleClick() {
diff --git a/app/javascript/app/document-capture/components/accordion.jsx b/app/javascript/app/document-capture/components/accordion.jsx
new file mode 100644
index 00000000000..76974a1c0ef
--- /dev/null
+++ b/app/javascript/app/document-capture/components/accordion.jsx
@@ -0,0 +1,80 @@
+import React, { useEffect, useRef, useMemo } from 'react';
+import PropTypes from 'prop-types';
+import BaseAccordion from '../../components/accordion';
+import useI18n from '../hooks/use-i18n';
+import Image from './image';
+
+function Accordion({ title, children }) {
+ const elementRef = useRef(null);
+ const instanceId = useMemo(() => {
+ Accordion.instances += 1;
+ return Accordion.instances;
+ }, []);
+ const t = useI18n();
+ useEffect(() => {
+ new BaseAccordion(elementRef.current).setup();
+ }, []);
+
+ const contentId = `accordion-content-${instanceId}`;
+
+ return (
+
+
+
+
{children}
+
+
+
+ {t('users.personal_key.close')}
+
+
+
+
+ );
+}
+
+Accordion.instances = 0;
+
+Accordion.propTypes = {
+ title: PropTypes.node.isRequired,
+ children: PropTypes.node.isRequired,
+};
+
+export default Accordion;
diff --git a/app/javascript/app/document-capture/components/document-capture.jsx b/app/javascript/app/document-capture/components/document-capture.jsx
index 154b3d1d9b0..8da4b8ee12d 100644
--- a/app/javascript/app/document-capture/components/document-capture.jsx
+++ b/app/javascript/app/document-capture/components/document-capture.jsx
@@ -1,13 +1,25 @@
import React from 'react';
import AcuantCapture from './acuant-capture';
+import DocumentTips from './document-tips';
+import Image from './image';
import useI18n from '../hooks/use-i18n';
function DocumentCapture() {
const t = useI18n();
+ const sample = (
+
+ );
+
return (
<>
{t('doc_auth.headings.welcome')}
+
>
);
diff --git a/app/javascript/app/document-capture/components/document-tips.jsx b/app/javascript/app/document-capture/components/document-tips.jsx
new file mode 100644
index 00000000000..07f0fe5c684
--- /dev/null
+++ b/app/javascript/app/document-capture/components/document-tips.jsx
@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Accordion from './accordion';
+import useI18n from '../hooks/use-i18n';
+
+function DocumentTips({ sample }) {
+ const t = useI18n();
+
+ const title = (
+ <>
+ {t('doc_auth.tips.title')}
+ {` ${t('doc_auth.tips.title_more')}`}
+ >
+ );
+
+ return (
+
+ {t('doc_auth.tips.header_text')}
+
+ - {t('doc_auth.tips.text1')}
+ - {t('doc_auth.tips.text2')}
+ - {t('doc_auth.tips.text3')}
+ - {t('doc_auth.tips.text4')}
+ - {t('doc_auth.tips.text5')}
+ - {t('doc_auth.tips.text6')}
+ - {t('doc_auth.tips.text7')}
+
+ {!!sample && {sample}
}
+
+ );
+}
+
+DocumentTips.propTypes = {
+ sample: PropTypes.node,
+};
+
+DocumentTips.defaultProps = {
+ sample: null,
+};
+
+export default DocumentTips;
diff --git a/app/javascript/app/document-capture/components/image.jsx b/app/javascript/app/document-capture/components/image.jsx
new file mode 100644
index 00000000000..26a73bdba3c
--- /dev/null
+++ b/app/javascript/app/document-capture/components/image.jsx
@@ -0,0 +1,27 @@
+import React, { useContext } from 'react';
+import PropTypes from 'prop-types';
+import AssetContext from '../context/asset';
+
+function Image({ assetPath, alt, ...imgProps }) {
+ const assets = useContext(AssetContext);
+
+ const src = Object.prototype.hasOwnProperty.call(assets, assetPath)
+ ? assets[assetPath]
+ : assetPath;
+
+ // Disable reason: While props spreading can introduce confusion to what is
+ // being passed down, in this case the component is intended to represent a
+ // pass-through to a base `
` element, with handling for asset paths.
+ //
+ // Seee: https://github.com/airbnb/javascript/tree/master/react#props
+
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return
;
+}
+
+Image.propTypes = {
+ assetPath: PropTypes.string.isRequired,
+ alt: PropTypes.string.isRequired,
+};
+
+export default Image;
diff --git a/app/javascript/app/document-capture/context/asset.js b/app/javascript/app/document-capture/context/asset.js
new file mode 100644
index 00000000000..91173cbe6d9
--- /dev/null
+++ b/app/javascript/app/document-capture/context/asset.js
@@ -0,0 +1,5 @@
+import { createContext } from 'react';
+
+const AssetContext = createContext({});
+
+export default AssetContext;
diff --git a/app/javascript/packs/document-capture.jsx b/app/javascript/packs/document-capture.jsx
index 4ee038f0a6a..ef61ee13f84 100644
--- a/app/javascript/packs/document-capture.jsx
+++ b/app/javascript/packs/document-capture.jsx
@@ -1,10 +1,11 @@
import React from 'react';
import { render } from 'react-dom';
import DocumentCapture from '../app/document-capture/components/document-capture';
+import AssetContext from '../app/document-capture/context/asset';
import I18nContext from '../app/document-capture/context/i18n';
import { Provider as AcuantProvider } from '../app/document-capture/context/acuant';
-const { I18n: i18n } = window.LoginGov;
+const { I18n: i18n, assets } = window.LoginGov;
function getMetaContent(name) {
return document.querySelector(`meta[name="${name}"]`)?.content ?? null;
@@ -18,7 +19,9 @@ render(
endpoint={getMetaContent('acuant-sdk-initialization-endpoint')}
>
-
+
+
+
,
appRoot,
diff --git a/config/locales/doc_auth/en.yml b/config/locales/doc_auth/en.yml
index 29f25e08c0a..8682fd90f0e 100644
--- a/config/locales/doc_auth/en.yml
+++ b/config/locales/doc_auth/en.yml
@@ -92,7 +92,9 @@ en:
information.
text7: Use a high-resolution camera. A good mobile phone or tablet camera will
work.
+ title: Don't take the photo on a white surface!
title_html: "Don't take the photo on a white surface! See
more tips..."
+ title_more: See more tips…
titles:
doc_auth: Document Authentication
diff --git a/config/locales/doc_auth/es.yml b/config/locales/doc_auth/es.yml
index fa31b60c9bf..8f4b8363858 100644
--- a/config/locales/doc_auth/es.yml
+++ b/config/locales/doc_auth/es.yml
@@ -99,6 +99,8 @@ es:
la información.
text7: Utilice una cámara de alta resolución. La cámara de un buen teléfono
móvil o tableta funcionará.
+ title: "¡No tome la foto en una superficie blanca!"
title_html: "¡No tome la foto en una superficie blanca! Ver más..."
+ title_more: Ver más…
titles:
doc_auth: Autenticación de documentos
diff --git a/config/locales/doc_auth/fr.yml b/config/locales/doc_auth/fr.yml
index f4c68e77c47..5f5e92e403b 100644
--- a/config/locales/doc_auth/fr.yml
+++ b/config/locales/doc_auth/fr.yml
@@ -107,6 +107,8 @@ fr:
de lire toutes les informations.
text7: Utilisez une caméra haute résolution. La caméra d'un bon téléphone mobile
ou d'une tablette fonctionnera.
+ title: Ne prenez pas la photo sur une surface blanche!
title_html: "Ne prenez pas la photo sur une surface blanche! Voir plus..."
+ title_more: Voir plus…
titles:
doc_auth: Authentification de document
diff --git a/spec/javascripts/app/document-capture/components/accordion-spec.jsx b/spec/javascripts/app/document-capture/components/accordion-spec.jsx
new file mode 100644
index 00000000000..e184731e8cd
--- /dev/null
+++ b/spec/javascripts/app/document-capture/components/accordion-spec.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import Accordion from '../../../../../app/javascript/app/document-capture/components/accordion';
+
+describe('document-capture/components/accordion', () => {
+ it('renders with a unique ID', () => {
+ const { container } = render(
+ <>
+ Content
+ Content
+ >,
+ );
+
+ const contents = container.querySelectorAll('[id^="accordion-content-"]');
+
+ expect(contents).to.have.lengthOf(2);
+ expect(contents[0].id).to.be.ok();
+ expect(contents[1].id).to.be.ok();
+ expect(contents[0].id).not.to.equal(contents[1].id);
+ });
+});
diff --git a/spec/javascripts/app/document-capture/components/image-spec.jsx b/spec/javascripts/app/document-capture/components/image-spec.jsx
new file mode 100644
index 00000000000..79251481273
--- /dev/null
+++ b/spec/javascripts/app/document-capture/components/image-spec.jsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import Image from '../../../../../app/javascript/app/document-capture/components/image';
+import AssetContext from '../../../../../app/javascript/app/document-capture/context/asset';
+
+describe('document-capture/components/image', () => {
+ it('renders the given assetPath as src if the asset is not known', () => {
+ const { getByAltText } = render(
+ ,
+ );
+
+ const img = getByAltText('unknown');
+
+ expect(img.src).to.equal('unknown.png');
+ });
+
+ it('renders an img at mapped src if known by context', () => {
+ const { getByAltText } = render(
+
+
+ ,
+ );
+
+ const img = getByAltText('icon');
+
+ expect(img.src).to.equal('icon-12345.png');
+ });
+
+ it('renders with given props', () => {
+ const { getByAltText } = render(
+ ,
+ );
+
+ const img = getByAltText('icon');
+
+ expect(img.width).to.equal(50);
+ });
+});
diff --git a/spec/javascripts/app/document-capture/context/asset-spec.jsx b/spec/javascripts/app/document-capture/context/asset-spec.jsx
new file mode 100644
index 00000000000..5986fa6ca69
--- /dev/null
+++ b/spec/javascripts/app/document-capture/context/asset-spec.jsx
@@ -0,0 +1,13 @@
+import React, { useContext } from 'react';
+import { render } from '@testing-library/react';
+import AssetContext from '../../../../../app/javascript/app/document-capture/context/asset';
+
+describe('document-capture/context/asset', () => {
+ const ContextValue = () => JSON.stringify(useContext(AssetContext));
+
+ it('defaults to empty object', () => {
+ const { container } = render();
+
+ expect(container.textContent).to.equal('{}');
+ });
+});