feat: implement multi step registration experiment

This commit is contained in:
Syed Sajjad Hussain Shah
2024-04-03 11:28:39 +05:00
committed by mubbsharanwar
parent e2cdfce832
commit d66afe98f0
19 changed files with 346 additions and 27 deletions

View File

@@ -132,6 +132,12 @@ const messages = defineMessages({
defaultMessage: 'Company or school credentials',
description: 'Company or school login link text.',
},
// multi step registration experiment messages
'tab.back.btn.text': {
id: 'tab.back.btn.text',
defaultMessage: 'Back',
description: 'Tab back button text',
},
});
export default messages;

View File

@@ -35,6 +35,8 @@ const configuration = {
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
// Multi Step Registration Experiment
MULTI_STEP_REGISTRATION_EXPERIMENT_ID: process.env.MULTI_STEP_REGISTRATION_EXPERIMENT_ID || '',
};
export default configuration;

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { connect, useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -7,10 +7,11 @@ import { getAuthService } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Icon,
IconButton,
Tab,
Tabs,
} from '@openedx/paragon';
import { ChevronLeft } from '@openedx/paragon/icons';
import { ArrowBackIos, ChevronLeft } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { Navigate, useNavigate } from 'react-router-dom';
@@ -27,7 +28,11 @@ import {
import { LoginPage } from '../login';
import { backupLoginForm } from '../login/data/actions';
import { RegistrationPage } from '../register';
import { backupRegistrationForm } from '../register/data/actions';
import { backupRegistrationForm, setMultiStepRegistrationExpData } from '../register/data/actions';
import {
FIRST_STEP,
getMultiStepRegistrationPreviousStep,
} from '../register/data/optimizelyExperiment/helper';
const Logistration = (props) => {
const { selectedPage, tpaProviders } = props;
@@ -42,6 +47,10 @@ const Logistration = (props) => {
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
const hideRegistrationLink = getConfig().SHOW_REGISTRATION_LINKS === false;
const dispatch = useDispatch();
const multiStepRegExpVariation = useSelector(state => state.register.multiStepRegExpVariation);
const multiStepRegistrationPageStep = useSelector(state => state.register.multiStepRegistrationPageStep);
useEffect(() => {
const authService = getAuthService();
if (authService) {
@@ -91,6 +100,39 @@ const Logistration = (props) => {
</div>
);
/**
* Temporary function created to resolve the complexity in tabs conditioning for multi-step
* registration experiment
*/
const getTabs = () => {
if (multiStepRegistrationPageStep !== FIRST_STEP) {
const prevStep = getMultiStepRegistrationPreviousStep(multiStepRegistrationPageStep);
return (
<div>
<IconButton
key="primary"
src={ArrowBackIos}
iconAs={Icon}
alt="Back"
onClick={() => {
dispatch(setMultiStepRegistrationExpData(multiStepRegExpVariation, prevStep));
}}
variant="primary"
size="inline"
className="mr-1"
/>
{formatMessage(messages['tab.back.btn.text'])}
</div>
);
}
return (
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={(tabKey) => handleOnSelect(tabKey, selectedPage)}>
<Tab title={formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
<Tab title={formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
</Tabs>
);
};
const isValidTpaHint = () => {
const { provider } = getTpaProvider(tpaHint, providers, secondaryProviders);
return !!provider;
@@ -123,12 +165,7 @@ const Logistration = (props) => {
<Tab title={tabTitle} eventKey={selectedPage === LOGIN_PAGE ? LOGIN_PAGE : REGISTER_PAGE} />
</Tabs>
)
: (!isValidTpaHint() && !hideRegistrationLink && (
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={(tabKey) => handleOnSelect(tabKey, selectedPage)}>
<Tab title={formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
<Tab title={formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
</Tabs>
))}
: (!isValidTpaHint() && !hideRegistrationLink && getTabs())}
{ key && (
<Navigate to={updatePathWithQueryParams(key)} replace />
)}

View File

@@ -15,12 +15,16 @@ import {
} from '../data/constants';
import { backupLoginForm } from '../login/data/actions';
import { backupRegistrationForm } from '../register/data/actions';
import { FIRST_STEP, NOT_INITIALIZED } from '../register/data/optimizelyExperiment/helper';
import useMultiStepRegistrationExperimentVariation
from '../register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
jest.mock('../register/data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
@@ -63,6 +67,8 @@ describe('Logistration', () => {
registrationError: {},
usernameSuggestions: [],
validationApiRateLimited: false,
multiStepRegExpVariation: '',
multiStepRegistrationPageStep: FIRST_STEP,
},
commonComponents: {
thirdPartyAuthContext: {
@@ -83,6 +89,7 @@ describe('Logistration', () => {
username: 'test-user',
})),
}));
useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
configure({
loggingService: { logError: jest.fn() },

View File

@@ -176,7 +176,7 @@ const RegistrationPage = (props) => {
// This is used by the "User Retention Rate Event" on GTM
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
}
}, [registrationResult]);
}, [registrationResult]); // eslint-disable-line react-hooks/exhaustive-deps
const handleOnChange = (event) => {
const { name } = event.target;

View File

@@ -17,6 +17,9 @@ import {
setUserPipelineDataLoaded,
} from './data/actions';
import { INTERNAL_SERVER_ERROR } from './data/constants';
import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
import useMultiStepRegistrationExperimentVariation
from './data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from './RegistrationPage';
import {
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
@@ -30,6 +33,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('./data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -128,6 +132,7 @@ describe('RegistrationPage', () => {
institutionLogin: false,
};
window.location = { search: '' };
useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {

View File

@@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
import { FormFieldRenderer } from '../../field-renderer';
import { FIELDS } from '../data/constants';
import { FIRST_STEP, shouldDisplayFieldInExperiment } from '../data/optimizelyExperiment/helper';
import messages from '../messages';
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
@@ -31,6 +32,8 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors,
setFormFields,
autoSubmitRegistrationForm,
multiStepRegistrationExpVariation,
multiStepRegistrationPageStep,
} = props;
/** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites
@@ -109,7 +112,9 @@ const ConfigurableRegistrationForm = (props) => {
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
};
if (flags.showConfigurableRegistrationFields) {
if (flags.showConfigurableRegistrationFields && shouldDisplayFieldInExperiment(
'other_fields', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
)) {
Object.keys(fieldDescriptions).forEach(fieldName => {
const fieldData = fieldDescriptions[fieldName];
switch (fieldData.name) {
@@ -161,7 +166,9 @@ const ConfigurableRegistrationForm = (props) => {
});
}
if (flags.showConfigurableEdxFields || showCountryField) {
if ((flags.showConfigurableEdxFields || showCountryField) && shouldDisplayFieldInExperiment(
'country', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
)) {
formFieldDescriptions.push(
<span key="country">
<CountryField
@@ -177,7 +184,9 @@ const ConfigurableRegistrationForm = (props) => {
);
}
if (flags.showMarketingEmailOptInCheckbox) {
if (flags.showMarketingEmailOptInCheckbox && shouldDisplayFieldInExperiment(
'marketing_email_opt_in', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
)) {
formFieldDescriptions.push(
<span key="marketing_email_opt_in">
<FormFieldRenderer
@@ -196,7 +205,10 @@ const ConfigurableRegistrationForm = (props) => {
);
}
if (flags.showConfigurableEdxFields || showTermsOfServiceAndHonorCode) {
if ((flags.showConfigurableEdxFields || showTermsOfServiceAndHonorCode)
&& shouldDisplayFieldInExperiment(
'honor_code', multiStepRegistrationExpVariation, multiStepRegistrationPageStep,
)) {
formFieldDescriptions.push(
<span key="honor_code">
<HonorCode fieldType="tos_and_honor_code" onChangeHandler={handleOnChange} value={formFields.honor_code} />
@@ -231,11 +243,15 @@ ConfigurableRegistrationForm.propTypes = {
setFieldErrors: PropTypes.func.isRequired,
setFormFields: PropTypes.func.isRequired,
autoSubmitRegistrationForm: PropTypes.bool,
multiStepRegistrationExpVariation: PropTypes.string,
multiStepRegistrationPageStep: PropTypes.string,
};
ConfigurableRegistrationForm.defaultProps = {
fieldDescriptions: {},
autoSubmitRegistrationForm: false,
multiStepRegistrationExpVariation: '',
multiStepRegistrationPageStep: FIRST_STEP,
};
export default ConfigurableRegistrationForm;

View File

@@ -13,12 +13,13 @@ import {
TPA_AUTHENTICATION_FAILURE,
TPA_SESSION_EXPIRED,
} from '../data/constants';
import { FIRST_STEP } from '../data/optimizelyExperiment/helper';
import messages from '../messages';
const RegistrationFailureMessage = (props) => {
const { formatMessage } = useIntl();
const {
context, errorCode, failureCount,
context, errorCode, failureCount, multiStepRegistrationPageStep,
} = props;
useEffect(() => {
@@ -49,7 +50,11 @@ const RegistrationFailureMessage = (props) => {
errorMessage = formatMessage(messages['registration.tpa.session.expired'], { provider: context.provider });
break;
default:
errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
if (multiStepRegistrationPageStep !== FIRST_STEP) {
errorMessage = formatMessage(messages['multistep.registration.form.submission.error']);
} else {
errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
}
break;
}
@@ -65,6 +70,7 @@ RegistrationFailureMessage.defaultProps = {
context: {
errorMessage: null,
},
multiStepRegistrationPageStep: FIRST_STEP,
};
RegistrationFailureMessage.propTypes = {
@@ -74,6 +80,7 @@ RegistrationFailureMessage.propTypes = {
}),
errorCode: PropTypes.string.isRequired,
failureCount: PropTypes.number.isRequired,
multiStepRegistrationPageStep: PropTypes.string,
};
export default RegistrationFailureMessage;

View File

@@ -11,6 +11,9 @@ import configureStore from 'redux-mock-store';
import { registerNewUser } from '../../data/actions';
import { FIELDS } from '../../data/constants';
import { FIRST_STEP, NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useMultiStepRegistrationExperimentVariation
from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
@@ -22,6 +25,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
@@ -93,6 +97,9 @@ describe('ConfigurableRegistrationForm', () => {
registrationError: {},
registrationFormData,
usernameSuggestions: [],
multiStepRegistrationPageStep: FIRST_STEP,
multiStepRegExpVariation: '',
isValidatingMultiStepRegistrationPage: false,
},
commonComponents: {
thirdPartyAuthApiStatus: null,
@@ -121,6 +128,7 @@ describe('ConfigurableRegistrationForm', () => {
};
window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us'));
useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {

View File

@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
import {
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
} from '../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useMultiStepRegistrationExperimentVariation
from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import RegistrationFailureMessage from '../RegistrationFailure';
@@ -23,6 +26,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
@@ -121,6 +125,7 @@ describe('RegistrationFailure', () => {
institutionLogin: false,
};
window.location = { search: '' };
useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {

View File

@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
import {
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useMultiStepRegistrationExperimentVariation
from '../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -22,6 +25,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('../../data/optimizelyExperiment/useMultiStepRegistrationExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -120,6 +124,7 @@ describe('ThirdPartyAuth', () => {
institutionLogin: false,
};
window.location = { search: '' };
useMultiStepRegistrationExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {

View File

@@ -8,6 +8,7 @@ export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERRO
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
export const REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA = 'REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA';
// Backup registration form
export const backupRegistrationForm = () => ({
@@ -20,18 +21,19 @@ export const backupRegistrationFormBegin = (data) => ({
});
// Validate fields from the backend
export const fetchRealtimeValidations = (formPayload) => ({
export const fetchRealtimeValidations = (formPayload, isValidatingMultiStepRegistrationPage) => ({
type: REGISTER_FORM_VALIDATIONS.BASE,
payload: { formPayload },
payload: { formPayload, isValidatingMultiStepRegistrationPage },
});
export const fetchRealtimeValidationsBegin = () => ({
export const fetchRealtimeValidationsBegin = (isValidatingMultiStepRegistrationPage) => ({
type: REGISTER_FORM_VALIDATIONS.BEGIN,
payload: { isValidatingMultiStepRegistrationPage },
});
export const fetchRealtimeValidationsSuccess = (validations) => ({
export const fetchRealtimeValidationsSuccess = (validations, isValidatingMultiStepRegistrationPage) => ({
type: REGISTER_FORM_VALIDATIONS.SUCCESS,
payload: { validations },
payload: { validations, isValidatingMultiStepRegistrationPage },
});
export const fetchRealtimeValidationsFailure = () => ({
@@ -83,3 +85,11 @@ export const setUserPipelineDataLoaded = (value) => ({
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
payload: { value },
});
// Multi Step Registration Experiment Actions
export const setMultiStepRegistrationExpData = (
multiStepRegExpVariation, multiStepRegistrationPageStep, isValidatingMultiStepRegistrationPage,
) => ({
type: REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA,
payload: { multiStepRegExpVariation, multiStepRegistrationPageStep, isValidatingMultiStepRegistrationPage },
});

View File

@@ -0,0 +1,105 @@
/**
* This file contains data for Multi Step Registration Optimizely experiment
*/
import { getConfig } from '@edx/frontend-platform';
import messages from '../../messages';
export const NOT_INITIALIZED = 'experiment-not-initialized';
export const CONTROL = 'control-registration-page';
export const MULTI_STEP_REGISTRATION_EXP_VARIATION = 'multi-step-registration-page';
export const FIRST_STEP = 'first-step';
export const SECOND_STEP = 'second-step';
export const THIRD_STEP = 'third-step';
export const CONTROL_REGISTER_PAGE_FIRST_STEP_FIELDS = ['name', 'email', 'password', 'marketing_email_opt_in', 'ThirdPartyAuth'];
export const CONTROL_REGISTER_PAGE_SECOND_STEP_FIELDS = ['username', 'country'];
export const CONTROL_REGISTER_PAGE_COMMON_FIELDS = ['tos_and_honor_code', 'honor_code'];
export const MULTI_STEP_REGISTER_PAGE_FIRST_STEP_FIELDS = ['email', 'marketing_email_opt_in', 'ThirdPartyAuth'];
export const MULTI_STEP_REGISTER_PAGE_SECOND_STEP_FIELDS = ['name', 'password'];
export const MULTI_STEP_REGISTER_PAGE_THIRD_STEP_FIELDS = ['username', 'country'];
export const MULTI_STEP_REGISTER_PAGE_COMMON_FIELDS = ['tos_and_honor_code', 'honor_code'];
const MULTI_STEP_REGISTRATION_EXP_PAGE = 'authn_register_page';
export function getMultiStepRegistrationExperimentVariation() {
try {
if (window.optimizely
&& window.optimizely.get('data').experiments[getConfig().MULTI_STEP_REGISTRATION_EXPERIMENT_ID]) {
const selectedVariant = window.optimizely.get('state').getVariationMap()[
getConfig().MULTI_STEP_REGISTRATION_EXPERIMENT_ID
];
return selectedVariant?.name;
}
} catch (e) { /* empty */ }
return '';
}
export function activateMultiStepRegistrationExperiment() {
window.optimizely = window.optimizely || [];
window.optimizely.push({
type: 'page',
pageName: MULTI_STEP_REGISTRATION_EXP_PAGE,
});
}
/**
* We want to display username and honor_code fields in second page if user is in multi-step
* registration page experiment
*/
export const shouldDisplayFieldInExperiment = (fieldName, expVariation, registerPageStep) => (
!expVariation || expVariation === NOT_INITIALIZED
|| (expVariation === CONTROL
&& (
CONTROL_REGISTER_PAGE_COMMON_FIELDS.includes(fieldName)
|| (registerPageStep === FIRST_STEP && CONTROL_REGISTER_PAGE_FIRST_STEP_FIELDS.includes(fieldName))
|| (registerPageStep === SECOND_STEP && CONTROL_REGISTER_PAGE_SECOND_STEP_FIELDS.includes(fieldName))
))
|| (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION
&& (
MULTI_STEP_REGISTER_PAGE_COMMON_FIELDS.includes(fieldName)
|| (registerPageStep === FIRST_STEP && MULTI_STEP_REGISTER_PAGE_FIRST_STEP_FIELDS.includes(fieldName))
|| (registerPageStep === SECOND_STEP && MULTI_STEP_REGISTER_PAGE_SECOND_STEP_FIELDS.includes(fieldName))
|| (registerPageStep === THIRD_STEP && MULTI_STEP_REGISTER_PAGE_THIRD_STEP_FIELDS.includes(fieldName))
))
);
export const getRegisterButtonLabelInExperiment = (
existingButtonLabel, expVariation, registerPageStep, formatMessage,
) => {
if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep === FIRST_STEP) {
return formatMessage(messages['multistep.registration.exp.continue.button']);
}
return existingButtonLabel;
};
export const getRegisterButtonSubmitStateInExperiment = (
registerSubmitState, validationsSubmitState, expVariation, registerPageStep,
) => {
if (expVariation === MULTI_STEP_REGISTRATION_EXP_VARIATION && registerPageStep !== THIRD_STEP) {
return validationsSubmitState;
}
return registerSubmitState;
};
export const getMultiStepRegistrationPreviousStep = (currentStep) => {
if (currentStep === THIRD_STEP) {
return SECOND_STEP;
}
if (currentStep === SECOND_STEP) {
return FIRST_STEP;
}
return currentStep;
};
export const getMultiStepRegistrationNextStep = (currentStep) => {
if (currentStep === FIRST_STEP) {
return SECOND_STEP;
}
if (currentStep === SECOND_STEP) {
return THIRD_STEP;
}
return currentStep;
};

View File

@@ -0,0 +1,54 @@
import { useEffect, useState } from 'react';
import {
activateMultiStepRegistrationExperiment,
getMultiStepRegistrationExperimentVariation,
NOT_INITIALIZED,
} from './helper';
import { COMPLETE_STATE } from '../../../data/constants';
/**
* This hook returns activates multi step registration experiment and returns the experiment
* variation for the user.
*/
const useMultiStepRegistrationExperimentVariation = (
initExpVariation,
registrationEmbedded,
tpaHint,
currentProvider,
thirdPartyAuthApiStatus,
) => {
const [variation, setVariation] = useState(initExpVariation);
useEffect(() => {
if (initExpVariation || registrationEmbedded || !!tpaHint || !!currentProvider
|| thirdPartyAuthApiStatus !== COMPLETE_STATE) {
return variation;
}
const getVariation = () => {
const expVariation = getMultiStepRegistrationExperimentVariation();
if (expVariation) {
setVariation(expVariation);
} else {
// This is to handle the case when user dont get variation for some reason, the register page
// shows unlimited spinner.
setVariation(NOT_INITIALIZED);
}
};
activateMultiStepRegistrationExperiment();
const timer = setTimeout(getVariation, 300);
return () => {
clearTimeout(timer);
};
}, [ // eslint-disable-line react-hooks/exhaustive-deps
currentProvider, initExpVariation, registrationEmbedded, thirdPartyAuthApiStatus, tpaHint,
]);
return variation;
};
export default useMultiStepRegistrationExperimentVariation;

View File

@@ -5,9 +5,11 @@ import {
REGISTER_NEW_USER,
REGISTER_SET_COUNTRY_CODE,
REGISTER_SET_EMAIL_SUGGESTIONS,
REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA,
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTRATION_CLEAR_BACKEND_ERROR,
} from './actions';
import { FIRST_STEP } from './optimizelyExperiment/helper';
import {
DEFAULT_STATE,
PENDING_STATE,
@@ -35,10 +37,14 @@ export const defaultState = {
},
validations: null,
submitState: DEFAULT_STATE,
validationsSubmitState: DEFAULT_STATE,
userPipelineDataLoaded: false,
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
multiStepRegExpVariation: '',
multiStepRegistrationPageStep: FIRST_STEP,
isValidatingMultiStepRegistrationPage: false,
};
const reducer = (state = defaultState, action = {}) => {
@@ -85,12 +91,22 @@ const reducer = (state = defaultState, action = {}) => {
registrationError: { ...registrationErrorTemp },
};
}
case REGISTER_FORM_VALIDATIONS.BEGIN: {
return {
...state,
validationsSubmitState: action.payload?.isValidatingMultiStepRegistrationPage
? PENDING_STATE
: state.validationsSubmitState,
};
}
case REGISTER_FORM_VALIDATIONS.SUCCESS: {
const { usernameSuggestions, ...validationWithoutUsernameSuggestions } = action.payload.validations;
return {
...state,
validations: validationWithoutUsernameSuggestions,
isValidatingMultiStepRegistrationPage: !!action.payload?.isValidatingMultiStepRegistrationPage,
usernameSuggestions: usernameSuggestions || state.usernameSuggestions,
validationsSubmitState: DEFAULT_STATE,
};
}
case REGISTER_FORM_VALIDATIONS.FAILURE:
@@ -98,6 +114,7 @@ const reducer = (state = defaultState, action = {}) => {
...state,
validationApiRateLimited: true,
validations: null,
validationsSubmitState: DEFAULT_STATE,
};
case REGISTER_CLEAR_USERNAME_SUGGESTIONS:
return {
@@ -129,6 +146,14 @@ const reducer = (state = defaultState, action = {}) => {
emailSuggestion: action.payload.emailSuggestion,
},
};
case REGISTER_SET_MULTI_STEP_REGISTRATION_EXP_DATA: {
return {
...state,
multiStepRegExpVariation: action.payload.multiStepRegExpVariation,
multiStepRegistrationPageStep: action.payload.multiStepRegistrationPageStep,
isValidatingMultiStepRegistrationPage: !!action.payload?.isValidatingMultiStepRegistrationPage,
};
}
default:
return {
...state,

View File

@@ -40,10 +40,13 @@ export function* handleNewUserRegistration(action) {
export function* fetchRealtimeValidations(action) {
try {
yield put(fetchRealtimeValidationsBegin());
yield put(fetchRealtimeValidationsBegin(action.payload?.isValidatingMultiStepRegistrationPage));
const { fieldValidations } = yield call(getFieldsValidations, action.payload.formPayload);
yield put(fetchRealtimeValidationsSuccess(camelCaseObject(fieldValidations)));
yield put(fetchRealtimeValidationsSuccess(
camelCaseObject(fieldValidations),
action.payload?.isValidatingMultiStepRegistrationPage,
));
} catch (e) {
if (e.response && e.response.status === 403) {
yield put(fetchRealtimeValidationsFailure());

View File

@@ -11,6 +11,7 @@ import {
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTRATION_CLEAR_BACKEND_ERROR,
} from '../actions';
import { FIRST_STEP } from '../optimizelyExperiment/helper';
import reducer from '../reducers';
describe('Registration Reducer Tests', () => {
@@ -34,10 +35,14 @@ describe('Registration Reducer Tests', () => {
},
validations: null,
submitState: DEFAULT_STATE,
validationsSubmitState: DEFAULT_STATE,
userPipelineDataLoaded: false,
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
multiStepRegExpVariation: '',
multiStepRegistrationPageStep: FIRST_STEP,
isValidatingMultiStepRegistrationPage: false,
};
it('should return the initial state', () => {

View File

@@ -201,6 +201,29 @@ const messages = defineMessages({
defaultMessage: 'Did you mean',
description: 'Did you mean alert suggestion',
},
// MultiStep Registration experiment
'multistep.registration.exp.continue.button': {
id: 'multistep.registration.exp.continue.button',
defaultMessage: 'Continue',
description: 'Label text for multistep registration page second step',
},
'multistep.registration.username.second.step.guideline.content': {
id: 'multistep.registration.username.second.step.guideline.content',
defaultMessage: 'Finish Registration',
description: 'Guideline content for username field in multi-step registration experiment step 2',
},
'multistep.registration.username.third.step.guideline.content': {
id: 'multistep.registration.username.third.step.guideline.content',
defaultMessage: 'To finalize your registration, please confirm your country of residence '
+ 'and create a public username that will identify you in your course communication forums. '
+ 'The username cannot be changed.',
description: 'Guideline content for username field in multi-step registration experiment step 2',
},
'multistep.registration.form.submission.error': {
id: 'multistep.registration.form.submission.error',
defaultMessage: 'Please check your responses for this and the previous step and try again.',
description: 'Error message that appears on top of the form when invalid form is submitted',
},
});
export default messages;

View File

@@ -1,7 +1,3 @@
.register-button {
min-width: 14.4rem;
}
.pgn__form-autosuggest__wrapper > .pgn__form-group {
margin-bottom: 0 !important;
}