18F/identity-idp

View on GitHub
app/javascript/packages/document-capture/context/failed-capture-attempts.tsx

Summary

Maintainability
A
0 mins
Test Coverage
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;
  isAssessedAsBlurry: boolean;
  isAssessedAsUnsupported: boolean;
}

interface UploadedImageFingerprints {
  /**
   * array url safe encoded base64  sha256 digest
   */
  front: string[] | null;
  back: string[] | null;
}

interface FailedCaptureAttemptsContextInterface {
  /**
   * Current number of failed capture attempts
   */
  failedCaptureAttempts: number;

  /**
   * Current number of failed submission attempts
   */
  failedSubmissionAttempts: number;

  /**
   * There's a bug with Safari on iOS where if you deny camera permissions
   * three times the prompt stops appearing. To avoid this we keep track
   * and force a full page reload on the third time.
   */
  failedCameraPermissionAttempts: number;

  /**
   * Callback when submission attempt fails.
   * Used to increment the failedSubmissionAttempts
   */
  onFailedSubmissionAttempt: (failedImageFingerprints: UploadedImageFingerprints) => void;

  /**
   * A wrapper around incrementFailedCameraPermissionAttempts
   */
  onFailedCameraPermissionAttempt: () => void;

  /**
   * The maximum number of failed Acuant capture attempts
   * before use of the native camera option is triggered
   */
  maxCaptureAttemptsBeforeNativeCamera: number;

  /**
   * The maximum number of failed document submission
   * attempts before use of the native camera option
   * is triggered
   */
  maxSubmissionAttemptsBeforeNativeCamera: number;

  /**
   * Callback triggered on attempt, to increment attempts
   */
  onFailedCaptureAttempt: (metadata: CaptureAttemptMetadata) => void;

  /**
   * Callback to trigger a reset of attempts
   */
  onResetFailedCaptureAttempts: () => void;

  /**
   * Metadata about the last attempt
   */
  lastAttemptMetadata: CaptureAttemptMetadata;

  /**
   * Whether or not the native camera is currently being forced
   * after maxCaptureAttemptsBeforeNativeCamera number of failed attempts
   */
  forceNativeCamera: boolean;

  failedSubmissionImageFingerprints: UploadedImageFingerprints;
}

const DEFAULT_LAST_ATTEMPT_METADATA: CaptureAttemptMetadata = {
  isAssessedAsGlare: false,
  isAssessedAsBlurry: false,
  isAssessedAsUnsupported: false,
};

const FailedCaptureAttemptsContext = createContext<FailedCaptureAttemptsContextInterface>({
  failedCaptureAttempts: 0,
  failedSubmissionAttempts: 0,
  failedCameraPermissionAttempts: 0,
  onFailedCaptureAttempt: () => {},
  onFailedSubmissionAttempt: () => {},
  onFailedCameraPermissionAttempt: () => {},
  onResetFailedCaptureAttempts: () => {},
  maxCaptureAttemptsBeforeNativeCamera: Infinity,
  maxSubmissionAttemptsBeforeNativeCamera: Infinity,
  lastAttemptMetadata: DEFAULT_LAST_ATTEMPT_METADATA,
  forceNativeCamera: false,
  failedSubmissionImageFingerprints: { front: [], back: [] },
});

FailedCaptureAttemptsContext.displayName = 'FailedCaptureAttemptsContext';

interface FailedCaptureAttemptsContextProviderProps {
  children: ReactNode;
  maxCaptureAttemptsBeforeNativeCamera: number;
  maxSubmissionAttemptsBeforeNativeCamera: number;
  failedFingerprints: { front: []; back: [] };
}

function FailedCaptureAttemptsContextProvider({
  children,
  maxCaptureAttemptsBeforeNativeCamera,
  maxSubmissionAttemptsBeforeNativeCamera,
  failedFingerprints = { front: [], back: [] },
}: FailedCaptureAttemptsContextProviderProps) {
  const [lastAttemptMetadata, setLastAttemptMetadata] = useState<CaptureAttemptMetadata>(
    DEFAULT_LAST_ATTEMPT_METADATA,
  );
  const [failedCaptureAttempts, incrementFailedCaptureAttempts, onResetFailedCaptureAttempts] =
    useCounter();
  const [failedSubmissionAttempts, incrementFailedSubmissionAttempts] = useCounter();
  const [failedCameraPermissionAttempts, incrementFailedCameraPermissionAttempts] = useCounter();
  const { isSelfieCaptureEnabled } = useContext(SelfieCaptureContext);

  const [failedSubmissionImageFingerprints, setFailedSubmissionImageFingerprints] =
    useState<UploadedImageFingerprints>(failedFingerprints);

  function onFailedCaptureAttempt(metadata: CaptureAttemptMetadata) {
    incrementFailedCaptureAttempts();
    setLastAttemptMetadata(metadata);
  }

  function onFailedSubmissionAttempt(failedOnes: UploadedImageFingerprints) {
    incrementFailedSubmissionAttempts();
    setFailedSubmissionImageFingerprints(failedOnes);
  }

  function onFailedCameraPermissionAttempt() {
    incrementFailedCameraPermissionAttempts();
  }

  const hasExhaustedAttempts =
    failedCaptureAttempts >= maxCaptureAttemptsBeforeNativeCamera ||
    failedSubmissionAttempts >= maxSubmissionAttemptsBeforeNativeCamera;

  const forceNativeCamera = isSelfieCaptureEnabled ? false : hasExhaustedAttempts;

  return (
    <FailedCaptureAttemptsContext.Provider
      value={{
        failedCaptureAttempts,
        onFailedCaptureAttempt,
        onResetFailedCaptureAttempts,
        failedSubmissionAttempts,
        onFailedSubmissionAttempt,
        failedCameraPermissionAttempts,
        onFailedCameraPermissionAttempt,
        maxCaptureAttemptsBeforeNativeCamera,
        maxSubmissionAttemptsBeforeNativeCamera,
        lastAttemptMetadata,
        forceNativeCamera,
        failedSubmissionImageFingerprints,
      }}
    >
      {children}
    </FailedCaptureAttemptsContext.Provider>
  );
}

export default FailedCaptureAttemptsContext;
export { FailedCaptureAttemptsContextProvider as Provider };
export { UploadedImageFingerprints };