diff --git a/app/javascript/packages/components/accordion.tsx b/app/javascript/packages/components/accordion.tsx new file mode 100644 index 00000000000..4625a07dc6e --- /dev/null +++ b/app/javascript/packages/components/accordion.tsx @@ -0,0 +1,36 @@ +import { useRef } from 'react'; +import type { ReactNode } from 'react'; +import { useInstanceId } from '@18f/identity-react-hooks'; + +interface AccordionProps { + header: string; + + children: ReactNode; +} + +function Accordion({ header, children }: AccordionProps) { + const uniqueId = useInstanceId(); + const ref = useRef(null as HTMLDivElement | null); + + return ( +
+
+
+ +
+ +
+
+ ); +} + +export default Accordion; diff --git a/app/javascript/packages/components/index.ts b/app/javascript/packages/components/index.ts index 2fd7807b018..f8d88ccd04e 100644 --- a/app/javascript/packages/components/index.ts +++ b/app/javascript/packages/components/index.ts @@ -1,3 +1,4 @@ +export { default as Accordion } from './accordion'; export { default as Alert } from './alert'; export { default as Button } from './button'; export { default as ButtonTo } from './button-to'; diff --git a/app/javascript/packages/verify-flow/package.json b/app/javascript/packages/verify-flow/package.json index 6686209ad1d..1d4ec406402 100644 --- a/app/javascript/packages/verify-flow/package.json +++ b/app/javascript/packages/verify-flow/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "private": true, "dependencies": { - "react": "^17.0.2", - "cleave.js": "^1.6.0" + "cleave.js": "^1.6.0", + "libphonenumber-js": "^1.9.53", + "react": "^17.0.2" } } diff --git a/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.spec.tsx b/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.spec.tsx new file mode 100644 index 00000000000..7dcf48f6514 --- /dev/null +++ b/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.spec.tsx @@ -0,0 +1,67 @@ +import sinon from 'sinon'; +import * as analytics from '@18f/identity-analytics'; +import { render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { t } from '@18f/identity-i18n'; +import { accordion } from 'identity-style-guide'; +import PasswordConfirmStep from './password-confirm-step'; + +describe('PasswordConfirmStep', () => { + before(() => { + accordion.on(); + }); + + after(() => { + accordion.off(); + }); + + const sandbox = sinon.createSandbox(); + const DEFAULT_PROPS = { + onChange() {}, + onError() {}, + errors: [], + toPreviousStep() {}, + registerField: () => () => {}, + unknownFieldErrors: [], + value: {}, + }; + + beforeEach(() => { + sandbox.spy(analytics, 'trackEvent'); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('has a collapsed accordion by default', () => { + const { getByText } = render(); + + const button = getByText(t('idv.messages.review.intro')); + expect(button.getAttribute('aria-expanded')).to.eq('false'); + }); + + it('expands accordion when the accordion is clicked on', async () => { + const toPreviousStep = sinon.spy(); + const { getByText } = render( + , + ); + + const button = getByText(t('idv.messages.review.intro')); + await userEvent.click(button); + expect(button.getAttribute('aria-expanded')).to.eq('true'); + }); + + it('displays user information when the accordion is clicked on', async () => { + const toPreviousStep = sinon.spy(); + const { getByText } = render( + , + ); + + const button = getByText(t('idv.messages.review.intro')); + await userEvent.click(button); + + expect(getByText('idv.review.full_name')).to.exist(); + expect(getByText('idv.review.mailing_address')).to.exist(); + }); +}); diff --git a/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.tsx b/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.tsx index d67c525abaa..ef09ef934b9 100644 --- a/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.tsx +++ b/app/javascript/packages/verify-flow/steps/password-confirm/password-confirm-step.tsx @@ -1,14 +1,17 @@ -import type { ChangeEvent } from 'react'; -import { PasswordToggle } from '@18f/identity-password-toggle'; +import { PageHeading, Accordion, Alert } from '@18f/identity-components'; +import { t } from '@18f/identity-i18n'; import { FormStepsButton } from '@18f/identity-form-steps'; -import { Alert } from '@18f/identity-components'; +import { PasswordToggle } from '@18f/identity-password-toggle'; import type { FormStepComponentProps } from '@18f/identity-form-steps'; +import type { ChangeEvent } from 'react'; +import { getConfigValue } from '@18f/identity-config'; +import PersonalInfoSummary from './personal-info-summary'; import StartOverOrCancel from '../../start-over-or-cancel'; -import type { VerifyFlowValues } from '../../verify-flow'; +import type { VerifyFlowValues } from '../..'; -interface PasswordConfirmStepStepProps extends FormStepComponentProps {} +interface PasswordConfirmStepProps extends FormStepComponentProps {} -function PasswordConfirmStep({ errors, registerField, onChange }: PasswordConfirmStepStepProps) { +function PasswordConfirmStep({ errors, registerField, onChange, value }: PasswordConfirmStepProps) { return ( <> {errors.map(({ error }) => ( @@ -16,13 +19,21 @@ function PasswordConfirmStep({ errors, registerField, onChange }: PasswordConfir {error.message} ))} - ) => { - onChange({ password: event.target.value }); - }} - /> + + {t('idv.titles.session.review', { app_name: getConfigValue('appName') })} + +
+ ) => { + onChange({ password: event.target.value }); + }} + /> +
+ + + diff --git a/app/javascript/packages/verify-flow/steps/password-confirm/personal-info-summary.tsx b/app/javascript/packages/verify-flow/steps/password-confirm/personal-info-summary.tsx new file mode 100644 index 00000000000..c31d9bf5966 --- /dev/null +++ b/app/javascript/packages/verify-flow/steps/password-confirm/personal-info-summary.tsx @@ -0,0 +1,42 @@ +import { parse, format } from 'libphonenumber-js'; +import { t } from '@18f/identity-i18n'; + +function PersonalInfoSummary({ pii }) { + const { firstName, lastName, dob, address1, address2, city, state, zipcode, ssn, phone } = pii; + const phoneNumber = parse(`+1${phone}`); + const formatted = format(phoneNumber, 'NATIONAL'); + + function getDateFormat(date) { + date = new Date(date); + const options = { year: 'numeric', month: 'long', day: 'numeric' }; + return date.toLocaleDateString(document.documentElement.lang, options); + } + + return ( +
+
{t('idv.review.full_name')}
+
+ {firstName} {lastName} +
+
{t('idv.review.mailing_address')}
+
+ {address1}
+ {address2 || ''} +
+ {city && state ? `${city}, ${state} ${zipcode}` : ''} +
+
{t('idv.review.dob')}
+
{getDateFormat(dob)}
+
{t('idv.review.ssn')}
+
{ssn}
+ {phone && ( + <> +
{t('idv.messages.phone.phone_of_record')}
+
{formatted}
+ + )} +
+ ); +} + +export default PersonalInfoSummary; diff --git a/yarn.lock b/yarn.lock index d17005dfe14..b2283607699 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4269,10 +4269,10 @@ levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -libphonenumber-js@^1.9.6: - version "1.9.6" - resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.6.tgz#cdab0552a6705c5a5c1d69db15be9562b248591e" - integrity sha512-Ob419L88HxP3iVXPMI1Z/146izHrUPcV70ClnoP9WyNBgdy6mtlkCxx4ewMDmCzsdY5D4diDOUz8kboqLdKBoQ== +libphonenumber-js@^1.9.53, libphonenumber-js@^1.9.6: + version "1.9.53" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.9.53.tgz#f4f3321f8fb0ee62952c2a8df4711236d2626088" + integrity sha512-3cuMrA2CY3TbKVC0wKye5dXYgxmVVi4g13gzotprQSguFHMqf0pIrMM2Z6ZtMsSWqvtIqi5TuQhGjMhxz0O9Mw== lilconfig@^2.0.3: version "2.0.5"