teamdigitale/italia-app

View on GitHub
ts/store/reducers/identification.ts

Summary

Maintainability
A
2 hrs
Test Coverage
import { getType } from "typesafe-actions";

import * as O from "fp-ts/lib/Option";
import { PersistPartial } from "redux-persist";
import { pipe } from "fp-ts/lib/function";
import { PinString } from "../../types/PinString";
import {
  identificationCancel,
  identificationFailure,
  identificationHideLockModal,
  identificationReset,
  identificationStart,
  identificationSuccess
} from "../actions/identification";
import { Action } from "../actions/types";
import { GlobalState } from "./types";

export const freeAttempts = 4;
// in seconds
export const deltaTimespanBetweenAttempts = 30;

export const maxAttempts = 8;

const maxDeltaTimespan =
  (maxAttempts - freeAttempts - 1) * deltaTimespanBetweenAttempts;

export enum IdentificationResult {
  "cancel" = "cancel",
  "pinreset" = "pinreset",
  "failure" = "failure",
  "success" = "success"
}

export type IdentificationGenericData = {
  message: string;
};

export type IdentificationCancelData = { label: string; onCancel: () => void };

export type IdentificationSuccessData = { onSuccess: () => void };

type IdentificationUnidentifiedState = {
  kind: "unidentified";
};

type IdentificationStartedState = {
  kind: "started";
  pin: PinString;
  canResetPin: boolean;
  isValidatingTask: boolean; // it is true if the identification process is occurring to confirm a task (eg. a payment)
  identificationGenericData?: IdentificationGenericData;
  identificationCancelData?: IdentificationCancelData;
  identificationSuccessData?: IdentificationSuccessData;
  shufflePad?: boolean;
};

type IdentificationIdentifiedState = {
  kind: "identified";
};

export type IdentificationProgressState =
  | IdentificationUnidentifiedState
  | IdentificationStartedState
  | IdentificationIdentifiedState;

export type IdentificationFailData = {
  remainingAttempts: number;
  nextLegalAttempt: Date;
  timespanBetweenAttempts: number;
  showLockModal?: boolean;
};

export type IdentificationState = {
  progress: IdentificationProgressState;
  fail?: IdentificationFailData;
};

export type PersistedIdentificationState = IdentificationState & PersistPartial;

const INITIAL_PROGRESS_STATE: IdentificationUnidentifiedState = {
  kind: "unidentified"
};

export const INITIAL_STATE: IdentificationState = {
  progress: INITIAL_PROGRESS_STATE,
  fail: undefined
};

export const fillShowLockModal = (actualErrorData: IdentificationFailData) => {
  // showLockModal is true if the time gap between now and the next legal attempt
  // is less than the timespanBetweenAttempts and the remaining attempts are less than 3
  const timeGap =
    new Date().getTime() - actualErrorData.nextLegalAttempt.getTime();
  const showLockModal =
    actualErrorData.remainingAttempts <= 3 &&
    timeGap < actualErrorData.timespanBetweenAttempts * 1000;
  return {
    ...actualErrorData,
    showLockModal
  };
};

const nextErrorData = (
  errorData: IdentificationFailData
): IdentificationFailData => {
  // avoid overflow of remaining attempts
  const nextRemainingAttempts = Math.max(1, errorData.remainingAttempts - 1);

  const newTimespan =
    maxAttempts - nextRemainingAttempts > freeAttempts
      ? Math.min(
          maxDeltaTimespan,
          errorData.timespanBetweenAttempts + deltaTimespanBetweenAttempts
        )
      : 0;
  return {
    nextLegalAttempt: new Date(Date.now() + newTimespan * 1000),
    remainingAttempts: nextRemainingAttempts,
    timespanBetweenAttempts: newTimespan,
    showLockModal: nextRemainingAttempts <= 3
  };
};

const reducer = (
  state: IdentificationState = INITIAL_STATE,
  action: Action
): IdentificationState => {
  switch (action.type) {
    case getType(identificationStart):
      return {
        ...state,
        progress: {
          kind: "started",
          ...action.payload
        }
      };

    case getType(identificationCancel):
      return {
        progress: {
          kind: "unidentified"
        },
        fail: state.fail
      };

    case getType(identificationSuccess):
      return {
        progress: {
          kind: "identified"
        }
      };

    case getType(identificationReset):
      return INITIAL_STATE;

    case getType(identificationHideLockModal):
      const failData = state.fail
        ? {
            ...state.fail,
            showLockModal: false
          }
        : undefined;
      return {
        ...state,
        fail: failData
      };

    case getType(identificationFailure):
      const newErrorData = pipe(
        state.fail,
        O.fromNullable,
        O.fold(
          () => ({
            nextLegalAttempt: new Date(),
            remainingAttempts: maxAttempts - 1,
            timespanBetweenAttempts: 0,
            showLockModal: false
          }),
          errorData => nextErrorData(errorData)
        )
      );
      return {
        ...state,
        fail: newErrorData
      };

    default:
      return state;
  }
};

export default reducer;

// Selectors
export const identificationFailSelector = (state: GlobalState) =>
  O.fromNullable(state.identification.fail);

export const progressSelector = (state: GlobalState) =>
  state.identification.progress;