department-of-veterans-affairs/vets-website

View on GitHub
src/applications/static-pages/cta-widget/index.js

Summary

Maintainability
D
1 day
Test Coverage
// Node modules.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import appendQuery from 'append-query';
import { connect } from 'react-redux';
// Relative imports.
import recordEvent from '~/platform/monitoring/record-event';
import { fetchMHVAccount } from '~/platform/user/profile/actions';
import { mhvUrl } from '~/platform/site-wide/mhv/utilities';
import sessionStorage from '~/platform/utilities/storage/sessionStorage';

import { isAuthenticatedWithSSOe } from '~/platform/user/authentication/selectors';
import { isLoggedIn, selectProfile } from '~/platform/user/selectors';
import {
  logout as IAMLogout,
  mfa,
} from '~/platform/user/authentication/utilities';
import { logoutUrlSiS } from '~/platform/utilities/oauth/utilities';
import { toggleLoginModal } from '~/platform/site-wide/user-nav/actions';
import { AUTH_EVENTS } from '~/platform/user/authentication/constants';
import MFA from './components/messages/DirectDeposit/MFA';
import ChangeAddress from './components/messages/ChangeAddress';
import DeactivatedMHVIds from './components/messages/DeactivatedMHVIds';
import DirectDeposit from './components/messages/DirectDeposit';
import DirectDepositUnAuthed from './components/messages/DirectDeposit/Unauthed';
import HealthToolsDown from './components/messages/HealthToolsDown';
import MultipleIds from './components/messages/MultipleIds';
import NeedsSSNResolution from './components/messages/NeedsSSNResolution';
import NoMHVAccount from './components/messages/NoMHVAccount';
import NotAuthorized from './components/messages/mvi/NotAuthorized';
import NotFound from './components/messages/mvi/NotFound';
import OpenMyHealtheVet from './components/messages/OpenMyHealtheVet';
import RegisterFailed from './components/messages/RegisterFailed';
import SignIn from './components/messages/SignIn';
import UpgradeAccount from './components/messages/UpgradeAccount';
import UpgradeFailed from './components/messages/UpgradeFailed';
import VAOnlineScheduling from './components/messages/VAOnlineScheduling';
import Verify from './components/messages/Verify';
import { ACCOUNT_STATES, ACCOUNT_STATES_SET } from './constants';
import { ctaWidgetsLookup, CTA_WIDGET_TYPES } from './ctaWidgets';

export class CallToActionWidget extends Component {
  static propTypes = {
    // Directly passed in props.
    appId: PropTypes.string,
    children: PropTypes.node,
    headerLevel: PropTypes.string,
    setFocus: PropTypes.bool,
    // From mapStateToProps.
    authenticatedWithSSOe: PropTypes.bool,
    featureToggles: PropTypes.object,
    isLoggedIn: PropTypes.bool,
    isVaPatient: PropTypes.bool,
    mhvAccount: PropTypes.object,
    mhvAccountIdState: PropTypes.string,
    mviStatus: PropTypes.string,
    profile: PropTypes.object,
    // From mapDispatchToProps.
    fetchMHVAccount: PropTypes.func.isRequired,
    toggleLoginModal: PropTypes.func.isRequired,
    ariaLabel: PropTypes.string,
    ariaDescribedby: PropTypes.string,
  };

  static defaultProps = {
    setFocus: true,
  };

  constructor(props) {
    super(props);
    const { appId } = props;
    const ctaWidget = ctaWidgetsLookup?.[appId];

    this._popup = null;
    this._requiredServices = ctaWidget?.requiredServices;
    this._serviceDescription = ctaWidget?.serviceDescription;
    this._mhvToolName = ctaWidget?.mhvToolName;
    this._toolDetails = ctaWidget?.deriveToolUrlDetails() || {};
    this._toolUrl = null;
    this._gaPrefix = 'register-mhv';
    this._featureToggle = ctaWidget?.featureToggle;
    this._headerLevel = ctaWidget?.headerLevel || props.headerLevel;
  }

  componentDidMount() {
    if (
      !this.props.featureToggles.loading &&
      this.props.isLoggedIn &&
      this.isHealthTool()
    ) {
      this.props.fetchMHVAccount();
    }
  }

  componentDidUpdate() {
    if (!this.props.isLoggedIn || this.props.featureToggles.loading) {
      return;
    }

    if (this.isAccessible()) {
      const { appId, authenticatedWithSSOe } = this.props;

      // Derive the CTA widget.
      const ctaWidget = ctaWidgetsLookup?.[appId];

      // Derive the url details.
      const toolURLDetails = ctaWidget?.deriveToolUrlDetails(
        authenticatedWithSSOe,
      );
      const url = toolURLDetails?.url;
      const redirect = toolURLDetails?.redirect;

      this._toolUrl = url;
      if (redirect && !this._popup) this.goToTool();
    }
    /*
      This is causing an infinite loop in production because the `v0/mhv_accounts` get request
      is returning a 404.

      else if (this.isHealthTool()) {
        const { accountState, loading } = this.props.mhvAccount;

        if (loading) return;

        if (!accountState) {
          this.props.fetchMHVAccount();
        } else if (
          new URLSearchParams(window.location.search).get('tc_accepted')
        ) {
          // Since T&C is still required to support the existing account states,
          // check the existence of a query param that gets appended after
          // successful T&C acceptance to complete account creation or upgrade.
          We are no longer creating or upgrading MHV accounts on VA.gov
        }
      }
    */
  }

  updateReturnUrl = () => {
    const { url, redirect } = this._toolDetails;
    if (url?.length > 0 && url?.startsWith('/') && redirect) {
      // fix the internal link
      sessionStorage.setItem(
        'authReturnUrl',
        `${window.location.origin}${url}`,
      );
    } else {
      sessionStorage.removeItem('authReturnUrl');
    }
  };

  getContent = () => {
    if (!this.props.isLoggedIn) {
      this.updateReturnUrl();
      if (this.props.appId === CTA_WIDGET_TYPES.DIRECT_DEPOSIT) {
        return (
          <DirectDepositUnAuthed
            primaryButtonHandler={this.openLoginModal}
            headerLevel={this._headerLevel}
            ariaLabel={this.props.ariaLabel}
            ariaDescribedby={this.props.ariaDescribedby}
          />
        );
      }
      return (
        <SignIn
          serviceDescription={this._serviceDescription}
          primaryButtonHandler={this.openLoginModal}
          headerLevel={this._headerLevel}
          ariaLabel={this.props.ariaLabel}
          ariaDescribedby={this.props.ariaDescribedby}
        />
      );
    }

    if (this.isHealthTool()) return this.getHealthToolContent();

    if (!this.props.profile.verified) {
      recordEvent({
        event: `${this._gaPrefix}-info-needs-identity-verification`,
      });
      return (
        <Verify
          serviceDescription={this._serviceDescription}
          primaryButtonHandler={this.verifyHandler}
          headerLevel={this._headerLevel}
        />
      );
    }

    if (this.isNonMHVSchedulingTool()) {
      if (this.hasMVIError()) {
        return this.getMviErrorContent();
      }

      return <VAOnlineScheduling featureToggles={this.props.featureToggles} />;
    }

    if (this.props.appId === CTA_WIDGET_TYPES.DIRECT_DEPOSIT) {
      if (!this.props.profile.multifactor) {
        return <MFA />;
      }
      return (
        <DirectDeposit
          serviceDescription={this._serviceDescription}
          primaryButtonHandler={() =>
            this.goToTool({
              event: 'nav-user-profile-cta',
            })
          }
        />
      );
    }

    if (this.props.appId === CTA_WIDGET_TYPES.CHANGE_ADDRESS) {
      return (
        <ChangeAddress
          featureToggles={this.props.featureToggles}
          serviceDescription={this._serviceDescription}
          primaryButtonHandler={this.goToTool}
        />
      );
    }

    return null;
  };

  getHealthToolContent = () => {
    const { mhvAccount, authenticatedWithSSOe } = this.props;

    if (this.hasMVIError()) {
      return this.getMviErrorContent();
    }

    if (authenticatedWithSSOe) {
      const errorContent = this.getInaccessibleHealthToolContentSSOe();
      if (errorContent) return errorContent;
      return (
        <OpenMyHealtheVet
          serviceDescription={this._serviceDescription}
          primaryButtonHandler={this.goToTool}
          toolName={this._mhvToolName}
        />
      );
    }

    if (this.isAccessible()) {
      return (
        <OpenMyHealtheVet
          serviceDescription={this._serviceDescription}
          primaryButtonHandler={this.goToTool}
          toolName={this._mhvToolName}
        />
      );
    }

    if (mhvAccount.errors) {
      recordEvent({ event: `${this._gaPrefix}-error-mhv-down` });
      return <HealthToolsDown />;
    }

    return this.getInaccessibleHealthToolContent();
  };

  getMviErrorContent = () => {
    switch (this.props.mviStatus) {
      case 'NOT_AUTHORIZED':
        return (
          <NotAuthorized
            authenticatedWithSSOe={this.props.authenticatedWithSSOe}
          />
        );
      case 'NOT_FOUND':
        return <NotFound />;
      default:
        return <HealthToolsDown />;
    }
  };

  getInaccessibleHealthToolContentSSOe = () => {
    const { profile, mhvAccountIdState } = this.props;

    if (!profile.verified) {
      recordEvent({
        event: `${this._gaPrefix}-info-needs-identity-verification`,
      });
      return (
        <Verify
          serviceDescription={this._serviceDescription}
          primaryButtonHandler={this.verifyHandler}
        />
      );
    }
    if (mhvAccountIdState === 'DEACTIVATED') {
      recordEvent({ event: `${this._gaPrefix}-error-has-deactivated-mhv-ids` });
      return <DeactivatedMHVIds />;
    }

    return null;
  };

  sendToMHV = () => {
    window.location = mhvUrl(this.props.authenticatedWithSSOe, 'home');
  };

  getInaccessibleHealthToolContent = () => {
    const { accountState } = this.props.mhvAccount;

    // If valid account error state, record GA event
    if (accountState && ACCOUNT_STATES_SET.has(accountState)) {
      recordEvent({
        event: `${this._gaPrefix}-${
          accountState === ACCOUNT_STATES.NEEDS_VERIFICATION ||
          accountState === ACCOUNT_STATES.NEEDS_TERMS_ACCEPTANCE
            ? 'info'
            : 'error'
        }-${accountState.replace(/_/g, '-')}`,
      });
    }

    switch (accountState) {
      case ACCOUNT_STATES.NEEDS_VERIFICATION:
        return (
          <Verify
            serviceDescription={this._serviceDescription}
            primaryButtonHandler={this.verifyHandler}
            headerLevel={this._headerLevel}
          />
        );

      case ACCOUNT_STATES.NEEDS_SSN_RESOLUTION:
        return <NeedsSSNResolution />;
      case ACCOUNT_STATES.DEACTIVATED_MHV_IDS:
        return <DeactivatedMHVIds />;

      case ACCOUNT_STATES.MULTIPLE_IDS:
        return <MultipleIds />;

      case ACCOUNT_STATES.REGISTER_FAILED:
        return <RegisterFailed />;

      case ACCOUNT_STATES.UPGRADE_FAILED:
        return <UpgradeFailed />;

      default: // Handle other content outside of block.
    }

    // If account creation or upgrade isn't blocked by any of the errors we
    // handle, show either create or upgrade CTA based on MHV account level.

    const { accountLevel } = this.props.mhvAccount;

    const redirectToTermsAndConditions = () => {
      const redirectQuery = { tc_redirect: window.location.pathname }; // eslint-disable-line camelcase
      const termsConditionsUrl = appendQuery(
        '/health-care/medical-information-terms-conditions/',
        redirectQuery,
      );
      window.location = termsConditionsUrl;
    };

    if (!accountLevel) {
      return (
        <NoMHVAccount
          serviceDescription={this._serviceDescription}
          primaryButtonHandler={
            accountState === 'needs_terms_acceptance'
              ? redirectToTermsAndConditions
              : this.sendToMHV
          }
          secondaryButtonHandler={this.signOut}
        />
      );
    }

    return (
      <UpgradeAccount
        serviceDescription={this._serviceDescription}
        primaryButtonHandler={
          accountState === 'needs_terms_acceptance'
            ? redirectToTermsAndConditions
            : this.sendToMHV
        }
      />
    );
  };

  hasMVIError = () => {
    const MviErrorStatuses = ['SERVER_ERROR', 'NOT_FOUND', 'NOT_AUTHORIZED'];

    return MviErrorStatuses.includes(this.props.mviStatus);
  };

  isNonMHVSchedulingTool = () =>
    this.props.featureToggles.vaOnlineScheduling &&
    (this.props.appId === CTA_WIDGET_TYPES.SCHEDULE_APPOINTMENTS ||
      this.props.appId === CTA_WIDGET_TYPES.VIEW_APPOINTMENTS);

  isAccessible = () => {
    if (this.isHealthTool()) {
      // Until MHV account eligibility rules no longer have to accommodate the
      // pre-brand consolidation flow, the frontend will gate access using the MHV
      // account level instead of the available services list from the backend,
      // which will already have validated the MHV account level policies.
      const { appId, mhvAccount } = this.props;

      // Derive the CTA widget.
      const ctaWidget = ctaWidgetsLookup?.[appId];

      return ctaWidget?.hasRequiredMhvAccount(mhvAccount.accountLevel);
    }
    if (this.props.appId === CTA_WIDGET_TYPES.DIRECT_DEPOSIT) {
      // Direct Deposit requires multifactor
      return this.props.profile.verified && this.props.profile.multifactor;
    }

    // Only check whether the account is verified here and leave any handling
    // of gated access for other reasons to the apps after redirect.
    return this.props.profile.verified;
  };

  isHealthTool = () => {
    const { appId } = this.props;

    // Derive the CTA widget.
    const ctaWidget = ctaWidgetsLookup?.[appId];

    return !this.isNonMHVSchedulingTool() && ctaWidget?.isHealthTool;
  };

  openLoginModal = () => this.props.toggleLoginModal(true);

  goToTool = gaEvent => {
    const url = this._toolUrl;
    if (!url) return;

    // Optionally push Google-Analytics event.
    if (gaEvent) {
      recordEvent(gaEvent);
    }

    if (url.startsWith('/')) {
      window.location = url;
    } else {
      this._popup = window.open(url, 'cta-popup');
      if (this._popup) this._popup.focus();
      else {
        // Indicate an attempted pop-up to avoid automatically showing a
        // pop-up later on a component update triggered by a state change.
        this._popup = true;
      }
    }
  };

  signOut = () => {
    recordEvent({ event: 'logout-link-clicked-createcta-mhv' });
    if (this.props.authenticatedWithSSOe) {
      IAMLogout();
    } else {
      window.location = logoutUrlSiS();
    }
  };

  mfaHandler = () => {
    recordEvent({ event: AUTH_EVENTS.MFA });
    mfa();
  };

  verifyHandler = () => {
    window.location.href = '/verify';
  };

  render() {
    const {
      appId,
      authenticatedWithSSOe,
      children,
      featureToggles,
      mhvAccount,
      profile,
      setFocus,
    } = this.props;

    // Show spinner if loading.
    if (profile.loading || mhvAccount.loading || featureToggles.loading) {
      return (
        <va-loading-indicator
          setFocus={setFocus}
          message="Loading your information..."
        />
      );
    }

    // Derive the CTA widget.
    const ctaWidget = ctaWidgetsLookup?.[appId];

    // Render nothing if feature toggle is off.
    if (!!this._featureToggle && !featureToggles[this._featureToggle]) {
      return null;
    }

    // Derive the CTA URL.
    const url = ctaWidget?.deriveToolUrlDetails(authenticatedWithSSOe)?.url;
    this._toolUrl = url;

    // Derive the content and show it if available.
    const content = this.getContent();
    if (content) return content;

    // Show the child nodes if provided.
    if (children) return children;

    // Derive anchor tag properties.
    const isInternalLink = this._toolUrl.startsWith('/');
    const target = isInternalLink ? '_self' : '_blank';
    const linkText = [
      this._serviceDescription[0].toUpperCase(),
      this._serviceDescription.slice(1),
    ].join('');

    // Show the CTA link.
    return (
      <a
        className="vads-c-action-link--green"
        href={this._toolUrl}
        target={target}
      >
        {linkText}
      </a>
    );
  }
}

const mapStateToProps = state => {
  // Derive profile properties.
  const {
    loading,
    mhvAccount,
    mhvAccountState,
    multifactor,
    status,
    vaPatient,
    verified,
  } = selectProfile(state);

  return {
    authenticatedWithSSOe: isAuthenticatedWithSSOe(state),
    featureToggles: state.featureToggles,
    isLoggedIn: isLoggedIn(state),
    isVaPatient: vaPatient,
    mhvAccount,
    mhvAccountIdState: mhvAccountState,
    mviStatus: status,
    profile: { loading, verified, multifactor },
  };
};

const mapDispatchToProps = {
  fetchMHVAccount,
  toggleLoginModal,
};

export default connect(
  mapStateToProps,
  mapDispatchToProps,
)(CallToActionWidget);