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);
};