feat: refactor registration page
This commit is contained in:
2
.env
2
.env
@@ -25,6 +25,8 @@ REGISTER_CONVERSION_COOKIE_NAME=null
|
||||
ENABLE_PROGRESSIVE_PROFILING=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS=''
|
||||
SHOW_DYNAMIC_PROFILING_PAGE=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
APP_ID=''
|
||||
|
||||
@@ -12,6 +12,9 @@ import PropTypes from 'prop-types';
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* This component renders the Single sign-on (SSO) button only for the tpa provider passed
|
||||
* */
|
||||
const EnterpriseSSO = (props) => {
|
||||
const { intl } = props;
|
||||
const tpaProvider = props.provider;
|
||||
|
||||
@@ -37,7 +37,6 @@ const FormGroup = (props) => {
|
||||
onClick={handleClick}
|
||||
onChange={props.handleChange}
|
||||
controlClassName={props.borderClass}
|
||||
|
||||
trailingElement={props.trailingElement}
|
||||
floatingLabel={props.floatingLabel}
|
||||
>
|
||||
|
||||
@@ -8,6 +8,9 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* This component renders the Institution login button
|
||||
* */
|
||||
export const RenderInstitutionButton = props => {
|
||||
const { onSubmitHandler, buttonTitle } = props;
|
||||
|
||||
@@ -24,6 +27,9 @@ export const RenderInstitutionButton = props => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This component renders the page list of available institutions for login
|
||||
* */
|
||||
const InstitutionLogistration = props => {
|
||||
const lmsBaseUrl = getConfig().LMS_BASE_URL;
|
||||
const {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
@@ -15,14 +16,21 @@ import { Redirect } from 'react-router-dom';
|
||||
|
||||
import BaseComponent from '../base-component';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
import { getTpaHint, updatePathWithQueryParams } from '../data/utils';
|
||||
import { getTpaHint, getTpaProvider, updatePathWithQueryParams } from '../data/utils';
|
||||
import { LoginPage } from '../login';
|
||||
import { RegistrationPage } from '../register';
|
||||
import { backupRegistrationForm } from '../register/data/actions';
|
||||
import {
|
||||
tpaProvidersSelector,
|
||||
} from './data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
const Logistration = (props) => {
|
||||
const { intl, selectedPage } = props;
|
||||
const tpa = getTpaHint();
|
||||
const { intl, selectedPage, tpaProviders } = props;
|
||||
const tpaHint = getTpaHint();
|
||||
const {
|
||||
providers, secondaryProviders,
|
||||
} = tpaProviders;
|
||||
const [institutionLogin, setInstitutionLogin] = useState(false);
|
||||
const [key, setKey] = useState('');
|
||||
|
||||
@@ -46,6 +54,9 @@ const Logistration = (props) => {
|
||||
|
||||
const handleOnSelect = (tabKey) => {
|
||||
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
|
||||
if (tabKey === LOGIN_PAGE) {
|
||||
props.backupRegistrationForm();
|
||||
}
|
||||
setKey(tabKey);
|
||||
};
|
||||
|
||||
@@ -60,6 +71,11 @@ const Logistration = (props) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const isValidTpaHint = () => {
|
||||
const { provider } = getTpaProvider(tpaHint, providers, secondaryProviders);
|
||||
return !!provider;
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseComponent>
|
||||
<div>
|
||||
@@ -69,16 +85,14 @@ const Logistration = (props) => {
|
||||
<Tab title={tabTitle} eventKey={selectedPage === LOGIN_PAGE ? LOGIN_PAGE : REGISTER_PAGE} />
|
||||
</Tabs>
|
||||
)
|
||||
: (
|
||||
: (!isValidTpaHint() && (
|
||||
<>
|
||||
{!tpa && (
|
||||
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={handleOnSelect}>
|
||||
<Tab title={intl.formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
|
||||
<Tab title={intl.formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
|
||||
</Tabs>
|
||||
)}
|
||||
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={handleOnSelect}>
|
||||
<Tab title={intl.formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
|
||||
<Tab title={intl.formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
))}
|
||||
{ key && (
|
||||
<Redirect to={updatePathWithQueryParams(key)} />
|
||||
)}
|
||||
@@ -95,10 +109,31 @@ const Logistration = (props) => {
|
||||
Logistration.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
selectedPage: PropTypes.string,
|
||||
backupRegistrationForm: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.shape({
|
||||
providers: PropTypes.array,
|
||||
secondaryProviders: PropTypes.array,
|
||||
}),
|
||||
};
|
||||
|
||||
Logistration.defaultProps = {
|
||||
tpaProviders: {
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
},
|
||||
};
|
||||
|
||||
Logistration.defaultProps = {
|
||||
selectedPage: REGISTER_PAGE,
|
||||
};
|
||||
|
||||
export default injectIntl(Logistration);
|
||||
const mapStateToProps = state => ({
|
||||
tpaProviders: tpaProvidersSelector(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
backupRegistrationForm,
|
||||
},
|
||||
)(injectIntl(Logistration));
|
||||
|
||||
@@ -30,11 +30,11 @@ const PasswordField = (props) => {
|
||||
};
|
||||
|
||||
const HideButton = (
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
|
||||
);
|
||||
|
||||
const ShowButton = (
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
|
||||
);
|
||||
const placement = window.innerWidth < 768 ? 'top' : 'left';
|
||||
const tooltip = (
|
||||
|
||||
@@ -19,22 +19,32 @@ const ThirdPartyAuthAlert = (props) => {
|
||||
message = intl.formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
}
|
||||
|
||||
if (!currentProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2' : 'alert-warning mt-n2'}>
|
||||
<>
|
||||
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2' : 'alert-warning mt-n2'}>
|
||||
{referrer === REGISTER_PAGE ? (
|
||||
<Alert.Heading>{intl.formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
) : null}
|
||||
<p>{ message }</p>
|
||||
</Alert>
|
||||
{referrer === REGISTER_PAGE ? (
|
||||
<Alert.Heading>{intl.formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
<h4 className="mt-4 mb-4">{intl.formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
||||
) : null}
|
||||
<p>{ message }</p>
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ThirdPartyAuthAlert.defaultProps = {
|
||||
currentProvider: '',
|
||||
referrer: LOGIN_PAGE,
|
||||
};
|
||||
|
||||
ThirdPartyAuthAlert.propTypes = {
|
||||
currentProvider: PropTypes.string.isRequired,
|
||||
currentProvider: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
referrer: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -6,6 +6,14 @@ export const defaultState = {
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
},
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
|
||||
@@ -23,3 +23,11 @@ export const optionalFieldsSelector = createSelector(
|
||||
commonComponentsSelector,
|
||||
commonComponents => commonComponents.optionalFields,
|
||||
);
|
||||
|
||||
export const tpaProvidersSelector = createSelector(
|
||||
commonComponentsSelector,
|
||||
commonComponents => ({
|
||||
providers: commonComponents.thirdPartyAuthContext.providers,
|
||||
secondaryProviders: commonComponents.thirdPartyAuthContext.secondaryProviders,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -20,10 +20,8 @@ export async function getThirdPartyAuthContext(urlParams) {
|
||||
return {
|
||||
fieldDescriptions: data.registration_fields || {},
|
||||
optionalFields: data.optional_fields || {},
|
||||
// For backward compatibility with the API, once https://github.com/openedx/edx-platform/pull/30198 is merged
|
||||
// and deployed update it to use data.context_data
|
||||
thirdPartyAuthContext: camelCaseObject(
|
||||
convertKeyNames(data.context_data || data, { fullname: 'name' }),
|
||||
convertKeyNames(data.context_data, { fullname: 'name' }),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -123,6 +123,11 @@ const messages = defineMessages({
|
||||
description: 'Message that appears on register page if user has successfully authenticated with TPA '
|
||||
+ 'but no associated platform account exists',
|
||||
},
|
||||
'registration.using.tpa.form.heading': {
|
||||
id: 'registration.using.tpa.form.heading',
|
||||
defaultMessage: 'Finish creating your account',
|
||||
description: 'Heading that appears above form when user is trying to create account using social auth',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -40,8 +40,6 @@ describe('Logistration', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'test-user' }));
|
||||
});
|
||||
it('should render registration page', () => {
|
||||
configure({
|
||||
loggingService: { logError: jest.fn() },
|
||||
config: {
|
||||
@@ -50,14 +48,19 @@ describe('Logistration', () => {
|
||||
},
|
||||
messages: { 'es-419': {}, de: {}, 'en-us': {} },
|
||||
});
|
||||
});
|
||||
|
||||
it('should render registration page', () => {
|
||||
store = mockStore({
|
||||
register: {
|
||||
registrationResult: { success: false, redirectUrl: '' },
|
||||
registrationError: {},
|
||||
},
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration />));
|
||||
@@ -71,7 +74,10 @@ describe('Logistration', () => {
|
||||
loginResult: { success: false, redirectUrl: '' },
|
||||
},
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -21,26 +21,33 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
|
||||
`;
|
||||
|
||||
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
|
||||
<div
|
||||
className="fade alert-content alert-success mt-n2 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
>
|
||||
Array [
|
||||
<div
|
||||
className="pgn__alert-message-wrapper"
|
||||
className="fade alert-content alert-success mt-n2 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
>
|
||||
<div
|
||||
className="alert-message-content"
|
||||
className="pgn__alert-message-wrapper"
|
||||
>
|
||||
<div
|
||||
className="alert-heading h4"
|
||||
className="alert-message-content"
|
||||
>
|
||||
Almost done!
|
||||
<div
|
||||
className="alert-heading h4"
|
||||
>
|
||||
Almost done!
|
||||
</div>
|
||||
<p>
|
||||
You've successfully signed into Google! We just need a little more information before you start learning with Your Platform Name Here.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
You've successfully signed into Google! We just need a little more information before you start learning with Your Platform Name Here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
<h4
|
||||
className="mt-4 mb-4"
|
||||
>
|
||||
Finish creating your account
|
||||
</h4>,
|
||||
]
|
||||
`;
|
||||
|
||||
@@ -30,7 +30,7 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
|
||||
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
||||
export const LETTER_REGEX = /[a-zA-Z]/;
|
||||
export const NUMBER_REGEX = /\d/;
|
||||
export const VALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
|
||||
export const INVALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
|
||||
|
||||
// Query string parameters that can be passed to LMS to manage
|
||||
// things like auto-enrollment upon login and registration.
|
||||
|
||||
@@ -49,7 +49,7 @@ export const updatePathWithQueryParams = (path) => {
|
||||
return `${path}${queryParams}`;
|
||||
};
|
||||
|
||||
export const getAllPossibleQueryParam = () => {
|
||||
export const getAllPossibleQueryParams = () => {
|
||||
const urlParams = QueryString.parse(window.location.search);
|
||||
const params = {};
|
||||
Object.entries(urlParams).forEach(([key, value]) => {
|
||||
|
||||
@@ -2,7 +2,7 @@ export {
|
||||
getTpaProvider,
|
||||
getTpaHint,
|
||||
updatePathWithQueryParams,
|
||||
getAllPossibleQueryParam,
|
||||
getAllPossibleQueryParams,
|
||||
getActivationStatus,
|
||||
windowScrollTo,
|
||||
} from './dataUtils';
|
||||
|
||||
@@ -44,6 +44,7 @@ initialize({
|
||||
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
|
||||
ENABLE_COPPA_COMPLIANCE: process.env.ENABLE_COPPA_COMPLIANCE || '',
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from '../data/constants';
|
||||
import {
|
||||
getActivationStatus,
|
||||
getAllPossibleQueryParam,
|
||||
getAllPossibleQueryParams,
|
||||
getTpaHint,
|
||||
getTpaProvider,
|
||||
setSurveyCookie,
|
||||
@@ -54,7 +54,7 @@ class LoginPage extends React.Component {
|
||||
},
|
||||
isSubmitted: false,
|
||||
};
|
||||
this.queryParams = getAllPossibleQueryParam();
|
||||
this.queryParams = getAllPossibleQueryParams();
|
||||
this.tpaHint = getTpaHint();
|
||||
}
|
||||
|
||||
@@ -248,13 +248,10 @@ class LoginPage extends React.Component {
|
||||
finishAuthUrl={thirdPartyAuthContext.finishAuthUrl}
|
||||
/>
|
||||
<div className="mw-xs mt-3">
|
||||
{thirdPartyAuthContext.currentProvider
|
||||
&& (
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={thirdPartyAuthContext.currentProvider}
|
||||
platformName={thirdPartyAuthContext.platformName}
|
||||
/>
|
||||
)}
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={thirdPartyAuthContext.currentProvider}
|
||||
platformName={thirdPartyAuthContext.platformName}
|
||||
/>
|
||||
{this.props.loginError ? <LoginFailureMessage loginError={this.props.loginError} /> : null}
|
||||
{submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null}
|
||||
{activationMsgType && <AccountActivationMessage messageType={activationMsgType} />}
|
||||
|
||||
205
src/register/ConfigurableRegistrationForm.jsx
Normal file
205
src/register/ConfigurableRegistrationForm.jsx
Normal file
@@ -0,0 +1,205 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import FormFieldRenderer from '../field-renderer';
|
||||
import { FIELDS } from './data/constants';
|
||||
import { validateCountryField } from './data/utils';
|
||||
import messages from './messages';
|
||||
import { HonorCode, TermsOfService } from './registrationFields';
|
||||
import CountryField from './registrationFields/CountryField';
|
||||
|
||||
/**
|
||||
* Fields on registration page that are not the default required fields (name, email, username, password).
|
||||
* These configurable required fields are defined on the backend using REGISTRATION_EXTRA_FIELDS setting.
|
||||
*
|
||||
* Country and Honor Code/Terms of Services (if enabled) will appear at the bottom of the form, even if they
|
||||
* appear higher in order returned by backend. This is to make the user experience better.
|
||||
*
|
||||
* For edX only:
|
||||
* Country and honor code fields are required by default, and we will continue to show them on
|
||||
* frontend even if the API doesn't return it. The `SHOW_CONFIGURABLE_EDX_FIELDS` flag will enable
|
||||
* it for edX.
|
||||
* */
|
||||
const ConfigurableRegistrationForm = (props) => {
|
||||
const {
|
||||
countryList,
|
||||
email,
|
||||
fieldDescriptions,
|
||||
fieldErrors,
|
||||
formFields,
|
||||
intl,
|
||||
setFieldErrors,
|
||||
setFocusedField,
|
||||
setFormFields,
|
||||
} = props;
|
||||
|
||||
let showTermsOfServiceAndHonorCode = false;
|
||||
let showCountryField = false;
|
||||
|
||||
const formFieldDescriptions = [];
|
||||
const honorCode = [];
|
||||
const flags = {
|
||||
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
|
||||
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!formFields.country) {
|
||||
setFormFields({ ...formFields, country: { countryCode: '', displayValue: '' } });
|
||||
}
|
||||
});
|
||||
|
||||
const handleOnChange = (event, countryValue = null) => {
|
||||
const { name, type } = event.target;
|
||||
let value;
|
||||
if (countryValue) {
|
||||
value = { ...countryValue };
|
||||
} else {
|
||||
value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
||||
if (type === 'checkbox') {
|
||||
setFieldErrors({ ...fieldErrors, [name]: '' });
|
||||
}
|
||||
}
|
||||
setFormFields({ ...formFields, [name]: value });
|
||||
};
|
||||
|
||||
const handleOnBlur = (event) => {
|
||||
const { name, value } = event.target;
|
||||
let error = '';
|
||||
if (name === 'country') {
|
||||
const countryValidation = validateCountryField(
|
||||
value.trim(), countryList, intl.formatMessage(messages['empty.country.field.error']),
|
||||
);
|
||||
const { countryCode, displayValue } = countryValidation;
|
||||
error = countryValidation.error;
|
||||
setFormFields({ ...formFields, country: { countryCode, displayValue } });
|
||||
} else if (!value || !value.trim()) {
|
||||
error = fieldDescriptions[name].error_message;
|
||||
} else if (name === 'confirm_email' && value !== email) {
|
||||
error = intl.formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
setFocusedField(null);
|
||||
setFieldErrors({ ...fieldErrors, [name]: error });
|
||||
};
|
||||
|
||||
const handleOnFocus = (event) => {
|
||||
const { name } = event.target;
|
||||
setFieldErrors({ ...fieldErrors, [name]: '' });
|
||||
// Since we are removing the form errors from the focused field, we will
|
||||
// need to rerun the validation for focused field on form submission.
|
||||
setFocusedField(name);
|
||||
};
|
||||
|
||||
if (flags.showConfigurableRegistrationFields) {
|
||||
Object.keys(fieldDescriptions).forEach(fieldName => {
|
||||
const fieldData = fieldDescriptions[fieldName];
|
||||
switch (fieldData.name) {
|
||||
case FIELDS.COUNTRY:
|
||||
showCountryField = true;
|
||||
break;
|
||||
case FIELDS.HONOR_CODE:
|
||||
if (fieldData.type === 'tos_and_honor_code') {
|
||||
showTermsOfServiceAndHonorCode = true;
|
||||
} else {
|
||||
honorCode.push(
|
||||
<span key={fieldData.name}>
|
||||
<HonorCode
|
||||
fieldType={fieldData.type}
|
||||
value={formFields[fieldData.name]}
|
||||
onChangeHandler={handleOnChange}
|
||||
errorMessage={fieldErrors[fieldData.name]}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case FIELDS.TERMS_OF_SERVICE:
|
||||
honorCode.push(
|
||||
<span key={fieldData.name}>
|
||||
<TermsOfService
|
||||
value={formFields[fieldData.name]}
|
||||
onChangeHandler={handleOnChange}
|
||||
errorMessage={fieldErrors[fieldData.name]}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
formFieldDescriptions.push(
|
||||
<span key={fieldData.name}>
|
||||
<FormFieldRenderer
|
||||
fieldData={fieldData}
|
||||
value={formFields[fieldData.name]}
|
||||
onChangeHandler={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={fieldErrors[fieldData.name]}
|
||||
isRequired
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (flags.showConfigurableEdxFields || showCountryField) {
|
||||
formFieldDescriptions.push(
|
||||
<span key="country">
|
||||
<CountryField
|
||||
countryList={countryList}
|
||||
selectedCountry={formFields.country}
|
||||
errorMessage={fieldErrors.country || ''}
|
||||
onChangeHandler={handleOnChange}
|
||||
onBlurHandler={handleOnBlur}
|
||||
onFocusHandler={handleOnFocus}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (flags.showConfigurableEdxFields || showTermsOfServiceAndHonorCode) {
|
||||
formFieldDescriptions.push(
|
||||
<span key="honor_code">
|
||||
<HonorCode fieldType="tos_and_honor_code" onChangeHandler={handleOnChange} value={formFields.honor_code} />
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{formFieldDescriptions}
|
||||
<div>
|
||||
{honorCode}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ConfigurableRegistrationForm.propTypes = {
|
||||
countryList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
fieldDescriptions: PropTypes.shape({}),
|
||||
fieldErrors: PropTypes.shape({
|
||||
country: PropTypes.string,
|
||||
}).isRequired,
|
||||
formFields: PropTypes.shape({
|
||||
country: PropTypes.shape({
|
||||
displayValue: PropTypes.string,
|
||||
countryCode: PropTypes.string,
|
||||
}),
|
||||
honor_code: PropTypes.bool,
|
||||
}).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
setFieldErrors: PropTypes.func.isRequired,
|
||||
setFocusedField: PropTypes.func.isRequired,
|
||||
setFormFields: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ConfigurableRegistrationForm.defaultProps = {
|
||||
fieldDescriptions: {},
|
||||
};
|
||||
|
||||
export default injectIntl(ConfigurableRegistrationForm);
|
||||
@@ -1,229 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Icon, IconButton } from '@edx/paragon';
|
||||
import { ExpandLess, ExpandMore } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
|
||||
import { FormGroup } from '../common-components';
|
||||
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, FORM_SUBMISSION_ERROR } from './data/constants';
|
||||
|
||||
class CountryDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleOnBlur = this.handleOnBlur.bind(this);
|
||||
|
||||
this.state = {
|
||||
displayValue: '',
|
||||
icon: this.expandMoreButton(),
|
||||
errorMessage: '',
|
||||
showFieldError: true,
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const selectedCountry = this.props.options.find((o) => o[COUNTRY_CODE_KEY] === nextProps.value);
|
||||
if (this.props.value !== nextProps.value) {
|
||||
if (selectedCountry) {
|
||||
this.setState({
|
||||
displayValue: selectedCountry[COUNTRY_DISPLAY_KEY],
|
||||
showFieldError: false,
|
||||
errorMessage: nextProps.errorMessage,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
// Set persisted country value as display value.
|
||||
this.setState({ displayValue: nextProps.value, showFieldError: true, errorMessage: nextProps.errorMessage });
|
||||
return false;
|
||||
// eslint-disable-next-line no-else-return
|
||||
} else if (nextProps.value && selectedCountry && this.state.displayValue === nextProps.value) {
|
||||
// Handling a case here where user enters a valid country code that needs to be
|
||||
// evaluated and set its display value.
|
||||
this.setState({ displayValue: selectedCountry[COUNTRY_DISPLAY_KEY] });
|
||||
return false;
|
||||
}
|
||||
if (this.props.errorCode !== nextProps.errorCode && nextProps.errorCode === 'invalid-country') {
|
||||
this.setState({ showFieldError: true, errorMessage: nextProps.errorMessage });
|
||||
return false;
|
||||
}
|
||||
if (this.state.errorMessage !== nextProps.errorMessage) {
|
||||
this.setState({ showFieldError: true, errorMessage: nextProps.errorMessage });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (props.errorCode === FORM_SUBMISSION_ERROR && state.showFieldError) {
|
||||
return { errorMessage: props.errorMessage };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getItems(strToFind = '') {
|
||||
let { options } = this.props;
|
||||
if (strToFind.length > 0) {
|
||||
options = options.filter((option) => (option.name.toLowerCase().includes(strToFind.toLowerCase())));
|
||||
}
|
||||
|
||||
return options.map((opt) => {
|
||||
const value = opt[COUNTRY_CODE_KEY];
|
||||
const displayValue = opt[COUNTRY_DISPLAY_KEY];
|
||||
|
||||
return (
|
||||
<button type="button" name="countryItem" className="dropdown-item data-hj-suppress" value={displayValue} key={value} onClick={(e) => { this.handleItemClick(e); }}>
|
||||
{displayValue.length > 30 ? displayValue.substring(0, 30).concat('...') : displayValue}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
handleOnChange = (e) => {
|
||||
const filteredItems = this.getItems(e.target.value);
|
||||
this.setState({
|
||||
dropDownItems: filteredItems,
|
||||
displayValue: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
if (this.state.dropDownItems?.length > 0) {
|
||||
this.setState(() => ({
|
||||
icon: this.expandMoreButton(),
|
||||
dropDownItems: '',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleExpandLess() {
|
||||
this.setState({ dropDownItems: '', icon: this.expandMoreButton() });
|
||||
}
|
||||
|
||||
handleExpandMore() {
|
||||
this.setState(prevState => ({
|
||||
dropDownItems: this.getItems(prevState.displayValue), icon: this.expandLessButton(), errorMessage: '', showFieldError: false,
|
||||
}));
|
||||
}
|
||||
|
||||
handleFocus(e) {
|
||||
const { name, value } = e.target;
|
||||
this.setState(prevState => ({
|
||||
dropDownItems: this.getItems(name === 'country' ? value : prevState.displayValue),
|
||||
icon: this.expandLessButton(),
|
||||
errorMessage: '',
|
||||
showFieldError: false,
|
||||
}));
|
||||
if (this.props.handleFocus) { this.props.handleFocus(e); }
|
||||
}
|
||||
|
||||
handleOnBlur(e, itemClicked = false, country = '') {
|
||||
const { name } = e.target;
|
||||
let countryValue = '';
|
||||
if (country) {
|
||||
countryValue = country;
|
||||
} else {
|
||||
countryValue = itemClicked ? e.target.value : this.state.displayValue;
|
||||
}
|
||||
// For a better user experience, do not validate when focus out from 'country' field
|
||||
// and focus on 'countryItem' or 'countryExpand' button.
|
||||
if (e.relatedTarget && e.relatedTarget.name === 'countryItem' && (name === 'country' || name === 'countryExpand')) {
|
||||
return;
|
||||
}
|
||||
const normalized = countryValue.toLowerCase();
|
||||
const selectedCountry = this.props.options.find((o) => o[COUNTRY_DISPLAY_KEY].toLowerCase() === normalized);
|
||||
if (!selectedCountry) {
|
||||
this.setState({ errorMessage: this.props.errorMessage, showFieldError: true });
|
||||
}
|
||||
if (this.props.handleBlur) { this.props.handleBlur({ target: { name: 'country', value: countryValue } }); }
|
||||
}
|
||||
|
||||
handleItemClick(e) {
|
||||
let countryValue = '';
|
||||
if (!e.target.value) {
|
||||
countryValue = e.target.parentElement.parentElement.value;
|
||||
}
|
||||
this.setState({ dropDownItems: '', icon: this.expandMoreButton() });
|
||||
this.handleOnBlur(e, true, countryValue);
|
||||
}
|
||||
|
||||
expandMoreButton() {
|
||||
return (
|
||||
<IconButton
|
||||
className="expand-more"
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleOnBlur}
|
||||
name="countryExpand"
|
||||
src={ExpandMore}
|
||||
iconAs={Icon}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt="expand-more"
|
||||
onClick={(e) => { this.handleExpandMore(e); }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
expandLessButton() {
|
||||
return (
|
||||
<IconButton
|
||||
className="expand-less"
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleOnBlur}
|
||||
name="countryExpand"
|
||||
src={ExpandLess}
|
||||
iconAs={Icon}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt="expand-less"
|
||||
onClick={(e) => { this.handleExpandLess(e); }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<FormGroup
|
||||
as="input"
|
||||
name={this.props.name}
|
||||
autoComplete="off"
|
||||
className="mb-0"
|
||||
floatingLabel={this.props.floatingLabel}
|
||||
trailingElement={this.state.icon}
|
||||
handleChange={this.handleOnChange}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleFocus}
|
||||
value={this.state.displayValue}
|
||||
errorMessage={this.state.errorMessage}
|
||||
/>
|
||||
<div className="dropdown-container">
|
||||
{ this.state.dropDownItems?.length > 0 ? this.state.dropDownItems : null }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CountryDropdown.defaultProps = {
|
||||
options: null,
|
||||
floatingLabel: null,
|
||||
handleFocus: null,
|
||||
handleBlur: null,
|
||||
value: null,
|
||||
errorMessage: null,
|
||||
errorCode: null,
|
||||
};
|
||||
|
||||
CountryDropdown.propTypes = {
|
||||
options: PropTypes.arrayOf(PropTypes.object),
|
||||
floatingLabel: PropTypes.string,
|
||||
handleFocus: PropTypes.func,
|
||||
handleBlur: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
errorCode: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default onClickOutside(CountryDropdown);
|
||||
@@ -18,6 +18,10 @@ const RegistrationFailureMessage = (props) => {
|
||||
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
||||
}, [errorCode, failureCount]);
|
||||
|
||||
if (!errorCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let errorMessage;
|
||||
switch (errorCode) {
|
||||
case INTERNAL_SERVER_ERROR:
|
||||
|
||||
@@ -1,659 +1,183 @@
|
||||
import React from 'react';
|
||||
import React, {
|
||||
useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
getCountryList, getLocale, injectIntl, intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert, Form, Icon, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { Close, Error } from '@edx/paragon/icons';
|
||||
import PropTypes, { string } from 'prop-types';
|
||||
import { Form, StatefulButton } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import {
|
||||
FormGroup, InstitutionLogistration, PasswordField, RedirectLogistration,
|
||||
RenderInstitutionButton, SocialAuthProviders, ThirdPartyAuthAlert,
|
||||
FormGroup, InstitutionLogistration, PasswordField, RedirectLogistration, ThirdPartyAuthAlert,
|
||||
} from '../common-components';
|
||||
import { getThirdPartyAuthContext } from '../common-components/data/actions';
|
||||
import {
|
||||
extendedProfileSelector,
|
||||
fieldDescriptionSelector,
|
||||
optionalFieldsSelector,
|
||||
thirdPartyAuthContextSelector,
|
||||
extendedProfileSelector, fieldDescriptionSelector, optionalFieldsSelector, thirdPartyAuthContextSelector,
|
||||
} from '../common-components/data/selectors';
|
||||
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
||||
import {
|
||||
DEFAULT_STATE, LETTER_REGEX, NUMBER_REGEX, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX, VALID_NAME_REGEX,
|
||||
DEFAULT_STATE, INVALID_NAME_REGEX, LETTER_REGEX, NUMBER_REGEX, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX,
|
||||
} from '../data/constants';
|
||||
import {
|
||||
getAllPossibleQueryParam, getTpaHint, getTpaProvider, setCookie, setSurveyCookie,
|
||||
getAllPossibleQueryParams, getTpaHint, getTpaProvider, setCookie, setSurveyCookie,
|
||||
} from '../data/utils';
|
||||
import FormFieldRenderer from '../field-renderer';
|
||||
import CountryDropdown from './CountryDropdown';
|
||||
import ConfigurableRegistrationForm from './ConfigurableRegistrationForm';
|
||||
import {
|
||||
backupRegistrationFormBegin,
|
||||
clearUsernameSuggestions,
|
||||
fetchRealtimeValidations,
|
||||
registerNewUser,
|
||||
resetRegistrationForm,
|
||||
setRegistrationFormData,
|
||||
setUserPipelineDataLoaded,
|
||||
} from './data/actions';
|
||||
import {
|
||||
COMMON_EMAIL_PROVIDERS, COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, DEFAULT_SERVICE_PROVIDER_DOMAINS,
|
||||
DEFAULT_TOP_LEVEL_DOMAINS, FIELDS, FORM_SUBMISSION_ERROR,
|
||||
COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, FORM_SUBMISSION_ERROR,
|
||||
} from './data/constants';
|
||||
import {
|
||||
registrationErrorSelector,
|
||||
registrationFormDataSelector,
|
||||
registrationRequestSelector,
|
||||
usernameSuggestionsSelector,
|
||||
validationsSelector,
|
||||
} from './data/selectors';
|
||||
import HonorCode from './HonorCode';
|
||||
import { registrationErrorSelector, validationsSelector } from './data/selectors';
|
||||
import { getSuggestionForInvalidEmail, validateCountryField, validateEmailAddress } from './data/utils';
|
||||
import messages from './messages';
|
||||
import RegistrationFailure from './RegistrationFailure';
|
||||
import TermsOfService from './TermsOfService';
|
||||
import UsernameField from './UsernameField';
|
||||
import { getLevenshteinSuggestion, getSuggestionForInvalidEmail } from './utils';
|
||||
import { EmailField, UsernameField } from './registrationFields';
|
||||
import ThirdPartyAuth from './ThirdPartyAuth';
|
||||
|
||||
class RegistrationPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.handleOnClose = this.handleOnClose.bind(this);
|
||||
this.countryList = getCountryList(getLocale());
|
||||
this.queryParams = getAllPossibleQueryParam();
|
||||
// TODO: Once we have tested it and ready for openedX we can remove this flag and make the code
|
||||
// permanent part of Authn and remove extra code
|
||||
this.showDynamicRegistrationFields = getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS;
|
||||
this.tpaHint = getTpaHint();
|
||||
const { registrationFormData } = this.props;
|
||||
this.state = {
|
||||
country: '',
|
||||
email: registrationFormData.email,
|
||||
name: registrationFormData.name,
|
||||
password: registrationFormData.password,
|
||||
username: registrationFormData.username,
|
||||
marketingOptIn: registrationFormData.marketingOptIn,
|
||||
errors: {
|
||||
email: registrationFormData.errors.email,
|
||||
name: registrationFormData.errors.name,
|
||||
username: registrationFormData.errors.username,
|
||||
password: registrationFormData.errors.password,
|
||||
country: '',
|
||||
},
|
||||
emailFieldBorderClass: registrationFormData.emailFieldBorderClass,
|
||||
emailErrorSuggestion: registrationFormData.emailErrorSuggestion,
|
||||
emailWarningSuggestion: registrationFormData.emailWarningSuggestion,
|
||||
errorCode: null,
|
||||
failureCount: 0,
|
||||
startTime: Date.now(),
|
||||
totalRegistrationTime: 0,
|
||||
validatePassword: false,
|
||||
values: {},
|
||||
focusedField: '',
|
||||
};
|
||||
}
|
||||
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||
const urlRegex = new RegExp(INVALID_NAME_REGEX);
|
||||
|
||||
componentDidMount() {
|
||||
sendPageEvent('login_and_registration', 'register');
|
||||
const payload = { ...this.queryParams };
|
||||
window.optimizely = window.optimizely || [];
|
||||
window.optimizely.push({
|
||||
type: 'page',
|
||||
pageName: 'authn_registration_page',
|
||||
isActive: true,
|
||||
});
|
||||
const RegistrationPage = (props) => {
|
||||
const {
|
||||
backedUpFormData,
|
||||
backendCountryCode,
|
||||
backendValidations,
|
||||
fieldDescriptions,
|
||||
handleInstitutionLogin,
|
||||
intl,
|
||||
institutionLogin,
|
||||
optionalFields,
|
||||
registrationErrorCode,
|
||||
registrationResult,
|
||||
shouldBackupState,
|
||||
submitState,
|
||||
thirdPartyAuthApiStatus,
|
||||
thirdPartyAuthContext,
|
||||
usernameSuggestions,
|
||||
validationApiRateLimited,
|
||||
// Actions
|
||||
backupFormState,
|
||||
setUserPipelineDetailsLoaded,
|
||||
getRegistrationDataFromBackend,
|
||||
userPipelineDataLoaded,
|
||||
validateFromBackend,
|
||||
} = props;
|
||||
|
||||
if (payload.save_for_later === 'true') {
|
||||
sendTrackEvent('edx.bi.user.saveforlater.course.enroll.clicked', { category: 'save-for-later' });
|
||||
}
|
||||
|
||||
if (this.tpaHint) {
|
||||
payload.tpa_hint = this.tpaHint;
|
||||
}
|
||||
payload.is_register_page = true;
|
||||
this.props.resetRegistrationForm();
|
||||
this.props.getThirdPartyAuthContext(payload);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.registrationFormData && this.props.registrationFormData !== nextProps.registrationFormData) {
|
||||
// Ensuring browser's autofill user credentials get filled and their state persists in the redux store.
|
||||
const nextState = {
|
||||
username: nextProps.registrationFormData.username || this.state.username,
|
||||
password: nextProps.registrationFormData.password || this.state.password,
|
||||
};
|
||||
// Do not set focused field's value from redux store to retain entered data in focused field.
|
||||
let { focusedField } = this.state;
|
||||
|
||||
// Exemption for country field's value as we need to set updated value from the store.
|
||||
if (focusedField === 'country') { focusedField = ''; }
|
||||
const { [focusedField]: _, ...registrationData } = { ...nextProps.registrationFormData, ...nextState };
|
||||
this.setState({
|
||||
...registrationData,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.usernameSuggestions.length > 0 && this.state.username === '') {
|
||||
this.props.setRegistrationFormData({
|
||||
username: ' ',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.props.validationDecisions !== nextProps.validationDecisions) {
|
||||
if (nextProps.validationDecisions) {
|
||||
const state = {};
|
||||
const errors = { ...this.state.errors, ...nextProps.validationDecisions };
|
||||
let validatePassword = false;
|
||||
|
||||
if (errors.password) {
|
||||
validatePassword = true;
|
||||
}
|
||||
|
||||
if (nextProps.registrationErrorCode) {
|
||||
state.errorCode = nextProps.registrationErrorCode;
|
||||
}
|
||||
|
||||
let {
|
||||
suggestedTldMessage,
|
||||
suggestedTopLevelDomain,
|
||||
suggestedSldMessage,
|
||||
suggestedServiceLevelDomain,
|
||||
} = this.state;
|
||||
|
||||
if (errors.email) {
|
||||
suggestedTldMessage = '';
|
||||
suggestedTopLevelDomain = '';
|
||||
suggestedSldMessage = '';
|
||||
suggestedServiceLevelDomain = '';
|
||||
}
|
||||
|
||||
this.setState({
|
||||
...state,
|
||||
suggestedTldMessage,
|
||||
suggestedTopLevelDomain,
|
||||
suggestedSldMessage,
|
||||
suggestedServiceLevelDomain,
|
||||
validatePassword,
|
||||
});
|
||||
|
||||
this.props.setRegistrationFormData({
|
||||
errors,
|
||||
}, true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.props.thirdPartyAuthContext.pipelineUserDetails !== nextProps.thirdPartyAuthContext.pipelineUserDetails) {
|
||||
const { pipelineUserDetails } = nextProps.thirdPartyAuthContext;
|
||||
const { registrationFormData } = this.props;
|
||||
|
||||
// Added a conditional errors check to not fall back on pipelines data when a user explicitly edits the form.
|
||||
this.props.setRegistrationFormData({
|
||||
name: registrationFormData.errors.name ? registrationFormData.name
|
||||
: (registrationFormData.name || pipelineUserDetails.name || ''),
|
||||
email: registrationFormData.errors.email ? registrationFormData.email
|
||||
: (registrationFormData.email || pipelineUserDetails.email || ''),
|
||||
username: registrationFormData.errors.username ? registrationFormData.username
|
||||
: (registrationFormData.username || pipelineUserDetails.username || ''),
|
||||
country: registrationFormData.country || nextProps.thirdPartyAuthContext.countryCode,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.props.thirdPartyAuthContext.countryCode !== nextProps.thirdPartyAuthContext.countryCode) {
|
||||
this.props.setRegistrationFormData({
|
||||
country: this.props.registrationFormData.country || nextProps.thirdPartyAuthContext.countryCode,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onChangeHandler = (e) => {
|
||||
const { name, value, checked } = e.target;
|
||||
const { errors, values } = this.state;
|
||||
if (e.target.type === 'checkbox') {
|
||||
errors[name] = '';
|
||||
values[name] = checked;
|
||||
} else {
|
||||
values[name] = value;
|
||||
}
|
||||
const state = { errors, values };
|
||||
this.setState({ ...state });
|
||||
const countryList = useMemo(() => getCountryList(getLocale()), []);
|
||||
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||
const tpaHint = useMemo(() => getTpaHint(), []);
|
||||
const flags = {
|
||||
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
|
||||
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields });
|
||||
const [configurableFormFields, setConfigurableFormFields] = useState({ ...backedUpFormData.configurableFormFields });
|
||||
const [errors, setErrors] = useState({ ...backedUpFormData.errors });
|
||||
const [emailSuggestion, setEmailSuggestion] = useState({ ...backedUpFormData.emailSuggestion });
|
||||
|
||||
const { startTime } = this.state;
|
||||
const totalRegistrationTime = (Date.now() - startTime) / 1000;
|
||||
const dynamicFieldErrorMessages = {};
|
||||
const [errorCode, setErrorCode] = useState({ type: '', count: 0 });
|
||||
const [formStartTime, setFormStartTime] = useState(null);
|
||||
const [focusedField, setFocusedField] = useState(null);
|
||||
|
||||
let payload = {
|
||||
name: this.state.name,
|
||||
username: this.state.username,
|
||||
email: this.state.email,
|
||||
is_authn_mfe: true,
|
||||
};
|
||||
const {
|
||||
providers, currentProvider, secondaryProviders, finishAuthUrl,
|
||||
} = thirdPartyAuthContext;
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
|
||||
if (this.props.thirdPartyAuthContext.currentProvider) {
|
||||
payload.social_auth_provider = this.props.thirdPartyAuthContext.currentProvider;
|
||||
} else {
|
||||
payload.password = this.state.password;
|
||||
}
|
||||
|
||||
if (this.showDynamicRegistrationFields) {
|
||||
payload.extendedProfile = [];
|
||||
Object.keys(this.props.fieldDescriptions).forEach((fieldName) => {
|
||||
if (this.props.extendedProfile.includes(fieldName)) {
|
||||
payload.extendedProfile.push({ fieldName, fieldValue: this.state.values[fieldName] });
|
||||
} else {
|
||||
payload[fieldName] = this.state.values[fieldName];
|
||||
}
|
||||
dynamicFieldErrorMessages[fieldName] = this.props.fieldDescriptions[fieldName].error_message;
|
||||
});
|
||||
if (
|
||||
this.props.fieldDescriptions[FIELDS.HONOR_CODE]
|
||||
&& this.props.fieldDescriptions[FIELDS.HONOR_CODE].type === 'tos_and_honor_code'
|
||||
) {
|
||||
payload[FIELDS.HONOR_CODE] = true;
|
||||
}
|
||||
} else {
|
||||
payload.country = this.state.country;
|
||||
payload.honor_code = true;
|
||||
}
|
||||
|
||||
if (!this.isFormValid(payload, dynamicFieldErrorMessages)) {
|
||||
this.setState(prevState => ({
|
||||
errorCode: FORM_SUBMISSION_ERROR,
|
||||
failureCount: prevState.failureCount + 1,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (getConfig().MARKETING_EMAILS_OPT_IN) {
|
||||
payload.marketing_emails_opt_in = this.state.marketingOptIn;
|
||||
}
|
||||
|
||||
payload = snakeCaseObject(payload);
|
||||
payload.totalRegistrationTime = totalRegistrationTime;
|
||||
|
||||
// add query params to the payload
|
||||
payload = { ...payload, ...this.queryParams };
|
||||
this.setState({
|
||||
totalRegistrationTime,
|
||||
}, () => {
|
||||
this.props.registerNewUser(payload);
|
||||
});
|
||||
}
|
||||
|
||||
handleOnBlur = (e) => {
|
||||
let { name, value } = e.target;
|
||||
this.setState({
|
||||
focusedField: '',
|
||||
});
|
||||
|
||||
if (name === 'passwordValidation') {
|
||||
name = 'password';
|
||||
value = this.state.password;
|
||||
}
|
||||
const payload = {
|
||||
is_authn_mfe: true,
|
||||
form_field_key: name,
|
||||
email: this.state.email,
|
||||
username: this.state.username,
|
||||
password: this.state.password,
|
||||
name: this.state.name,
|
||||
honor_code: true,
|
||||
country: this.state.country,
|
||||
};
|
||||
this.validateInput(name, value, payload);
|
||||
}
|
||||
|
||||
handleOnChange = (e) => {
|
||||
let { value } = e.target;
|
||||
if (e.target.name === 'username') {
|
||||
if (value.length > 30) {
|
||||
return;
|
||||
}
|
||||
if (value.startsWith(' ')) {
|
||||
value = value.substring(1);
|
||||
/**
|
||||
* Set the userPipelineDetails data in formFields for only first time
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!userPipelineDataLoaded) {
|
||||
const { pipelineUserDetails } = thirdPartyAuthContext;
|
||||
if (pipelineUserDetails && Object.keys(pipelineUserDetails).length !== 0) {
|
||||
setFormFields(prevState => ({ ...prevState, ...pipelineUserDetails }));
|
||||
setUserPipelineDetailsLoaded(true);
|
||||
}
|
||||
}
|
||||
}, [thirdPartyAuthContext, userPipelineDataLoaded, setUserPipelineDetailsLoaded]);
|
||||
|
||||
this.setState({
|
||||
[e.target.name]: value,
|
||||
});
|
||||
}
|
||||
|
||||
handleOnFocus = (e) => {
|
||||
const fieldName = e.target.name;
|
||||
this.setState({
|
||||
focusedField: fieldName,
|
||||
});
|
||||
const { errors } = this.state;
|
||||
errors[fieldName] = '';
|
||||
if (fieldName === 'username') {
|
||||
this.props.clearUsernameSuggestions();
|
||||
}
|
||||
if (fieldName === 'countryExpand') {
|
||||
errors.country = '';
|
||||
}
|
||||
if (fieldName === 'passwordValidation') {
|
||||
errors.password = '';
|
||||
useEffect(() => {
|
||||
if (!formStartTime) {
|
||||
sendPageEvent('login_and_registration', 'register');
|
||||
const payload = { ...queryParams, is_register_page: true };
|
||||
if (tpaHint) {
|
||||
payload.tpa_hint = tpaHint;
|
||||
}
|
||||
getRegistrationDataFromBackend(payload);
|
||||
setFormStartTime(Date.now());
|
||||
}
|
||||
}, [formStartTime, getRegistrationDataFromBackend, queryParams, tpaHint]);
|
||||
|
||||
this.props.setRegistrationFormData({
|
||||
errors,
|
||||
});
|
||||
}
|
||||
|
||||
handleSuggestionClick = (e, suggestion) => {
|
||||
const { errors } = this.state;
|
||||
if (e.target.name === 'username') {
|
||||
errors.username = '';
|
||||
this.props.setRegistrationFormData({
|
||||
username: suggestion,
|
||||
errors,
|
||||
});
|
||||
this.props.clearUsernameSuggestions();
|
||||
} else if (e.target.name === 'email') {
|
||||
e.preventDefault();
|
||||
errors.email = '';
|
||||
this.props.setRegistrationFormData({
|
||||
email: suggestion,
|
||||
emailErrorSuggestion: null,
|
||||
emailWarningSuggestion: null,
|
||||
emailFieldBorderClass: '',
|
||||
errors,
|
||||
/**
|
||||
* Backup the registration form in redux when register page is toggled.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (shouldBackupState) {
|
||||
backupFormState({
|
||||
configurableFormFields: { ...configurableFormFields },
|
||||
formFields: { ...formFields },
|
||||
emailSuggestion: { ...emailSuggestion },
|
||||
errors: { ...errors },
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [shouldBackupState, configurableFormFields, formFields, errors, emailSuggestion, backupFormState]);
|
||||
|
||||
handleUsernameSuggestionClose = () => {
|
||||
this.props.setRegistrationFormData({
|
||||
username: '',
|
||||
});
|
||||
this.props.clearUsernameSuggestions();
|
||||
}
|
||||
|
||||
validateDynamicFields = (e) => {
|
||||
const { intl } = this.props;
|
||||
const { errors } = this.state;
|
||||
const { name, value } = e.target;
|
||||
if (!value.trim()) {
|
||||
errors[name] = this.props.fieldDescriptions[name].error_message;
|
||||
useEffect(() => {
|
||||
if (backendValidations) {
|
||||
setErrors(prevErrors => ({ ...prevErrors, ...backendValidations }));
|
||||
}
|
||||
if (name === 'confirm_email' && value.length > 0 && this.state.email && value !== this.state.email) {
|
||||
errors.confirm_email = intl.formatMessage(messages['email.do.not.match']);
|
||||
}, [backendValidations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (registrationErrorCode) {
|
||||
setErrorCode(prevState => ({ type: registrationErrorCode, count: prevState.count + 1 }));
|
||||
}
|
||||
this.setState({ errors });
|
||||
}
|
||||
}, [registrationErrorCode]);
|
||||
|
||||
isFormValid(payload, dynamicFieldError) {
|
||||
const { errors } = this.state;
|
||||
let isValid = true;
|
||||
Object.keys(payload).forEach(key => {
|
||||
if (!payload[key]) {
|
||||
errors[key] = (key in dynamicFieldError) ? dynamicFieldError[key] : this.props.intl.formatMessage(messages[`empty.${key}.field.error`]);
|
||||
}
|
||||
// Mark form invalid, if there was already a validation error for this key or we added empty field error
|
||||
if (errors[key]) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
const state = { ...payload, errors };
|
||||
this.props.setRegistrationFormData({
|
||||
...state,
|
||||
});
|
||||
return isValid;
|
||||
}
|
||||
|
||||
validateInput(fieldName, value, payload) {
|
||||
let state = {};
|
||||
const { errors } = this.state;
|
||||
const { intl, statusCode } = this.props;
|
||||
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||
const urlRegex = new RegExp(VALID_NAME_REGEX);
|
||||
|
||||
switch (fieldName) {
|
||||
case 'email':
|
||||
if (!value) {
|
||||
errors.email = intl.formatMessage(messages['empty.email.field.error']);
|
||||
} else if (value.length <= 2) {
|
||||
errors.email = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
} else {
|
||||
let emailWarningSuggestion = null;
|
||||
let emailErrorSuggestion = null;
|
||||
|
||||
const [username, domainName] = value.split('@');
|
||||
|
||||
// Check if email address is invalid. If we have a suggestion for invalid email provide that along with
|
||||
// error message.
|
||||
if (!emailRegex.test(value)) {
|
||||
errors.email = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
emailErrorSuggestion = getSuggestionForInvalidEmail(domainName, username);
|
||||
} else {
|
||||
let suggestion = null;
|
||||
const hasMultipleSubdomains = value.match(/\./g).length > 1;
|
||||
const [serviceLevelDomain, topLevelDomain] = domainName.split('.');
|
||||
const tldSuggestion = !DEFAULT_TOP_LEVEL_DOMAINS.includes(topLevelDomain);
|
||||
const serviceSuggestion = getLevenshteinSuggestion(serviceLevelDomain, DEFAULT_SERVICE_PROVIDER_DOMAINS, 2);
|
||||
|
||||
if (DEFAULT_SERVICE_PROVIDER_DOMAINS.includes(serviceSuggestion || serviceLevelDomain)) {
|
||||
suggestion = `${username}@${serviceSuggestion || serviceLevelDomain}.com`;
|
||||
}
|
||||
|
||||
if (!hasMultipleSubdomains && tldSuggestion) {
|
||||
emailErrorSuggestion = suggestion;
|
||||
} else if (serviceSuggestion) {
|
||||
emailWarningSuggestion = suggestion;
|
||||
} else {
|
||||
suggestion = getLevenshteinSuggestion(domainName, COMMON_EMAIL_PROVIDERS, 3);
|
||||
if (suggestion) {
|
||||
emailWarningSuggestion = `${username}@${suggestion}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMultipleSubdomains && tldSuggestion) {
|
||||
errors.email = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
} else if (payload && statusCode !== 403) {
|
||||
this.props.fetchRealtimeValidations(payload);
|
||||
} else {
|
||||
errors.email = '';
|
||||
}
|
||||
if (this.state.values && this.state.values.confirm_email && value !== this.state.values.confirm_email) {
|
||||
errors.confirm_email = intl.formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
useEffect(() => {
|
||||
if (backendCountryCode !== '') {
|
||||
const selectedCountry = countryList.find(
|
||||
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()),
|
||||
);
|
||||
if (selectedCountry) {
|
||||
setConfigurableFormFields(prevState => (
|
||||
{
|
||||
...prevState,
|
||||
country: {
|
||||
countryCode: selectedCountry[COUNTRY_CODE_KEY], displayValue: selectedCountry[COUNTRY_DISPLAY_KEY],
|
||||
},
|
||||
}
|
||||
state = {
|
||||
emailWarningSuggestion,
|
||||
emailErrorSuggestion,
|
||||
emailFieldBorderClass: emailWarningSuggestion ? 'yellow-border' : null,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'name':
|
||||
if (!value.trim()) {
|
||||
errors.name = intl.formatMessage(messages['empty.name.field.error']);
|
||||
} else if (value && value.match(urlRegex)) {
|
||||
errors.name = intl.formatMessage(messages['name.validation.message']);
|
||||
} else {
|
||||
errors.name = '';
|
||||
}
|
||||
|
||||
if (!this.state.username.trim() && value) {
|
||||
// fetch username suggestions based on the full name
|
||||
this.props.fetchRealtimeValidations(payload);
|
||||
}
|
||||
break;
|
||||
case 'username':
|
||||
if (value === ' ' && this.props.usernameSuggestions.length > 0) {
|
||||
errors.username = '';
|
||||
break;
|
||||
}
|
||||
if (!value || value.length <= 1 || value.length > 30) {
|
||||
errors.username = intl.formatMessage(messages['username.validation.message']);
|
||||
} else if (!value.match(/^[a-zA-Z0-9_-]*$/i)) {
|
||||
errors.username = intl.formatMessage(messages['username.format.validation.message']);
|
||||
} else if (payload && statusCode !== 403) {
|
||||
this.props.fetchRealtimeValidations(payload);
|
||||
} else {
|
||||
errors.username = '';
|
||||
}
|
||||
|
||||
if (this.state.validatePassword) {
|
||||
this.props.fetchRealtimeValidations({ ...payload, form_field_key: 'password' });
|
||||
}
|
||||
break;
|
||||
case 'password':
|
||||
errors.password = '';
|
||||
if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) {
|
||||
errors.password = intl.formatMessage(messages['password.validation.message']);
|
||||
} else if (payload && statusCode !== 403) {
|
||||
this.props.fetchRealtimeValidations(payload);
|
||||
}
|
||||
break;
|
||||
case 'country':
|
||||
value = value.trim(); // eslint-disable-line no-param-reassign
|
||||
if (value) {
|
||||
const normalizedValue = value.toLowerCase();
|
||||
let selectedCountry = (
|
||||
this.countryList.find((o) => o[COUNTRY_DISPLAY_KEY].toLowerCase().trim() === normalizedValue));
|
||||
if (selectedCountry) {
|
||||
value = selectedCountry[COUNTRY_CODE_KEY]; // eslint-disable-line no-param-reassign
|
||||
errors.country = '';
|
||||
break;
|
||||
} else {
|
||||
// Handling a case here where user enters a valid country code that needs to be
|
||||
// evaluated and set its value as a valid value.
|
||||
selectedCountry = (
|
||||
this.countryList.find((o) => o[COUNTRY_CODE_KEY].toLowerCase().trim() === normalizedValue));
|
||||
if (selectedCountry) {
|
||||
value = selectedCountry[COUNTRY_CODE_KEY]; // eslint-disable-line no-param-reassign
|
||||
errors.country = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
errors.country = intl.formatMessage(messages['empty.country.field.error']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
));
|
||||
}
|
||||
}
|
||||
}, [backendCountryCode, countryList]);
|
||||
|
||||
state = {
|
||||
...state,
|
||||
[fieldName]: value,
|
||||
};
|
||||
this.props.setRegistrationFormData({
|
||||
...state,
|
||||
errors,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
handleOnClose() {
|
||||
this.props.setRegistrationFormData({
|
||||
emailErrorSuggestion: null,
|
||||
});
|
||||
}
|
||||
|
||||
renderEmailFeedback() {
|
||||
if (this.state.emailErrorSuggestion) {
|
||||
return (
|
||||
<Alert variant="danger" className="email-error-alert" icon={Error}>
|
||||
<span className="alert-text">
|
||||
{this.props.intl.formatMessage(messages['did.you.mean.alert.text'])}{' '}
|
||||
<Alert.Link
|
||||
href="#"
|
||||
name="email"
|
||||
onClick={e => { this.handleSuggestionClick(e, this.state.emailErrorSuggestion); }}
|
||||
>
|
||||
{this.state.emailErrorSuggestion}
|
||||
</Alert.Link>?<Icon src={Close} className="alert-close" onClick={this.handleOnClose} tabIndex="0" />
|
||||
</span>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
if (this.state.emailWarningSuggestion) {
|
||||
return (
|
||||
<span className="small">
|
||||
{this.props.intl.formatMessage(messages['did.you.mean.alert.text'])}:{' '}
|
||||
<Alert.Link
|
||||
href="#"
|
||||
name="email"
|
||||
className="email-warning-alert-link"
|
||||
onClick={e => { this.handleSuggestionClick(e, this.state.emailWarningSuggestion); }}
|
||||
>
|
||||
{this.state.emailWarningSuggestion}
|
||||
</Alert.Link>?
|
||||
</span>
|
||||
);
|
||||
/**
|
||||
* We need to remove the placeholder from the field, adding a space will do that.
|
||||
* This is needed because we are placing the username suggestions on top of the field.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (usernameSuggestions.length && !formFields.username) {
|
||||
setFormFields({ ...formFields, username: ' ' });
|
||||
}
|
||||
}, [usernameSuggestions, formFields]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl) {
|
||||
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
|
||||
const isSocialAuthActive = !!providers.length && !currentProvider;
|
||||
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
|
||||
|
||||
return (
|
||||
<>
|
||||
{((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive) && (
|
||||
<div className="mt-4 mb-3 h4">
|
||||
{intl.formatMessage(messages['registration.other.options.heading'])}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{thirdPartyAuthApiStatus === PENDING_STATE ? (
|
||||
<Skeleton className="tpa-skeleton" height={36} count={2} />
|
||||
) : (
|
||||
<>
|
||||
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
|
||||
<RenderInstitutionButton
|
||||
onSubmitHandler={this.props.handleInstitutionLogin}
|
||||
buttonTitle={intl.formatMessage(messages['register.institution.login.button'])}
|
||||
/>
|
||||
)}
|
||||
{isSocialAuthActive && (
|
||||
<div className="row m-0">
|
||||
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm(currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthApiStatus,
|
||||
finishAuthUrl,
|
||||
submitState,
|
||||
intl) {
|
||||
if (this.props.institutionLogin) {
|
||||
return (
|
||||
<InstitutionLogistration
|
||||
secondaryProviders={this.props.thirdPartyAuthContext.secondaryProviders}
|
||||
headingTitle={intl.formatMessage(messages['register.institution.login.page.title'])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.registrationResult.success) {
|
||||
useEffect(() => {
|
||||
if (registrationResult.success) {
|
||||
// TODO: Do we still need this cookie?
|
||||
setSurveyCookie('register');
|
||||
setCookie(getConfig().REGISTER_CONVERSION_COOKIE_NAME, true);
|
||||
setCookie('authn-returning-user');
|
||||
@@ -663,200 +187,364 @@ class RegistrationPage extends React.Component {
|
||||
window.optimizely.push({
|
||||
type: 'event',
|
||||
eventName: 'authn-register-conversion',
|
||||
tags: {
|
||||
value: this.state.totalRegistrationTime,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [registrationResult]);
|
||||
|
||||
const validateInput = (fieldName, value, payload, shouldValidateFromBackend, setError = true) => {
|
||||
let fieldError = '';
|
||||
let confirmEmailError = ''; // This is to handle the use case where the form contains "confirm email" field
|
||||
let countryFieldCode = '';
|
||||
|
||||
switch (fieldName) {
|
||||
case 'name':
|
||||
if (!value.trim()) {
|
||||
fieldError = intl.formatMessage(messages['empty.name.field.error']);
|
||||
} else if (value && value.match(urlRegex)) {
|
||||
fieldError = intl.formatMessage(messages['name.validation.message']);
|
||||
} else if (value && !payload.username.trim() && shouldValidateFromBackend) {
|
||||
validateFromBackend(payload);
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
if (!value) {
|
||||
fieldError = intl.formatMessage(messages['empty.email.field.error']);
|
||||
} else if (value.length <= 2) {
|
||||
fieldError = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
} else {
|
||||
const [username, domainName] = value.split('@');
|
||||
// Check if email address is invalid. If we have a suggestion for invalid email
|
||||
// provide that along with the error message.
|
||||
if (!emailRegex.test(value)) {
|
||||
fieldError = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
setEmailSuggestion({
|
||||
suggestion: getSuggestionForInvalidEmail(domainName, username),
|
||||
type: 'error',
|
||||
});
|
||||
} else {
|
||||
const response = validateEmailAddress(value, username, domainName);
|
||||
if (response.hasError) {
|
||||
fieldError = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
delete response.hasError;
|
||||
} else if (shouldValidateFromBackend) {
|
||||
validateFromBackend(payload);
|
||||
}
|
||||
setEmailSuggestion({ ...response });
|
||||
|
||||
if (configurableFormFields.confirm_email && value !== configurableFormFields.confirm_email) {
|
||||
confirmEmailError = intl.formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'username':
|
||||
if (!value || value.length <= 1 || value.length > 30) {
|
||||
fieldError = intl.formatMessage(messages['username.validation.message']);
|
||||
} else if (!value.match(/^[a-zA-Z\d_-]*$/i)) {
|
||||
fieldError = intl.formatMessage(messages['username.format.validation.message']);
|
||||
} else if (shouldValidateFromBackend) {
|
||||
validateFromBackend(payload);
|
||||
}
|
||||
break;
|
||||
case 'password':
|
||||
if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) {
|
||||
fieldError = intl.formatMessage(messages['password.validation.message']);
|
||||
} else if (shouldValidateFromBackend) {
|
||||
validateFromBackend(payload);
|
||||
}
|
||||
break;
|
||||
case 'country':
|
||||
if (flags.showConfigurableEdxFields || flags.showConfigurableRegistrationFields) {
|
||||
const { countryCode, displayValue, error } = validateCountryField(
|
||||
value.displayValue.trim(), countryList, intl.formatMessage(messages['empty.country.field.error']),
|
||||
);
|
||||
fieldError = error;
|
||||
countryFieldCode = countryCode;
|
||||
setConfigurableFormFields({ ...configurableFormFields, country: { countryCode, displayValue } });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (flags.showConfigurableRegistrationFields) {
|
||||
if (!value && fieldDescriptions[fieldName].error_message) {
|
||||
fieldError = fieldDescriptions[fieldName].error_message;
|
||||
} else if (fieldName === 'confirm_email' && formFields.email && value !== formFields.email) {
|
||||
fieldError = intl.formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (setError) {
|
||||
setErrors({
|
||||
...errors,
|
||||
confirm_email: flags.showConfigurableRegistrationFields ? confirmEmailError : '',
|
||||
[fieldName]: fieldError,
|
||||
});
|
||||
}
|
||||
return { fieldError, countryFieldCode };
|
||||
};
|
||||
|
||||
const isFormValid = (payload, focusedFieldError) => {
|
||||
const fieldErrors = { ...errors };
|
||||
let isValid = !focusedFieldError;
|
||||
Object.keys(payload).forEach(key => {
|
||||
if (!payload[key]) {
|
||||
fieldErrors[key] = intl.formatMessage(messages[`empty.${key}.field.error`]);
|
||||
}
|
||||
if (fieldErrors[key]) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (flags.showConfigurableEdxFields) {
|
||||
if (!configurableFormFields.country.displayValue) {
|
||||
fieldErrors.country = intl.formatMessage(messages['empty.country.field.error']);
|
||||
}
|
||||
if (fieldErrors.country) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.showConfigurableRegistrationFields) {
|
||||
Object.keys(fieldDescriptions).forEach(key => {
|
||||
if (key === 'country' && !configurableFormFields.country.displayValue) {
|
||||
fieldErrors[key] = intl.formatMessage(messages['empty.country.field.error']);
|
||||
} else if (!configurableFormFields[key]) {
|
||||
fieldErrors[key] = fieldDescriptions[key].error_message;
|
||||
}
|
||||
if (fieldErrors[key]) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const honorCode = [];
|
||||
const formFields = this.showDynamicRegistrationFields ? (
|
||||
Object.keys(this.props.fieldDescriptions).map((fieldName) => {
|
||||
const fieldData = this.props.fieldDescriptions[fieldName];
|
||||
switch (fieldData.name) {
|
||||
case FIELDS.COUNTRY:
|
||||
return (
|
||||
<span key={fieldData.name}>
|
||||
<CountryDropdown
|
||||
name="country"
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
options={this.countryList}
|
||||
value={this.state.values[fieldData.name]}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
|
||||
handleChange={
|
||||
(value) => this.setState(prevState => ({ values: { ...prevState.values, country: value } }))
|
||||
}
|
||||
errorCode={this.state.errorCode}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
case FIELDS.HONOR_CODE:
|
||||
honorCode.push(
|
||||
<span key={fieldData.name}>
|
||||
<HonorCode
|
||||
fieldType={fieldData.type}
|
||||
value={this.state.values[fieldData.name]}
|
||||
onChangeHandler={this.onChangeHandler}
|
||||
errorMessage={this.state.errors[fieldData.name]}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
return null;
|
||||
case FIELDS.TERMS_OF_SERVICE:
|
||||
honorCode.push(
|
||||
<span key={fieldData.name}>
|
||||
<TermsOfService
|
||||
value={this.state.values[fieldData.name]}
|
||||
onChangeHandler={this.onChangeHandler}
|
||||
errorMessage={this.state.errors[fieldData.name]}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
return null;
|
||||
default:
|
||||
return (
|
||||
<span key={fieldData.name}>
|
||||
<FormFieldRenderer
|
||||
fieldData={fieldData}
|
||||
value={this.state.values[fieldData.name]}
|
||||
onChangeHandler={this.onChangeHandler}
|
||||
handleBlur={this.validateDynamicFields}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors[fieldData.name]}
|
||||
isRequired
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
})
|
||||
) : null;
|
||||
if (focusedField) {
|
||||
fieldErrors[focusedField] = focusedFieldError;
|
||||
}
|
||||
setErrors({ ...fieldErrors });
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (event, fieldName, suggestion = '') => {
|
||||
event.preventDefault();
|
||||
setErrors({ ...errors, [fieldName]: '' });
|
||||
switch (fieldName) {
|
||||
case 'email':
|
||||
setFormFields({ ...formFields, email: emailSuggestion.suggestion });
|
||||
setEmailSuggestion({ suggestion: '', type: '' });
|
||||
break;
|
||||
case 'username':
|
||||
setFormFields({ ...formFields, username: suggestion });
|
||||
props.resetUsernameSuggestions();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailSuggestionClosed = () => setEmailSuggestion({ suggestion: '', type: '' });
|
||||
const handleUsernameSuggestionClosed = () => props.resetUsernameSuggestions();
|
||||
|
||||
const handleOnChange = (event) => {
|
||||
let value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
||||
|
||||
if (event.target.name === 'username') {
|
||||
if (value.length > 30) {
|
||||
return;
|
||||
}
|
||||
if (value.startsWith(' ')) {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
setFormFields({ ...formFields, [event.target.name]: value });
|
||||
};
|
||||
|
||||
const handleOnBlur = (event) => {
|
||||
const { name, value } = event.target;
|
||||
const payload = {
|
||||
name: formFields.name,
|
||||
email: formFields.email,
|
||||
username: formFields.username,
|
||||
password: formFields.password,
|
||||
form_field_key: name,
|
||||
};
|
||||
|
||||
setFocusedField(null);
|
||||
validateInput(name, name === 'password' ? formFields.password : value, payload, !validationApiRateLimited);
|
||||
};
|
||||
|
||||
const handleOnFocus = (event) => {
|
||||
const { name, value } = event.target;
|
||||
setErrors({ ...errors, [name]: '' });
|
||||
// Since we are removing the form errors from the focused field, we will
|
||||
// need to rerun the validation for focused field on form submission.
|
||||
setFocusedField(name);
|
||||
|
||||
if (name === 'username') {
|
||||
props.resetUsernameSuggestions();
|
||||
// If we added a space character to username field to display the suggestion
|
||||
// remove it before user enters the input. This is to ensure user doesn't
|
||||
// have a space prefixed to the username.
|
||||
if (value === ' ') {
|
||||
setFormFields({ ...formFields, [name]: '' });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
|
||||
let payload = { ...formFields };
|
||||
|
||||
if (currentProvider) {
|
||||
delete payload.password;
|
||||
payload.social_auth_provider = currentProvider;
|
||||
}
|
||||
|
||||
const { focusedFieldError, countryFieldCode } = focusedField ? (
|
||||
validateInput(
|
||||
focusedField,
|
||||
(focusedField in fieldDescriptions || focusedField === 'country') ? (
|
||||
configurableFormFields[focusedField]
|
||||
) : formFields[focusedField],
|
||||
payload,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
) : '';
|
||||
|
||||
if (!isFormValid(payload, focusedFieldError)) {
|
||||
setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
|
||||
return;
|
||||
}
|
||||
|
||||
payload.extendedProfile = [];
|
||||
Object.keys(configurableFormFields).forEach((fieldName) => {
|
||||
if (props.extendedProfile.includes(fieldName)) {
|
||||
payload.extendedProfile.push({ fieldName, fieldValue: configurableFormFields[fieldName] });
|
||||
} else if (fieldName === 'country') {
|
||||
payload[fieldName] = focusedField === 'country' ? countryFieldCode : configurableFormFields[fieldName].countryCode;
|
||||
} else {
|
||||
payload[fieldName] = configurableFormFields[fieldName];
|
||||
}
|
||||
});
|
||||
|
||||
payload = snakeCaseObject(payload);
|
||||
payload.totalRegistrationTime = totalRegistrationTime;
|
||||
|
||||
// add query params to the payload
|
||||
payload = { ...payload, ...queryParams };
|
||||
props.registerNewUser(payload);
|
||||
};
|
||||
|
||||
const renderForm = () => {
|
||||
if (institutionLogin) {
|
||||
return (
|
||||
<InstitutionLogistration
|
||||
secondaryProviders={secondaryProviders}
|
||||
headingTitle={intl.formatMessage(messages['register.institution.login.page.title'])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['register.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
<title>{intl.formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}</title>
|
||||
</Helmet>
|
||||
<RedirectLogistration
|
||||
success={this.props.registrationResult.success}
|
||||
redirectUrl={this.props.registrationResult.redirectUrl}
|
||||
success={registrationResult.success}
|
||||
redirectUrl={registrationResult.redirectUrl}
|
||||
finishAuthUrl={finishAuthUrl}
|
||||
optionalFields={this.props.optionalFields}
|
||||
redirectToWelcomePage={getConfig().ENABLE_PROGRESSIVE_PROFILING
|
||||
&& Object.keys(this.props.optionalFields).length !== 0}
|
||||
optionalFields={optionalFields}
|
||||
redirectToWelcomePage={
|
||||
getConfig().ENABLE_PROGRESSIVE_PROFILING && Object.keys(optionalFields).length !== 0
|
||||
}
|
||||
/>
|
||||
<div className="mw-xs mt-3">
|
||||
{this.state.errorCode ? (
|
||||
<RegistrationFailure
|
||||
errorCode={this.state.errorCode}
|
||||
failureCount={this.state.failureCount}
|
||||
context={{ provider: currentProvider }}
|
||||
/>
|
||||
) : null}
|
||||
{currentProvider && (
|
||||
<>
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={currentProvider}
|
||||
platformName={this.props.thirdPartyAuthContext.platformName}
|
||||
referrer={REGISTER_PAGE}
|
||||
/>
|
||||
<h4 className="mt-4 mb-4">{intl.formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
||||
</>
|
||||
)}
|
||||
<RegistrationFailure
|
||||
errorCode={errorCode.type}
|
||||
failureCount={errorCode.count}
|
||||
context={{ provider: currentProvider }}
|
||||
/>
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={currentProvider}
|
||||
platformName={platformName}
|
||||
referrer={REGISTER_PAGE}
|
||||
/>
|
||||
<Form id="registration-form" name="registration-form">
|
||||
<FormGroup
|
||||
name="name"
|
||||
value={this.state.name}
|
||||
autoComplete="on"
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.name}
|
||||
value={formFields.name}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={errors.name}
|
||||
helpText={[intl.formatMessage(messages['help.text.name'])]}
|
||||
floatingLabel={intl.formatMessage(messages['registration.fullname.label'])}
|
||||
/>
|
||||
<FormGroup
|
||||
<EmailField
|
||||
name="email"
|
||||
value={this.state.email}
|
||||
autoComplete="on"
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
errorMessage={this.state.errors.email}
|
||||
handleFocus={this.handleOnFocus}
|
||||
value={formFields.email}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
handleSuggestionClick={(e) => handleSuggestionClick(e, 'email')}
|
||||
handleOnClose={handleEmailSuggestionClosed}
|
||||
emailSuggestion={emailSuggestion}
|
||||
errorMessage={errors.email}
|
||||
helpText={[intl.formatMessage(messages['help.text.email'])]}
|
||||
floatingLabel={intl.formatMessage(messages['registration.email.label'])}
|
||||
borderClass={this.state.emailFieldBorderClass}
|
||||
>
|
||||
{this.renderEmailFeedback()}
|
||||
</FormGroup>
|
||||
|
||||
/>
|
||||
<UsernameField
|
||||
name="username"
|
||||
spellCheck="false"
|
||||
value={this.state.username}
|
||||
autoComplete="on"
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.username}
|
||||
value={formFields.username}
|
||||
handleBlur={handleOnBlur}
|
||||
handleChange={handleOnChange}
|
||||
handleFocus={handleOnFocus}
|
||||
handleSuggestionClick={handleSuggestionClick}
|
||||
handleUsernameSuggestionClose={handleUsernameSuggestionClosed}
|
||||
usernameSuggestions={usernameSuggestions}
|
||||
errorMessage={errors.username}
|
||||
helpText={[intl.formatMessage(messages['help.text.username.1']), intl.formatMessage(messages['help.text.username.2'])]}
|
||||
floatingLabel={intl.formatMessage(messages['registration.username.label'])}
|
||||
handleSuggestionClick={this.handleSuggestionClick}
|
||||
usernameSuggestions={this.props.usernameSuggestions}
|
||||
handleUsernameSuggestionClose={this.handleUsernameSuggestionClose}
|
||||
/>
|
||||
|
||||
{!currentProvider && (
|
||||
<PasswordField
|
||||
name="password"
|
||||
value={this.state.password}
|
||||
autoComplete="off"
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.password}
|
||||
value={formFields.password}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={errors.password}
|
||||
floatingLabel={intl.formatMessage(messages['registration.password.label'])}
|
||||
/>
|
||||
)}
|
||||
{!(this.showDynamicRegistrationFields)
|
||||
&& (
|
||||
<CountryDropdown
|
||||
name="country"
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
options={this.countryList}
|
||||
value={this.state.country}
|
||||
autoComplete="on"
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.country}
|
||||
errorCode={this.state.errorCode}
|
||||
/>
|
||||
)}
|
||||
{formFields}
|
||||
{(getConfig().MARKETING_EMAILS_OPT_IN)
|
||||
&& (
|
||||
<Form.Checkbox
|
||||
className="opt-checkbox"
|
||||
name="marketing_emails_opt_in"
|
||||
checked={this.state.marketingOptIn}
|
||||
onChange={(e) => this.props.setRegistrationFormData({
|
||||
marketingOptIn: e.target.checked,
|
||||
})}
|
||||
>
|
||||
{intl.formatMessage(messages['registration.opt.in.label'], { siteName: getConfig().SITE_NAME })}
|
||||
</Form.Checkbox>
|
||||
)}
|
||||
{!(this.showDynamicRegistrationFields) ? (
|
||||
<HonorCode
|
||||
fieldType="tos_and_honor_code"
|
||||
/>
|
||||
) : <div>{honorCode}</div>}
|
||||
{getConfig().MARKETING_EMAILS_OPT_IN
|
||||
&& (
|
||||
<Form.Checkbox
|
||||
name="marketingEmailsOptIn"
|
||||
className="opt-checkbox"
|
||||
checked={formFields.marketingEmailsOptIn}
|
||||
onChange={handleOnChange}
|
||||
>
|
||||
{intl.formatMessage(messages['registration.opt.in.label'], { siteName: getConfig().SITE_NAME })}
|
||||
</Form.Checkbox>
|
||||
)}
|
||||
<ConfigurableRegistrationForm
|
||||
countryList={countryList}
|
||||
email={formFields.email}
|
||||
fieldErrors={errors}
|
||||
formFields={configurableFormFields}
|
||||
setFieldErrors={setErrors}
|
||||
setFormFields={setConfigurableFormFields}
|
||||
setFocusedField={setFocusedField}
|
||||
fieldDescriptions={fieldDescriptions}
|
||||
/>
|
||||
<StatefulButton
|
||||
name="register-user"
|
||||
id="register-user"
|
||||
name="register-user"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="register-stateful-button-width mt-4 mb-4"
|
||||
@@ -865,132 +553,86 @@ class RegistrationPage extends React.Component {
|
||||
default: intl.formatMessage(messages['create.account.for.free.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={this.handleSubmit}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
{this.renderThirdPartyAuth(providers,
|
||||
secondaryProviders,
|
||||
currentProvider,
|
||||
thirdPartyAuthApiStatus,
|
||||
intl)}
|
||||
<ThirdPartyAuth
|
||||
currentProvider={currentProvider}
|
||||
providers={providers}
|
||||
secondaryProviders={secondaryProviders}
|
||||
handleInstitutionLogin={handleInstitutionLogin}
|
||||
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { intl, submitState, thirdPartyAuthApiStatus } = this.props;
|
||||
const {
|
||||
currentProvider, finishAuthUrl, providers, secondaryProviders,
|
||||
} = this.props.thirdPartyAuthContext;
|
||||
|
||||
if (this.tpaHint) {
|
||||
if (thirdPartyAuthApiStatus === PENDING_STATE) {
|
||||
return <Skeleton height={36} />;
|
||||
}
|
||||
const { provider, skipHintedLogin } = getTpaProvider(this.tpaHint, providers, secondaryProviders);
|
||||
if (skipHintedLogin) {
|
||||
window.location.href = getConfig().LMS_BASE_URL + provider.registerUrl;
|
||||
return null;
|
||||
}
|
||||
return provider ? (<EnterpriseSSO provider={provider} intl={intl} />)
|
||||
: this.renderForm(
|
||||
currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthApiStatus,
|
||||
finishAuthUrl,
|
||||
submitState,
|
||||
intl,
|
||||
);
|
||||
if (tpaHint) {
|
||||
if (thirdPartyAuthApiStatus === PENDING_STATE) {
|
||||
return <Skeleton height={36} />;
|
||||
}
|
||||
return this.renderForm(
|
||||
currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthApiStatus,
|
||||
finishAuthUrl,
|
||||
submitState,
|
||||
intl,
|
||||
);
|
||||
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} intl={intl} /> : renderForm();
|
||||
}
|
||||
}
|
||||
return (
|
||||
renderForm()
|
||||
);
|
||||
};
|
||||
|
||||
RegistrationPage.defaultProps = {
|
||||
extendedProfile: [],
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
registrationResult: null,
|
||||
registerNewUser: null,
|
||||
registrationErrorCode: null,
|
||||
submitState: DEFAULT_STATE,
|
||||
thirdPartyAuthApiStatus: 'pending',
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
},
|
||||
registrationFormData: {
|
||||
country: '',
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
marketingOptIn: true,
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
},
|
||||
emailFieldBorderClass: '',
|
||||
emailErrorSuggestion: null,
|
||||
emailWarningSuggestion: null,
|
||||
},
|
||||
validationDecisions: null,
|
||||
statusCode: null,
|
||||
usernameSuggestions: [],
|
||||
const mapStateToProps = state => {
|
||||
const registerPageState = state.register;
|
||||
return {
|
||||
backedUpFormData: registerPageState.registrationFormData,
|
||||
backendCountryCode: registerPageState.backendCountryCode,
|
||||
backendValidations: validationsSelector(state),
|
||||
fieldDescriptions: fieldDescriptionSelector(state),
|
||||
extendedProfile: extendedProfileSelector(state),
|
||||
optionalFields: optionalFieldsSelector(state),
|
||||
registrationErrorCode: registrationErrorSelector(state),
|
||||
registrationResult: registerPageState.registrationResult,
|
||||
shouldBackupState: registerPageState.shouldBackupState,
|
||||
userPipelineDataLoaded: registerPageState.userPipelineDataLoaded,
|
||||
submitState: registerPageState.submitState,
|
||||
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
|
||||
thirdPartyAuthContext: thirdPartyAuthContextSelector(state),
|
||||
validationApiRateLimited: registerPageState.validationApiRateLimited,
|
||||
usernameSuggestions: registerPageState.usernameSuggestions,
|
||||
};
|
||||
};
|
||||
|
||||
RegistrationPage.propTypes = {
|
||||
backedUpFormData: PropTypes.shape({
|
||||
configurableFormFields: PropTypes.shape({}),
|
||||
formFields: PropTypes.shape({}),
|
||||
errors: PropTypes.shape({}),
|
||||
emailSuggestion: PropTypes.shape({}),
|
||||
}),
|
||||
backendCountryCode: PropTypes.string,
|
||||
backendValidations: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
}),
|
||||
extendedProfile: PropTypes.arrayOf(PropTypes.string),
|
||||
fieldDescriptions: PropTypes.shape({}),
|
||||
optionalFields: PropTypes.shape({}),
|
||||
institutionLogin: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
getThirdPartyAuthContext: PropTypes.func.isRequired,
|
||||
registerNewUser: PropTypes.func,
|
||||
resetRegistrationForm: PropTypes.func.isRequired,
|
||||
setRegistrationFormData: PropTypes.func.isRequired,
|
||||
optionalFields: PropTypes.shape({}),
|
||||
registrationErrorCode: PropTypes.string,
|
||||
registrationResult: PropTypes.shape({
|
||||
redirectUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
}),
|
||||
registrationErrorCode: PropTypes.string,
|
||||
shouldBackupState: PropTypes.bool,
|
||||
submitState: PropTypes.string,
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
registrationFormData: PropTypes.shape({
|
||||
country: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
marketingOptIn: PropTypes.bool,
|
||||
errors: PropTypes.shape({
|
||||
email: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
country: PropTypes.string,
|
||||
}),
|
||||
emailFieldBorderClass: PropTypes.string,
|
||||
emailErrorSuggestion: PropTypes.string,
|
||||
emailWarningSuggestion: PropTypes.string,
|
||||
}),
|
||||
thirdPartyAuthContext: PropTypes.shape({
|
||||
currentProvider: PropTypes.string,
|
||||
platformName: PropTypes.string,
|
||||
@@ -1006,48 +648,63 @@ RegistrationPage.propTypes = {
|
||||
username: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
fetchRealtimeValidations: PropTypes.func.isRequired,
|
||||
validationDecisions: PropTypes.shape({
|
||||
country: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
}),
|
||||
clearUsernameSuggestions: PropTypes.func.isRequired,
|
||||
statusCode: PropTypes.number,
|
||||
usernameSuggestions: PropTypes.arrayOf(string),
|
||||
institutionLogin: PropTypes.bool.isRequired,
|
||||
usernameSuggestions: PropTypes.arrayOf(PropTypes.string),
|
||||
userPipelineDataLoaded: PropTypes.bool,
|
||||
validationApiRateLimited: PropTypes.bool,
|
||||
// Actions
|
||||
backupFormState: PropTypes.func.isRequired,
|
||||
getRegistrationDataFromBackend: PropTypes.func.isRequired,
|
||||
handleInstitutionLogin: PropTypes.func.isRequired,
|
||||
registerNewUser: PropTypes.func.isRequired,
|
||||
resetUsernameSuggestions: PropTypes.func.isRequired,
|
||||
setUserPipelineDetailsLoaded: PropTypes.func.isRequired,
|
||||
validateFromBackend: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const registrationResult = registrationRequestSelector(state);
|
||||
const thirdPartyAuthContext = thirdPartyAuthContextSelector(state);
|
||||
return {
|
||||
registrationErrorCode: registrationErrorSelector(state),
|
||||
submitState: state.register.submitState,
|
||||
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
|
||||
registrationResult,
|
||||
thirdPartyAuthContext,
|
||||
validationDecisions: validationsSelector(state),
|
||||
statusCode: state.register.statusCode,
|
||||
usernameSuggestions: usernameSuggestionsSelector(state),
|
||||
registrationFormData: registrationFormDataSelector(state),
|
||||
fieldDescriptions: fieldDescriptionSelector(state),
|
||||
optionalFields: optionalFieldsSelector(state),
|
||||
extendedProfile: extendedProfileSelector(state),
|
||||
};
|
||||
RegistrationPage.defaultProps = {
|
||||
backedUpFormData: {
|
||||
configurableFormFields: {},
|
||||
formFields: {
|
||||
name: '', email: '', username: '', password: '', marketingEmailsOptIn: true,
|
||||
},
|
||||
errors: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
},
|
||||
emailSuggestion: {
|
||||
suggestion: '', type: '',
|
||||
},
|
||||
},
|
||||
backendCountryCode: '',
|
||||
backendValidations: null,
|
||||
extendedProfile: [],
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
registrationErrorCode: '',
|
||||
registrationResult: null,
|
||||
shouldBackupState: false,
|
||||
submitState: DEFAULT_STATE,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
},
|
||||
usernameSuggestions: [],
|
||||
userPipelineDataLoaded: false,
|
||||
validationApiRateLimited: false,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
clearUsernameSuggestions,
|
||||
getThirdPartyAuthContext,
|
||||
fetchRealtimeValidations,
|
||||
backupFormState: backupRegistrationFormBegin,
|
||||
getRegistrationDataFromBackend: getThirdPartyAuthContext,
|
||||
resetUsernameSuggestions: clearUsernameSuggestions,
|
||||
validateFromBackend: fetchRealtimeValidations,
|
||||
registerNewUser,
|
||||
resetRegistrationForm,
|
||||
setRegistrationFormData,
|
||||
setUserPipelineDetailsLoaded: setUserPipelineDataLoaded,
|
||||
},
|
||||
)(injectIntl(RegistrationPage));
|
||||
|
||||
73
src/register/ThirdPartyAuth.jsx
Normal file
73
src/register/ThirdPartyAuth.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import {
|
||||
RenderInstitutionButton,
|
||||
SocialAuthProviders,
|
||||
} from '../common-components';
|
||||
import {
|
||||
PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* This component renders the Single sign-on (SSO) buttons for the providers passed.
|
||||
* */
|
||||
const ThirdPartyAuth = (props) => {
|
||||
const {
|
||||
providers, secondaryProviders, currentProvider, handleInstitutionLogin, thirdPartyAuthApiStatus, intl,
|
||||
} = props;
|
||||
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
|
||||
const isSocialAuthActive = !!providers.length && !currentProvider;
|
||||
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
|
||||
|
||||
return (
|
||||
<>
|
||||
{((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive) && (
|
||||
<div className="mt-4 mb-3 h4">
|
||||
{intl.formatMessage(messages['registration.other.options.heading'])}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{thirdPartyAuthApiStatus === PENDING_STATE ? (
|
||||
<Skeleton className="tpa-skeleton" height={36} count={2} />
|
||||
) : (
|
||||
<>
|
||||
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
|
||||
<RenderInstitutionButton
|
||||
onSubmitHandler={handleInstitutionLogin}
|
||||
buttonTitle={intl.formatMessage(messages['register.institution.login.button'])}
|
||||
/>
|
||||
)}
|
||||
{isSocialAuthActive && (
|
||||
<div className="row m-0">
|
||||
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ThirdPartyAuth.defaultProps = {
|
||||
currentProvider: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
thirdPartyAuthApiStatus: 'pending',
|
||||
};
|
||||
|
||||
ThirdPartyAuth.propTypes = {
|
||||
currentProvider: PropTypes.string,
|
||||
handleInstitutionLogin: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
providers: PropTypes.arrayOf(PropTypes.any),
|
||||
secondaryProviders: PropTypes.arrayOf(PropTypes.any),
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(ThirdPartyAuth);
|
||||
@@ -1,15 +1,39 @@
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const REGISTER_NEW_USER = new AsyncActionType('REGISTRATION', 'REGISTER_NEW_USER');
|
||||
export const BACKUP_REGISTRATION_DATA = new AsyncActionType('REGISTRATION', 'BACKUP_REGISTRATION_DATA');
|
||||
export const REGISTER_FORM_VALIDATIONS = new AsyncActionType('REGISTRATION', 'GET_FORM_VALIDATIONS');
|
||||
export const REGISTRATION_FORM = new AsyncActionType('REGISTRATION', 'REGISTRATION_FORM');
|
||||
export const REGISTER_NEW_USER = new AsyncActionType('REGISTRATION', 'REGISTER_NEW_USER');
|
||||
export const REGISTER_CLEAR_USERNAME_SUGGESTIONS = 'REGISTRATION_CLEAR_USERNAME_SUGGESTIONS';
|
||||
export const REGISTER_PERSIST_FORM_DATA = 'REGISTER_PERSIST_FORM_DATA';
|
||||
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
|
||||
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
|
||||
|
||||
// Reset Form
|
||||
export const resetRegistrationForm = () => ({
|
||||
type: REGISTRATION_FORM.RESET,
|
||||
// Backup registration form
|
||||
export const backupRegistrationForm = () => ({
|
||||
type: BACKUP_REGISTRATION_DATA.BASE,
|
||||
});
|
||||
|
||||
export const backupRegistrationFormBegin = (data) => ({
|
||||
type: BACKUP_REGISTRATION_DATA.BEGIN,
|
||||
payload: { ...data },
|
||||
});
|
||||
|
||||
// Validate fields from the backend
|
||||
export const fetchRealtimeValidations = (formPayload) => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.BASE,
|
||||
payload: { formPayload },
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsBegin = () => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.BEGIN,
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsSuccess = (validations) => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.SUCCESS,
|
||||
payload: { validations },
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsFailure = () => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.FAILURE,
|
||||
});
|
||||
|
||||
// Register
|
||||
@@ -33,35 +57,16 @@ export const registerNewUserFailure = (error) => ({
|
||||
payload: { ...error },
|
||||
});
|
||||
|
||||
// Realtime Field validations
|
||||
export const fetchRealtimeValidations = (formPayload) => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.BASE,
|
||||
payload: { formPayload },
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsBegin = () => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.BEGIN,
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsSuccess = (validations) => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.SUCCESS,
|
||||
payload: { validations },
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsFailure = () => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.FAILURE,
|
||||
});
|
||||
|
||||
export const clearUsernameSuggestions = () => ({
|
||||
type: REGISTER_CLEAR_USERNAME_SUGGESTIONS,
|
||||
});
|
||||
|
||||
export const setRegistrationFormData = (formData, clearRegistrationError = false) => ({
|
||||
type: REGISTER_PERSIST_FORM_DATA,
|
||||
payload: { formData, clearRegistrationError },
|
||||
});
|
||||
|
||||
export const setCountryFromThirdPartyAuthContext = (countryCode) => ({
|
||||
type: REGISTER_SET_COUNTRY_CODE,
|
||||
payload: { countryCode },
|
||||
});
|
||||
|
||||
export const setUserPipelineDataLoaded = (value) => ({
|
||||
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
|
||||
payload: { value },
|
||||
});
|
||||
|
||||
@@ -3,49 +3,50 @@ import {
|
||||
PENDING_STATE,
|
||||
} from '../../data/constants';
|
||||
import {
|
||||
BACKUP_REGISTRATION_DATA,
|
||||
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
|
||||
REGISTER_FORM_VALIDATIONS,
|
||||
REGISTER_NEW_USER,
|
||||
REGISTER_PERSIST_FORM_DATA, REGISTER_SET_COUNTRY_CODE,
|
||||
REGISTRATION_FORM,
|
||||
REGISTER_SET_COUNTRY_CODE, REGISTER_SET_USER_PIPELINE_DATA_LOADED,
|
||||
} from './actions';
|
||||
|
||||
export const defaultState = {
|
||||
backendCountryCode: '',
|
||||
registrationError: {},
|
||||
registrationResult: {},
|
||||
registrationFormData: {
|
||||
country: '',
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
marketingOptIn: true,
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
configurableFormFields: {},
|
||||
formFields: {
|
||||
name: '', email: '', username: '', password: '', marketingEmailsOptIn: true,
|
||||
},
|
||||
emailSuggestion: {
|
||||
suggestion: '', type: '',
|
||||
},
|
||||
errors: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
},
|
||||
emailFieldBorderClass: '',
|
||||
emailErrorSuggestion: null,
|
||||
emailWarningSuggestion: null,
|
||||
},
|
||||
validations: null,
|
||||
statusCode: null,
|
||||
submitState: DEFAULT_STATE,
|
||||
userPipelineDataLoaded: false,
|
||||
usernameSuggestions: [],
|
||||
extendedProfile: [],
|
||||
fieldDescriptions: {},
|
||||
formRenderState: DEFAULT_STATE,
|
||||
validationApiRateLimited: false,
|
||||
shouldBackupState: false,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
switch (action.type) {
|
||||
case REGISTRATION_FORM.RESET:
|
||||
case BACKUP_REGISTRATION_DATA.BASE:
|
||||
return {
|
||||
...state,
|
||||
shouldBackupState: true,
|
||||
};
|
||||
case BACKUP_REGISTRATION_DATA.BEGIN:
|
||||
return {
|
||||
...defaultState,
|
||||
registrationFormData: state.registrationFormData,
|
||||
usernameSuggestions: state.usernameSuggestions,
|
||||
registrationFormData: { ...action.payload },
|
||||
userPipelineDataLoaded: state.userPipelineDataLoaded,
|
||||
};
|
||||
case REGISTER_NEW_USER.BEGIN:
|
||||
return {
|
||||
@@ -69,22 +70,18 @@ const reducer = (state = defaultState, action) => {
|
||||
usernameSuggestions: usernameSuggestions || state.usernameSuggestions,
|
||||
};
|
||||
}
|
||||
case REGISTER_FORM_VALIDATIONS.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
case REGISTER_FORM_VALIDATIONS.SUCCESS: {
|
||||
const { usernameSuggestions } = action.payload.validations;
|
||||
const { usernameSuggestions, ...validationWithoutUsernameSuggestions } = action.payload.validations;
|
||||
return {
|
||||
...state,
|
||||
validations: action.payload.validations,
|
||||
validations: validationWithoutUsernameSuggestions,
|
||||
usernameSuggestions: usernameSuggestions || state.usernameSuggestions,
|
||||
};
|
||||
}
|
||||
case REGISTER_FORM_VALIDATIONS.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
statusCode: 403,
|
||||
validationApiRateLimited: true,
|
||||
validations: null,
|
||||
};
|
||||
case REGISTER_CLEAR_USERNAME_SUGGESTIONS:
|
||||
@@ -92,33 +89,28 @@ const reducer = (state = defaultState, action) => {
|
||||
...state,
|
||||
usernameSuggestions: [],
|
||||
};
|
||||
case REGISTER_PERSIST_FORM_DATA: {
|
||||
const { formData, clearRegistrationError } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
registrationError: clearRegistrationError ? {} : state.registrationError,
|
||||
registrationFormData: {
|
||||
...state.registrationFormData,
|
||||
...formData,
|
||||
},
|
||||
};
|
||||
}
|
||||
case REGISTER_SET_COUNTRY_CODE: {
|
||||
const { countryCode } = action.payload;
|
||||
if (state.registrationFormData.country === '') {
|
||||
if (!state.registrationFormData.configurableFormFields.country) {
|
||||
return {
|
||||
...state,
|
||||
registrationFormData: {
|
||||
...state.registrationFormData,
|
||||
country: countryCode,
|
||||
errors: { ...state.registrationFormData.errors, country: '' },
|
||||
},
|
||||
backendCountryCode: countryCode,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
}
|
||||
case REGISTER_SET_USER_PIPELINE_DATA_LOADED: {
|
||||
const { value } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
userPipelineDataLoaded: value,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
return {
|
||||
...state,
|
||||
shouldBackupState: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,11 +4,6 @@ export const storeName = 'register';
|
||||
|
||||
export const registerSelector = state => ({ ...state[storeName] });
|
||||
|
||||
export const registrationRequestSelector = createSelector(
|
||||
registerSelector,
|
||||
register => register.registrationResult,
|
||||
);
|
||||
|
||||
export const registrationErrorSelector = createSelector(
|
||||
registerSelector,
|
||||
register => register.registrationError.errorCode,
|
||||
@@ -36,13 +31,3 @@ export const validationsSelector = createSelector(
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
export const usernameSuggestionsSelector = createSelector(
|
||||
registerSelector,
|
||||
register => register.usernameSuggestions,
|
||||
);
|
||||
|
||||
export const registrationFormDataSelector = createSelector(
|
||||
registerSelector,
|
||||
register => register.registrationFormData,
|
||||
);
|
||||
|
||||
@@ -1,254 +1,106 @@
|
||||
import { DEFAULT_STATE } from '../../../data/constants';
|
||||
import {
|
||||
BACKUP_REGISTRATION_DATA,
|
||||
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
|
||||
REGISTER_FORM_VALIDATIONS,
|
||||
REGISTER_NEW_USER,
|
||||
REGISTER_PERSIST_FORM_DATA,
|
||||
REGISTER_SET_COUNTRY_CODE,
|
||||
REGISTRATION_FORM,
|
||||
} from '../actions';
|
||||
import reducer from '../reducers';
|
||||
|
||||
describe('register reducer', () => {
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {})).toEqual(
|
||||
{
|
||||
registrationError: {},
|
||||
registrationResult: {},
|
||||
registrationFormData: {
|
||||
country: '',
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
marketingOptIn: true,
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
},
|
||||
emailFieldBorderClass: '',
|
||||
emailErrorSuggestion: null,
|
||||
emailWarningSuggestion: null,
|
||||
},
|
||||
validations: null,
|
||||
statusCode: null,
|
||||
usernameSuggestions: [],
|
||||
extendedProfile: [],
|
||||
fieldDescriptions: {},
|
||||
formRenderState: DEFAULT_STATE,
|
||||
describe('Registration Reducer Tests', () => {
|
||||
const defaultState = {
|
||||
backendCountryCode: '',
|
||||
registrationError: {},
|
||||
registrationResult: {},
|
||||
registrationFormData: {
|
||||
configurableFormFields: {},
|
||||
formFields: {
|
||||
name: '', email: '', username: '', password: '', marketingEmailsOptIn: true,
|
||||
},
|
||||
);
|
||||
emailSuggestion: {
|
||||
suggestion: '', type: '',
|
||||
},
|
||||
errors: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
},
|
||||
},
|
||||
validations: null,
|
||||
submitState: DEFAULT_STATE,
|
||||
userPipelineDataLoaded: false,
|
||||
usernameSuggestions: [],
|
||||
validationApiRateLimited: false,
|
||||
shouldBackupState: false,
|
||||
};
|
||||
|
||||
it('should return the initial state', () => {
|
||||
expect(reducer(undefined, {})).toEqual(defaultState);
|
||||
});
|
||||
|
||||
it('should set username suggestions upon validation failed case', () => {
|
||||
const state = {
|
||||
usernameSuggestions: [],
|
||||
registrationFormData: {
|
||||
country: '',
|
||||
email: '',
|
||||
it('should set username suggestions returned by the backend validations', () => {
|
||||
const validations = {
|
||||
usernameSuggestions: ['test12'],
|
||||
validationDecisions: {
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
marketingOptIn: true,
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
},
|
||||
emailFieldBorderClass: '',
|
||||
emailErrorSuggestion: null,
|
||||
emailWarningSuggestion: null,
|
||||
},
|
||||
};
|
||||
const validations = { usernameSuggestions: ['test12'], validationDecisions: {} };
|
||||
const { usernameSuggestions, ...validationWithoutUsernameSuggestions } = validations;
|
||||
const action = {
|
||||
type: REGISTER_FORM_VALIDATIONS.SUCCESS,
|
||||
payload: { validations },
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
).toEqual(
|
||||
expect(reducer(defaultState, action)).toEqual(
|
||||
{
|
||||
validations,
|
||||
registrationFormData: {
|
||||
country: '',
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
marketingOptIn: true,
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
},
|
||||
emailFieldBorderClass: '',
|
||||
emailErrorSuggestion: null,
|
||||
emailWarningSuggestion: null,
|
||||
},
|
||||
usernameSuggestions: validations.usernameSuggestions,
|
||||
...defaultState,
|
||||
usernameSuggestions,
|
||||
validations: validationWithoutUsernameSuggestions,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should set username suggestions upon registration error case', () => {
|
||||
const state = {
|
||||
usernameSuggestions: [],
|
||||
};
|
||||
it('should show username suggestions returned by registration error', () => {
|
||||
const payload = { usernameSuggestions: ['test12'] };
|
||||
const action = {
|
||||
type: REGISTER_NEW_USER.FAILURE,
|
||||
payload,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
).toEqual(
|
||||
expect(reducer(defaultState, action)).toEqual(
|
||||
{
|
||||
...defaultState,
|
||||
registrationError: payload,
|
||||
submitState: DEFAULT_STATE,
|
||||
usernameSuggestions: payload.usernameSuggestions,
|
||||
validations: null,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear username suggestions from validations state', () => {
|
||||
it('should clear username suggestions', () => {
|
||||
const state = {
|
||||
...defaultState,
|
||||
usernameSuggestions: ['test_1'],
|
||||
};
|
||||
const action = {
|
||||
type: REGISTER_CLEAR_USERNAME_SUGGESTIONS,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
).toEqual(
|
||||
{
|
||||
usernameSuggestions: [],
|
||||
},
|
||||
);
|
||||
expect(reducer(state, action)).toEqual({ ...defaultState });
|
||||
});
|
||||
it('should not reset username suggestions and form data in form reset', () => {
|
||||
|
||||
it('should not reset username suggestions and fields data during form reset', () => {
|
||||
const state = {
|
||||
registrationError: {},
|
||||
registrationResult: {},
|
||||
registrationFormData: {
|
||||
country: 'PK',
|
||||
email: 'test@email.com',
|
||||
name: 'John Doe',
|
||||
password: 'johndoe',
|
||||
username: 'john',
|
||||
marketingOptIn: true,
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
},
|
||||
emailErrorSuggestion: 'test@email.com',
|
||||
emailWarningSuggestion: 'test@email.com',
|
||||
},
|
||||
validations: null,
|
||||
statusCode: null,
|
||||
extendedProfile: [],
|
||||
fieldDescriptions: {},
|
||||
formRenderState: DEFAULT_STATE,
|
||||
...defaultState,
|
||||
usernameSuggestions: ['test1', 'test2'],
|
||||
};
|
||||
const action = {
|
||||
type: REGISTRATION_FORM.RESET,
|
||||
type: BACKUP_REGISTRATION_DATA.BEGIN,
|
||||
payload: { ...state.registrationFormData },
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
).toEqual(
|
||||
state,
|
||||
);
|
||||
expect(reducer(state, action)).toEqual(state);
|
||||
});
|
||||
|
||||
it('should set registrationFormData', () => {
|
||||
const state = {
|
||||
registrationFormData: {
|
||||
country: '',
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
marketingOptIn: true,
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
},
|
||||
emailErrorSuggestion: null,
|
||||
emailWarningSuggestion: null,
|
||||
},
|
||||
};
|
||||
const formData = {
|
||||
country: 'PK',
|
||||
email: 'test@email.com',
|
||||
name: 'John Doe',
|
||||
password: 'johndoe',
|
||||
username: 'john',
|
||||
emailErrorSuggestion: 'test@email.com',
|
||||
emailWarningSuggestion: 'test@email.com',
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: REGISTER_PERSIST_FORM_DATA,
|
||||
payload: { formData },
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
).toEqual(
|
||||
{
|
||||
registrationFormData: {
|
||||
...state.registrationFormData,
|
||||
country: 'PK',
|
||||
email: 'test@email.com',
|
||||
name: 'John Doe',
|
||||
password: 'johndoe',
|
||||
username: 'john',
|
||||
emailErrorSuggestion: 'test@email.com',
|
||||
emailWarningSuggestion: 'test@email.com',
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should set country code from context', () => {
|
||||
const state = {
|
||||
registrationFormData: {
|
||||
country: '',
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
marketingOptIn: true,
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
},
|
||||
emailErrorSuggestion: null,
|
||||
emailWarningSuggestion: null,
|
||||
},
|
||||
};
|
||||
it('should set country code', () => {
|
||||
const countryCode = 'PK';
|
||||
|
||||
const action = {
|
||||
@@ -256,14 +108,10 @@ describe('register reducer', () => {
|
||||
payload: { countryCode },
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
).toEqual(
|
||||
expect(reducer(defaultState, action)).toEqual(
|
||||
{
|
||||
registrationFormData: {
|
||||
...state.registrationFormData,
|
||||
country: 'PK',
|
||||
},
|
||||
...defaultState,
|
||||
backendCountryCode: countryCode,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
111
src/register/data/utils.js
Normal file
111
src/register/data/utils.js
Normal file
@@ -0,0 +1,111 @@
|
||||
import { distance } from 'fastest-levenshtein';
|
||||
|
||||
import {
|
||||
COMMON_EMAIL_PROVIDERS,
|
||||
COUNTRY_CODE_KEY,
|
||||
COUNTRY_DISPLAY_KEY,
|
||||
DEFAULT_SERVICE_PROVIDER_DOMAINS,
|
||||
DEFAULT_TOP_LEVEL_DOMAINS,
|
||||
} from './constants';
|
||||
|
||||
function getLevenshteinSuggestion(word, knownWords, similarityThreshold = 4) {
|
||||
if (!word) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let minEditDistance = 100;
|
||||
let mostSimilar = word;
|
||||
|
||||
for (let i = 0; i < knownWords.length; i++) {
|
||||
const editDistance = distance(knownWords[i].toLowerCase(), word.toLowerCase());
|
||||
if (editDistance < minEditDistance) {
|
||||
minEditDistance = editDistance;
|
||||
mostSimilar = knownWords[i];
|
||||
}
|
||||
}
|
||||
|
||||
return minEditDistance <= similarityThreshold && word !== mostSimilar ? mostSimilar : null;
|
||||
}
|
||||
|
||||
export function getSuggestionForInvalidEmail(domain, username) {
|
||||
if (!domain) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const defaultDomains = ['yahoo', 'aol', 'hotmail', 'live', 'outlook', 'gmail'];
|
||||
const suggestion = getLevenshteinSuggestion(domain, COMMON_EMAIL_PROVIDERS);
|
||||
|
||||
if (suggestion) {
|
||||
return `${username}@${suggestion}`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < defaultDomains.length; i++) {
|
||||
if (domain.includes(defaultDomains[i])) {
|
||||
return `${username}@${defaultDomains[i]}.com`;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export function validateEmailAddress(value, username, domainName) {
|
||||
let suggestion = null;
|
||||
const validation = {
|
||||
hasError: false,
|
||||
suggestion: '',
|
||||
type: '',
|
||||
};
|
||||
|
||||
const hasMultipleSubdomains = value.match(/\./g).length > 1;
|
||||
const [serviceLevelDomain, topLevelDomain] = domainName.split('.');
|
||||
const tldSuggestion = !DEFAULT_TOP_LEVEL_DOMAINS.includes(topLevelDomain);
|
||||
const serviceSuggestion = getLevenshteinSuggestion(serviceLevelDomain, DEFAULT_SERVICE_PROVIDER_DOMAINS, 2);
|
||||
|
||||
if (DEFAULT_SERVICE_PROVIDER_DOMAINS.includes(serviceSuggestion || serviceLevelDomain)) {
|
||||
suggestion = `${username}@${serviceSuggestion || serviceLevelDomain}.com`;
|
||||
}
|
||||
|
||||
if (!hasMultipleSubdomains && tldSuggestion) {
|
||||
validation.suggestion = suggestion;
|
||||
validation.type = 'error';
|
||||
} else if (serviceSuggestion) {
|
||||
validation.suggestion = suggestion;
|
||||
validation.type = 'warning';
|
||||
} else {
|
||||
suggestion = getLevenshteinSuggestion(domainName, COMMON_EMAIL_PROVIDERS, 3);
|
||||
if (suggestion) {
|
||||
validation.suggestion = `${username}@${suggestion}`;
|
||||
validation.type = 'warning';
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMultipleSubdomains && tldSuggestion) {
|
||||
validation.hasError = true;
|
||||
}
|
||||
|
||||
return validation;
|
||||
}
|
||||
|
||||
export function validateCountryField(value, countryList, errorMessage) {
|
||||
let countryCode = '';
|
||||
let displayValue = value;
|
||||
let error = errorMessage;
|
||||
|
||||
if (value) {
|
||||
const normalizedValue = value.toLowerCase();
|
||||
// Handling a case here where user enters a valid country code that needs to be
|
||||
// evaluated and set its value as a valid value.
|
||||
const selectedCountry = countryList.find(
|
||||
(country) => (
|
||||
country[COUNTRY_DISPLAY_KEY].toLowerCase() === normalizedValue
|
||||
|| country[COUNTRY_CODE_KEY].toLowerCase() === normalizedValue
|
||||
),
|
||||
);
|
||||
if (selectedCountry) {
|
||||
countryCode = selectedCountry[COUNTRY_CODE_KEY];
|
||||
displayValue = selectedCountry[COUNTRY_DISPLAY_KEY];
|
||||
error = '';
|
||||
}
|
||||
}
|
||||
return { error, countryCode, displayValue };
|
||||
}
|
||||
@@ -285,11 +285,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Suggested:',
|
||||
description: 'Suggested usernames label text.',
|
||||
},
|
||||
'registration.using.tpa.form.heading': {
|
||||
id: 'registration.using.tpa.form.heading',
|
||||
defaultMessage: 'Finish creating your account',
|
||||
description: 'Heading that appears above form when user is trying to create account using social auth',
|
||||
},
|
||||
'did.you.mean.alert.text': {
|
||||
id: 'did.you.mean.alert.text',
|
||||
defaultMessage: 'Did you mean',
|
||||
|
||||
194
src/register/registrationFields/CountryField.jsx
Normal file
194
src/register/registrationFields/CountryField.jsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, IconButton } from '@edx/paragon';
|
||||
import { ExpandLess, ExpandMore } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormGroup } from '../../common-components';
|
||||
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from '../data/constants';
|
||||
import messages from '../messages';
|
||||
|
||||
const CountryField = (props) => {
|
||||
const {
|
||||
intl, countryList, selectedCountry,
|
||||
} = props;
|
||||
|
||||
const dropdownRef = useRef(null);
|
||||
const [errorMessage, setErrorMessage] = useState(props.errorMessage);
|
||||
const [dropDownItems, setDropDownItems] = useState([]);
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
const [trailingIcon, setTrailingIcon] = useState(null);
|
||||
|
||||
const onBlurHandler = (event, itemClicked = false) => {
|
||||
const { name } = event.target;
|
||||
const relatedName = event.relatedTarget ? event.relatedTarget.name : '';
|
||||
// For a better user experience, do not validate when focus out from 'country' field
|
||||
// and focus on 'countryItem' or 'countryExpand' button.
|
||||
if ((relatedName === 'countryItem' || relatedName === 'countryExpand') && name === 'country') {
|
||||
return;
|
||||
}
|
||||
const countryValue = itemClicked ? event.target.value : displayValue;
|
||||
if (props.onBlurHandler) {
|
||||
props.onBlurHandler({ target: { name: 'country', value: countryValue } });
|
||||
}
|
||||
setTrailingIcon(<ExpandMoreButton />);
|
||||
setDropDownItems([]);
|
||||
};
|
||||
|
||||
const getDropdownItems = (countryToFind = null) => {
|
||||
let updatedCountryList = countryList;
|
||||
if (countryToFind) {
|
||||
updatedCountryList = countryList.filter(
|
||||
(option) => (option.name.toLowerCase().includes(countryToFind.toLowerCase())),
|
||||
);
|
||||
}
|
||||
|
||||
return updatedCountryList.map((country) => {
|
||||
const countryName = country[COUNTRY_DISPLAY_KEY];
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
name="countryItem"
|
||||
className="dropdown-item data-hj-suppress"
|
||||
value={countryName}
|
||||
key={country[COUNTRY_CODE_KEY]}
|
||||
onClick={(event) => onBlurHandler(event, true)}
|
||||
>
|
||||
{countryName.length > 30 ? countryName.substring(0, 30).concat('...') : countryName}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const onFocusHandler = (event) => {
|
||||
const { name, value } = event.target;
|
||||
setDropDownItems(getDropdownItems(name === 'country' ? value : displayValue));
|
||||
setTrailingIcon(<ExpandLessButton />);
|
||||
setErrorMessage('');
|
||||
if (props.onFocusHandler) { props.onFocusHandler(event); }
|
||||
};
|
||||
|
||||
const onChangeHandler = (event) => {
|
||||
const filteredItems = getDropdownItems(event.target.value);
|
||||
setDropDownItems(filteredItems);
|
||||
setDisplayValue(event.target.value);
|
||||
if (props.onChangeHandler) { props.onChangeHandler(event, { countryCode: '', displayValue: event.target.value }); }
|
||||
};
|
||||
|
||||
const handleOnClickOutside = () => {
|
||||
setTrailingIcon(<ExpandMoreButton />);
|
||||
setDropDownItems([]);
|
||||
};
|
||||
|
||||
const handleExpandMore = () => {
|
||||
setTrailingIcon(<ExpandLessButton />);
|
||||
setDropDownItems(getDropdownItems());
|
||||
};
|
||||
|
||||
const handleExpandLess = () => {
|
||||
setTrailingIcon(<ExpandMoreButton />);
|
||||
setDropDownItems([]);
|
||||
};
|
||||
|
||||
const ExpandMoreButton = () => (
|
||||
<IconButton
|
||||
name="countryExpand"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt="expand-more"
|
||||
className="expand-more"
|
||||
iconAs={Icon}
|
||||
src={ExpandMore}
|
||||
onBlur={() => {}}
|
||||
onClick={handleExpandMore}
|
||||
onFocus={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
const ExpandLessButton = () => (
|
||||
<IconButton
|
||||
name="countryExpand"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt="expand-less"
|
||||
className="expand-less"
|
||||
iconAs={Icon}
|
||||
src={ExpandLess}
|
||||
onBlur={() => {}}
|
||||
onClick={handleExpandLess}
|
||||
onFocus={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||||
handleOnClickOutside();
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', handleClickOutside, true);
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside, true);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!trailingIcon) {
|
||||
setTrailingIcon(<ExpandMoreButton />);
|
||||
}
|
||||
}, [trailingIcon]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedCountry.displayValue) {
|
||||
setDisplayValue(selectedCountry.displayValue);
|
||||
}
|
||||
}, [selectedCountry]);
|
||||
|
||||
useEffect(() => {
|
||||
setErrorMessage(props.errorMessage);
|
||||
}, [props.errorMessage]);
|
||||
|
||||
return (
|
||||
<div ref={dropdownRef} className="mb-4">
|
||||
<FormGroup
|
||||
as="input"
|
||||
name="country"
|
||||
autoComplete="chrome-off"
|
||||
className="mb-0"
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
trailingElement={trailingIcon}
|
||||
value={displayValue}
|
||||
errorMessage={errorMessage}
|
||||
handleChange={onChangeHandler}
|
||||
handleBlur={onBlurHandler}
|
||||
handleFocus={onFocusHandler}
|
||||
/>
|
||||
<div className="dropdown-container">
|
||||
{ dropDownItems?.length > 0 ? dropDownItems : null }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CountryField.propTypes = {
|
||||
countryList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
onBlurHandler: PropTypes.func.isRequired,
|
||||
onChangeHandler: PropTypes.func.isRequired,
|
||||
onFocusHandler: PropTypes.func.isRequired,
|
||||
selectedCountry: PropTypes.shape({
|
||||
displayValue: PropTypes.string,
|
||||
countryCode: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
CountryField.defaultProps = {
|
||||
errorMessage: null,
|
||||
selectedCountry: {
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
|
||||
export default injectIntl(CountryField);
|
||||
82
src/register/registrationFields/EmailField.jsx
Normal file
82
src/register/registrationFields/EmailField.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Icon } from '@edx/paragon';
|
||||
import { Close, Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormGroup } from '../../common-components';
|
||||
import messages from '../messages';
|
||||
|
||||
const EmailField = (props) => {
|
||||
const {
|
||||
intl,
|
||||
emailSuggestion,
|
||||
handleSuggestionClick,
|
||||
handleOnClose,
|
||||
} = props;
|
||||
|
||||
const renderEmailFeedback = () => {
|
||||
if (emailSuggestion.type === 'error') {
|
||||
return (
|
||||
<Alert variant="danger" className="email-error-alert mt-1" icon={Error}>
|
||||
<span className="alert-text">
|
||||
{intl.formatMessage(messages['did.you.mean.alert.text'])}{' '}
|
||||
<Alert.Link
|
||||
href="#"
|
||||
name="email"
|
||||
onClick={handleSuggestionClick}
|
||||
>
|
||||
{emailSuggestion.suggestion}
|
||||
</Alert.Link>?<Icon src={Close} className="alert-close" onClick={handleOnClose} tabIndex="0" />
|
||||
</span>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span className="small">
|
||||
{intl.formatMessage(messages['did.you.mean.alert.text'])}:{' '}
|
||||
<Alert.Link
|
||||
href="#"
|
||||
name="email"
|
||||
className="email-warning-alert-link"
|
||||
onClick={handleSuggestionClick}
|
||||
>
|
||||
{emailSuggestion.suggestion}
|
||||
</Alert.Link>?
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
borderClass={emailSuggestion.type === 'warning' ? 'yellow-border' : ''}
|
||||
maxLength={254} // Limit per RFCs is 254
|
||||
{...props}
|
||||
>
|
||||
{emailSuggestion.suggestion ? renderEmailFeedback() : null}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
EmailField.defaultProps = {
|
||||
emailSuggestion: {
|
||||
suggestion: '',
|
||||
type: '',
|
||||
},
|
||||
errorMessage: '',
|
||||
};
|
||||
|
||||
EmailField.propTypes = {
|
||||
errorMessage: PropTypes.string,
|
||||
emailSuggestion: PropTypes.shape({
|
||||
suggestion: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
}),
|
||||
intl: intlShape.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
handleOnClose: PropTypes.func.isRequired,
|
||||
handleSuggestionClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(EmailField);
|
||||
@@ -1,17 +1,23 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Hyperlink } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
import messages from '../messages';
|
||||
|
||||
const HonorCode = (props) => {
|
||||
const {
|
||||
intl, errorMessage, onChangeHandler, fieldType, value,
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (fieldType === 'tos_and_honor_code' && !value) {
|
||||
onChangeHandler({ target: { name: 'honor_code', value: true } });
|
||||
}
|
||||
}, [fieldType, onChangeHandler, value]);
|
||||
|
||||
if (fieldType === 'tos_and_honor_code') {
|
||||
return (
|
||||
<div id="honor-code" className="micro text-muted mt-4">
|
||||
@@ -5,7 +5,7 @@ import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/
|
||||
import { Form, Hyperlink } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
import messages from '../messages';
|
||||
|
||||
const TermsOfService = (props) => {
|
||||
const {
|
||||
@@ -5,11 +5,13 @@ import { Button, Icon, IconButton } from '@edx/paragon';
|
||||
import { Close } from '@edx/paragon/icons';
|
||||
import PropTypes, { string } from 'prop-types';
|
||||
|
||||
import { FormGroup } from '../common-components';
|
||||
import messages from './messages';
|
||||
import { FormGroup } from '../../common-components';
|
||||
import messages from '../messages';
|
||||
|
||||
const UsernameField = (props) => {
|
||||
const { intl, usernameSuggestions, errorMessage } = props;
|
||||
const {
|
||||
intl, handleSuggestionClick, handleUsernameSuggestionClose, usernameSuggestions, errorMessage,
|
||||
} = props;
|
||||
let className = '';
|
||||
let suggestedUsernameDiv = <></>;
|
||||
let iconButton = <></>;
|
||||
@@ -25,7 +27,7 @@ const UsernameField = (props) => {
|
||||
className="username-suggestion data-hj-suppress"
|
||||
autoComplete={props.autoComplete}
|
||||
key={`suggestion-${index.toString()}`}
|
||||
onClick={(e) => props.handleSuggestionClick(e, username)}
|
||||
onClick={(e) => handleSuggestionClick(e, 'username', username)}
|
||||
>
|
||||
{username}
|
||||
</Button>
|
||||
@@ -36,11 +38,11 @@ const UsernameField = (props) => {
|
||||
);
|
||||
if (usernameSuggestions.length > 0 && errorMessage && props.value === ' ') {
|
||||
className = 'suggested-username-with-error';
|
||||
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => props.handleUsernameSuggestionClose()} variant="black" size="sm" className="suggested-username-close-button" />;
|
||||
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="suggested-username-close-button" />;
|
||||
suggestedUsernameDiv = suggestedUsernames();
|
||||
} else if (usernameSuggestions.length > 0 && props.value === ' ') {
|
||||
className = 'suggested-username';
|
||||
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => props.handleUsernameSuggestionClose()} variant="black" size="sm" className="suggested-username-close-button" />;
|
||||
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="suggested-username-close-button" />;
|
||||
suggestedUsernameDiv = suggestedUsernames();
|
||||
} else if (usernameSuggestions.length > 0 && errorMessage) {
|
||||
suggestedUsernameDiv = suggestedUsernames();
|
||||
@@ -54,16 +56,14 @@ const UsernameField = (props) => {
|
||||
|
||||
UsernameField.defaultProps = {
|
||||
usernameSuggestions: [],
|
||||
handleSuggestionClick: () => {},
|
||||
handleUsernameSuggestionClose: () => {},
|
||||
errorMessage: '',
|
||||
autoComplete: null,
|
||||
};
|
||||
|
||||
UsernameField.propTypes = {
|
||||
usernameSuggestions: PropTypes.arrayOf(string),
|
||||
handleSuggestionClick: PropTypes.func,
|
||||
handleUsernameSuggestionClose: PropTypes.func,
|
||||
handleSuggestionClick: PropTypes.func.isRequired,
|
||||
handleUsernameSuggestionClose: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
5
src/register/registrationFields/index.js
Normal file
5
src/register/registrationFields/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export { default as EmailField } from './EmailField';
|
||||
export { default as UsernameField } from './UsernameField';
|
||||
export { default as CountryField } from './CountryField';
|
||||
export { default as HonorCode } from './HonorCode';
|
||||
export { default as TermsOfService } from './TermsOfService';
|
||||
@@ -4,7 +4,7 @@ import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import HonorCode from '../HonorCode';
|
||||
import HonorCode from '../registrationFields/HonorCode';
|
||||
|
||||
const IntlHonorCode = injectIntl(HonorCode);
|
||||
|
||||
|
||||
@@ -4,21 +4,17 @@ import { Provider } from 'react-redux';
|
||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
configure, getLocale, injectIntl, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { COMPLETE_STATE, PENDING_STATE, WELCOME_PAGE } from '../../data/constants';
|
||||
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
|
||||
import {
|
||||
backupRegistrationFormBegin,
|
||||
clearUsernameSuggestions,
|
||||
fetchRealtimeValidations,
|
||||
registerNewUser,
|
||||
resetRegistrationForm,
|
||||
setRegistrationFormData,
|
||||
} from '../data/actions';
|
||||
import {
|
||||
FIELDS, FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_SESSION_EXPIRED,
|
||||
@@ -27,10 +23,6 @@ import RegistrationFailureMessage from '../RegistrationFailure';
|
||||
import RegistrationPage from '../RegistrationPage';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
getLocale: jest.fn(),
|
||||
}));
|
||||
|
||||
analytics.sendTrackEvent = jest.fn();
|
||||
analytics.sendPageEvent = jest.fn();
|
||||
@@ -41,15 +33,27 @@ const mockStore = configureStore();
|
||||
|
||||
describe('RegistrationPage', () => {
|
||||
mergeConfig({
|
||||
PRIVACY_POLICY: 'http://privacy-policy.com',
|
||||
TOS_AND_HONOR_CODE: 'http://tos-and-honot-code.com',
|
||||
PRIVACY_POLICY: 'https://privacy-policy.com',
|
||||
TOS_AND_HONOR_CODE: 'https://tos-and-honot-code.com',
|
||||
USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME,
|
||||
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME,
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: true,
|
||||
});
|
||||
|
||||
let props = {};
|
||||
let store = {};
|
||||
let registrationFormData = {};
|
||||
const registrationFormData = {
|
||||
configurableFormFields: {},
|
||||
formFields: {
|
||||
name: '', email: '', username: '', password: '', marketingEmailsOptIn: true,
|
||||
},
|
||||
emailSuggestion: {
|
||||
suggestion: '', type: '',
|
||||
},
|
||||
errors: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
},
|
||||
};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
@@ -58,7 +62,6 @@ describe('RegistrationPage', () => {
|
||||
);
|
||||
|
||||
const thirdPartyAuthContext = {
|
||||
platformName: 'openedX',
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
providers: [],
|
||||
@@ -88,24 +91,6 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
messages: { 'es-419': {}, de: {}, 'en-us': {} },
|
||||
});
|
||||
registrationFormData = {
|
||||
country: '',
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
marketingOptIn: true,
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
},
|
||||
emailFieldBorderClass: '',
|
||||
emailErrorSuggestion: null,
|
||||
emailWarningSuggestion: null,
|
||||
};
|
||||
props = {
|
||||
registrationResult: jest.fn(),
|
||||
handleInstitutionLogin: jest.fn(),
|
||||
@@ -122,14 +107,16 @@ describe('RegistrationPage', () => {
|
||||
registerPage.find('input#name').simulate('change', { target: { value: payload.name, name: 'name' } });
|
||||
registerPage.find('input#username').simulate('change', { target: { value: payload.username, name: 'username' } });
|
||||
registerPage.find('input#email').simulate('change', { target: { value: payload.email, name: 'email' } });
|
||||
|
||||
registerPage.find('input#country').simulate('change', { target: { value: payload.country, name: 'country' } });
|
||||
registerPage.find('input#country').simulate('blur', { target: { value: payload.country, name: 'country' } });
|
||||
|
||||
if (!isThirdPartyAuth) {
|
||||
registerPage.find('input#password').simulate('change', { target: { value: payload.password, name: 'password' } });
|
||||
}
|
||||
};
|
||||
|
||||
describe('TestRegistrationPage', () => {
|
||||
describe('Test Registration Page', () => {
|
||||
const emptyFieldValidation = {
|
||||
name: 'Enter your full name',
|
||||
username: 'Username must be between 2 and 30 characters',
|
||||
@@ -153,7 +140,6 @@ describe('RegistrationPage', () => {
|
||||
// ******** test registration form submission ********
|
||||
|
||||
it('should submit form for valid input', () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
delete window.location;
|
||||
@@ -167,19 +153,14 @@ describe('RegistrationPage', () => {
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
totalRegistrationTime: 0,
|
||||
is_authn_mfe: true,
|
||||
marketing_emails_opt_in: true,
|
||||
next: '/course/demo-course-url',
|
||||
};
|
||||
const nextProps = {
|
||||
registrationFormData: {
|
||||
country: 'PK',
|
||||
},
|
||||
extended_profile: [],
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
populateRequiredFields(registerPage, payload);
|
||||
registerPage.find('RegistrationPage').instance().shouldComponentUpdate(nextProps);
|
||||
registerPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
@@ -195,12 +176,8 @@ describe('RegistrationPage', () => {
|
||||
honor_code: true,
|
||||
social_auth_provider: ssoProvider.name,
|
||||
totalRegistrationTime: 0,
|
||||
is_authn_mfe: true,
|
||||
};
|
||||
const nextProps = {
|
||||
registrationFormData: {
|
||||
country: 'PK',
|
||||
},
|
||||
marketing_emails_opt_in: true,
|
||||
extended_profile: [],
|
||||
};
|
||||
|
||||
store = mockStore({
|
||||
@@ -217,7 +194,6 @@ describe('RegistrationPage', () => {
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
|
||||
populateRequiredFields(registerPage, formPayload, true);
|
||||
registerPage.find('RegistrationPage').instance().shouldComponentUpdate(nextProps);
|
||||
registerPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' }));
|
||||
});
|
||||
@@ -243,54 +219,61 @@ describe('RegistrationPage', () => {
|
||||
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country);
|
||||
|
||||
const alertBanner = 'We couldn\'t create your account.Please check your responses and try again.';
|
||||
registrationPage.find('RegistrationPage').setState({ failureCount: 1 });
|
||||
|
||||
expect(registrationPage.find('#validation-errors').first().text()).toEqual(alertBanner);
|
||||
expect(registrationPage.find('RegistrationPage').state('failureCount')).toEqual(1);
|
||||
});
|
||||
|
||||
it('should update errors for frontend validations', () => {
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
|
||||
registrationPage.find('input#name').simulate('blur', { target: { value: 'http://test.com', name: 'name' } });
|
||||
expect(
|
||||
registrationPage.find('div[feedback-for="name"]').text(),
|
||||
).toEqual('Enter a valid name');
|
||||
|
||||
registrationPage.find('input#password').simulate('blur', { target: { value: 'pas', name: 'password' } });
|
||||
expect(registrationPage.find('RegistrationPage').state('errors')).toEqual({
|
||||
email: '', name: 'Enter a valid name', username: '', password: 'Password criteria has not been met', country: '',
|
||||
});
|
||||
expect(
|
||||
registrationPage.find('div[feedback-for="password"]').text(),
|
||||
).toContain('Password criteria has not been met');
|
||||
|
||||
registrationPage.find('input#password').simulate('blur', { target: { value: 'invalid-email', name: 'email' } });
|
||||
expect(registrationPage.find('RegistrationPage').state('errors')).toEqual({
|
||||
email: 'Enter a valid email address', name: 'Enter a valid name', username: '', password: 'Password criteria has not been met', country: '',
|
||||
});
|
||||
expect(
|
||||
registrationPage.find('div[feedback-for="email"]').text(),
|
||||
).toEqual('Enter a valid email address');
|
||||
});
|
||||
|
||||
it('should validate fields on blur event', () => {
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
|
||||
registrationPage.find('input#username').simulate('blur', { target: { value: '', name: 'username' } });
|
||||
expect(registrationPage.find('div[feedback-for="username"]').text()).toEqual(emptyFieldValidation.username);
|
||||
|
||||
registrationPage.find('input#name').simulate('blur', { target: { value: '', name: 'name' } });
|
||||
expect(registrationPage.find('div[feedback-for="name"]').text()).toEqual(emptyFieldValidation.name);
|
||||
|
||||
registrationPage.find('input#email').simulate('blur', { target: { value: '', name: 'email' } });
|
||||
expect(registrationPage.find('div[feedback-for="email"]').text()).toEqual(emptyFieldValidation.email);
|
||||
|
||||
registrationPage.find('input#password').simulate('blur', { target: { value: '', name: 'password' } });
|
||||
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
|
||||
|
||||
registrationPage.find('input#country').simulate('blur', { target: { value: '', name: 'country' } });
|
||||
expect(registrationPage.find('RegistrationPage').state('errors')).toEqual(emptyFieldValidation);
|
||||
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country);
|
||||
});
|
||||
|
||||
it('should call validation api on blur event, if frontend validations have passed', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
|
||||
// enter a valid username so that frontend validations are passed
|
||||
// Enter a valid username so that frontend validations are passed
|
||||
registrationPage.find('input#username').simulate('change', { target: { value: 'test', name: 'username' } });
|
||||
registrationPage.find('input#username').simulate('blur');
|
||||
|
||||
const formPayload = {
|
||||
form_field_key: 'username',
|
||||
is_authn_mfe: true,
|
||||
email: '',
|
||||
name: '',
|
||||
username: 'test',
|
||||
password: '',
|
||||
country: '',
|
||||
honor_code: true,
|
||||
};
|
||||
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations(formPayload));
|
||||
});
|
||||
@@ -307,34 +290,12 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />)).find('RegistrationPage');
|
||||
expect(registrationPage.prop('validationDecisions')).toEqual({
|
||||
expect(registrationPage.prop('backendValidations')).toEqual({
|
||||
email: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account`,
|
||||
username: 'It looks like this username is already taken',
|
||||
});
|
||||
});
|
||||
|
||||
it('should change validations in shouldComponentUpdate', () => {
|
||||
const formData = {
|
||||
errors: {
|
||||
...registrationFormData.errors,
|
||||
username: 'It looks like this username is already taken',
|
||||
},
|
||||
};
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const nextProps = {
|
||||
thirdPartyAuthContext,
|
||||
registrationErrorCode: 'duplicate-username',
|
||||
validationDecisions: {
|
||||
username: 'It looks like this username is already taken',
|
||||
},
|
||||
};
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />)).find('RegistrationPage');
|
||||
expect(registrationPage.instance().shouldComponentUpdate(nextProps)).toBe(false);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData(formData, true));
|
||||
expect(registrationPage.state('errorCode')).toEqual('duplicate-username');
|
||||
});
|
||||
|
||||
it('should remove space from the start of username', () => {
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registrationPage.find('input#username').simulate('change', { target: { value: ' test-user', name: 'username' } });
|
||||
@@ -345,27 +306,28 @@ describe('RegistrationPage', () => {
|
||||
// ******** test field focus in functionality ********
|
||||
|
||||
it('should clear field related error messages on input field Focus', () => {
|
||||
const errors = {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
};
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(registrationPage.find('div[feedback-for="name"]').text()).toEqual(emptyFieldValidation.name);
|
||||
registrationPage.find('input#name').simulate('focus');
|
||||
expect(registrationPage.find('div[feedback-for="name"]').exists()).toBeFalsy();
|
||||
|
||||
expect(registrationPage.find('div[feedback-for="username"]').text()).toEqual(emptyFieldValidation.username);
|
||||
registrationPage.find('input#username').simulate('focus');
|
||||
expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeFalsy();
|
||||
|
||||
expect(registrationPage.find('div[feedback-for="email"]').text()).toEqual(emptyFieldValidation.email);
|
||||
registrationPage.find('input#email').simulate('focus');
|
||||
expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeFalsy();
|
||||
|
||||
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
|
||||
registrationPage.find('input#password').simulate('focus');
|
||||
expect(registrationPage.find('div[feedback-for="password"]').exists()).toBeFalsy();
|
||||
|
||||
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country);
|
||||
registrationPage.find('input#country').simulate('focus');
|
||||
expect(registrationPage.find('RegistrationPage').state('errors')).toEqual(errors);
|
||||
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should clear username suggestions when username field is focused in', () => {
|
||||
@@ -459,6 +421,19 @@ describe('RegistrationPage', () => {
|
||||
expect(button.find('.sr-only').text()).toEqual('pending');
|
||||
});
|
||||
|
||||
it('should display opt-in/opt-out checkbox', () => {
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: 'true',
|
||||
});
|
||||
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(registerPage.find('div.opt-checkbox').length).toEqual(1);
|
||||
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should show single sign on provider button', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -550,7 +525,6 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registerPage.find('RegistrationPage').instance().shouldComponentUpdate(props);
|
||||
expect(registerPage.find('button.username-suggestion').length).toEqual(3);
|
||||
});
|
||||
|
||||
@@ -559,7 +533,7 @@ describe('RegistrationPage', () => {
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
usernameSuggestions: ['testname', 't.name', 'test_0'],
|
||||
usernameSuggestions: ['test_1', 'test_12', 'test_123'],
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
username: ' ',
|
||||
@@ -578,7 +552,7 @@ describe('RegistrationPage', () => {
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
usernameSuggestions: ['testname', 't.name', 'test_0'],
|
||||
usernameSuggestions: ['test_1', 'test_12', 'test_123'],
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
username: ' ',
|
||||
@@ -588,28 +562,27 @@ describe('RegistrationPage', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registerPage.find('RegistrationPage').instance().shouldComponentUpdate(props);
|
||||
registerPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
|
||||
registerPage.find('button.suggested-username-close-button').at(0).simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
|
||||
});
|
||||
|
||||
it('should redirect to url returned in registration result after successful account creation', () => {
|
||||
const dasboardUrl = 'http://test.com/testing-dashboard/';
|
||||
const dashboardURL = 'https://test.com/testing-dashboard/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
registrationResult: {
|
||||
success: true,
|
||||
redirectUrl: dasboardUrl,
|
||||
redirectUrl: dashboardURL,
|
||||
},
|
||||
},
|
||||
});
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
renderer.create(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(window.location.href).toBe(dasboardUrl);
|
||||
expect(window.location.href).toBe(dashboardURL);
|
||||
});
|
||||
|
||||
it('should redirect to social auth provider url on SSO button click', () => {
|
||||
@@ -663,6 +636,27 @@ describe('RegistrationPage', () => {
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
|
||||
});
|
||||
|
||||
it('should redirect to dashboard if features flags are configured but no optional fields are configured', () => {
|
||||
mergeConfig({
|
||||
ENABLE_PROGRESSIVE_PROFILING: true,
|
||||
});
|
||||
const dashboardUrl = 'https://test.com/testing-dashboard/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
registrationResult: {
|
||||
success: true,
|
||||
redirectUrl: dashboardUrl,
|
||||
},
|
||||
},
|
||||
});
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
renderer.create(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(window.location.href).toBe(dashboardUrl);
|
||||
});
|
||||
|
||||
// ******** test hinted third party auth ********
|
||||
|
||||
it('should render tpa button for tpa_hint id matching one of the primary providers', () => {
|
||||
@@ -733,10 +727,18 @@ describe('RegistrationPage', () => {
|
||||
|
||||
// ******** miscellaneous tests ********
|
||||
|
||||
it('tests componentDidMount calls the reset form action', () => {
|
||||
it('should backup the registration form state when shouldBackupState is true', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
shouldBackupState: true,
|
||||
},
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(resetRegistrationForm());
|
||||
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationFormBegin({ ...registrationFormData }));
|
||||
});
|
||||
|
||||
it('should render cookie banner', () => {
|
||||
@@ -749,319 +751,102 @@ describe('RegistrationPage', () => {
|
||||
expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
|
||||
});
|
||||
|
||||
it('should send track event for save_for_later param', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat('/register'), search: '?save_for_later=true' };
|
||||
renderer.create(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.saveforlater.course.enroll.clicked',
|
||||
{ category: 'save-for-later' });
|
||||
});
|
||||
|
||||
// ******** shouldComponentUpdate tests ********
|
||||
|
||||
it('should populate form with pipeline user details', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
backedUpFormData: { ...registrationFormData },
|
||||
},
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
pipelineUserDetails: {
|
||||
email: 'test@example.com',
|
||||
username: 'test',
|
||||
},
|
||||
countryCode: 'US',
|
||||
},
|
||||
},
|
||||
});
|
||||
const nextProps = {
|
||||
thirdPartyAuthContext: {
|
||||
pipelineUserDetails: {},
|
||||
},
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
country: 'US',
|
||||
email: 'test@example.com',
|
||||
},
|
||||
validationDecisions: null,
|
||||
};
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />)).find('RegistrationPage');
|
||||
registrationPage.instance().shouldComponentUpdate(nextProps);
|
||||
expect(registrationPage.state('country')).toEqual('US');
|
||||
expect(registrationPage.state('email')).toEqual('test@example.com');
|
||||
expect(registrationPage.find('input#email').props().value).toEqual('test@example.com');
|
||||
expect(registrationPage.find('input#username').props().value).toEqual('test');
|
||||
});
|
||||
|
||||
it('should update state from country code present in redux store', () => {
|
||||
const nextProps = {
|
||||
registrationErrorCode: null,
|
||||
thirdPartyAuthContext: {
|
||||
...thirdPartyAuthContext,
|
||||
countryCode: 'US',
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
backendCountryCode: 'PK',
|
||||
},
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
country: 'US',
|
||||
},
|
||||
validationDecisions: null,
|
||||
};
|
||||
});
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
const shouldUpdate = registrationPage.find('RegistrationPage').instance().shouldComponentUpdate(nextProps);
|
||||
expect(registrationPage.find('RegistrationPage').state('country')).toEqual('US');
|
||||
expect(shouldUpdate).toBe(false);
|
||||
expect(registrationPage.find('input#country').props().value).toEqual('Pakistan');
|
||||
});
|
||||
|
||||
it('should update error code state with error returned by registration api', () => {
|
||||
const nextProps = {
|
||||
registrationErrorCode: INTERNAL_SERVER_ERROR,
|
||||
thirdPartyAuthContext,
|
||||
validationDecisions: {},
|
||||
};
|
||||
it('should display error message based on the error code returned by API', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
registrationError: {
|
||||
errorCode: INTERNAL_SERVER_ERROR,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registrationPage.find('RegistrationPage').instance().shouldComponentUpdate(nextProps);
|
||||
|
||||
expect(registrationPage.find('RegistrationPage').state('errorCode')).toEqual(INTERNAL_SERVER_ERROR);
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />)).find('RegistrationPage');
|
||||
expect(registrationPage.find('div#validation-errors').first().text()).toContain(
|
||||
'An error has occurred. Try refreshing the page, or check your internet connection.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should update form fields state if updated in redux store', () => {
|
||||
const nextProps = {
|
||||
thirdPartyAuthContext,
|
||||
registrationFormData: {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
password: 'password1',
|
||||
emailErrorSuggestion: 'test@gmail.com',
|
||||
},
|
||||
};
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registrationPage.find('RegistrationPage').instance().shouldComponentUpdate(nextProps);
|
||||
|
||||
expect(registrationPage.find('RegistrationPage').state('name')).toEqual('John Doe');
|
||||
expect(registrationPage.find('RegistrationPage').state('username')).toEqual('john_doe');
|
||||
expect(registrationPage.find('RegistrationPage').state('email')).toEqual('john.doe@example.com');
|
||||
expect(registrationPage.find('RegistrationPage').state('emailErrorSuggestion')).toEqual('test@gmail.com');
|
||||
expect(registrationPage.find('RegistrationPage').state('password')).toEqual('password1');
|
||||
});
|
||||
|
||||
it('should display opt-in/opt-out checkbox', () => {
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: 'true',
|
||||
});
|
||||
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(registerPage.find('div.opt-checkbox').length).toEqual(1);
|
||||
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: '',
|
||||
});
|
||||
});
|
||||
|
||||
// ******** persist state tests ********
|
||||
|
||||
it('should clear form field errors in redux store on onFocus', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registrationPage.find('input#name').simulate('focus');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData({ errors: registrationFormData.errors }));
|
||||
});
|
||||
|
||||
it('should set username in redux store if usernameSuggestion is clicked', () => {
|
||||
const formData = {
|
||||
username: 'testname',
|
||||
errors: {
|
||||
...registrationFormData.errors,
|
||||
},
|
||||
};
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
usernameSuggestions: ['testname', 't.name', 'test_0'],
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
username: ' ',
|
||||
formFields: {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@yopmail.com',
|
||||
password: 'password1',
|
||||
},
|
||||
emailSuggestion: {
|
||||
suggestion: 'john.doe@hotmail.com', type: 'warning',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registerPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
|
||||
registerPage.find('button.username-suggestion').at(0).simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData(formData));
|
||||
});
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />)).find('RegistrationPage');
|
||||
|
||||
it('should set email in redux store if emailSuggestion is clicked', () => {
|
||||
const formData = {
|
||||
email: 'test@gmail.com',
|
||||
emailErrorSuggestion: null,
|
||||
emailWarningSuggestion: null,
|
||||
emailFieldBorderClass: '',
|
||||
errors: {
|
||||
...registrationFormData.errors,
|
||||
},
|
||||
};
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
},
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registrationPage.find('input#email').simulate('blur', { target: { value: 'test@gmail.con', name: 'email' } });
|
||||
registrationPage.find('RegistrationPage').setState({ emailErrorSuggestion: 'test@gmail.com' });
|
||||
registrationPage.find('.alert-link').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData(formData));
|
||||
});
|
||||
|
||||
it('should clear username in redux store if usernameSuggestion close button is clicked', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
usernameSuggestions: ['testname', 't.name', 'test_0'],
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
username: ' ',
|
||||
},
|
||||
},
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registerPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
|
||||
registerPage.find('button.suggested-username-close-button').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData({ username: '' }));
|
||||
});
|
||||
|
||||
it('should clear emailErrorSuggestion in redux store if close button is clicked', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
},
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registrationPage.find('input#email').simulate('blur', { target: { value: 'test@gmail.con', name: 'email' } });
|
||||
registrationPage.find('RegistrationPage').setState({ emailErrorSuggestion: 'test@gmail.com' });
|
||||
registrationPage.find('.alert-close').at(0).simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData({ emailErrorSuggestion: null }));
|
||||
});
|
||||
|
||||
it('should set error in redux store if form field is invalid', () => {
|
||||
const formData = {
|
||||
name: '',
|
||||
errors: {
|
||||
...registrationFormData.errors,
|
||||
name: emptyFieldValidation.name,
|
||||
},
|
||||
};
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registerPage.find('input#name').simulate('blur', { target: { value: '', name: 'name' } });
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData(formData));
|
||||
});
|
||||
|
||||
it('should set country code in redux store on country field blur', () => {
|
||||
const formData = {
|
||||
country: 'PK',
|
||||
errors: {
|
||||
country: '',
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
},
|
||||
};
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registerPage.find('RegistrationPage').setState({ country: 'PK' });
|
||||
registerPage.find('input#country').simulate('blur');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData(formData));
|
||||
});
|
||||
|
||||
it('should set country value with field error in redux store on country field blur', () => {
|
||||
const formData = {
|
||||
country: 'test',
|
||||
errors: {
|
||||
country: 'Select your country or region of residence',
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
},
|
||||
};
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registerPage.find('RegistrationPage').setState({ country: 'test' });
|
||||
registerPage.find('input#country').simulate('blur');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData(formData));
|
||||
});
|
||||
|
||||
it('should set country in component state on country change', () => {
|
||||
registrationFormData = {
|
||||
...registrationFormData,
|
||||
country: 'PK',
|
||||
};
|
||||
const nextProps = {
|
||||
registrationFormData: {
|
||||
country: 'PK',
|
||||
},
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registerPage.find('input#country').simulate('change', { target: { value: 'Pakistan', name: 'country' } });
|
||||
registerPage.find('RegistrationPage').instance().shouldComponentUpdate(nextProps);
|
||||
expect(registerPage.find('RegistrationPage').state('country')).toEqual('PK');
|
||||
});
|
||||
|
||||
it('should set country in component state on country change with translations', () => {
|
||||
getLocale.mockImplementation(() => ('ar-ae'));
|
||||
registrationFormData = {
|
||||
errors: { ...registrationFormData.errors },
|
||||
country: 'AF',
|
||||
};
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registerPage.find('input#country').simulate('focus');
|
||||
registerPage.find('button.dropdown-item').at(0).simulate('click', { target: { value: 'أفغانستان ', name: 'countryItem' } });
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData(registrationFormData));
|
||||
});
|
||||
|
||||
it('should set country in component state on country change with chrome translations', () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
registrationFormData = {
|
||||
errors: { ...registrationFormData.errors },
|
||||
country: 'AF',
|
||||
};
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registerPage.find('input#country').simulate('focus');
|
||||
registerPage.find('button.dropdown-item').at(0).simulate('click', { target: { value: undefined, name: undefined, parentElement: { parentElement: { value: 'Afghanistan' } } } });
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setRegistrationFormData(registrationFormData));
|
||||
expect(registrationPage.find('input#name').props().value).toEqual('John Doe');
|
||||
expect(registrationPage.find('input#username').props().value).toEqual('john_doe');
|
||||
expect(registrationPage.find('input#email').props().value).toEqual('john.doe@yopmail.com');
|
||||
expect(registrationPage.find('input#password').props().value).toEqual('password1');
|
||||
expect(registrationPage.find('.email-warning-alert-link').first().text()).toEqual('john.doe@hotmail.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('TestDynamicFields', () => {
|
||||
describe('Test Configurable Fields', () => {
|
||||
mergeConfig({
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
|
||||
});
|
||||
|
||||
it('should render fields returned by backend', () => {
|
||||
mergeConfig({
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
|
||||
});
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
fieldDescriptions: {
|
||||
country: { name: 'country', error_message: true },
|
||||
profession: { name: 'profession', type: 'text', label: 'Profession' },
|
||||
honor_code: { name: FIELDS.HONOR_CODE, error_message: 'You must agree to Honor Code of our site' },
|
||||
terms_of_service: {
|
||||
name: FIELDS.TERMS_OF_SERVICE,
|
||||
error_message: 'You must agree to the Terms and Service agreement of our site',
|
||||
@@ -1070,9 +855,7 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(registerPage.find('#country').exists()).toBeTruthy();
|
||||
expect(registerPage.find('#profession').exists()).toBeTruthy();
|
||||
expect(registerPage.find('#honor-code').exists()).toBeTruthy();
|
||||
expect(registerPage.find('#tos').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -1083,9 +866,7 @@ describe('RegistrationPage', () => {
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
fieldDescriptions: {
|
||||
country: { name: 'country', error_message: true },
|
||||
profession: { name: 'profession', type: 'text', label: 'Profession' },
|
||||
honor_code: { name: 'honor_code', type: 'tos_and_honor_code' },
|
||||
},
|
||||
extendedProfile: ['profession'],
|
||||
},
|
||||
@@ -1097,9 +878,9 @@ describe('RegistrationPage', () => {
|
||||
email: 'john.doe@example.com',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
totalRegistrationTime: 0,
|
||||
is_authn_mfe: true,
|
||||
honor_code: true,
|
||||
totalRegistrationTime: 0,
|
||||
marketing_emails_opt_in: true,
|
||||
extended_profile: [{ field_name: 'profession', field_value: 'Engineer' }],
|
||||
};
|
||||
|
||||
@@ -1107,7 +888,6 @@ describe('RegistrationPage', () => {
|
||||
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
|
||||
populateRequiredFields(registerPage, payload);
|
||||
registerPage.find('RegistrationPage').setState({ values: { country: 'PK' } });
|
||||
registerPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } });
|
||||
registerPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
@@ -1151,11 +931,7 @@ describe('RegistrationPage', () => {
|
||||
expect(registrationPage.find('#confirm_email-error').last().text()).toEqual('Enter your confirm email');
|
||||
});
|
||||
|
||||
it('should show error if email and confirm email fields not match', () => {
|
||||
mergeConfig({
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
|
||||
});
|
||||
|
||||
it('should show error if email and confirm email fields do not match', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
@@ -1168,68 +944,10 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
});
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
registrationPage.find('RegistrationPage').setState({ email: 'test1@gmail.com' });
|
||||
registrationPage.find('input#confirm_email').simulate('blur', { target: { value: 'test@gmail.com', name: 'confirm_email' } });
|
||||
registrationPage.find('input#email').simulate('change', { target: { value: 'test1@gmail.com', name: 'email' } });
|
||||
registrationPage.find('input#confirm_email').simulate('blur', { target: { value: 'test2@gmail.com', name: 'confirm_email' } });
|
||||
|
||||
expect(registrationPage.find('#confirm_email-error').last().text()).toEqual('The email addresses do not match.');
|
||||
});
|
||||
|
||||
it('should redirect to dashboard if features flags are configured but no optional fields are configured', () => {
|
||||
mergeConfig({
|
||||
ENABLE_PROGRESSIVE_PROFILING: true,
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
|
||||
});
|
||||
const dasboardUrl = 'http://test.com/testing-dashboard/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
registrationResult: {
|
||||
success: true,
|
||||
redirectUrl: dasboardUrl,
|
||||
},
|
||||
commonComponents: {
|
||||
optionalFields: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
renderer.create(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(window.location.href).toBe(dasboardUrl);
|
||||
});
|
||||
|
||||
it('should redirect to welcome page when optional fields are configured with feature flags', () => {
|
||||
mergeConfig({
|
||||
ENABLE_PROGRESSIVE_PROFILING: true,
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
|
||||
});
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
optionalFields: {
|
||||
country: { name: 'country', error_message: false },
|
||||
},
|
||||
},
|
||||
register: {
|
||||
...initialState.register,
|
||||
registrationResult: {
|
||||
success: true,
|
||||
},
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL + WELCOME_PAGE };
|
||||
renderer.create(reduxWrapper(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<Provider store={store}><IntlRegistrationPage {...props} /></Provider>
|
||||
</MemoryRouter>
|
||||
</IntlProvider>,
|
||||
));
|
||||
expect(window.location.href).toBe(getConfig().BASE_URL + WELCOME_PAGE);
|
||||
expect(registrationPage.find('div#confirm_email-error').text()).toEqual('The email addresses do not match.');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import TermsOfService from '../TermsOfService';
|
||||
import TermsOfService from '../registrationFields/TermsOfService';
|
||||
|
||||
const IntlTermsOfService = injectIntl(TermsOfService);
|
||||
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { distance } from 'fastest-levenshtein';
|
||||
|
||||
import { COMMON_EMAIL_PROVIDERS } from './data/constants';
|
||||
|
||||
export function getLevenshteinSuggestion(word, knownWords, similarityThreshold = 4) {
|
||||
if (!word) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let minEditDistance = 100;
|
||||
let mostSimilar = word;
|
||||
|
||||
for (let i = 0; i < knownWords.length; i++) {
|
||||
const editDistance = distance(knownWords[i].toLowerCase(), word.toLowerCase());
|
||||
if (editDistance < minEditDistance) {
|
||||
minEditDistance = editDistance;
|
||||
mostSimilar = knownWords[i];
|
||||
}
|
||||
}
|
||||
|
||||
return minEditDistance <= similarityThreshold && word !== mostSimilar ? mostSimilar : null;
|
||||
}
|
||||
|
||||
export function getSuggestionForInvalidEmail(domain, username) {
|
||||
if (!domain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultDomains = ['yahoo', 'aol', 'hotmail', 'live', 'outlook', 'gmail'];
|
||||
const suggestion = getLevenshteinSuggestion(domain, COMMON_EMAIL_PROVIDERS);
|
||||
|
||||
if (suggestion) {
|
||||
return `${username}@${suggestion}`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < defaultDomains.length; i++) {
|
||||
if (domain.includes(defaultDomains[i])) {
|
||||
return `${username}@${defaultDomains[i]}.com`;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -94,12 +94,12 @@ const ResetPasswordPage = (props) => {
|
||||
// Do not validate when focus out from 'newPassword' and focus on 'passwordValidation' icon
|
||||
// for better user experience.
|
||||
if (event.relatedTarget
|
||||
&& event.relatedTarget.name === 'passwordValidation'
|
||||
&& event.relatedTarget.name === 'password'
|
||||
&& name === 'newPassword'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (name === 'passwordValidation') {
|
||||
if (name === 'password') {
|
||||
name = 'newPassword';
|
||||
value = newPassword;
|
||||
}
|
||||
|
||||
3
src/sass/_registration.scss
Normal file
3
src/sass/_registration.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.register-stateful-button-width {
|
||||
min-width: 14.4rem;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
// Load component based styles
|
||||
@import "_base_component.scss";
|
||||
@import "_registration.scss";
|
||||
//
|
||||
// ----------------------------
|
||||
// #COLORS
|
||||
@@ -46,10 +47,6 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.register-stateful-button-width {
|
||||
min-width: 14.4rem;
|
||||
}
|
||||
|
||||
.login-button-width {
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user