diff --git a/app/javascript/packages/document-capture/components/acuant-capture.tsx b/app/javascript/packages/document-capture/components/acuant-capture.tsx index a02fdd68835..99703652313 100644 --- a/app/javascript/packages/document-capture/components/acuant-capture.tsx +++ b/app/javascript/packages/document-capture/components/acuant-capture.tsx @@ -8,7 +8,7 @@ import { useImperativeHandle, } from 'react'; import { useI18n } from '@18f/identity-react-i18n'; -import { useIfStillMounted, useDidUpdateEffect } from '@18f/identity-react-hooks'; +import { useDidUpdateEffect } from '@18f/identity-react-hooks'; import { Button, FullScreen } from '@18f/identity-components'; import type { FullScreenRefHandle } from '@18f/identity-components'; import type { FocusTrap } from 'focus-trap'; @@ -95,12 +95,6 @@ interface AcuantCaptureProps { * Camera permission declined callback */ onCameraAccessDeclined?: () => void; - /** - * Facing mode of caopture. If capture is not - * specified and a camera is supported, defaults - * to the Acuant environment camera capture. - */ - capture: 'user' | 'environment'; /** * Optional additional class names */ @@ -261,7 +255,6 @@ function AcuantCapture( value, onChange = () => {}, onCameraAccessDeclined = () => {}, - capture, className, allowUpload = true, errorMessage, @@ -272,7 +265,6 @@ function AcuantCapture( const { isReady, isActive: isAcuantInstanceActive, - isAcuantLoaded, isError, isCameraSupported, glareThreshold, @@ -287,7 +279,6 @@ function AcuantCapture( const [isCapturingEnvironment, setIsCapturingEnvironment] = useState(false); const [ownErrorMessage, setOwnErrorMessage] = useState(null); const [hasStartedCropping, setHasStartedCropping] = useState(false); - const ifStillMounted = useIfStillMounted(); useMemo(() => setOwnErrorMessage(null), [value]); const { isMobile } = useContext(DeviceContext); const { t, formatHTML } = useI18n(); @@ -419,16 +410,10 @@ function AcuantCapture( function startCaptureOrTriggerUpload(event: MouseEvent) { if (event.target === inputRef.current) { const isAcuantCaptureCapable = hasCapture && !acuantFailureCookie; - const isEnvironmentCapture = capture !== 'user'; - const shouldStartSelfieCapture = - isAcuantLoaded && capture === 'user' && !isForceUploading.current; let shouldStartAcuantCapture = - isAcuantCaptureCapable && - capture !== 'user' && - !isForceUploading.current && - !forceNativeCamera; + isAcuantCaptureCapable && !isForceUploading.current && !forceNativeCamera; - if (isAcuantCaptureCapable && isEnvironmentCapture && forceNativeCamera) { + if (isAcuantCaptureCapable && forceNativeCamera) { trackEvent('IdV: Native camera forced after failed attempts', { field: name, failed_capture_attempts: failedCaptureAttempts, @@ -443,18 +428,11 @@ function AcuantCapture( shouldStartAcuantCapture = !nativeCameraOnly; } - if (!allowUpload || shouldStartSelfieCapture || shouldStartAcuantCapture) { + if (!allowUpload || shouldStartAcuantCapture) { event.preventDefault(); } - if (shouldStartSelfieCapture) { - window.AcuantPassiveLiveness.startSelfieCapture( - ifStillMounted((nextImageData) => { - const dataURI = `data:image/jpeg;base64,${nextImageData}`; - onChangeAndResetError(dataURI); - }), - ); - } else if (shouldStartAcuantCapture && !isAcuantInstanceActive) { + if (shouldStartAcuantCapture && !isAcuantInstanceActive) { setIsCapturingEnvironment(true); } @@ -571,7 +549,6 @@ function AcuantCapture( fileLoadingText={t('doc_auth.info.image_loading')} fileLoadedText={t('doc_auth.info.image_loaded')} accept={isMockClient ? undefined : ['image/jpeg', 'image/png']} - capture={capture} value={value} errorMessage={ownErrorMessage ?? errorMessage} isValuePending={hasStartedCropping} diff --git a/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.spec.tsx b/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.spec.tsx index b08b07acfaa..b479c5800cb 100644 --- a/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.spec.tsx +++ b/app/javascript/packages/document-capture/components/document-capture-troubleshooting-options.spec.tsx @@ -17,7 +17,6 @@ describe('DocumentCaptureTroubleshootingOptions', () => { const serviceProviderContext: ServiceProviderContext = { name: 'Example SP', failureToProofURL: 'http://example.test/url/to/failure-to-proof', - isLivenessRequired: false, getFailureToProofURL: () => '', }; const wrappers: Record = { diff --git a/app/javascript/packages/document-capture/components/document-capture.tsx b/app/javascript/packages/document-capture/components/document-capture.tsx index 082cca85521..82ca8aba328 100644 --- a/app/javascript/packages/document-capture/components/document-capture.tsx +++ b/app/javascript/packages/document-capture/components/document-capture.tsx @@ -7,12 +7,10 @@ import { useDidUpdateEffect } from '@18f/identity-react-hooks'; import type { FormStep } from '@18f/identity-form-steps'; import { UploadFormEntriesError } from '../services/upload'; import DocumentsStep from './documents-step'; -import SelfieStep from './selfie-step'; import InPersonPrepareStep from './in-person-prepare-step'; import InPersonLocationStep from './in-person-location-step'; import InPersonSwitchBackStep from './in-person-switch-back-step'; import ReviewIssuesStep from './review-issues-step'; -import ServiceProviderContext from '../context/service-provider'; import UploadContext from '../context/upload'; import AnalyticsContext from '../context/analytics'; import Submission from './submission'; @@ -57,7 +55,6 @@ function DocumentCapture({ isAsyncForm = false, onStepChange = () => {} }: Docum const [submissionError, setSubmissionError] = useState(undefined); const [stepName, setStepName] = useState(undefined); const { t } = useI18n(); - const serviceProvider = useContext(ServiceProviderContext); const { flowPath } = useContext(UploadContext); const { trackSubmitEvent, trackVisitEvent } = useContext(AnalyticsContext); const { inPersonURL } = useContext(FlowContext); @@ -81,7 +78,7 @@ function DocumentCapture({ isAsyncForm = false, onStepChange = () => {} }: Docum const submissionFormValues = useMemo( () => formValues && { - ...(isAsyncForm ? except(formValues, 'front', 'back', 'selfie') : formValues), + ...(isAsyncForm ? except(formValues, 'front', 'back') : formValues), flow_path: flowPath, }, [isAsyncForm, formValues, flowPath], @@ -146,10 +143,6 @@ function DocumentCapture({ isAsyncForm = false, onStepChange = () => {} }: Docum name: 'documents', form: DocumentsStep, }, - serviceProvider.isLivenessRequired && { - name: 'selfie', - form: SelfieStep, - }, ].filter(Boolean) as FormStep[]); const stepIndicatorPath = diff --git a/app/javascript/packages/document-capture/components/document-side-acuant-capture.jsx b/app/javascript/packages/document-capture/components/document-side-acuant-capture.jsx index cbcac8045d0..5d8e200d622 100644 --- a/app/javascript/packages/document-capture/components/document-side-acuant-capture.jsx +++ b/app/javascript/packages/document-capture/components/document-side-acuant-capture.jsx @@ -67,7 +67,6 @@ function DocumentSideAcuantCapture({ errorMessage={error ? error.message : undefined} name={side} className={className} - capture="environment" /> ); } diff --git a/app/javascript/packages/document-capture/components/file-input.jsx b/app/javascript/packages/document-capture/components/file-input.jsx index b1cfd991b47..346b5bd100e 100644 --- a/app/javascript/packages/document-capture/components/file-input.jsx +++ b/app/javascript/packages/document-capture/components/file-input.jsx @@ -32,7 +32,6 @@ import usePrevious from '../hooks/use-previous'; * @prop {string} fileLoadingText Status message text to show when file is pending. * @prop {string} fileLoadedText Status message text to show once pending file is loaded. * @prop {string[]=} accept Optional array of file input accept patterns. - * @prop {'user'|'environment'=} capture Optional facing mode if file input is used for capture. * @prop {Blob|string|null|undefined} value Current value. * @prop {ReactNode=} errorMessage Error to show. * @prop {boolean=} isValuePending Whether to show the input in an indeterminate loading state, @@ -115,7 +114,6 @@ function FileInput(props, ref) { fileLoadingText, fileLoadedText, accept, - capture, value, errorMessage, isValuePending, @@ -325,7 +323,6 @@ function FileInput(props, ref) { aria-label={getLabelFromValue(label, value)} aria-busy={isValuePending} onChange={onChangeIfValid} - capture={capture} onClick={onClick} onDrop={onDrop} accept={accept ? accept.join() : undefined} diff --git a/app/javascript/packages/document-capture/components/review-issues-step.tsx b/app/javascript/packages/document-capture/components/review-issues-step.tsx index 481ab2dd2b2..74eb52c6cc1 100644 --- a/app/javascript/packages/document-capture/components/review-issues-step.tsx +++ b/app/javascript/packages/document-capture/components/review-issues-step.tsx @@ -1,16 +1,11 @@ import { useContext, useEffect, useState } from 'react'; -import { hasMediaAccess } from '@18f/identity-device'; import { useI18n } from '@18f/identity-react-i18n'; import { useDidUpdateEffect } from '@18f/identity-react-hooks'; import { FormStepsContext, FormStepsButton } from '@18f/identity-form-steps'; import { PageHeading } from '@18f/identity-components'; import { Cancel } from '@18f/identity-verify-flow'; import type { FormStepComponentProps } from '@18f/identity-form-steps'; -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 type { PII } from '../services/upload'; import DocumentCaptureTroubleshootingOptions from './document-capture-troubleshooting-options'; @@ -32,11 +27,6 @@ interface ReviewIssuesStepValue { */ back: Blob | string | null | undefined; - /** - * Back image value. - */ - selfie: Blob | string | null | undefined; - /** * Front image metadata. */ @@ -78,10 +68,7 @@ function ReviewIssuesStep({ captureHints = false, }: ReviewIssuesStepProps) { const { t } = useI18n(); - const { isMobile } = useContext(DeviceContext); - const serviceProvider = useContext(ServiceProviderContext); const { trackEvent } = useContext(AnalyticsContext); - const selfieError = errors.find(({ field }) => field === 'selfie')?.error; const [hasDismissed, setHasDismissed] = useState(remainingAttempts === Infinity); const { onPageTransition, changeStepCanComplete } = useContext(FormStepsContext); useDidUpdateEffect(onPageTransition, [hasDismissed]); @@ -122,7 +109,7 @@ function ReviewIssuesStep({ > {!!unknownFieldErrors && unknownFieldErrors - .filter((error) => !['front', 'back', 'selfie'].includes(error.field!)) + .filter((error) => !['front', 'back'].includes(error.field!)) .map(({ error }) =>

{error.message}

)} {remainingAttempts <= DISPLAY_ATTEMPTS && ( @@ -166,45 +153,7 @@ function ReviewIssuesStep({ className="document-capture-review-issues-step__input" /> ))} - {serviceProvider.isLivenessRequired && ( - <> -
-

{t('doc_auth.tips.review_issues_selfie_header_text')}

-
    -
  • {t('doc_auth.tips.review_issues_selfie_text1')}
  • -
  • {t('doc_auth.tips.review_issues_selfie_text2')}
  • -
  • {t('doc_auth.tips.review_issues_selfie_text3')}
  • -
  • {t('doc_auth.tips.review_issues_selfie_text4')}
  • -
- {isMobile || !hasMediaAccess() ? ( - onChange({ selfie: nextSelfie })} - allowUpload={false} - className="document-capture-review-issues-step__input" - errorMessage={selfieError?.message} - name="selfie" - /> - ) : ( - onChange({ selfie: nextSelfie })} - errorMessage={selfieError?.message} - className={[ - 'document-capture-review-issues-step__input', - !value.selfie && 'document-capture-review-issues-step__input--unconstrained-width', - ] - .filter(Boolean) - .join(' ')} - /> - )} - - )} + diff --git a/app/javascript/packages/document-capture/components/selfie-capture.jsx b/app/javascript/packages/document-capture/components/selfie-capture.jsx deleted file mode 100644 index d1e9ca0d157..00000000000 --- a/app/javascript/packages/document-capture/components/selfie-capture.jsx +++ /dev/null @@ -1,289 +0,0 @@ -import { - forwardRef, - useRef, - useState, - useEffect, - useCallback, - useContext, - useMemo, - useImperativeHandle, -} from 'react'; -import { Icon } from '@18f/identity-components'; -import { useI18n } from '@18f/identity-react-i18n'; -import { useIfStillMounted, useInstanceId } from '@18f/identity-react-hooks'; -import FileImage from './file-image'; -import useFocusFallbackRef from '../hooks/use-focus-fallback-ref'; -import AppContext from '../context/app'; - -/** @typedef {import('react').ReactNode} ReactNode */ - -/** - * @typedef SelfieCaptureProps - * - * @prop {Blob|string|null|undefined} value Current value. - * @prop {(nextValue:Blob|string|null)=>void} onChange Change handler. - * @prop {ReactNode=} errorMessage Error to show. - * @prop {string=} className Optional additional class names to apply to wrapper element. - */ - -/** - * @param {SelfieCaptureProps} props Props object. - */ -function SelfieCapture({ value, onChange, errorMessage, className }, ref) { - const instanceId = useInstanceId(); - const { t, formatHTML } = useI18n(); - const labelRef = useRef(/** @type {HTMLDivElement?} */ (null)); - const wrapperRef = useRef(/** @type {HTMLDivElement?} */ (null)); - const hadValue = useRef(false); - const isUpdated = useMemo(() => { - const nextIsUpdated = Boolean(value && hadValue.current); - hadValue.current = hadValue.current || Boolean(value); - return nextIsUpdated; - }, [value]); - const retryButtonRef = useFocusFallbackRef(labelRef); - const captureButtonRef = useFocusFallbackRef(labelRef); - useImperativeHandle(ref, () => labelRef.current); - - const videoRef = useRef(/** @type {HTMLVideoElement?} */ (null)); - const setVideoRef = useCallback((nextVideoRef) => { - // React will call an assigned `ref` callback with `null` at the time the element is being - // removed, which is an opportunity to stop any in-progress capture. - if (!nextVideoRef && videoRef.current?.srcObject instanceof window.MediaStream) { - videoRef.current.srcObject.getTracks().forEach((track) => track.stop()); - } - - videoRef.current = nextVideoRef; - }, []); - - const [isAccessRejected, setIsAccessRejected] = useState(false); - const [isCapturing, setIsCapturing] = useState(false); - // Sync capturing state with the availability of a value. If a value is assigned while capture is - // in progress, reset state. Most often, this is a direct result of calling `onChange` with the - // next value. - useMemo(() => setIsCapturing(isCapturing && !value), [value]); - - const ifStillMounted = useIfStillMounted(); - - function startCapture() { - navigator.mediaDevices - .getUserMedia({ video: { width: 1920, height: 1080 } }) - .then( - ifStillMounted((/** @type {MediaStream} */ stream) => { - if (!videoRef.current) { - return; - } - - videoRef.current.srcObject = stream; - videoRef.current.play(); - setIsCapturing(true); - setIsAccessRejected(false); - }), - ) - .catch( - ifStillMounted((error) => { - if (error.name !== 'NotAllowedError') { - throw error; - } - - setIsAccessRejected(true); - }), - ); - } - - useEffect(() => { - // Start capturing only if not already capturing, and if value has yet to be assigned. - if (value || isCapturing || !navigator.permissions) { - return; - } - - // Type-casting necessary due to: https://github.com/microsoft/TypeScript/issues/33923 - navigator.permissions.query({ name: /** @type {PermissionName} */ ('camera') }).then( - ifStillMounted((/** @type {PermissionStatus} */ result) => { - if (result.state === 'granted') { - startCapture(); - } else if (result.state === 'denied') { - setIsAccessRejected(true); - } - }), - ); - }, [value]); - - function onCapture() { - if (!videoRef.current || !wrapperRef.current) { - return; - } - - const canvas = document.createElement('canvas'); - const { videoWidth, videoHeight } = videoRef.current; - const { clientWidth, clientHeight } = wrapperRef.current; - - const height = Math.min(videoHeight, 720); - const aspectRatio = clientWidth / clientHeight; - const width = height * aspectRatio; - - const sourceX = (videoWidth - width) / 2; - - canvas.height = height; - canvas.width = width; - - canvas - .getContext('2d') - ?.drawImage(videoRef.current, sourceX, 0, width, height, 0, 0, width, height); - - onChange(canvas.toDataURL('image/jpeg', 0.8)); - } - - let shownErrorMessage; - if (isAccessRejected) { - shownErrorMessage = t('doc_auth.errors.camera.blocked'); - } else if (errorMessage) { - shownErrorMessage = errorMessage; - } - - const classes = [ - 'selfie-capture', - isCapturing && 'selfie-capture--capturing', - shownErrorMessage && 'selfie-capture--error', - isUpdated && !shownErrorMessage && 'selfie-capture--updated', - value && 'selfie-capture--has-value', - className, - ] - .filter(Boolean) - .join(' '); - - const labelId = `selfie-capture-label-${instanceId}`; - - const { appName } = useContext(AppContext); - - return ( - <> -
- {t('doc_auth.headings.document_capture_selfie')} -
- {shownErrorMessage && ( - - {shownErrorMessage} - - )} - {isUpdated && !shownErrorMessage && ( - - {t('doc_auth.info.image_updated')} - - )} -
- {value ? ( - <> -
- - -
- {value instanceof window.Blob ? ( - - ) : ( - {t('doc_auth.accessible_labels.selfie_alt_text')} - )} - - ) : ( - <> - {/* Disable reason: Video is used only for direct capture */} - {/* eslint-disable-next-line jsx-a11y/media-has-caption */} -