Skip to content
Closed
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
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,8 @@ normalize_yaml:
i18n-tasks normalize
find ./config/locales -type f | xargs ./scripts/normalize-yaml

check_asset_strings:
find ./app/javascript -name "*.js*" | xargs ./scripts/check-assets

generate_deploy_checklist:
ruby lib/release_management/generate_deploy_checklist.rb
1 change: 1 addition & 0 deletions app/assets/javascripts/application.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
//= require i18n-strings
//= require assets
//= require local-time
13 changes: 13 additions & 0 deletions app/assets/javascripts/assets.js.erb
Original file line number Diff line number Diff line change
@@ -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 %>
15 changes: 14 additions & 1 deletion app/assets/javascripts/i18n-strings.js.erb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,20 @@ window.LoginGov = window.LoginGov || {};
'zxcvbn.feedback.this_is_similar_to_a_commonly_used_password',
'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.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 = {
Expand Down
11 changes: 9 additions & 2 deletions app/javascript/app/components/accordion.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ class Accordion extends Events {
}

setup() {
this.bindEvents();
this.onInitialize();
if (!this.isInitialized()) {
this.bindEvents();
this.onInitialize();
}
}

bindEvents() {
Expand All @@ -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() {
Expand Down
80 changes: 80 additions & 0 deletions app/javascript/app/document-capture/components/accordion.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<div ref={elementRef} className="accordion mb4 col-12 fs-16p">
<div aria-describedby={contentId} className="accordion-header">
<div
aria-controls={contentId}
aria-expanded="false"
role="button"
tabIndex={0}
className="accordion-header-controls py1 px2 mt-tiny mb-tiny"
>
<span className="mb0 mr2">{title}</span>
<Image
assetPath="plus.svg"
alt={t('image_description.accordian_plus_buttom')}
width={16}
className="plus-icon display-none"
/>
<Image
assetPath="minus.svg"
alt={t('image_description.accordian_minus_buttom')}
width={16}
className="minus-icon display-none"
/>
</div>
</div>
<div
id={contentId}
className="accordion-content clearfix pt1"
role="region"
aria-hidden="true"
>
<div className="px2">{children}</div>
<div
className="py1 accordion-footer"
aria-controls={contentId}
role="button"
tabIndex={0}
>
<div className="pb-tiny pt-tiny">
<Image
assetPath="up-carat-thin.svg"
alt=""
width={14}
className="mr1"
/>
{t('users.personal_key.close')}
</div>
</div>
</div>
</div>
);
}

Accordion.instances = 0;

Accordion.propTypes = {
title: PropTypes.node.isRequired,
children: PropTypes.node.isRequired,
};

export default Accordion;
Original file line number Diff line number Diff line change
@@ -1,9 +1,26 @@
import React from 'react';
import DocumentTips from './document-tips';
import Image from './image';
import useI18n from '../hooks/use-i18n';

function DocumentCapture() {
const t = useI18n();

return t('doc_auth.headings.welcome');
const sample = (
<Image
assetPath="state-id-sample-front.jpg"
alt="Sample front of state issued ID"
width={450}
height={338}
/>
);

return (
<>
<DocumentTips sample={sample} />
{t('doc_auth.headings.welcome')}
</>
);
}

export default DocumentCapture;
41 changes: 41 additions & 0 deletions app/javascript/app/document-capture/components/document-tips.jsx
Original file line number Diff line number Diff line change
@@ -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 = (
<>
<strong>{t('doc_auth.tips.title')}</strong>
{` ${t('doc_auth.tips.title_more')}`}
</>
);

return (
<Accordion title={title}>
<strong>{t('doc_auth.tips.header_text')}</strong>
<ul>
<li>{t('doc_auth.tips.text1')}</li>
<li>{t('doc_auth.tips.text2')}</li>
<li>{t('doc_auth.tips.text3')}</li>
<li>{t('doc_auth.tips.text4')}</li>
<li>{t('doc_auth.tips.text5')}</li>
<li>{t('doc_auth.tips.text6')}</li>
<li>{t('doc_auth.tips.text7')}</li>
</ul>
{!!sample && <div className="center">{sample}</div>}
</Accordion>
);
}

DocumentTips.propTypes = {
sample: PropTypes.node,
};

DocumentTips.defaultProps = {
sample: null,
};

export default DocumentTips;
27 changes: 27 additions & 0 deletions app/javascript/app/document-capture/components/image.jsx
Original file line number Diff line number Diff line change
@@ -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 `<img />` 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 <img src={src} alt={alt} {...imgProps} />;
}

Image.propTypes = {
assetPath: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
};

export default Image;
5 changes: 5 additions & 0 deletions app/javascript/app/document-capture/context/asset.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createContext } from 'react';

const AssetContext = createContext({});

export default AssetContext;
11 changes: 7 additions & 4 deletions app/javascript/packs/document-capture.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
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';

const { I18n: i18n } = window.LoginGov;
const { I18n: i18n, assets } = window.LoginGov;

const appRoot = document.getElementById('document-capture-form');
appRoot.innerHTML = '';
render(
<I18nContext.Provider value={i18n.strings[i18n.currentLocale()]}>
<DocumentCapture />
</I18nContext.Provider>,
<AssetContext.Provider value={assets}>
<I18nContext.Provider value={i18n.strings[i18n.currentLocale()]}>
<DocumentCapture />
</I18nContext.Provider>
</AssetContext.Provider>,
appRoot,
);
2 changes: 2 additions & 0 deletions config/locales/doc_auth/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<b>Don't take the photo on a white surface!</b> &nbsp;&nbsp; See
more tips..."
title_more: See more tips…
titles:
doc_auth: Document Authentication
2 changes: 2 additions & 0 deletions config/locales/doc_auth/es.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<b>¡No tome la foto en una superficie blanca!</b> Ver más..."
title_more: Ver más…
titles:
doc_auth: Autenticación de documentos
2 changes: 2 additions & 0 deletions config/locales/doc_auth/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: "<b>Ne prenez pas la photo sur une surface blanche!</b> Voir plus..."
title_more: Voir plus…
titles:
doc_auth: Authentification de document
39 changes: 39 additions & 0 deletions lib/asset_checker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class AssetChecker
def self.run(argv)
assets_file = 'app/assets/javascripts/assets.js.erb'
translations_file = 'app/assets/javascripts/i18n-strings.js.erb'
@asset_strings = load_included_strings(assets_file)
@translation_strings = load_included_strings(translations_file)
argv.map { |f| check_file(f) }
end

def self.check_file(file)
data = File.open(file).read
missing_translations = find_missing(data, /\Wt\(['"](.*)['"]\)/, @translation_strings)
missing_assets = find_missing(data, /\WassetPath=["'](.*)['"]/, @asset_strings)
if missing_translations.any? || missing_assets.any? # rubocop:disable Style/GuardClause
warn file
missing_translations.each do |t|
warn "Missing translation, #{t}"
end
missing_assets.each do |a|
warn "Missing asset, #{a}"
end
end
end

def self.find_missing(file_data, pattern, source)
missing = []
strings = file_data.scan pattern
strings.each do |s|
missing.push(s) unless source.include? s
end
missing
end

def self.load_included_strings(file)
data = File.open(file).read
key_data = data.split('<% keys = [')[1].split('] %>')[0]
key_data.scan(/['"](.*)['"]/)
end
end
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"jquery": "^3.5.0",
"libphonenumber-js": "^1.7.26",
"normalize.css": "^4.2.0",
"prop-types": "^15.7.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"sinon": "^1.17.7",
Expand All @@ -37,7 +38,7 @@
"@babel/preset-react": "^7.10.4",
"@babel/register": "^7.4.4",
"atob": "^2.1.2",
"babel-eslint": "^7.2.3",
"babel-eslint": "^10.1.0",
"btoa": "^1.2.1",
"chai": "^3.5.0",
"dirty-chai": "^1.2.2",
Expand Down
6 changes: 6 additions & 0 deletions scripts/check-assets
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env ruby
$LOAD_PATH.unshift(File.expand_path('../lib', File.dirname(__FILE__)))
require 'asset_checker'

AssetChecker.run(ARGV)

Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import React from 'react';
import { render } from '@testing-library/react';
import Accordion from '../../../../../app/javascript/app/document-capture/components/accordion';
import { useDOM } from '../../../support/dom';

describe('document-capture/components/accordion', () => {
useDOM();

it('renders with a unique ID', () => {
const { container } = render(
<>
<Accordion title="Title">Content</Accordion>
<Accordion title="Title">Content</Accordion>
</>,
);

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);
});
});
Loading