/* global WP_DEFINE_IS_NODE */
import moment from 'moment';
import { formValueSelector } from 'redux-form';
import { journeyFields } from 'common/src/app/data/enum/FieldNames/AccountFieldNames';
import AccountState from 'common/src/app/data/enum/AccountState';
import MemberType, { MemberTypeStrings } from 'common/src/app/data/enum/MemberType';
import FormNames from 'common/src/app/data/enum/FormNames';
import { subscriptionTypeSelector } from 'common/src/app/selectors/accountStateSelectors';
import marketConfig from 'common/src/app/config/market/market.configdefinitions';
import { userProfileSelector } from '../selectors/userProfileSelectors';
import Pages from '../data/enum/Pages';
import { hasFlag } from '../util/bitwiseUtils';
import { hasValidSubscriptionSelector } from '../selectors/registrationSelector';
import RouteRequirement from '../util/route-requirements/RouteRequirement';
import authenticate from '../util/auth/authenticate';
import { createPath } from '../util/routeUtils';
import RedirectError from '../util/RedirectError';
import AdyenStatus from '../data/enum/AdyenStatus';
import {
userAccountSelector,
userGroupIdsSelector,
isUserLoggedInSelector,
memberTypeSelector,
} from '../selectors/userAccountSelectors';
import { formIsSubmitted } from '../util/wizardStepDisable';
import UserRole from '../data/enum/UserRole';
import WebHost from '../data/enum/WebHost';
import { getUserPermissionStoredData } from '../util/userPermissionStateUtil';
/** @module */
/**
* Requires that the user is logged in __if on the server__. On the client, the check
* for authentication will happen in ClientAuthenticationManager. This is because
* the page render can start before authentication happens.
* @type {RouteRequirement}
*/
export const authenticated = new RouteRequirement(
'authenticated',
[],
({ getState }) =>
!WP_DEFINE_IS_NODE ||
typeof getUserPermissionStoredData({ getState }).accountState === 'number',
({}, { redirectToLogin }) => redirectToLogin(),
);
/**
* Requires that the user has completed migration flow.
*
* Note: On the client side, this RouteRequirement will wait until authentication is
* handled before checking for a migration status
* @type {RouteRequirement}
*/
export const isAuthenticatedAndNotMigrating = new RouteRequirement(
'isAuthenticatedAndNotMigrating',
[authenticated],
({ accountState }) =>
authenticate().then(() => {
showAccountStateWarning(accountState);
// return false if the user is still in Migration flow
return !hasFlag(accountState, AccountState.ONLINE_IS_MIGRATING);
}),
({}, { redirect }) => redirect(Pages.HOME, WebHost.MIGRATION),
);
/**
* Requires that a valid payment response has NOT parsed from the query string. If
* there is a valid payment response, redirect to the payment finished route.
* @type {RouteRequirement}
*/
export const noValidPaymentResponse = new RouteRequirement(
'noValidPaymentResponse',
[],
({ getState }) => {
const state = getState();
if (typeof state.payment.authResult === 'undefined') {
return true;
}
const authResult = parseInt(state.payment.authResult, 10);
return authResult !== AdyenStatus.SUCCESS;
},
({ renderProps }, { redirect }) =>
// keep query in url so the finished page can show the bought package
redirect(`${Pages.REGISTRATION_FINISHED}${renderProps.location.search}`),
);
/**
* Requires that the user has a payment response (including a non-success response) in the
* redux state
* @type {RouteRequirement}
*/
export const hasPaymentResponse = new RouteRequirement(
'hasPaymentResponse',
[],
({ getState }) => {
const state = getState();
return typeof state.payment.authResult !== 'undefined';
},
({}, { redirect }) => redirect('/'),
);
/**
* Requires that the user has a developer role
* @type {RouteRequirement}
*/
export const isDeveloper = new RouteRequirement(
'isDeveloper',
[isAuthenticatedAndNotMigrating],
({ getState, accountState }) =>
authenticate().then(() => {
showAccountStateWarning(accountState);
const userRoles = getUserPermissionStoredData({ getState }).roles;
return userRoles.includes(UserRole.DEVELOPER);
}),
({}, { redirect }) => redirect('/'),
);
/**
* @type function that @returns @type {RouteRequirement}
* Check group or online sub or just group if @param checkOnlyGroup is true
* Note: On the client side, this RouteRequirement will wait until authentication is
* handled before checking for a subscription
*/
export const subscriptionValid = new RouteRequirement(
'subscriptionValid',
[isAuthenticatedAndNotMigrating],
({ accountState }) =>
authenticate().then(() => {
showAccountStateWarning(accountState);
const onlineSubscriptionValid = hasFlag(accountState, AccountState.ONLINE_SUBSCRIPTION_VALID);
const groupSubscriptionvalid = hasFlag(accountState, AccountState.GROUP_MEMBERSHIP_VALID);
return onlineSubscriptionValid || groupSubscriptionvalid; // return true if we have a valid group or online subscripion
}),
({}, { redirect }) => {
redirect(Pages.ACCOUNT_SETTINGS_SUBSCRIPTION_STATUS, WebHost.MEMBER);
},
);
/**
* Requires that the user has a completed profile. If no completed profile is present,
* we will redirect the user to finish their registration. If the user has not payed yet, they will
* be redirected to account to finish payment. If a valid subscription is already present, the
* user will be redirected to the profile builder.
*
* Note: On the client side, this RouteRequirement will wait until authentication is
* handled before checking for a completed profile
* @type {RouteRequirement}
*/
export const profileCompleted = new RouteRequirement(
'profileCompleted',
[isAuthenticatedAndNotMigrating],
({ accountState }) =>
authenticate().then(() => {
showAccountStateWarning(accountState);
const onlineProfileComplete = hasFlag(accountState, AccountState.ONLINE_PROFILE_COMPLETE);
const groupProfileComplete = hasFlag(accountState, AccountState.GROUP_PROFILE_COMPLETE);
return onlineProfileComplete || groupProfileComplete; // return true if we have a completed group or online profile
}),
({ accountState }, { redirect }) => {
const onlineSubscriptionIsValid = hasFlag(accountState, AccountState.ONLINE_SUBSCRIPTION_VALID);
const groupSubscriptionIsValid = hasFlag(accountState, AccountState.GROUP_MEMBERSHIP_VALID);
if (!onlineSubscriptionIsValid && !groupSubscriptionIsValid) {
// payment should be completed before going to the online profile builder
return redirect(Pages.REGISTRATION_CHECKOUT, WebHost.ACCOUNT);
}
if (onlineSubscriptionIsValid) {
return redirect(Pages.PROFILE_BUILDER_ONLINE, WebHost.MEMBER);
}
if (groupSubscriptionIsValid) {
return redirect(Pages.PROFILE_BUILDER_GROUP, WebHost.MEMBER);
}
return redirect(Pages.PROFILE_BUILDER_ONLINE, WebHost.MEMBER);
},
);
/**
* Requires that the user does not have a completed profile
*
* Note: On the client side, this RouteRequirement will wait until authentication is
* handled before checking for a completed profile
* @type {RouteRequirement}
*/
export const profileNotCompleted = new RouteRequirement(
'profileNotComplete',
[isAuthenticatedAndNotMigrating],
({ accountState }) =>
authenticate().then(() => {
showAccountStateWarning(accountState);
const onlineProfileComplete = hasFlag(accountState, AccountState.ONLINE_PROFILE_COMPLETE);
const groupProfileComplete = hasFlag(accountState, AccountState.GROUP_PROFILE_COMPLETE);
// return false if the member has completed group or online profile builder and redirect the user to the homepage
return !(onlineProfileComplete || groupProfileComplete);
}),
({}, { redirect }) => redirect(`${Pages.HOME}`),
);
/**
* You cannot have an account during registration.
* If you do, when you don't have a subscription, you are redirected to the payment page.
* Otherwise you are redirected to the member site, or to the finished page when doing client-side
* routing.
*
* @type {RouteRequirement}
*/
export const noAccount = new RouteRequirement(
'noAccount',
[],
({ getState }) => !isUserLoggedInSelector(getState()),
({ getState }, { redirect }) => {
if (hasValidSubscriptionSelector(getState())) {
redirect(
WP_DEFINE_IS_NODE
? getState().config.environmentConfig.web.member.host
: Pages.REGISTRATION_FINISHED,
);
} else {
redirect(Pages.REGISTRATION_CHECKOUT);
}
},
);
export const weighInAllowed = new RouteRequirement(
'weighInAllowed',
[],
({ getState, accountState }) =>
authenticate().then(() => {
showAccountStateWarning(accountState);
const state = getState();
const profile = userProfileSelector(state);
// Weigh in is complete, see if the commitment was not set to allow access
if (profile?.isCurrentWeighInFinished === true) {
return !profile?.commitments?.[0]?.weight;
}
return profile?.isWeighInAllowed || profile?.isCurrentWeighInFinished !== true;
}),
({ getState }) => {
const state = getState();
const currentHost = state.config.environmentConfig.web.member.host;
const returnPath = `${currentHost}${Pages.HOME}`;
// Must use window location or it'll navigate to relative within /?=modal/
if (typeof window !== 'undefined') {
window.location = returnPath;
return null;
}
// for server side rendering
throw new RedirectError(returnPath);
},
);
/**
* A user hasn't confirmed their initial weights and heights before attempting
* the first weigh in of a journey, send them to do so.
* We also let them through if they've got more than one weigh in already
* as when this work is released we don't want to prompt all users
*/
export const hasConfirmedFirstWeighIn = new RouteRequirement(
'hasConfirmedFirstWeighIn',
[weighInAllowed],
({ getState, accountState }) =>
authenticate().then(() => {
showAccountStateWarning(accountState);
const state = getState();
const profile = userProfileSelector(state);
if (memberTypeSelector(state) === MemberType.GROUP) {
return true;
}
return profile?.currentJourney?.startDetailsConfirmed || profile?.weighIns?.length !== 0;
}),
({}, { redirect }) => redirect(Pages.FIRST_WEIGH_CONFIRMATION),
);
/**
* Same as hasConfirmedFirstWeighIn above but used for disabling wizard steps
*/
export const firstWeighInConfirmed = getState => {
const state = getState();
const profile = userProfileSelector(state);
return profile?.currentJourney?.startDetailsConfirmed;
};
/**
* Allow access to the initial height and weight confirmation process if on the first weigh in of a journey
* we also allow access if the form is in progress, because on step 3 we set the journey confirmation state,
* breaking the unconfirmed requirement
*/
export const allowFirstWeighInConfirmation = new RouteRequirement(
'allowFirstWeighInConfirmation',
[weighInAllowed],
({ getState, accountState }) =>
authenticate().then(() => {
showAccountStateWarning(accountState);
const state = getState();
const profile = userProfileSelector(state);
if (memberTypeSelector(state) === MemberType.GROUP) {
return false;
}
return (
(profile?.weighIns.length < 1 && !profile?.currentJourney?.startDetailsConfirmed) ||
state.routing.locationBeforeTransitions.query.modal === Pages.FIRST_WEIGH_BMI_OK_PROCEED
);
}),
({}, { redirect }) => redirect(Pages.WEIGHIN),
);
/**
* When a user has completed the initial checks but their BMI was too low
* and we don't want them to proceed with weigh in
*/
export const firstWeighInConfirmFailed = new RouteRequirement(
'firstWeighInConfirmFailed',
[],
({ getState, accountState }) =>
authenticate().then(() => {
showAccountStateWarning(accountState);
const state = getState();
const profile = userProfileSelector(state);
return profile?.currentJourney?.startDetailsConfirmed === false;
}),
({}, { redirect }) => redirect(Pages.WEIGHIN),
);
/**
* For weigh in, if you've confirmed weight you shouldn't be able
* to go back to that screen again as the BE will block you anyway
*/
export const hasNoWeightEntry = new RouteRequirement(
'hasNoWeightEntry',
[],
({ getState }) => !formIsSubmitted(getState, FormNames.WI_CONFIRM_WEIGHT),
({}, { redirect }) => redirect(Pages.WEIGHIN_MOOD),
);
export const isPregnant = new RouteRequirement(
'isPregnant',
[],
({ getState }) => {
const state = getState();
const profile = userProfileSelector(state);
return profile && profile.isPregnant;
},
({}, { redirect }) => redirect(Pages.WEIGHIN_AWARD),
);
/**
* Redirects the user to the correct page when going to the my slimming group
* @type {RouteRequirement}
*/
export const requireNoGroupId = new RouteRequirement(
'requireNoGroupId',
[isAuthenticatedAndNotMigrating],
({ getState, accountState }) => {
showAccountStateWarning(accountState);
const state = getState();
const account = userAccountSelector(state);
return !account.groupId;
},
({ getState }, { redirect }) => {
const groupId = userGroupIdsSelector(getState())[0];
return redirect(createPath(Pages.MY_SLIMMING_GROUP_OVERVIEW, { containerId: groupId }));
},
);
/**
* UK group registration - ensure that we have an inviteId to procceeed through group registration
* - check if /group-register/ has an inviteId
*/
export const inviteRequiredForGroupRegistration = new RouteRequirement(
'inviteRequiredForGroupRegistration',
[],
({ getState }) => getState().registration.invite?.id || null,
({}, { redirect }) => redirect(Pages.LOGIN),
);
export const marketHasCyprusGroups = new RouteRequirement(
'marketHasCyprusGroups',
[],
() => marketConfig.hasCyprusGroups,
({}, { redirect }) => redirect(Pages.UK_PUBLIC_NEAREST_GROUP_LANDING),
);
/**
* TODO: GR-336 can this be replaced entirely by the definitive check (e.g. memberType)
* Requires that a user has a valid subscription or is migrating
* by checking accountState flags.
*/
export const isOnlineMember = new RouteRequirement(
'isNotGroupMember',
[isAuthenticatedAndNotMigrating],
({ accountState }) =>
authenticate().then(() => {
showAccountStateWarning(accountState);
return subscriptionTypeSelector(accountState) === MemberTypeStrings.ONLINE;
}),
({ renderProps }, { redirect }) =>
redirect(
`${Pages.ACCESS_DENIED}${
renderProps?.location?.pathname === Pages.COMMUNITY ? '?from=community' : ''
}`,
WebHost.MEMBER,
),
);
/**
* Requires that a user has a valid group membership
*
* Note: On the client side, this RouteRequirement will wait until authentication is
* handled before checking for a subscription
* @param returnPath {String}
* @type {RouteRequirement}
*/
// TODO: TAT-586 redirect user to access denied page once it is role aware
export const isGroupMember = new RouteRequirement(
'isNotOnlineMember',
[isAuthenticatedAndNotMigrating],
({ accountState }) =>
authenticate().then(() => {
showAccountStateWarning(accountState);
return subscriptionTypeSelector(accountState) === MemberTypeStrings.GROUP;
}),
({}, { redirect }) => redirect(Pages.ACCOUNT_SETTINGS, WebHost.MEMBER),
);
/**
* As above, but using memberType rather than account state flags
* GR-336 to review and see if this can become the principal way to determine group or online
*/
export const memberTypeIsOnline = new RouteRequirement(
'memberTypeIsOnline',
[],
({ getState }) => memberTypeSelector(getState()) === MemberType.ONLINE,
({}, { redirect }) => redirect(Pages.ACCESS_DENIED, WebHost.MEMBER),
);
export const isOnAJourney = new RouteRequirement(
'isOnAJourney',
[isAuthenticatedAndNotMigrating],
getState =>
authenticate(getState).then(() => {
const state = getState();
const profile = userProfileSelector(state);
const currentJourney = profile?.currentJourney;
return !!currentJourney; // return true if we have a current journey
}),
(getState, renderProps, { redirect }) =>
redirect(`/?modal=${Pages.NEW_GROUP_JOURNEY_DATE}`, WebHost.MEMBER),
);
// Disable step if journey started today
export const journeyBeforeToday = state => {
const formSelector = formValueSelector(FormNames.NEW_GROUP_JOURNEY_START_DATE);
const theDate = formSelector(state, journeyFields.JOURNEY_START_DATE);
const dateFormat = 'DD,MM,YYYY';
return moment(theDate).format(dateFormat) === moment().format(dateFormat);
};
/**
* Requires that the user does NOT have the Free2Go role
* If they do have the Free2Go role - (redirect them to the Free2Go landing page)
* @type {RouteRequirement}
*/
export const isNotAFree2GoMember = new RouteRequirement(
'isNotAFree2GoMember',
[isAuthenticatedAndNotMigrating],
async ({ getState }) =>
await authenticate().then(() => {
const userRoles = getUserPermissionStoredData({ getState }).roles;
return userRoles && !userRoles.includes(UserRole.FREE2GO); // Return true if the user does not have the Free2Go role
}),
({}, { redirect }) => redirect(Pages.FREE_2_GO_LANDING, WebHost.ACCOUNT),
);
export const showAccountStateWarning = accountState => {
if (typeof accountState !== 'number') {
// the presence of the access token is already checked by the authenticated RouteRequirement
throw new ReferenceError('Expected access token to be present');
}
};