Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic FormSteps flow for IdV React implementation #6195

Merged
merged 12 commits into from
Apr 14, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ export { default as Alert } from './alert';
export { default as Button } from './button';
export { default as BlockLink } from './block-link';
export { default as Icon } from './icon';
export { default as PageHeading } from './page-heading';
export { default as SpinnerDots } from './spinner-dots';
export { default as TroubleshootingOptions } from './troubleshooting-options';
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createRef } from 'react';
import PageHeading from '@18f/identity-document-capture/components/page-heading';
import { render } from '../../../support/document-capture';
import { render } from '@testing-library/react';
import PageHeading from './page-heading';

describe('document-capture/components/page-heading', () => {
it('renders as h1', () => {
Expand All @@ -20,10 +20,10 @@ describe('document-capture/components/page-heading', () => {
});

it('forwards ref', () => {
const ref = createRef();
const ref = createRef<HTMLHeadingElement>();
render(<PageHeading ref={ref} />);

expect(ref.current.nodeName).to.equal('H1');
expect(ref.current!.nodeName).to.equal('H1');
});

it('forwards additional props', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { forwardRef } from 'react';
import type { ReactNode } from 'react';

/**
* @typedef PageHeadingProps
*
* @prop {import('react').ReactNode} children Child elements.
*/
interface PageHeadingProps extends Record<string, any> {
className?: string;

/**
* @param {PageHeadingProps & Record<string,any>} props Props object.
*/
function PageHeading({ children, className, ...props }, ref) {
children?: ReactNode;
}

function PageHeading({ children, className, ...props }: PageHeadingProps, ref) {
const classes = ['page-heading', className].filter(Boolean).join(' ');

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useContext } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import { FormStepsContinueButton } from '@18f/identity-form-steps';
import { PageHeading } from '@18f/identity-components';
import DocumentSideAcuantCapture from './document-side-acuant-capture';
import DeviceContext from '../context/device';
import withBackgroundEncryptedUpload from '../higher-order/with-background-encrypted-upload';
import CaptureTroubleshooting from './capture-troubleshooting';
import DocumentCaptureTroubleshootingOptions from './document-capture-troubleshooting-options';
import PageHeading from './page-heading';
import StartOverOrCancel from './start-over-or-cancel';

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { hasMediaAccess } from '@18f/identity-device';
import { useI18n } from '@18f/identity-react-i18n';
import { useDidUpdateEffect } from '@18f/identity-react-hooks';
import { FormStepsContext, FormStepsContinueButton } from '@18f/identity-form-steps';
import { PageHeading } from '@18f/identity-components';
import DeviceContext from '../context/device';
import DocumentSideAcuantCapture from './document-side-acuant-capture';
import AcuantCapture from './acuant-capture';
import SelfieCapture from './selfie-capture';
import ServiceProviderContext from '../context/service-provider';
import withBackgroundEncryptedUpload from '../higher-order/with-background-encrypted-upload';
import DocumentCaptureTroubleshootingOptions from './document-capture-troubleshooting-options';
import PageHeading from './page-heading';
import StartOverOrCancel from './start-over-or-cancel';
import Warning from './warning';
import AnalyticsContext from '../context/analytics';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { useContext } from 'react';
import { hasMediaAccess } from '@18f/identity-device';
import { useI18n } from '@18f/identity-react-i18n';
import { FormStepsContinueButton } from '@18f/identity-form-steps';
import { PageHeading } from '@18f/identity-components';
import DeviceContext from '../context/device';
import AcuantCapture from './acuant-capture';
import SelfieCapture from './selfie-capture';
import withBackgroundEncryptedUpload from '../higher-order/with-background-encrypted-upload';
import PageHeading from './page-heading';
import StartOverOrCancel from './start-over-or-cancel';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useRef, useEffect } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import { PageHeading } from '@18f/identity-components';
import useAsset from '../hooks/use-asset';
import PageHeading from './page-heading';

/**
* @typedef SubmissionInterstitialProps
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useContext, useEffect } from 'react';
import { useI18n } from '@18f/identity-react-i18n';
import { PageHeading } from '@18f/identity-components';
import AnalyticsContext from '../context/analytics';
import useAsset from '../hooks/use-asset';
import PageHeading from './page-heading';

/** @typedef {import('react').ReactNode} ReactNode */

Expand Down
2 changes: 1 addition & 1 deletion app/javascript/packages/form-steps/form-steps.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { waitFor } from '@testing-library/dom';
import sinon from 'sinon';
import PageHeading from '@18f/identity-document-capture/components/page-heading';
import { PageHeading } from '@18f/identity-components';
import FormSteps, { FormStepComponentProps, getStepIndexByName } from './form-steps';
import FormError from './form-error';
import FormStepsContext from './form-steps-context';
Expand Down
9 changes: 4 additions & 5 deletions app/javascript/packages/react-i18n/index.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { createElement, cloneElement, createContext, useContext, useMemo } from 'react';
import { I18n } from '@18f/identity-i18n';
import { createElement, cloneElement, createContext, useContext } from 'react';
import { i18n } from '@18f/identity-i18n';

/** @typedef {import('react').FC|import('react').ComponentClass} Component */

export const I18nContext = createContext({});
export const I18nContext = createContext(i18n);

I18nContext.displayName = 'I18nContext';

Expand Down Expand Up @@ -53,8 +53,7 @@ export function formatHTML(html, handlers) {
}

export function useI18n() {
const strings = useContext(I18nContext);
const { t } = useMemo(() => new I18n({ strings }), [strings]);
const { t } = useContext(I18nContext);

return { t, formatHTML };
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { useContext } from 'react';
import { render } from '@testing-library/react';
import { renderHook } from '@testing-library/react-hooks';
import { I18n } from '@18f/identity-i18n';
import { I18nContext, useI18n } from './index.js';

describe('I18nContext', () => {
it('defaults to empty object', () => {
it('defaults to an instance of I18n', () => {
const { result } = renderHook(() => useContext(I18nContext));

expect(result.current).to.deep.equal({});
expect(result.current).to.be.instanceof(I18n);
});
});

Expand Down Expand Up @@ -90,7 +91,9 @@ describe('useI18n', () => {
it('returns localized key value', () => {
const { result } = renderHook(() => useI18n(), {
wrapper: ({ children }) => (
<I18nContext.Provider value={{ sample: 'translation' }}>{children}</I18nContext.Provider>
<I18nContext.Provider value={new I18n({ strings: { sample: 'translation' } })}>
{children}
</I18nContext.Provider>
),
});

Expand Down
5 changes: 4 additions & 1 deletion app/javascript/packages/verify-flow/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { FormSteps } from '@18f/identity-form-steps';
import { STEPS } from './steps';

export function VerifyFlow() {
return <div>Verify</div>;
return <FormSteps steps={STEPS} />;
}
9 changes: 9 additions & 0 deletions app/javascript/packages/verify-flow/steps/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { FormStep } from '@18f/identity-form-steps';
import PersonalKeyStep from './personal-key/personal-key-step';

export const STEPS: FormStep[] = [
{
name: 'personal-key',
form: PersonalKeyStep,
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { PageHeading, Button } from '@18f/identity-components';
import { t } from '@18f/identity-i18n';
import { FormStepsContinueButton } from '@18f/identity-form-steps';

function PersonalKeyStep() {
return (
<>
<PageHeading>{t('headings.personal_key')}</PageHeading>
<p>{t('instructions.personal_key.info')}</p>
<div className="full-width-box margin-y-5" />
<Button isOutline className="margin-right-2 margin-bottom-2 tablet:margin-bottom-0">
{t('forms.backup_code.download')}
</Button>
<Button isOutline className="margin-right-2 margin-bottom-2 tablet:margin-bottom-0">
{t('users.personal_key.print')}
</Button>
<Button isOutline className="margin-bottom-2 tablet:margin-bottom-0">
{t('links.copy')}
</Button>
<div className="margin-y-5 clearfix">
<p className="margin-bottom-0">
<strong>{t('instructions.personal_key.email_title')}</strong>
</p>
<p>{t('instructions.personal_key.email_body')}</p>
</div>
<FormStepsContinueButton />
</>
);
}

export default PersonalKeyStep;
3 changes: 0 additions & 3 deletions app/javascript/packs/document-capture.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ import {
FailedCaptureAttemptsContextProvider,
HelpCenterContextProvider,
} from '@18f/identity-document-capture';
import { i18n } from '@18f/identity-i18n';
import { isCameraCapableMobile } from '@18f/identity-device';
import { trackEvent } from '@18f/identity-analytics';
import { I18nContext } from '@18f/identity-react-i18n';

/** @typedef {import('@18f/identity-document-capture').FlowPath} FlowPath */
/** @typedef {import('@18f/identity-i18n').I18n} I18n */
Expand Down Expand Up @@ -199,7 +197,6 @@ const noticeError = (error) =>
cancelURL,
},
],
[I18nContext.Provider, { value: i18n.strings }],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason we can remove this here is because we switched to globalThis._locale_data right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason we can remove this here is because we switched to globalThis._locale_data right?

Yeah, indirectly that's what the previous logic here was doing anyways, since the default i18n instance from @18f/identity-i18n is also initialized with globalThis._locale_data.

const i18n = new I18n({ strings: globalThis._locale_data });

Now that I'm thinking about it, I wonder if there's a better way for the React context to reference that default i18n, rather than creating a new instance 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I'm thinking about it, I wonder if there's a better way for the React context to reference that default i18n, rather than creating a new instance 🤔

Updated in 0997d97. The value of an I18nContext is pretty low at this point since we have the shared instance to use now. Technically it could allow to use different locale data specific contexts, though I can't imagine we'd ever want to do that.

Might consider a future refactor:

import { t } from '@18f/identity-i18n';
import { formatHTML } from '@18f/identity-react-i18n';

Or maybe even merge the packages.

import { t, formatHTML } from '@18f/identity-i18n';

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically it could allow to use different locale data specific contexts, though I can't imagine we'd ever want to do that.

I guess there's one use-case after all 😅 77952d5b6

[ServiceProviderContextProvider, { value: getServiceProvider() }],
[AssetContext.Provider, { value: assets }],
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import AcuantCapture, {
import { AcuantContextProvider, AnalyticsContext } from '@18f/identity-document-capture';
import DeviceContext from '@18f/identity-document-capture/context/device';
import { I18nContext } from '@18f/identity-react-i18n';
import { I18n } from '@18f/identity-i18n';
import { render, useAcuant } from '../../../support/document-capture';
import { getFixtureFile } from '../../../support/file';

Expand Down Expand Up @@ -768,7 +769,13 @@ describe('document-capture/components/acuant-capture', () => {
it('triggers forced upload', () => {
const { getByText } = render(
<I18nContext.Provider
value={{ 'doc_auth.buttons.take_or_upload_picture': '<lg-upload>Upload</lg-upload>' }}
value={
new I18n({
strings: {
'doc_auth.buttons.take_or_upload_picture': '<lg-upload>Upload</lg-upload>',
},
})
}
>
<DeviceContext.Provider value={{ isMobile: true }}>
<AcuantContextProvider sdkSrc="about:blank" cameraSrc="about:blank">
Expand All @@ -790,7 +797,13 @@ describe('document-capture/components/acuant-capture', () => {
it('triggers forced upload with `capture` value', () => {
const { getByText, getByLabelText } = render(
<I18nContext.Provider
value={{ 'doc_auth.buttons.take_or_upload_picture': '<lg-upload>Upload</lg-upload>' }}
value={
new I18n({
strings: {
'doc_auth.buttons.take_or_upload_picture': '<lg-upload>Upload</lg-upload>',
},
})
}
>
<DeviceContext.Provider value={{ isMobile: true }}>
<AcuantContextProvider sdkSrc="about:blank" cameraSrc="about:blank">
Expand All @@ -815,7 +828,13 @@ describe('document-capture/components/acuant-capture', () => {
it('optionally disallows upload', () => {
const { getByText, getByLabelText } = render(
<I18nContext.Provider
value={{ 'doc_auth.buttons.take_or_upload_picture': '<lg-upload>Upload</lg-upload>' }}
value={
new I18n({
strings: {
'doc_auth.buttons.take_or_upload_picture': '<lg-upload>Upload</lg-upload>',
},
})
}
>
<DeviceContext.Provider value={{ isMobile: true }}>
<AcuantContextProvider sdkSrc="about:blank" cameraSrc="about:blank">
Expand Down Expand Up @@ -1000,7 +1019,11 @@ describe('document-capture/components/acuant-capture', () => {
const addPageAction = sinon.stub();
const { getByText, getByLabelText } = render(
<I18nContext.Provider
value={{ 'doc_auth.buttons.take_or_upload_picture': '<lg-upload>Upload</lg-upload>' }}
value={
new I18n({
strings: { 'doc_auth.buttons.take_or_upload_picture': '<lg-upload>Upload</lg-upload>' },
})
}
>
<DeviceContext.Provider value={{ isMobile: true }}>
<AnalyticsContext.Provider value={{ addPageAction }}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import sinon from 'sinon';
import userEvent from '@testing-library/user-event';
import { cleanup } from '@testing-library/react';
import { I18nContext } from '@18f/identity-react-i18n';
import { I18n } from '@18f/identity-i18n';
import SelfieCapture from '@18f/identity-document-capture/components/selfie-capture';
import { render } from '../../../support/document-capture';
import { useSandbox } from '../../../support/sinon';
Expand All @@ -16,10 +17,14 @@ describe('document-capture/components/selfie-capture', () => {

const wrapper = ({ children }) => (
<I18nContext.Provider
value={{
'doc_auth.instructions.document_capture_selfie_consent_action':
'<lg-underline>Allow access</lg-underline>',
}}
value={
new I18n({
strings: {
'doc_auth.instructions.document_capture_selfie_consent_action':
'<lg-underline>Allow access</lg-underline>',
},
})
}
>
{children}
</I18nContext.Provider>
Expand Down