18F/identity-idp

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

Summary

Maintainability
A
0 mins
Test Coverage
import { createContext } from 'react';
import { useObjectMemo } from '@18f/identity-react-hooks';
import type { ReactNode } from 'react';
import defaultUpload from '../services/upload';
import type { PII } from '../services/upload';

const UploadContext = createContext({
  upload: defaultUpload,
  getStatus: () => Promise.resolve({} as UploadSuccessResponse),
  statusPollInterval: undefined as number | undefined,
  isMockClient: false,
  flowPath: 'standard' as FlowPath,
  formData: {} as Record<string, any>,
});

UploadContext.displayName = 'UploadContext';

export type FlowPath = 'standard' | 'hybrid';

/**
 * Upload field error, after normalized to error instance.
 */
export interface UploadFieldError {
  /**
   * Field name
   */
  field: 'front' | 'back' | 'network';

  /**
   * Error message.
   */
  message: string;
}

interface UploadOptions {
  /**
   * HTTP method to send payload.
   */
  method?: 'POST' | 'PUT';

  /**
   * Endpoint to which payload should be sent.
   */
  endpoint: string;
}

export interface UploadSuccessResponse {
  /**
   * Whether request was successful.
   */
  success: true;

  /**
   * Whether verification result is still pending.
   */
  isPending: boolean;
}
export interface ImageFingerprints {
  front: string[] | null;
  back: string[] | null;
}
export interface UploadErrorResponse {
  /**
   * Whether request was successful.
   */
  success: false;

  /**
   * Error messages.
   */
  errors?: UploadFieldError[];

  /**
   * URL to which user should be redirected.
   */
  redirect?: string;

  /**
   * Number of remaining doc capture attempts for user.
   */
  remaining_submit_attempts?: number;

  /**
   * Boolean to decide if capture hints should be shown with error.
   */
  hints?: boolean;

  /**
   * Personally-identifiable information from OCR analysis.
   */
  ocr_pii?: PII;

  /**
   * Whether the unsuccessful result was any result other than passed or attention with barcode.
   */
  result_code_invalid: boolean;

  /**
   * Whether the unsuccessful result was the failure type.
   */
  result_failed: boolean;

  /**
   * Whether the selfie captured matched the image on the id.
   */
  selfie_status?: string;

  /**
   * Whether the doc type is clearly not supported type.
   */
  doc_type_supported: boolean;

  /*
   * Whether the selfie passed the liveness check from trueid
   */
  selfie_live?: boolean;

  /*
   * Whether the selfie passed the quality check from trueid.
   */
  selfie_quality_good?: boolean;

  /**
   * Record of failed image fingerprints
   */
  failed_image_fingerprints: ImageFingerprints | null;
}

export type UploadImplementation = (
  payload: Record<string, any>,
  options: UploadOptions,
) => Promise<UploadSuccessResponse>;

interface UploadContextProviderProps {
  /**
   * Custom upload implementation.
   */
  upload?: UploadImplementation;

  /**
   * Whether to treat upload as a mock implementation.
   */
  isMockClient?: boolean;

  /**
   * Endpoint to which payload should be sent.
   */
  endpoint: string;

  /**
   * Endpoint from which to request async upload status.
   */
  statusEndpoint?: string;

  /**
   * Interval at which to poll for status, in milliseconds.
   */
  statusPollInterval?: number;

  /**
   * Extra form data to merge into the payload before uploading
   */
  formData?: Record<string, any>;

  /**
   * The user's session flow path, one of "standard" or "hybrid".
   */
  flowPath: FlowPath;

  /**
   *  Child elements.
   */
  children: ReactNode;
}

/**
 * Default form data. Assigned as a constant to avoid creating a new object reference for each call
 * to the component.
 */
const DEFAULT_FORM_DATA = {};

function UploadContextProvider({
  upload = defaultUpload,
  isMockClient = false,
  endpoint,
  statusEndpoint,
  statusPollInterval,
  formData = DEFAULT_FORM_DATA,
  flowPath,
  children,
}: UploadContextProviderProps) {
  const uploadWithFormData = (payload) => upload({ ...payload, ...formData }, { endpoint });

  const getStatus = () =>
    statusEndpoint
      ? upload({ ...formData }, { endpoint: statusEndpoint, method: 'PUT' })
      : Promise.reject();

  const value = useObjectMemo({
    upload: uploadWithFormData,
    getStatus,
    statusPollInterval,
    isMockClient,
    flowPath,
    formData,
  });

  return <UploadContext.Provider value={value}>{children}</UploadContext.Provider>;
}

export default UploadContext;
export { UploadContextProvider as Provider };