Source: app/actions/resources/accountActions.js

import createAction from 'redux-actions/lib/createAction';
import { arraySplice } from 'redux-form';
import { push as historyPush } from 'react-router-redux';

import { isFree2GoRegisteringMember } from 'common/src/app/selectors/free2GoSelector';

import authenticate from '../../util/auth/authenticate';
import { apiPut, apiPatch, apiGet, apiPost, apiDelete, apiHead } from './apiActions/apiRequest';
import { STORE_IDENTITY } from '../storeIdentity';
import { startPayment, savePurchaseInfo } from './paymentActions';
import { createPackagePurchase, getPurchaseDetail } from './shopActions';
import { countryCodeValue } from '../../data/enum/Countries';
import { avatarPreview } from './profileActions';
import {
  addressFields,
  emailFields,
  measurementFields,
  membershipCardFields,
  passwordFields,
  shopFields,
  userDetailFields,
  termsFields,
} from '../../data/enum/FieldNames/AccountFieldNames';
import { getValue } from '../../util/injector';
import {
  GATEWAY_ACCOUNT,
  GATEWAY_ACCOUNT_AUTH,
  GATEWAY_ACCOUNT_IDS,
  GATEWAY_CONTENT,
} from '../../data/Injectables';
import { PAYMENT_COLLECTION_ID } from '../../data/collectionIds';
import AccountState from '../../data/enum/AccountState';
import MemberType from '../../data/enum/MemberType';
import apiGetEntity from './apiActions/apiGetEntity';
import { userAndEntityIdSelector, userIdSelector } from '../../selectors/userAccountSelectors';
import dedupeAsyncThunk from '../../util/dedupeAsyncThunk';
import { ACCOUNT, PAYMENT } from '../../data/entityTypes';
import apiGetCollection, { collectionCachingNoPagination } from './apiActions/apiGetCollection';
import isSubmissionError from '../../enhanced-redux-form/utils/isSubmissionError';
import FormNames from '../../data/enum/FormNames';
import { handleSubmitErrors } from '../../enhanced-redux-form/actions/enhancedFormActions';
import { setEntity } from '../../actions/entities/entityActions';
import { saveAccountsEmail, saveVerifiedIdentityToken } from './groupAccountActions';

// TODO SWO-4939 We need to split out this file.

export const SET_ACCOUNT_STATE = 'accountActions/SET_ACCOUNT_STATE';
export const setAccountState = createAction(SET_ACCOUNT_STATE);

export const CSRF_TOKEN = 'X-XSRF-Token';

export const getAntiForgeryToken = () => () =>
  getValue(GATEWAY_ACCOUNT).get('/antiforgery', null, {
    credentials: 'include',
  });

export const forgotPasswordHandler = email => dispatch =>
  dispatch(getAntiForgeryToken()).then(result =>
    dispatch(
      apiPost(
        'accountActions/IDS_RESET_PASSWORD',
        GATEWAY_ACCOUNT_IDS,
        '/request-password-reset',
        {
          email,
        },
        {
          headers: { [CSRF_TOKEN]: result.data },
        },
      ),
    ),
  );

export const resetPasswordHandler = (newPassword, userId, token) => dispatch =>
  dispatch(getAntiForgeryToken()).then(result =>
    dispatch(
      apiPost(
        'accountActions/IDS_CONFIRM_RESET_PASSWORD',
        GATEWAY_ACCOUNT_IDS,
        '/confirm-password-reset',
        {
          userId,
          token,
          newPassword,
        },
        {
          headers: { [CSRF_TOKEN]: result.data },
        },
      ),
    ),
  );

// eslint-disable-next-line no-underscore-dangle
const _SW_USER = '_sw_user';
export const GET_ACCOUNT = 'accountActions/GET_ACCOUNT';
export const getAccount = dedupeAsyncThunk(
  (getFresh = false, id) => async (dispatch, getState) => {
    await authenticate();
    const { selectedId, entityId } = userAndEntityIdSelector(getState(), id);
    return dispatch(
      apiGetEntity(
        GET_ACCOUNT,
        GATEWAY_ACCOUNT_AUTH,
        `/accounts/${selectedId}`,
        ACCOUNT,
        entityId || id,
        {
          caching: getFresh ? false : undefined,
        },
      ),
    ).then(response => {
      /**
       * This global user data is used to prepopulate the salesforce chat in GTM instead of pushing
       * the PII data into the dataLayer
       * https://www.analyticsmania.com/post/get-data-user-javascript-variable/
       *
       * see GTM for salesforce trigger
       */
      if (response?.entity && window) {
        const { email, firstName, lastName, memberType } = response.entity;

        // eslint-disable-next-line no-underscore-dangle
        window[_SW_USER] = {
          email,
          firstName,
          lastName,
          isGroupMember: memberType === MemberType.GROUP,
        };
      }
    });
  },
  true,
);

/*
export const getAccount = dedupeAsyncThunk(
  (getFresh = false, id) => async (dispatch, getState) => dispatch(
    apiGetEntity(
      GET_ACCOUNT,
      GATEWAY_ACCOUNT_AUTH,
      `/accounts/${selectedId}`,
      ACCOUNT,
      await getEntityId(),
      {
        caching: getFresh ? false : undefined,
      },
    ),
  ),
  true,
);
*/

export const CHANGE_PASSWORD = 'accountActions/CHANGE_PASSWORD';

export const changePassword = (oldPassword, newPassword) => dispatch =>
  dispatch(
    apiPost(CHANGE_PASSWORD, GATEWAY_ACCOUNT_IDS, '/change-password', {
      newPassword,
      oldPassword,
    }),
  );

export const CHANGE_EMAIL = 'accountActions/CHANGE_EMAIL';

export const changeEmail = (password, newEmail) => dispatch =>
  dispatch(
    apiPost(CHANGE_EMAIL, GATEWAY_ACCOUNT_IDS, '/change-email', {
      password,
      newEmail,
    }),
  );

export const GET_TERMS_VERSION = 'accountActions/GET_TERMS_VERSION';

export const getTermsVersion = () => dispatch =>
  dispatch(apiGet(GET_TERMS_VERSION, GATEWAY_CONTENT, '/pages/terms-of-use', null)).then(
    response => response.data.pageVersion,
  );

export const createAccount = (values, redirectOnPayment, redirectSkipPayment, isGroupRegister) => (
  dispatch,
  getState,
) => {
  // Get the state object
  const state = getState();
  const inviteSkipPayment = state.registration?.invite?.skipPaymentDuringRegistration;
  const inviteId = state.registration?.invite?.id || null;
  const isFree2GoRegistration = isFree2GoRegisteringMember(state) || false;

  const data = [
    emailFields.EMAIL_ADDRESS,
    passwordFields.PASSWORD,
    measurementFields.HEIGHT,
    measurementFields.INITIAL_WEIGHT,
    measurementFields.WEIGHT_UNIT,
    measurementFields.HEIGHT_UNIT,
    membershipCardFields.PIN,
    membershipCardFields.CARD_NUMBER,
    userDetailFields.FIRST_NAME,
    userDetailFields.LAST_NAME,
    userDetailFields.GENDER,
    userDetailFields.DATE_OF_BIRTH,
    shopFields.VOUCHER_CODE,
    addressFields.ADDRESS_LINE_1,
    addressFields.ADDRESS_LINE_2,
    addressFields.ADDRESS_LINE_3,
    addressFields.CITY_OR_TOWN,
    addressFields.STATE,
    addressFields.ZIP_OR_POSTAL,
    addressFields.COUNTRY,
    addressFields.COUNTY,
    userDetailFields.PHONE_NUMBER,
    userDetailFields.SECURITY_QUESTION,
    userDetailFields.SECURITY_ANSWER,
    userDetailFields.IS_PREGNANT,
    userDetailFields.IS_BREASTFEEDING,
    userDetailFields.RECORD_WEIGHT,
    shopFields.REGION,
    termsFields.RECEIVE_CONSULTANT_SUPPORT_EMAILS,
  ].reduce((d, field) => {
    d[field] = values[field]; // eslint-disable-line no-param-reassign

    /**
     * Coerce the isPregnant and isBreastfeeding options to booleans the hacky
     * way because the required basic-validation doesn't support true/false values
     */
    if (field === userDetailFields.IS_PREGNANT || field === userDetailFields.IS_BREASTFEEDING)
      d[field] = values[field] === '1'; // eslint-disable-line no-param-reassign

    return d;
  }, {});

  return dispatch(getTermsVersion()).then(termsOfServiceVersion =>
    dispatch(getAntiForgeryToken())
      .then(({ data: xsrfToken }) => {
        data.termsOfServiceVersion = termsOfServiceVersion;
        data.inviteId = inviteId;
        data.isGroupRegister = isGroupRegister;
        // the securityQuestionId needs to be an int
        data.securityQuestionId = data.securityQuestionId && parseInt(data.securityQuestionId, 10);

        if (data.country !== countryCodeValue.US) {
          delete data.state;
        }

        return getValue(GATEWAY_ACCOUNT)
          .post('/accounts/', data, {
            headers: { [CSRF_TOKEN]: xsrfToken },
            credentials: 'include',
          })
          .then(({ skipPaymentDuringRegistration }) => {
            getValue(GATEWAY_ACCOUNT)
              .post(
                '/registrations/',
                {
                  referrer: state.registration.referer,
                  source: state.registration.source,
                  campaign: state.registration.campaign,
                },
                {
                  credentials: 'include',
                },
              )
              .catch(() => {
                // this one can fail, doesn't impact registration
              });

            return skipPaymentDuringRegistration;
          });
      })
      .then(skipPaymentDuringRegistration => {
        if (inviteSkipPayment || skipPaymentDuringRegistration || isGroupRegister) {
          let skipReason;
          // if the flow is groupRegister and we have an invite:
          // set the reason to: invite group register
          if (isGroupRegister && inviteId) {
            skipReason = 'invite-group-register';
          } else if (isGroupRegister) {
            skipReason = 'groupRegister';
          } else if (inviteSkipPayment) {
            skipReason = 'invite';
          } else {
            skipReason = 'voucher';
          }
          // since we skip payment, we asume this account will have a valid subscription
          // there is no page reload, so we need to update this internally
          if (isGroupRegister) {
            // only setAccountState when the flow is not free2GoRegistration
            if (!isFree2GoRegistration) {
              dispatch(setAccountState(AccountState.GROUP_MEMBERSHIP_VALID));
            }
            // we need to navigate to a different domain
            window.location.href = redirectSkipPayment;
            return null;
          }
          dispatch(setAccountState(AccountState.ONLINE_SUBSCRIPTION_VALID));
          // since we skip payment, we asume this account will have a valid subscription
          // there is no page reload, so we need to update this internally

          // need to do a hard redirect to request identity and cookies
          document.location.href = `${redirectSkipPayment}?skipReason=${skipReason}`;
          return null; // This will prevent the purchase request be called
        }
        dispatch(setAccountState(AccountState.NONE));

        const purchaseDetail = getPurchaseDetail(data);

        return authenticate(getState)
          .then(() =>
            dispatch(
              createPackagePurchase(
                purchaseDetail,
                values[shopFields.PACKAGE],
                values[shopFields.VOUCHER_CODE],
              ),
            ),
          )
          .then(({ data: purchaseId }) => {
            savePurchaseInfo(purchaseId, {
              [shopFields.PACKAGE]: values[shopFields.PACKAGE],
              [shopFields.VOUCHER_CODE]: values[shopFields.VOUCHER_CODE],
            });

            return dispatch(startPayment(redirectOnPayment, purchaseId));
          })
          .catch(error => {
            // error during purchase request. redirect to payment because registration is complete
            if (isSubmissionError(error)) {
              dispatch(handleSubmitErrors(FormNames.CHECKOUT, error.response.parsed.error));
            }
            return dispatch(historyPush(redirectOnPayment));
          });
      }),
  );
};

// Generic account updates. Updates both api and state.
export const UPDATE_ACCOUNT = 'accountActions/UPDATE_ACCOUNT';

export const updateAccount = (values, userId) => (dispatch, getState) => {
  const memberId = userIdSelector(getState());
  return dispatch(
    apiPatch(UPDATE_ACCOUNT, GATEWAY_ACCOUNT_AUTH, `/accounts/${userId || memberId}`, values),
  ).then(() => dispatch(setEntity(ACCOUNT, userId || memberId, values, true)));
};

// update username and security. Updates both api and state.
export const UPDATE_USERNAME_AND_SECURITY = 'accountActions/UPDATE_USERNAME_AND_SECURITY';

export const updateUsernameAndSecurity = (values, notifyUser = true) => (dispatch, getState) => {
  const memberId = userIdSelector(getState());
  return dispatch(
    apiPatch(
      UPDATE_USERNAME_AND_SECURITY,
      GATEWAY_ACCOUNT_AUTH,
      `/accounts/${memberId}/username-and-security?notifyUser=${notifyUser}`,
      values,
    ),
  ).then(() => dispatch(setEntity(ACCOUNT, memberId, values, true)));
};

export const UPDATE_USERNAME = 'accountActions/UPDATE_USERNAME';

export const updateUserName = (userName, notifyUser = true) => (dispatch, getState) => {
  const memberId = userIdSelector(getState());
  return dispatch(
    apiPut(
      UPDATE_USERNAME,
      GATEWAY_ACCOUNT_AUTH,
      `/accounts/${memberId}/username?notifyUser=${notifyUser}`,
      userName,
    ),
  ).then(() => dispatch(setEntity(ACCOUNT, memberId, { userName }, true)));
};

export const UPDATE_TIMEZONE = 'accountActions/UPDATE_TIMEZONE';

export const updateUserTimeZone = timeZone => (dispatch, getState) => {
  const memberId = userIdSelector(getState());
  return dispatch(
    apiPatch(UPDATE_TIMEZONE, GATEWAY_ACCOUNT_AUTH, `/accounts/${memberId}/`, {
      timeZoneId: timeZone,
    }),
  ).then(() => dispatch(getAccount(true)));
};

export const UPDATE_ACCOUNT_AVATAR = 'accountActions/UPDATE_ACCOUNT_AVATAR';

export const updateAvatar = avatarData => (dispatch, getState) => {
  const userId = userIdSelector(getState());
  return dispatch(
    apiPut(UPDATE_ACCOUNT_AVATAR, GATEWAY_ACCOUNT_AUTH, `/accounts/${userId}/avatar`, {
      avatar: avatarData,
    }),
  ).then(() => dispatch(getAccount(true)));
};

export const getCroppedAvatarImage = () => (_, getState) => getState().imageUpload.avatarPreview;
export const saveAvatar = (formName, fieldName) => dispatch => {
  const avatar = dispatch(getCroppedAvatarImage());
  if (avatar) {
    dispatch(updateAvatar(avatar))
      .then(() => dispatch(arraySplice(formName, fieldName, 0, 1)))
      .then(() => dispatch(avatarPreview('', formName)));
  }
};

export const DELETE_ACCOUNT_AVATAR = 'accountActions/DELETE_ACCOUNT_AVATAR';

export const deleteAvatar = () => (dispatch, getState) => {
  const userId = userIdSelector(getState());
  return dispatch(
    apiDelete(DELETE_ACCOUNT_AVATAR, GATEWAY_ACCOUNT_AUTH, `/accounts/${userId}/avatar`),
  ).then(() => dispatch(setEntity(ACCOUNT, userId, { avatar: null }, true)));
};

export const GET_PAYMENTS = 'accountActions/GET_PAYMENTS';

export const getPayments = dedupeAsyncThunk(
  (id = 'me') => (dispatch, getState) => {
    const entityId = id === 'me' ? userIdSelector(getState()) : id;
    return dispatch(
      apiGetCollection(
        GET_PAYMENTS,
        GATEWAY_ACCOUNT_AUTH,
        `/accounts/${entityId}/subscription/payments`,
        PAYMENT_COLLECTION_ID,
        {},
        {
          entityType: PAYMENT,
          caching: collectionCachingNoPagination,
        },
      ),
    );
  },
  true,
);

export const SEND_EMAIL_CONFIRMATION = 'accountActions/SEND_EMAIL_CONFIRMATION';

export const sendEmailConfirmation = dedupeAsyncThunk(
  (id = 'me') => (dispatch, getState) => {
    const userId = id === 'me' ? userIdSelector(getState()) : id;
    return dispatch(
      apiPost(
        SEND_EMAIL_CONFIRMATION,
        GATEWAY_ACCOUNT_AUTH,
        `/accounts/${userId}/send-email-confirmation`,
      ),
    );
  },
  true,
);

export const GET_ACCOUNT_STATE = 'accountActions/GET_ACCOUNT_STATE';

export const getAccountState = () => dispatch =>
  dispatch(apiGet(GET_ACCOUNT_STATE, GATEWAY_ACCOUNT, '/accounts/account-state'));

export const LOGIN = 'accountActions/LOGIN';

export const loginCallback = (state, location) => {
  const form = document.createElement('form');
  const { host } = state.config.environmentConfig.api.account;
  const search = location && location.search ? location.search : '';

  form.method = 'post';
  form.action = `${host}/login/callback${search}`;

  document.body.appendChild(form);
  form.submit();
};

export const login = ({ userName, password, rememberMe }, location) => (dispatch, getState) =>
  dispatch(getAntiForgeryToken()).then(result =>
    dispatch(
      apiPost(
        LOGIN,
        GATEWAY_ACCOUNT_IDS,
        '/login',
        {
          userName,
          password,
          rememberMe,
        },
        {
          headers: { [CSRF_TOKEN]: result.data },
        },
      ),
    ).then(response => {
      if (!response) {
        loginCallback(getState(), location);
      }
    }),
  );

export const GET_CONFIRM_EMAIL = 'accountActions/GET_CONFIRM_EMAIL';

export const getConfirmEmail = (userId, token) => dispatch =>
  dispatch(getAntiForgeryToken()).then(result =>
    dispatch(
      apiPost(
        GET_CONFIRM_EMAIL,
        GATEWAY_ACCOUNT_IDS,
        '/confirm-email',
        {
          userId,
          token,
        },
        {
          headers: { [CSRF_TOKEN]: result.data },
        },
      ),
    ),
  );

export const GET_CONFIRM_EMAIL_CHANGE = 'accountActions/GET_CONFIRM_EMAIL_CHANGE';

export const getConfirmEmailChange = (userId, token) => dispatch =>
  dispatch(getAntiForgeryToken()).then(result =>
    dispatch(
      apiPost(
        GET_CONFIRM_EMAIL_CHANGE,
        GATEWAY_ACCOUNT_IDS,
        '/confirm-email-change',
        {
          userId,
          token,
        },
        {
          headers: { [CSRF_TOKEN]: result.data },
        },
      ),
    ),
  );

export const VERIFY_DETAILS = 'accountActions/VERIFY_DETAILS';

export const verifySecurityDetails = values => (dispatch, getState) => {
  const cardToken = getState().view.components.membershipCardCountry?.accountRecoveryTrackingToken;

  return dispatch(getAntiForgeryToken()).then(result =>
    dispatch(
      apiPost(
        VERIFY_DETAILS,
        GATEWAY_ACCOUNT,
        '/accounts/verify/name',
        { ...values, cardToken },
        {
          headers: {
            'X-XSRF-Token': result.data,
          },
          credentials: 'include',
        },
      ),
    ).then(obsfucatedEmail => {
      dispatch(saveAccountsEmail(obsfucatedEmail.data));
      return obsfucatedEmail.data;
    }),
  );
};

/**
 * Verify security answer
 * @param {string} SecurityQuestionAnswer
 * @param {string} CardToken
 *
 * @return {object}
 *
 * Card token should be return from the previous cardCheck funtion call
 */

export const VERIFY_SECURITY_ANSWER = 'accountActions/VERIFY_SECURITY_ANSWER';

export const verifySecurityAnswer = values => (dispatch, getState) => {
  const cardToken = getState().view.components.membershipCardCountry?.accountRecoveryTrackingToken;

  return dispatch(getAntiForgeryToken()).then(result =>
    dispatch(
      apiPost(
        VERIFY_SECURITY_ANSWER,
        GATEWAY_ACCOUNT,
        '/accounts/verify/answer',
        { ...values, cardToken },
        {
          headers: {
            'X-XSRF-Token': result.data,
          },
          credentials: 'include',
        },
      ),
    ).then(verifiedIdentityToken => {
      dispatch(saveVerifiedIdentityToken(verifiedIdentityToken.data));
      return verifiedIdentityToken.data;
    }),
  );
};

export const ADD_VERIFY_PASSWORD_TOKEN_ERROR = 'accountActions/ADD_VERIFY_PASSWORD_TOKEN_ERROR';
const addVerifyPasswordTokenError = createAction(ADD_VERIFY_PASSWORD_TOKEN_ERROR);

export const VERIFY_PASSWORD_TOKEN = 'accountActions/VERIFY_PASSWORD_TOKEN';
export const verifyPasswordToken = (userId, token) => dispatch =>
  dispatch(
    apiHead(VERIFY_PASSWORD_TOKEN, GATEWAY_ACCOUNT_IDS, '/verify-password-token', {
      token,
      userId,
    }),
  )
    .then(() => dispatch(addVerifyPasswordTokenError(false)))
    .catch(() => dispatch(addVerifyPasswordTokenError(true)));

export const LOGOUT = 'accountActions/LOGOUT';
export const identityServerLogout = (logoutId, confirmed = false) => dispatch =>
  dispatch(apiPost(LOGOUT, GATEWAY_ACCOUNT_IDS, '/logout', { logoutId, confirmed })).then(
    data => !data.showLogoutPrompt && dispatch(cleanUserIdentity()),
  );

export const cleanUserIdentity = createAction(STORE_IDENTITY);

// Reset the email address during the email recovery flow
export const RESET_EMAIL_ADDRESS = 'accountActions/RESET_EMAIL_ADDRESS';

export const resetEmailAddress = newEmail => (dispatch, getState) => {
  const state = getState();
  const verifiedIdentityToken = state.view.components.membershipCardCountry?.verifiedIdentityToken;
  const cardToken = state.view.components.membershipCardCountry?.accountRecoveryTrackingToken;

  return dispatch(getAntiForgeryToken()).then(result =>
    dispatch(
      apiPost(
        RESET_EMAIL_ADDRESS,
        GATEWAY_ACCOUNT_IDS,
        '/request-email-reset',
        {
          newEmail,
          verifiedIdentityToken,
          cardToken,
        },
        {
          headers: {
            'X-XSRF-Token': result.data,
          },
          credentials: 'include',
        },
      ),
    ),
  );
};

/**
 * Get user origin for tracking purposes
 */
export const GET_ACCOUNT_ORIGIN = 'accountActions/GET_ACCOUNT_ORIGIN';
export const getUserOrigin = () => async (dispatch, getState) => {
  await authenticate();
  const userId = userIdSelector(getState());
  return dispatch(
    apiGet(GET_ACCOUNT_ORIGIN, GATEWAY_ACCOUNT_AUTH, `/accounts/${userId}/user-origin`),
    // eslint-disable-next-line no-console
  ).catch(console.error);
};

export const SET_MEMBER_TYPE = 'accountActions/SET_MEMBER_TYPE';
export const setMemberType = type => (dispatch, getState) => {
  const userId = userIdSelector(getState());

  return dispatch(
    apiPut(SET_MEMBER_TYPE, GATEWAY_ACCOUNT_AUTH, `/accounts/${userId}/member-type-select`, {
      memberType: type,
    }),
  ).catch(console.error);
};