18F/identity-idp

View on GitHub
app/javascript/packages/document-capture/components/document-capture.tsx

Summary

Maintainability
A
0 mins
Test Coverage
import { useState, useMemo, useContext, useEffect } from 'react';
import { Alert } from '@18f/identity-components';
import { useI18n } from '@18f/identity-react-i18n';
import { FormSteps, PromptOnNavigate } from '@18f/identity-form-steps';
import { VerifyFlowStepIndicator, VerifyFlowPath } from '@18f/identity-verify-flow';
import { useDidUpdateEffect } from '@18f/identity-react-hooks';
import type { FormStep } from '@18f/identity-form-steps';
import { getConfigValue } from '@18f/identity-config';
import { UploadFormEntriesError } from '../services/upload';
import DocumentsAndSelfieStep from './documents-and-selfie-step';
import SelfieStep from './selfie-step';
import DocumentsStep from './documents-step';
import InPersonPrepareStep from './in-person-prepare-step';
import InPersonLocationPostOfficeSearchStep from './in-person-location-post-office-search-step';
import InPersonLocationFullAddressEntryPostOfficeSearchStep from './in-person-location-full-address-entry-post-office-search-step';
import InPersonSwitchBackStep from './in-person-switch-back-step';
import ReviewIssuesStep from './review-issues-step';
import UploadContext from '../context/upload';
import AnalyticsContext from '../context/analytics';
import Submission from './submission';
import SubmissionStatus from './submission-status';
import { RetrySubmissionError } from './submission-complete';
import SuspenseErrorBoundary from './suspense-error-boundary';
import SubmissionInterstitial from './submission-interstitial';
import withProps from '../higher-order/with-props';
import { InPersonContext, SelfieCaptureContext } from '../context';

interface DocumentCaptureProps {
  /**
   * Callback triggered on step change.
   */
  onStepChange?: () => void;
}

function DocumentCapture({ onStepChange = () => {} }: DocumentCaptureProps) {
  const [formValues, setFormValues] = useState<Record<string, any> | null>(null);
  const [submissionError, setSubmissionError] = useState<Error | undefined>(undefined);
  const [stepName, setStepName] = useState<string | undefined>(undefined);
  const { t } = useI18n();
  const { flowPath } = useContext(UploadContext);
  const { trackSubmitEvent, trackVisitEvent } = useContext(AnalyticsContext);
  const { isSelfieCaptureEnabled, docAuthSeparatePagesEnabled } = useContext(SelfieCaptureContext);
  const { inPersonFullAddressEntryEnabled, inPersonURL, skipDocAuth, skipDocAuthFromHandoff } =
    useContext(InPersonContext);
  useDidUpdateEffect(onStepChange, [stepName]);
  useEffect(() => {
    if (stepName) {
      trackVisitEvent(stepName);
    }
  }, [stepName]);
  const appName = getConfigValue('appName');
  const inPersonLocationPostOfficeSearchForm = inPersonFullAddressEntryEnabled
    ? InPersonLocationFullAddressEntryPostOfficeSearchStep
    : InPersonLocationPostOfficeSearchStep;

  // Define different states to be used in human readable array declaration
  const documentAndSelfieFormStep: FormStep = {
    name: 'documentsAndSelfie',
    form: DocumentsAndSelfieStep,
    title: t('doc_auth.headings.document_capture'),
  };
  const documentFormStep: FormStep = {
    name: 'documents',
    form: DocumentsStep,
    title: t('doc_auth.headings.document_capture'),
  };
  const selfieFormStep: FormStep = {
    name: 'selfie',
    form: SelfieStep,
    title: t('doc_auth.headings.selfie_capture'),
  };
  const documentsFormSteps: FormStep[] =
    isSelfieCaptureEnabled && docAuthSeparatePagesEnabled && submissionError === undefined
      ? [documentFormStep, selfieFormStep]
      : [documentAndSelfieFormStep];
  const reviewFormStep: FormStep = {
    name: 'review',
    form:
      submissionError instanceof UploadFormEntriesError
        ? withProps({
            remainingSubmitAttempts: submissionError.remainingSubmitAttempts,
            isResultCodeInvalid: submissionError.isResultCodeInvalid,
            isFailedResult: submissionError.isFailedResult,
            isFailedDocType: submissionError.isFailedDocType,
            isFailedSelfie: submissionError.isFailedSelfie,
            isFailedSelfieLivenessOrQuality:
              submissionError.selfieNotLive || submissionError.selfieNotGoodQuality,
            captureHints: submissionError.hints,
            pii: submissionError.pii,
            failedImageFingerprints: submissionError.failed_image_fingerprints,
          })(ReviewIssuesStep)
        : ReviewIssuesStep,
    title: t('doc_auth.errors.rate_limited_heading'),
  };

  // In Person Steps
  const prepareFormStep: FormStep = {
    name: 'prepare',
    form: InPersonPrepareStep,
    title: t('in_person_proofing.headings.prepare'),
  };
  const locationFormStep: FormStep = {
    name: 'location',
    form: inPersonLocationPostOfficeSearchForm,
    title: t('in_person_proofing.headings.po_search.location'),
  };
  const hybridFormStep: FormStep = {
    name: 'switch_back',
    form: InPersonSwitchBackStep,
    title: t('in_person_proofing.headings.switch_back'),
  };

  /**
   * Clears error state and sets form values for submission.
   *
   * @param nextFormValues Submitted form values.
   */
  function submitForm(nextFormValues: Record<string, any>) {
    setSubmissionError(undefined);
    setFormValues(nextFormValues);
  }

  const submissionFormValues = useMemo(
    () =>
      formValues && {
        ...formValues,
        flow_path: flowPath,
      },
    [formValues, flowPath],
  );

  let initialActiveErrors;
  if (submissionError instanceof UploadFormEntriesError) {
    initialActiveErrors = submissionError.formEntryErrors.map((error) => ({
      field: error.field,
      error,
    }));
  }

  let initialValues;
  if (submissionError && formValues) {
    initialValues = formValues;
  }
  // If the user got here by opting-in to in-person proofing, when skipDocAuth === true,
  // then set steps to inPersonSteps
  const isInPersonStepEnabled = skipDocAuth || skipDocAuthFromHandoff;
  const inPersonSteps: FormStep[] =
    inPersonURL === undefined
      ? []
      : ([prepareFormStep, locationFormStep, flowPath === 'hybrid' && hybridFormStep].filter(
          Boolean,
        ) as FormStep[]);

  let steps = documentsFormSteps;
  if (isInPersonStepEnabled) {
    steps = inPersonSteps;
  } else if (submissionError) {
    steps = [reviewFormStep, ...inPersonSteps];
  }
  // If the user got here by opting-in to in-person proofing, when skipDocAuth === true;
  // or opting-in ipp from handoff page, and selfie is required, when skipDocAuthFromHandoff === true
  // then set stepIndicatorPath to VerifyFlowPath.IN_PERSON
  const stepIndicatorPath =
    (stepName && ['location', 'prepare', 'switch_back'].includes(stepName)) || isInPersonStepEnabled
      ? VerifyFlowPath.IN_PERSON
      : VerifyFlowPath.DEFAULT;

  return (
    <>
      <VerifyFlowStepIndicator currentStep="document_capture" path={stepIndicatorPath} />
      {submissionFormValues &&
      (!submissionError || submissionError instanceof RetrySubmissionError) ? (
        <>
          <SubmissionInterstitial autoFocus />
          <SuspenseErrorBoundary
            fallback={<PromptOnNavigate />}
            onError={setSubmissionError}
            handledError={submissionError}
          >
            {submissionError instanceof RetrySubmissionError ? (
              <SubmissionStatus />
            ) : (
              <Submission payload={submissionFormValues} />
            )}
          </SuspenseErrorBoundary>
        </>
      ) : (
        <>
          {submissionError && !(submissionError instanceof UploadFormEntriesError) && (
            <Alert type="error" className="margin-bottom-4">
              {t('doc_auth.errors.general.network_error')}
            </Alert>
          )}
          <FormSteps
            steps={steps}
            initialValues={initialValues}
            initialActiveErrors={initialActiveErrors}
            onComplete={submitForm}
            onStepChange={setStepName}
            onStepSubmit={trackSubmitEvent}
            autoFocus={!!submissionError}
            titleFormat={`%{step} - ${appName}`}
            initialStep={isInPersonStepEnabled ? steps[0].name : undefined}
          />
        </>
      )}
    </>
  );
}

export default DocumentCapture;