Zizzamia/perfume.js

View on GitHub
src/steps/navigationSteps.ts

Summary

Maintainability
B
5 hrs
Test Coverage
import { config } from './../config';
import { getNavigationState, getStepsFromNavigation, steps } from './steps';

/**
 * We only report the active steps in the last 2 navigation step to prevent
 * Convert the steps.navigationSteps to an array of active steps.
 * "stale steps" from being reported.
 *
 * "stale steps" are defined as steps where the start mark was executed but the
 * corresponding end mark was never executed - thus leaving it as a "stale step"
 */
export const getActiveStepsFromNavigationSteps = (): Record<
  string,
  boolean
> => {
  const navigationSteps = getNavigationState();
  const { startMarkToStepsMap } = steps;
  const numberOfSteps = Object.keys(navigationSteps).length;
  if (numberOfSteps === 0) {
    return {};
  }

  const activeSteps: Record<string, boolean> = {};
  const currentNavIndex = numberOfSteps - 1;

  const currentNavStep = getStepsFromNavigation(currentNavIndex);

  Object.keys(currentNavStep).forEach((startMark: string) => {
    const ongoingSteps = startMarkToStepsMap[startMark] ?? [];
    Object.keys(ongoingSteps).forEach(step => {
      activeSteps[step] = true;
    });
  });

  // if nav length is >1, then check the nav step prior as well
  if (numberOfSteps > 1) {
    const prevNavStep = getStepsFromNavigation(currentNavIndex - 1);

    Object.keys(prevNavStep).forEach((startMark: string) => {
      const prevNavSteps = startMarkToStepsMap[startMark] ?? [];
      Object.keys(prevNavSteps).forEach(step => {
        activeSteps[step] = true;
      });
    });
  }

  return activeSteps;
};

// start mark for navigationSteps data structure
export const recordStartMark = (startMark: string) => {
  const { navigationSteps } = steps;
  const numberOfSteps = Object.keys(navigationSteps).length;

  // if we don't have any initial states recorded, initialize one
  const navigationLength = numberOfSteps > 0 ? numberOfSteps : 1;
  const lastNavIndex = navigationLength - 1;
  const currentMarks = getStepsFromNavigation(lastNavIndex) || [];

  const newCurrentMark = currentMarks;
  newCurrentMark[startMark] = true;
  navigationSteps[lastNavIndex] = newCurrentMark;
};

export const recordEndMark = (endMark: string) => {
  const { navigationSteps, finalMarkToStepsMap } = steps;

  const navigationLength = Object.keys(navigationSteps).length;
  if (navigationLength === 0) {
    return;
  }

  const currentNavIndex = navigationLength - 1;
  const ongoingNavStep = getStepsFromNavigation(currentNavIndex);

  if (!ongoingNavStep || !finalMarkToStepsMap[endMark]) {
    return;
  }

  const endMarkMap = finalMarkToStepsMap[endMark];
  if (!endMarkMap) {
    return;
  }

  /**
   * loop through all steps where end mark is present and check if their start mark is present
   * in the navigation state. If so, remove it to signal that the step has been completed
   */
  Object.keys(endMarkMap).forEach(startMark => {
    if (ongoingNavStep[startMark]) {
      const currentNavStep = getStepsFromNavigation(currentNavIndex) || {};

      // Only remove the startMark, not other ongoing steps
      currentNavStep[startMark] = false;
      // if start mark is in navigation state, remove it to signal that the step has completed
      navigationSteps[currentNavIndex] = currentNavStep;
    }

    // if nav length is >1, then check the nav step prior as well
    if (navigationLength > 1) {
      const lastNavIndex = currentNavIndex - 1;
      const lastNavStep = getStepsFromNavigation(lastNavIndex);
      if (lastNavStep[startMark]) {
        lastNavStep[startMark] = false;
        navigationSteps[lastNavIndex] = lastNavStep;
      }
    }
  });
};

/**
 * An API consumers must use to update internal state based on page/screen navigations.
 * Internal state must be updated when the following are true
 * 1. Journey marks are hit
 * 2. Page navigations occur
 * This function ensures the 2nd case is covered to help prevent "stale"
 * steps that don't hit their final mark because the users navigates
 * backwards in journey.
 */
export const trackUJNavigation = () => {
  // navigationSteps are 0-index based, so size will give us the next key value
  const navigationLength = Object.keys(steps.navigationSteps).length;
  steps.navigationSteps[navigationLength] = {};

  // when navigating, we should notify the user in case stale steps are
  // removed from the state during navigations
  const activeSteps = getActiveStepsFromNavigationSteps();
  config.onMarkStep?.('', Object.keys(activeSteps));
};