-
Notifications
You must be signed in to change notification settings - Fork 166
LG-12307: don't allow upload when selfie capture is enabled #10232
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
Changes from all commits
288cd84
3b37ac9
94a1aaf
53d47b9
aeecf5f
54f48c7
2eca1f8
35ccbe5
51aa7f7
4680c34
f542847
5d94d3e
d81b08c
6b5ff50
52cb866
992ce68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { createContext, useState } from 'react'; | ||
| import { createContext, useContext, useState } from 'react'; | ||
| import type { ReactNode } from 'react'; | ||
| import useCounter from '../hooks/use-counter'; | ||
| import SelfieCaptureContext from './selfie-capture'; | ||
|
|
||
| interface CaptureAttemptMetadata { | ||
| isAssessedAsGlare: boolean; | ||
|
|
@@ -125,6 +126,7 @@ function FailedCaptureAttemptsContextProvider({ | |
| useCounter(); | ||
| const [failedSubmissionAttempts, incrementFailedSubmissionAttempts] = useCounter(); | ||
| const [failedCameraPermissionAttempts, incrementFailedCameraPermissionAttempts] = useCounter(); | ||
| const { isSelfieCaptureEnabled } = useContext(SelfieCaptureContext); | ||
|
|
||
| const [failedSubmissionImageFingerprints, setFailedSubmissionImageFingerprints] = | ||
| useState<UploadedImageFingerprints>(failedFingerprints); | ||
|
|
@@ -143,10 +145,12 @@ function FailedCaptureAttemptsContextProvider({ | |
| incrementFailedCameraPermissionAttempts(); | ||
| } | ||
|
|
||
| const forceNativeCamera = | ||
| const hasExhaustedAttempts = | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice variable naming here. 👍🏻
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you! Credit goes to @aduth for thinking of this name 😁 |
||
| failedCaptureAttempts >= maxCaptureAttemptsBeforeNativeCamera || | ||
| failedSubmissionAttempts >= maxSubmissionAttemptsBeforeNativeCamera; | ||
|
|
||
| const forceNativeCamera = isSelfieCaptureEnabled ? false : hasExhaustedAttempts; | ||
|
|
||
| return ( | ||
| <FailedCaptureAttemptsContext.Provider | ||
| value={{ | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,175 @@ | ||
| import { DeviceContext, SelfieCaptureContext } from '@18f/identity-document-capture'; | ||
| import DocumentSideAcuantCapture from '@18f/identity-document-capture/components/document-side-acuant-capture'; | ||
| import { expect } from 'chai'; | ||
| import { render } from '../../../support/document-capture'; | ||
|
|
||
| describe('DocumentSideAcuantCapture', () => { | ||
| const DEFAULT_PROPS = { | ||
| errors: [], | ||
| registerField: () => undefined, | ||
| value: '', | ||
| onChange: () => undefined, | ||
| onError: () => undefined, | ||
| }; | ||
|
|
||
| context('when selfie is _not_ enabled', () => { | ||
| context('and using mobile', () => { | ||
| context('and doc_auth_selfie_desktop_test_mode is false', () => { | ||
| it('_does_ display a photo upload button', () => { | ||
| const { queryAllByText } = render( | ||
| <DeviceContext.Provider value={{ isMobile: true }}> | ||
| <SelfieCaptureContext.Provider | ||
| value={{ isSelfieCaptureEnabled: false, isSelfieDesktopTestMode: false }} | ||
| > | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="front" /> | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="back" /> | ||
| </SelfieCaptureContext.Provider> | ||
| </DeviceContext.Provider>, | ||
| ); | ||
|
|
||
| const takeOrUploadPictureText = queryAllByText( | ||
| 'doc_auth.buttons.take_or_upload_picture_html', | ||
| ); | ||
| expect(takeOrUploadPictureText).to.have.lengthOf(2); | ||
| }); | ||
| }); | ||
|
|
||
| context('and doc_auth_selfie_desktop_test_mode is true', () => { | ||
| it('_does_ display a photo upload button', () => { | ||
| const { queryAllByText } = render( | ||
| <DeviceContext.Provider value={{ isMobile: true }}> | ||
| <SelfieCaptureContext.Provider | ||
| value={{ isSelfieCaptureEnabled: false, isSelfieDesktopTestMode: true }} | ||
| > | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="front" /> | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="back" /> | ||
| </SelfieCaptureContext.Provider> | ||
| </DeviceContext.Provider>, | ||
| ); | ||
|
|
||
| const takeOrUploadPictureText = queryAllByText( | ||
| 'doc_auth.buttons.take_or_upload_picture_html', | ||
| ); | ||
| expect(takeOrUploadPictureText).to.have.lengthOf(2); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| context('and using desktop', () => { | ||
| context('and doc_auth_selfie_desktop_test_mode is false', () => { | ||
| it('shows a file pick area for each field', () => { | ||
| const { queryAllByText } = render( | ||
| <DeviceContext.Provider value={{ isMobile: false }}> | ||
| <SelfieCaptureContext.Provider | ||
| value={{ isSelfieCaptureEnabled: false, isSelfieDesktopTestMode: false }} | ||
| > | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="front" /> | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="back" /> | ||
| </SelfieCaptureContext.Provider> | ||
| </DeviceContext.Provider>, | ||
| ); | ||
|
|
||
| const uploadPictureText = queryAllByText('doc_auth.forms.choose_file_html'); | ||
| expect(uploadPictureText).to.have.lengthOf(2); | ||
| }); | ||
| }); | ||
|
|
||
| context('and doc_auth_selfie_desktop_test_mode is true', () => { | ||
| it('shows a file pick area for each field', () => { | ||
| const { queryAllByText } = render( | ||
| <DeviceContext.Provider value={{ isMobile: false }}> | ||
| <SelfieCaptureContext.Provider | ||
| value={{ isSelfieCaptureEnabled: false, isSelfieDesktopTestMode: true }} | ||
| > | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="front" /> | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="back" /> | ||
| </SelfieCaptureContext.Provider> | ||
| </DeviceContext.Provider>, | ||
| ); | ||
|
|
||
| const uploadPictureText = queryAllByText('doc_auth.forms.choose_file_html'); | ||
| expect(uploadPictureText).to.have.lengthOf(2); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| context('when selfie _is_ enabled', () => { | ||
| context('and using mobile', () => { | ||
| context('and doc_auth_selfie_desktop_test_mode is false', () => { | ||
| it('does _not_ display a photo upload button', () => { | ||
| const { queryAllByText } = render( | ||
| <DeviceContext.Provider value={{ isMobile: true }}> | ||
| <SelfieCaptureContext.Provider | ||
| value={{ isSelfieCaptureEnabled: true, isSelfieDesktopTestMode: false }} | ||
| > | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="front" /> | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="back" /> | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="selfie" /> | ||
| </SelfieCaptureContext.Provider> | ||
| </DeviceContext.Provider>, | ||
| ); | ||
|
|
||
| const takePictureText = queryAllByText('doc_auth.buttons.take_picture'); | ||
| expect(takePictureText).to.have.lengthOf(3); | ||
|
|
||
| const takeOrUploadPictureText = queryAllByText( | ||
| 'doc_auth.buttons.take_or_upload_picture_html', | ||
| ); | ||
| expect(takeOrUploadPictureText).to.have.lengthOf(0); | ||
| }); | ||
| }); | ||
|
|
||
| context('and doc_auth_selfie_desktop_test_mode is true', () => { | ||
| it('does _not_ display a photo upload button', () => { | ||
| const { queryAllByText } = render( | ||
| <DeviceContext.Provider value={{ isMobile: true }}> | ||
| <SelfieCaptureContext.Provider | ||
| value={{ isSelfieCaptureEnabled: true, isSelfieDesktopTestMode: true }} | ||
| > | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="front" /> | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="back" /> | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="selfie" /> | ||
| </SelfieCaptureContext.Provider> | ||
| </DeviceContext.Provider>, | ||
| ); | ||
|
|
||
| const takePictureText = queryAllByText('doc_auth.buttons.take_picture'); | ||
| expect(takePictureText).to.have.lengthOf(3); | ||
|
|
||
| const takeOrUploadPictureText = queryAllByText( | ||
| 'doc_auth.buttons.take_or_upload_picture_html', | ||
| ); | ||
| expect(takeOrUploadPictureText).to.have.lengthOf(3); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| context('and using desktop', () => { | ||
| context('and doc_auth_selfie_desktop_test_mode is false', () => { | ||
| it('never loads these components', () => { | ||
| // noop | ||
| }); | ||
| }); | ||
|
|
||
| context('and doc_auth_selfie_desktop_test_mode is true', () => { | ||
| it('shows a file pick area for each field', () => { | ||
| const { queryAllByText } = render( | ||
| <DeviceContext.Provider value={{ isMobile: false }}> | ||
| <SelfieCaptureContext.Provider | ||
| value={{ isSelfieCaptureEnabled: true, isSelfieDesktopTestMode: true }} | ||
| > | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="front" /> | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="back" /> | ||
| <DocumentSideAcuantCapture {...DEFAULT_PROPS} side="selfie" /> | ||
| </SelfieCaptureContext.Provider> | ||
| </DeviceContext.Provider>, | ||
| ); | ||
|
|
||
| const uploadPictureText = queryAllByText('doc_auth.forms.choose_file_html'); | ||
| expect(uploadPictureText).to.have.lengthOf(3); | ||
| }); | ||
| }); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,11 @@ | ||
| import { useContext } from 'react'; | ||
| import { renderHook } from '@testing-library/react-hooks'; | ||
| import userEvent from '@testing-library/user-event'; | ||
| import { DeviceContext, AnalyticsContext } from '@18f/identity-document-capture'; | ||
| import { | ||
| DeviceContext, | ||
| AnalyticsContext, | ||
| SelfieCaptureContext, | ||
| } from '@18f/identity-document-capture'; | ||
| import { Provider as AcuantContextProvider } from '@18f/identity-document-capture/context/acuant'; | ||
| import AcuantCapture from '@18f/identity-document-capture/components/acuant-capture'; | ||
| import FailedCaptureAttemptsContext, { | ||
|
|
@@ -164,6 +168,59 @@ describe('FailedCaptureAttemptsContext testing of forceNativeCamera logic', () = | |
| expect(result.current.failedCaptureAttempts).to.equal(1); | ||
| expect(result.current.forceNativeCamera).to.equal(false); | ||
| }); | ||
|
|
||
| describe('when selfie is enabled', () => { | ||
| it('forceNativeCamera is always false, no matter how many times any attempt fails', () => { | ||
| const trackEvent = sinon.spy(); | ||
| const { result, rerender } = renderHook(() => useContext(FailedCaptureAttemptsContext), { | ||
| wrapper: ({ children }) => ( | ||
| <SelfieCaptureContext.Provider value={{ isSelfieCaptureEnabled: true }}> | ||
| <Provider | ||
| maxCaptureAttemptsBeforeNativeCamera={2} | ||
| maxSubmissionAttemptsBeforeNativeCamera={2} | ||
| > | ||
| {children} | ||
| </Provider> | ||
| </SelfieCaptureContext.Provider> | ||
| ), | ||
| }); | ||
|
|
||
| result.current.onFailedCaptureAttempt({ | ||
| isAssessedAsGlare: true, | ||
| isAssessedAsBlurry: false, | ||
| }); | ||
| rerender(true); | ||
| expect(result.current.forceNativeCamera).to.equal(false); | ||
| result.current.onFailedCaptureAttempt({ | ||
| isAssessedAsGlare: false, | ||
| isAssessedAsBlurry: true, | ||
| }); | ||
| rerender(true); | ||
| expect(result.current.forceNativeCamera).to.equal(false); | ||
| result.current.onFailedCaptureAttempt({ | ||
| isAssessedAsGlare: false, | ||
| isAssessedAsBlurry: true, | ||
| }); | ||
| rerender({}); | ||
| expect(result.current.failedCaptureAttempts).to.equal(3); | ||
| expect(result.current.forceNativeCamera).to.equal(false); | ||
|
|
||
| result.current.onFailedSubmissionAttempt(); | ||
| rerender(true); | ||
| expect(result.current.forceNativeCamera).to.equal(false); | ||
| result.current.onFailedSubmissionAttempt(); | ||
| rerender(true); | ||
| expect(result.current.forceNativeCamera).to.equal(false); | ||
| result.current.onFailedSubmissionAttempt(); | ||
| rerender({}); | ||
| expect(result.current.failedSubmissionAttempts).to.equal(3); | ||
| expect(result.current.forceNativeCamera).to.equal(false); | ||
|
|
||
| expect(trackEvent).to.not.have.been.calledWith( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍🏻 |
||
| 'IdV: Native camera forced after failed attempts', | ||
| ); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
| describe('maxCaptureAttemptsBeforeNativeCamera logging tests', () => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@kellular and @daviddsilvanava - This is replacing #10165 and thanks to other work from Timnit, by now shouldn't have the UX issues we talked about on that PR.