diff --git a/Makefile b/Makefile index b779ffaf0de..f1ee0b813e3 100644 --- a/Makefile +++ b/Makefile @@ -239,7 +239,7 @@ normalize_yaml: ## Normalizes YAML files (alphabetizes keys, fixes line length, optimize_svg: ## Optimizes SVG images # Exclusions: # - `login-icon-bimi.svg` is hand-optimized and includes required metadata that would be stripped by SVGO - find app/assets/images public -name '*.svg' -not -name 'login-icon-bimi.svg' | xargs ./node_modules/.bin/svgo + find app/assets/images public -name '*.svg' -not -name 'login-icon-bimi.svg' -not -name 'selfie-capture-accept-help.svg' | xargs ./node_modules/.bin/svgo optimize_assets: optimize_svg ## Optimizes all assets diff --git a/app/assets/images/idv/selfie-capture-accept-help.svg b/app/assets/images/idv/selfie-capture-accept-help.svg new file mode 100644 index 00000000000..4fe255bebae --- /dev/null +++ b/app/assets/images/idv/selfie-capture-accept-help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/idv/selfie-capture-help.svg b/app/assets/images/idv/selfie-capture-help.svg new file mode 100644 index 00000000000..a90ed990f52 --- /dev/null +++ b/app/assets/images/idv/selfie-capture-help.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/packages/document-capture/components/acuant-capture.tsx b/app/javascript/packages/document-capture/components/acuant-capture.tsx index 8e5e0778869..e40bee7fb58 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.tsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.tsx @@ -327,12 +327,11 @@ function AcuantCapture( } = useContext(AcuantContext); const { isMockClient } = useContext(UploadContext); const { trackEvent } = useContext(AnalyticsContext); - const { isSelfieCaptureEnabled } = useContext(SelfieCaptureContext); + const { isSelfieCaptureEnabled, immediatelyBeginCapture } = useContext(SelfieCaptureContext); const fullScreenRef = useRef(null); const inputRef = useRef(null); const isForceUploading = useRef(false); const isSuppressingClickLogging = useRef(false); - const [isCapturingEnvironment, setIsCapturingEnvironment] = useState(false); const [ownErrorMessage, setOwnErrorMessage] = useState(null); const [hasStartedCropping, setHasStartedCropping] = useState(false); useMemo(() => setOwnErrorMessage(null), [value]); @@ -350,6 +349,9 @@ function AcuantCapture( // This hook does that. const isBackOfId = name === 'back'; useLogCameraInfo({ isBackOfId, hasStartedCropping }); + const [isCapturingEnvironment, setIsCapturingEnvironment] = useState( + selfieCapture && immediatelyBeginCapture, + ); const { failedCaptureAttempts, diff --git a/app/javascript/packages/document-capture/components/acuant-selfie-camera.tsx b/app/javascript/packages/document-capture/components/acuant-selfie-camera.tsx index 6063dce58b6..45b668081eb 100644 --- a/app/javascript/packages/document-capture/components/acuant-selfie-camera.tsx +++ b/app/javascript/packages/document-capture/components/acuant-selfie-camera.tsx @@ -157,12 +157,12 @@ function AcuantSelfieCamera({ CAPTURE_ALT: t('doc_auth.info.selfie_capture.action.capture'), }; const cleanupSelfieCamera = () => { - window.AcuantPassiveLiveness.end(); + window.AcuantPassiveLiveness?.end(); setIsActive(false); }; const startSelfieCamera = () => { - window.AcuantPassiveLiveness.start(faceCaptureCallback, faceDetectionStates); + window.AcuantPassiveLiveness?.start(faceCaptureCallback, faceDetectionStates); setIsActive(true); }; diff --git a/app/javascript/packages/document-capture/components/acuant-selfie-instructions.spec.tsx b/app/javascript/packages/document-capture/components/acuant-selfie-instructions.spec.tsx new file mode 100644 index 00000000000..c29f0378e8f --- /dev/null +++ b/app/javascript/packages/document-capture/components/acuant-selfie-instructions.spec.tsx @@ -0,0 +1,29 @@ +import { render } from '@testing-library/react'; +import AcuantSelfieInstructions from './acuant-selfie-instructions'; + +describe('SelfieInstructions', () => { + let getByText; + let queryAllByRole; + + beforeEach(() => { + const renderedComponent = render(); + getByText = renderedComponent.getByText; + queryAllByRole = renderedComponent.queryAllByRole; + }); + + it('renders the header', () => { + expect(getByText('doc_auth.headings.selfie_instructions.howto')).to.exist(); + }); + + it('renders the instruction graphics', () => { + expect(queryAllByRole('img').length).to.equal(2); + }); + + it('renders the first instruction block', () => { + expect(getByText('doc_auth.info.selfie_capture_help_1')).to.exist(); + }); + + it('renders the second instruction block', () => { + expect(getByText('doc_auth.info.selfie_capture_help_2')).to.exist(); + }); +}); diff --git a/app/javascript/packages/document-capture/components/acuant-selfie-instructions.tsx b/app/javascript/packages/document-capture/components/acuant-selfie-instructions.tsx new file mode 100644 index 00000000000..715eb1b666e --- /dev/null +++ b/app/javascript/packages/document-capture/components/acuant-selfie-instructions.tsx @@ -0,0 +1,26 @@ +import { getAssetPath } from '@18f/identity-assets'; +import { t } from '@18f/identity-i18n'; + +export default function AcuantSelfieInstructions() { + return ( + <> + + {t('doc_auth.headings.selfie_instructions.howto')} + + + + {t('doc_auth.info.selfie_capture_help_1')} + + + + {t('doc_auth.info.selfie_capture_help_2')} + + > + ); +} diff --git a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx index e551bab079a..d5cf14d439a 100644 --- a/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx +++ b/app/javascript/packages/document-capture/components/document-capture-review-issues.tsx @@ -82,6 +82,7 @@ function DocumentCaptureReviewIssues({ defaultSideProps={defaultSideProps} selfieValue={value.selfie} isReviewStep + showHelp={false} /> )} diff --git a/app/javascript/packages/document-capture/components/selfie-step.tsx b/app/javascript/packages/document-capture/components/selfie-step.tsx index 20a930e1d96..05521b021b9 100644 --- a/app/javascript/packages/document-capture/components/selfie-step.tsx +++ b/app/javascript/packages/document-capture/components/selfie-step.tsx @@ -1,4 +1,4 @@ -import { useContext } from 'react'; +import { useContext, useState } from 'react'; import { useI18n } from '@18f/identity-react-i18n'; import { FormStepComponentProps, @@ -6,6 +6,9 @@ import { FormStepsContext, } from '@18f/identity-form-steps'; import { Cancel } from '@18f/identity-verify-flow'; +import { SpinnerButton } from '@18f/identity-spinner-button'; +import AcuantSelfieInstructions from './acuant-selfie-instructions'; +import SelfieCaptureContext from '../context/selfie-capture'; import HybridDocCaptureWarning from './hybrid-doc-capture-warning'; import DocumentSideAcuantCapture from './document-side-acuant-capture'; import TipList from './tip-list'; @@ -20,12 +23,15 @@ export function SelfieCaptureStep({ defaultSideProps, selfieValue, isReviewStep, + showHelp, }: { defaultSideProps: DefaultSideProps; selfieValue: ImageValue; isReviewStep: boolean; + showHelp: boolean; }) { const { t } = useI18n(); + return ( <> {t('doc_auth.headings.document_capture_subheader_selfie')} @@ -40,13 +46,17 @@ export function SelfieCaptureStep({ t('doc_auth.tips.document_capture_selfie_text4'), ]} /> - + + {showHelp && } + {!showHelp && ( + + )} > ); } @@ -58,8 +68,29 @@ export default function SelfieStep({ onError = () => {}, registerField = () => undefined, }: FormStepComponentProps) { + const { t } = useI18n(); const { isLastStep } = useContext(FormStepsContext); const { flowPath } = useContext(UploadContext); + const { showHelpInitially } = useContext(SelfieCaptureContext); + const [showHelp, setShowHelp] = useState(showHelpInitially); + + function TakeSelfieButton() { + return ( + + { + setShowHelp(false); + }} + type="button" + isBig + isWide + > + {t('doc_auth.buttons.take_picture')} + + + ); + } const defaultSideProps: DefaultSideProps = { registerField, @@ -74,8 +105,11 @@ export default function SelfieStep({ defaultSideProps={defaultSideProps} selfieValue={value.selfie} isReviewStep={false} + showHelp={showHelp} /> - {isLastStep ? : } + {showHelp && } + {!showHelp && isLastStep && } + {!showHelp && !isLastStep && } > ); diff --git a/app/javascript/packages/document-capture/context/selfie-capture.tsx b/app/javascript/packages/document-capture/context/selfie-capture.tsx index e0dee44e50c..6115ccd7f48 100644 --- a/app/javascript/packages/document-capture/context/selfie-capture.tsx +++ b/app/javascript/packages/document-capture/context/selfie-capture.tsx @@ -9,11 +9,22 @@ interface SelfieCaptureProps { * Specify whether to allow uploads for selfie when in test mode. */ isSelfieDesktopTestMode: boolean; + /** + * Specify whether to show help and an action button before showing + * the capture component. + */ + showHelpInitially: boolean; + /** + * Specify whether we should try to capture using Acuant immediately + */ + immediatelyBeginCapture: boolean; } const SelfieCaptureContext = createContext({ isSelfieCaptureEnabled: false, isSelfieDesktopTestMode: false, + showHelpInitially: true, + immediatelyBeginCapture: false, }); SelfieCaptureContext.displayName = 'SelfieCaptureContext'; diff --git a/app/javascript/packs/document-capture.tsx b/app/javascript/packs/document-capture.tsx index f75d987e8b3..d21223f415b 100644 --- a/app/javascript/packs/document-capture.tsx +++ b/app/javascript/packs/document-capture.tsx @@ -177,6 +177,8 @@ render( value={{ isSelfieCaptureEnabled: getSelfieCaptureEnabled(), isSelfieDesktopTestMode: String(docAuthSelfieDesktopTestMode) === 'true', + showHelpInitially: true, + immediatelyBeginCapture: true, }} > { value={{ isSelfieCaptureEnabled: false, isSelfieDesktopTestMode: false, + showHelpInitially: false, + immediatelyBeginCapture: false, }} > @@ -46,6 +48,8 @@ describe('DocumentSideAcuantCapture', () => { value={{ isSelfieCaptureEnabled: false, isSelfieDesktopTestMode: true, + showHelpInitially: false, + immediatelyBeginCapture: false, }} > @@ -71,6 +75,8 @@ describe('DocumentSideAcuantCapture', () => { value={{ isSelfieCaptureEnabled: false, isSelfieDesktopTestMode: false, + showHelpInitially: false, + immediatelyBeginCapture: false, }} > @@ -92,6 +98,8 @@ describe('DocumentSideAcuantCapture', () => { value={{ isSelfieCaptureEnabled: false, isSelfieDesktopTestMode: true, + showHelpInitially: false, + immediatelyBeginCapture: false, }} > @@ -117,6 +125,8 @@ describe('DocumentSideAcuantCapture', () => { value={{ isSelfieCaptureEnabled: true, isSelfieDesktopTestMode: false, + showHelpInitially: false, + immediatelyBeginCapture: false, }} > @@ -144,6 +154,8 @@ describe('DocumentSideAcuantCapture', () => { value={{ isSelfieCaptureEnabled: true, isSelfieDesktopTestMode: true, + showHelpInitially: false, + immediatelyBeginCapture: false, }} > @@ -179,6 +191,8 @@ describe('DocumentSideAcuantCapture', () => { value={{ isSelfieCaptureEnabled: true, isSelfieDesktopTestMode: true, + showHelpInitially: false, + immediatelyBeginCapture: false, }} > diff --git a/spec/javascript/packages/document-capture/components/documents-step-spec.tsx b/spec/javascript/packages/document-capture/components/documents-step-spec.tsx index a02b1b284ad..80bedd1160f 100644 --- a/spec/javascript/packages/document-capture/components/documents-step-spec.tsx +++ b/spec/javascript/packages/document-capture/components/documents-step-spec.tsx @@ -147,6 +147,8 @@ describe('document-capture/components/documents-step', () => { value={{ isSelfieCaptureEnabled: true, isSelfieDesktopTestMode: false, + showHelpInitially: true, + immediatelyBeginCapture: true, }} > { - it('renders with only selfie input by default', () => { - const { queryByLabelText } = render( - undefined} - errors={[]} - onError={() => undefined} - registerField={() => undefined} - unknownFieldErrors={[]} - toPreviousStep={() => undefined} - />, - ); + let getByLabelText; + let queryByLabelText; - const front = queryByLabelText('doc_auth.headings.document_capture_front'); - const back = queryByLabelText('doc_auth.headings.document_capture_back'); - const selfie = queryByLabelText('doc_auth.headings.document_capture_selfie'); + context('when initially shown', () => { + beforeEach(() => { + ({ queryByLabelText } = render( + undefined} + errors={[]} + onError={() => undefined} + registerField={() => undefined} + unknownFieldErrors={[]} + toPreviousStep={() => undefined} + />, + )); + }); + }); + + context('when show help is turned off ', () => { + beforeEach(() => { + ({ queryByLabelText } = render( + + undefined} + errors={[]} + onError={() => undefined} + registerField={() => undefined} + unknownFieldErrors={[]} + toPreviousStep={() => undefined} + /> + , + , + )); + }); + + it('renders with only selfie input', () => { + const front = queryByLabelText('doc_auth.headings.document_capture_front'); + const back = queryByLabelText('doc_auth.headings.document_capture_back'); + const selfie = queryByLabelText('doc_auth.headings.document_capture_selfie'); - expect(front).to.not.exist(); - expect(back).to.not.exist(); - expect(selfie).to.be.ok(); + expect(front).to.not.exist(); + expect(back).to.not.exist(); + expect(selfie).to.be.ok(); + }); }); it('calls onChange callback with uploaded image', async () => { const onChange = sinon.stub(); - const { getByLabelText } = render( + ({ getByLabelText } = render( - undefined} - registerField={() => undefined} - unknownFieldErrors={[]} - toPreviousStep={() => undefined} - /> + + undefined} + registerField={() => undefined} + unknownFieldErrors={[]} + toPreviousStep={() => undefined} + /> + , , - ); + )); const file = await getFixtureFile('doc_auth_images/id-back.jpg'); await Promise.all([ diff --git a/spec/javascript/packages/document-capture/context/selfie-capture-spec.jsx b/spec/javascript/packages/document-capture/context/selfie-capture-spec.jsx index bdba2f03f83..796377d6da3 100644 --- a/spec/javascript/packages/document-capture/context/selfie-capture-spec.jsx +++ b/spec/javascript/packages/document-capture/context/selfie-capture-spec.jsx @@ -6,7 +6,12 @@ describe('document-capture/context/selfie-capture', () => { it('has expected default properties', () => { const { result } = renderHook(() => useContext(SelfieCaptureContext)); - expect(result.current).to.have.keys(['isSelfieCaptureEnabled', 'isSelfieDesktopTestMode']); + expect(result.current).to.have.keys([ + 'isSelfieCaptureEnabled', + 'isSelfieDesktopTestMode', + 'showHelpInitially', + 'immediatelyBeginCapture', + ]); expect(result.current.isSelfieCaptureEnabled).to.be.a('boolean'); }); }); diff --git a/spec/support/features/document_capture_step_helper.rb b/spec/support/features/document_capture_step_helper.rb index 42a0972a44e..179b333acfb 100644 --- a/spec/support/features/document_capture_step_helper.rb +++ b/spec/support/features/document_capture_step_helper.rb @@ -26,6 +26,7 @@ def attach_liveness_images( ) attach_images(file) click_continue + click_button 'Take photo' if page.has_button? 'Take photo' attach_selfie end