476 lines
18 KiB
JavaScript
476 lines
18 KiB
JavaScript
import React, {
|
|
useEffect, useMemo, useState,
|
|
} from 'react';
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
|
|
import { getConfig } from '@edx/frontend-platform';
|
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
import { Form, Spinner, StatefulButton } from '@openedx/paragon';
|
|
import classNames from 'classnames';
|
|
import PropTypes from 'prop-types';
|
|
import { Helmet } from 'react-helmet';
|
|
import Skeleton from 'react-loading-skeleton';
|
|
|
|
import {
|
|
InstitutionLogistration,
|
|
PasswordField,
|
|
RedirectLogistration,
|
|
ThirdPartyAuthAlert,
|
|
} from '../common-components';
|
|
import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm';
|
|
import RegistrationFailure from './components/RegistrationFailure';
|
|
import {
|
|
backupRegistrationFormBegin,
|
|
clearRegistrationBackendError,
|
|
registerNewUser,
|
|
setAutoGeneratedUsernameExperimentData,
|
|
setEmailSuggestionInStore,
|
|
setUserPipelineDataLoaded,
|
|
} from './data/actions';
|
|
import {
|
|
FORM_SUBMISSION_ERROR,
|
|
TPA_AUTHENTICATION_FAILURE,
|
|
} from './data/constants';
|
|
import useRecaptchaSubmission from './data/hooks';
|
|
import { AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION, NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
|
|
import useAutoGeneratedUsernameExperimentVariation from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
|
import getBackendValidations from './data/selectors';
|
|
import {
|
|
isFormValid, prepareRegistrationPayload,
|
|
} from './data/utils';
|
|
import messages from './messages';
|
|
import { EmailField, NameField, UsernameField } from './RegistrationFields';
|
|
import {
|
|
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
|
|
} from '../cohesion/constants';
|
|
import { setCohesionEventStates } from '../cohesion/data/actions';
|
|
import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../common-components/data/actions';
|
|
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
|
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
|
|
import {
|
|
APP_NAME, COMPLETE_STATE, PENDING_STATE,
|
|
REGISTER_PAGE,
|
|
} from '../data/constants';
|
|
import {
|
|
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, removeCookie, setCookie,
|
|
} from '../data/utils';
|
|
import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
|
|
/**
|
|
* Main Registration Page component
|
|
*/
|
|
const RegistrationPage = (props) => {
|
|
const { formatMessage } = useIntl();
|
|
const dispatch = useDispatch();
|
|
|
|
const { executeWithFallback } = useRecaptchaSubmission('submit_registration_form');
|
|
const registrationEmbedded = isHostAvailableInQueryParams();
|
|
const platformName = getConfig().SITE_NAME;
|
|
const flags = {
|
|
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
|
|
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
|
|
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
|
|
autoGeneratedUsernameEnabled: getConfig().ENABLE_AUTO_GENERATED_USERNAME,
|
|
};
|
|
const {
|
|
handleInstitutionLogin,
|
|
institutionLogin,
|
|
} = props;
|
|
|
|
const backedUpFormData = useSelector(state => state.register.registrationFormData);
|
|
const initExpVariation = useSelector(state => state.register.autoGeneratedUsernameExperimentVariation);
|
|
const registrationError = useSelector(state => state.register.registrationError);
|
|
const registrationErrorCode = registrationError?.errorCode;
|
|
const registrationResult = useSelector(state => state.register.registrationResult);
|
|
const shouldBackupState = useSelector(state => state.register.shouldBackupState);
|
|
const userPipelineDataLoaded = useSelector(state => state.register.userPipelineDataLoaded);
|
|
const submitState = useSelector(state => state.register.submitState);
|
|
|
|
const fieldDescriptions = useSelector(state => state.commonComponents.fieldDescriptions);
|
|
const optionalFields = useSelector(state => state.commonComponents.optionalFields);
|
|
const thirdPartyAuthApiStatus = useSelector(state => state.commonComponents.thirdPartyAuthApiStatus);
|
|
const autoSubmitRegForm = useSelector(state => state.commonComponents.thirdPartyAuthContext.autoSubmitRegForm);
|
|
const thirdPartyAuthErrorMessage = useSelector(state => state.commonComponents.thirdPartyAuthContext.errorMessage);
|
|
const finishAuthUrl = useSelector(state => state.commonComponents.thirdPartyAuthContext.finishAuthUrl);
|
|
const currentProvider = useSelector(state => state.commonComponents.thirdPartyAuthContext.currentProvider);
|
|
const providers = useSelector(state => state.commonComponents.thirdPartyAuthContext.providers);
|
|
const secondaryProviders = useSelector(state => state.commonComponents.thirdPartyAuthContext.secondaryProviders);
|
|
const pipelineUserDetails = useSelector(state => state.commonComponents.thirdPartyAuthContext.pipelineUserDetails);
|
|
const countriesCodesList = useSelector(state => state.commonComponents.countriesCodesList);
|
|
|
|
const backendValidations = useSelector(getBackendValidations);
|
|
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
|
const tpaHint = useMemo(() => getTpaHint(), []);
|
|
|
|
const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields });
|
|
const [configurableFormFields, setConfigurableFormFields] = useState({ ...backedUpFormData.configurableFormFields });
|
|
const [errors, setErrors] = useState({ ...backedUpFormData.errors });
|
|
const [errorCode, setErrorCode] = useState({ type: '', count: 0 });
|
|
const [formStartTime, setFormStartTime] = useState(null);
|
|
// temporary error state for embedded experience because we don't want to show errors on blur
|
|
const [temporaryErrors, setTemporaryErrors] = useState({ ...backedUpFormData.errors });
|
|
|
|
const { cta, host } = queryParams;
|
|
const buttonLabel = cta
|
|
? formatMessage(messages['create.account.cta.button'], { label: cta })
|
|
: formatMessage(messages['create.account.for.free.button']);
|
|
|
|
const autoGeneratedUsernameExpVariation = useAutoGeneratedUsernameExperimentVariation(
|
|
initExpVariation, registrationEmbedded, tpaHint, currentProvider, thirdPartyAuthApiStatus,
|
|
);
|
|
|
|
const hideUsernameField = flags.autoGeneratedUsernameEnabled
|
|
|| autoGeneratedUsernameExpVariation === AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION;
|
|
/**
|
|
* Set the userPipelineDetails data in formFields for only first time
|
|
*/
|
|
useEffect(() => {
|
|
if (!userPipelineDataLoaded && thirdPartyAuthApiStatus === COMPLETE_STATE) {
|
|
if (thirdPartyAuthErrorMessage) {
|
|
setErrorCode(prevState => ({ type: TPA_AUTHENTICATION_FAILURE, count: prevState.count + 1 }));
|
|
}
|
|
if (pipelineUserDetails && Object.keys(pipelineUserDetails).length !== 0) {
|
|
const { name = '', username = '', email = '' } = pipelineUserDetails;
|
|
setFormFields(prevState => ({
|
|
...prevState, name, username, email,
|
|
}));
|
|
dispatch(setUserPipelineDataLoaded(true));
|
|
}
|
|
}
|
|
}, [ // eslint-disable-line react-hooks/exhaustive-deps
|
|
thirdPartyAuthApiStatus,
|
|
thirdPartyAuthErrorMessage,
|
|
pipelineUserDetails,
|
|
userPipelineDataLoaded,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
if (!formStartTime) {
|
|
trackRegistrationPageViewed();
|
|
const payload = { ...queryParams, is_register_page: true };
|
|
if (tpaHint) {
|
|
payload.tpa_hint = tpaHint;
|
|
}
|
|
dispatch(getRegistrationDataFromBackend(payload));
|
|
setFormStartTime(Date.now());
|
|
}
|
|
}, [dispatch, formStartTime, queryParams, tpaHint]);
|
|
|
|
/**
|
|
* Backup the registration form in redux when register page is toggled.
|
|
*/
|
|
useEffect(() => {
|
|
if (shouldBackupState) {
|
|
dispatch(backupRegistrationFormBegin({
|
|
...backedUpFormData,
|
|
configurableFormFields: { ...configurableFormFields },
|
|
formFields: { ...formFields },
|
|
errors: { ...errors },
|
|
}));
|
|
dispatch(setAutoGeneratedUsernameExperimentData(autoGeneratedUsernameExpVariation));
|
|
}
|
|
}, [shouldBackupState, configurableFormFields, // eslint-disable-line react-hooks/exhaustive-deps
|
|
formFields, errors, dispatch, backedUpFormData]);
|
|
|
|
useEffect(() => {
|
|
if (backendValidations) {
|
|
if (registrationEmbedded) {
|
|
setTemporaryErrors(prevErrors => ({ ...prevErrors, ...backendValidations }));
|
|
} else {
|
|
setErrors(prevErrors => ({ ...prevErrors, ...backendValidations }));
|
|
}
|
|
}
|
|
}, [backendValidations, registrationEmbedded]);
|
|
|
|
useEffect(() => {
|
|
if (registrationErrorCode) {
|
|
setErrorCode(prevState => ({ type: registrationErrorCode, count: prevState.count + 1 }));
|
|
}
|
|
}, [registrationErrorCode]);
|
|
|
|
useEffect(() => {
|
|
if (registrationResult.success) {
|
|
// This event is used by GTM
|
|
trackRegistrationSuccess();
|
|
|
|
// This is used by the "User Retention Rate Event" on GTM
|
|
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
|
|
|
|
// Remove marketingEmailsOptIn cookie that was set on SSO registration flow
|
|
removeCookie('marketingEmailsOptIn');
|
|
// Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
|
|
removeCookie('ssoPipelineRedirectionDone');
|
|
}
|
|
}, [registrationResult]);
|
|
|
|
const handleOnChange = (event) => {
|
|
const { name } = event.target;
|
|
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
|
if (registrationError[name]) {
|
|
dispatch(clearRegistrationBackendError(name));
|
|
}
|
|
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
|
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
|
};
|
|
|
|
const handleErrorChange = (fieldName, error) => {
|
|
if (registrationEmbedded) {
|
|
setTemporaryErrors(prevErrors => ({
|
|
...prevErrors,
|
|
[fieldName]: error,
|
|
}));
|
|
if (error === '' && errors[fieldName] !== '') {
|
|
setErrors(prevErrors => ({
|
|
...prevErrors,
|
|
[fieldName]: error,
|
|
}));
|
|
}
|
|
} else {
|
|
setErrors(prevErrors => ({
|
|
...prevErrors,
|
|
[fieldName]: error,
|
|
}));
|
|
}
|
|
};
|
|
|
|
const registerUser = async () => {
|
|
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
|
|
let payload = { ...formFields, app_name: APP_NAME };
|
|
|
|
if (currentProvider) {
|
|
delete payload.password;
|
|
payload.social_auth_provider = currentProvider;
|
|
}
|
|
if (hideUsernameField) {
|
|
delete payload.username;
|
|
}
|
|
|
|
// Validating form data before submitting
|
|
const { isValid, fieldErrors, emailSuggestion } = isFormValid(
|
|
payload,
|
|
registrationEmbedded ? temporaryErrors : errors,
|
|
configurableFormFields,
|
|
fieldDescriptions,
|
|
formatMessage,
|
|
);
|
|
setErrors({ ...fieldErrors, captchaError: '' });
|
|
dispatch(setEmailSuggestionInStore(emailSuggestion));
|
|
|
|
// returning if not valid
|
|
if (!isValid) {
|
|
setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
|
|
return;
|
|
}
|
|
|
|
let recaptchaToken = null;
|
|
try {
|
|
recaptchaToken = await executeWithFallback();
|
|
} catch (err) {
|
|
setErrors(prev => ({
|
|
...prev,
|
|
captchaError: formatMessage(messages['registration.captcha.verification.label']) || err.message,
|
|
}));
|
|
return;
|
|
}
|
|
|
|
payload = prepareRegistrationPayload(
|
|
payload,
|
|
configurableFormFields,
|
|
flags.showMarketingEmailOptInCheckbox,
|
|
totalRegistrationTime,
|
|
queryParams,
|
|
);
|
|
if (recaptchaToken) {
|
|
payload = { ...payload, captcha_token: recaptchaToken };
|
|
}
|
|
|
|
dispatch(registerNewUser(payload));
|
|
};
|
|
|
|
const handleSubmit = (e) => {
|
|
e.preventDefault();
|
|
const eventData = {
|
|
pageType: PAGE_TYPES.ACCOUNT_CREATION,
|
|
elementType: ELEMENT_TYPES.BUTTON,
|
|
webElementText: ELEMENT_TEXT.CREATE_ACCOUNT,
|
|
webElementName: ELEMENT_NAME.CREATE_ACCOUNT,
|
|
};
|
|
|
|
dispatch(setCohesionEventStates(eventData));
|
|
registerUser();
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (autoSubmitRegForm && userPipelineDataLoaded) {
|
|
registerUser();
|
|
}
|
|
}, [autoSubmitRegForm, userPipelineDataLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
const renderForm = () => {
|
|
if (institutionLogin) {
|
|
return (
|
|
<InstitutionLogistration
|
|
secondaryProviders={secondaryProviders}
|
|
headingTitle={formatMessage(messages['register.institution.login.page.title'])}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<>
|
|
<Helmet>
|
|
<title>{formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}</title>
|
|
</Helmet>
|
|
<RedirectLogistration
|
|
host={host}
|
|
authenticatedUser={registrationResult.authenticatedUser}
|
|
success={registrationResult.success}
|
|
redirectUrl={registrationResult.redirectUrl}
|
|
finishAuthUrl={finishAuthUrl}
|
|
optionalFields={optionalFields}
|
|
registrationEmbedded={registrationEmbedded}
|
|
redirectToProgressiveProfilingPage={
|
|
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
|
|
}
|
|
currentProvider={currentProvider}
|
|
/>
|
|
{(autoSubmitRegForm && !errorCode.type)
|
|
|| (!autoGeneratedUsernameExpVariation && !(
|
|
autoGeneratedUsernameExpVariation === NOT_INITIALIZED
|
|
|| registrationEmbedded || !!tpaHint || !!currentProvider))
|
|
? (
|
|
<div className="mw-xs mt-5 text-center">
|
|
<Spinner animation="border" variant="primary" id="tpa-spinner" />
|
|
</div>
|
|
) : (
|
|
<div
|
|
className={classNames(
|
|
'mw-xs mt-3',
|
|
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded },
|
|
)}
|
|
>
|
|
<ThirdPartyAuthAlert
|
|
currentProvider={currentProvider}
|
|
platformName={platformName}
|
|
referrer={REGISTER_PAGE}
|
|
/>
|
|
<RegistrationFailure
|
|
errorCode={errorCode.type}
|
|
failureCount={errorCode.count}
|
|
context={{ provider: currentProvider, errorMessage: thirdPartyAuthErrorMessage }}
|
|
/>
|
|
<Form id="registration-form" name="registration-form">
|
|
<NameField
|
|
name="name"
|
|
value={formFields.name}
|
|
shouldFetchUsernameSuggestions={!formFields.username.trim()}
|
|
handleChange={handleOnChange}
|
|
handleErrorChange={handleErrorChange}
|
|
errorMessage={errors.name}
|
|
helpText={[formatMessage(messages['help.text.name'])]}
|
|
floatingLabel={formatMessage(messages['registration.fullname.label'])}
|
|
/>
|
|
<EmailField
|
|
name="email"
|
|
value={formFields.email}
|
|
confirmEmailValue={configurableFormFields?.confirm_email}
|
|
handleErrorChange={handleErrorChange}
|
|
handleChange={handleOnChange}
|
|
errorMessage={errors.email}
|
|
helpText={[formatMessage(messages['help.text.email'])]}
|
|
floatingLabel={formatMessage(messages['registration.email.label'])}
|
|
/>
|
|
{!hideUsernameField && (
|
|
<UsernameField
|
|
name="username"
|
|
spellCheck="false"
|
|
value={formFields.username}
|
|
handleChange={handleOnChange}
|
|
handleErrorChange={handleErrorChange}
|
|
errorMessage={errors.username}
|
|
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
|
|
floatingLabel={formatMessage(messages['registration.username.label'])}
|
|
/>
|
|
)}
|
|
{!currentProvider && (
|
|
<PasswordField
|
|
name="password"
|
|
value={formFields.password}
|
|
handleChange={handleOnChange}
|
|
handleErrorChange={handleErrorChange}
|
|
errorMessage={errors.password}
|
|
floatingLabel={formatMessage(messages['registration.password.label'])}
|
|
/>
|
|
)}
|
|
<ConfigurableRegistrationForm
|
|
email={formFields.email}
|
|
fieldErrors={errors}
|
|
formFields={configurableFormFields}
|
|
setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors}
|
|
setFormFields={setConfigurableFormFields}
|
|
autoSubmitRegisterForm={autoSubmitRegForm}
|
|
fieldDescriptions={fieldDescriptions}
|
|
countriesCodesList={countriesCodesList}
|
|
/>
|
|
{errors?.captchaError && (
|
|
<div className="mt-3 pgn__form-text-invalid pgn__form-text">
|
|
{errors.captchaError}
|
|
</div>
|
|
)}
|
|
<StatefulButton
|
|
id="register-user"
|
|
name="register-user"
|
|
type="submit"
|
|
variant="brand"
|
|
className="register-button mt-4 mb-4"
|
|
state={submitState}
|
|
labels={{
|
|
default: buttonLabel,
|
|
pending: '',
|
|
}}
|
|
onClick={handleSubmit}
|
|
onMouseDown={(e) => e.preventDefault()}
|
|
/>
|
|
{!registrationEmbedded && (
|
|
<ThirdPartyAuth
|
|
currentProvider={currentProvider}
|
|
providers={providers}
|
|
secondaryProviders={secondaryProviders}
|
|
handleInstitutionLogin={handleInstitutionLogin}
|
|
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
|
/>
|
|
)}
|
|
</Form>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
if (tpaHint) {
|
|
if (thirdPartyAuthApiStatus === PENDING_STATE) {
|
|
return <Skeleton height={36} />;
|
|
}
|
|
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
|
|
if (skipHintedLogin) {
|
|
window.location.href = getConfig().LMS_BASE_URL + provider.registerUrl;
|
|
return null;
|
|
}
|
|
return provider ? <EnterpriseSSO provider={provider} /> : renderForm();
|
|
}
|
|
return (
|
|
renderForm()
|
|
);
|
|
};
|
|
|
|
RegistrationPage.propTypes = {
|
|
institutionLogin: PropTypes.bool,
|
|
// Actions
|
|
handleInstitutionLogin: PropTypes.func,
|
|
};
|
|
|
|
RegistrationPage.defaultProps = {
|
|
handleInstitutionLogin: null,
|
|
institutionLogin: false,
|
|
};
|
|
|
|
export default RegistrationPage;
|