Compare commits
3 Commits
open-relea
...
sajjad/VAN
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07f5f56509 | ||
|
|
aeec576d8c | ||
|
|
90db7ba1d8 |
@@ -24,7 +24,7 @@ import { ForgotPasswordPage } from './forgot-password';
|
||||
import Logistration from './logistration/Logistration';
|
||||
import { ProgressiveProfiling } from './progressive-profiling';
|
||||
import { RecommendationsPage } from './recommendations';
|
||||
import { RegistrationPage } from './register';
|
||||
import { EmbeddableRegistrationPage } from './register';
|
||||
import { ResetPasswordPage } from './reset-password';
|
||||
|
||||
import './index.scss';
|
||||
@@ -41,7 +41,7 @@ const MainApp = () => (
|
||||
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||
<Route
|
||||
path={REGISTER_EMBEDDED_PAGE}
|
||||
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
|
||||
element={<EmbeddedRegistrationRoute><EmbeddableRegistrationPage /></EmbeddedRegistrationRoute>}
|
||||
/>
|
||||
<Route
|
||||
path={LOGIN_PAGE}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
@@ -11,31 +12,78 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
|
||||
import { clearRegistertionBackendError, fetchRealtimeValidations } from '../register/data/actions';
|
||||
import { PASSWORD_FIELD_LABEL } from '../register/data/constants';
|
||||
import { validatePasswordField } from '../register/data/utils';
|
||||
|
||||
const PasswordField = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
|
||||
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const handleBlur = (e) => {
|
||||
if (e.target?.name === PASSWORD_FIELD_LABEL && e.relatedTarget?.name === 'passwordIcon') {
|
||||
return; // resolving a bug where validations get run on password icon focus
|
||||
}
|
||||
|
||||
if (props.handleBlur) { props.handleBlur(e); }
|
||||
setShowTooltip(props.showRequirements && false);
|
||||
if (props.handleErrorChange) { // If rendering from register page
|
||||
const fieldError = validatePasswordField(props.value, formatMessage);
|
||||
if (fieldError) {
|
||||
props.handleErrorChange(PASSWORD_FIELD_LABEL, fieldError);
|
||||
} else if (!validationApiRateLimited) {
|
||||
dispatch(fetchRealtimeValidations({ [PASSWORD_FIELD_LABEL]: props.value }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = (e) => {
|
||||
if (e.target?.name === 'passwordIcon') {
|
||||
return; // resolving a bug where error gets cleared on password icon focus
|
||||
}
|
||||
|
||||
if (props.handleFocus) {
|
||||
props.handleFocus(e);
|
||||
}
|
||||
if (props.handleErrorChange) {
|
||||
props.handleErrorChange(PASSWORD_FIELD_LABEL, '');
|
||||
dispatch(clearRegistertionBackendError(PASSWORD_FIELD_LABEL));
|
||||
}
|
||||
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
|
||||
};
|
||||
|
||||
const HideButton = (
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
|
||||
<IconButton
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
name="passwordIcon"
|
||||
src={VisibilityOff}
|
||||
iconAs={Icon}
|
||||
onClick={setHiddenTrue}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt={formatMessage(messages['hide.password'])}
|
||||
/>
|
||||
);
|
||||
|
||||
const ShowButton = (
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
|
||||
<IconButton
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
name="passwordIcon"
|
||||
src={Visibility}
|
||||
iconAs={Icon}
|
||||
onClick={setHiddenFalse}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt={formatMessage(messages['show.password'])}
|
||||
/>
|
||||
);
|
||||
|
||||
const placement = window.innerWidth < 768 ? 'top' : 'left';
|
||||
const tooltip = (
|
||||
<Tooltip id={`password-requirement-${placement}`}>
|
||||
@@ -89,6 +137,7 @@ PasswordField.defaultProps = {
|
||||
handleBlur: null,
|
||||
handleFocus: null,
|
||||
handleChange: () => {},
|
||||
handleErrorChange: null,
|
||||
showRequirements: true,
|
||||
autoComplete: null,
|
||||
};
|
||||
@@ -100,6 +149,7 @@ PasswordField.propTypes = {
|
||||
handleBlur: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
handleErrorChange: PropTypes.func,
|
||||
name: PropTypes.string.isRequired,
|
||||
showRequirements: PropTypes.bool,
|
||||
value: PropTypes.string.isRequired,
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
|
||||
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS,
|
||||
} from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
|
||||
@@ -17,8 +17,6 @@ const RedirectLogistration = (props) => {
|
||||
redirectToRecommendationsPage,
|
||||
educationLevel,
|
||||
userId,
|
||||
registrationEmbedded,
|
||||
host,
|
||||
} = props;
|
||||
let finalRedirectUrl = '';
|
||||
|
||||
@@ -38,13 +36,6 @@ const RedirectLogistration = (props) => {
|
||||
// TODO: Do we still need this cookie?
|
||||
setCookie('van-504-returning-user', true);
|
||||
|
||||
if (registrationEmbedded) {
|
||||
window.parent.postMessage({
|
||||
action: REDIRECT,
|
||||
redirectUrl: getConfig().POST_REGISTRATION_REDIRECT_URL,
|
||||
}, host);
|
||||
return null;
|
||||
}
|
||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||
return (
|
||||
<Navigate
|
||||
@@ -89,8 +80,6 @@ RedirectLogistration.defaultProps = {
|
||||
optionalFields: {},
|
||||
redirectToRecommendationsPage: false,
|
||||
userId: null,
|
||||
registrationEmbedded: false,
|
||||
host: '',
|
||||
};
|
||||
|
||||
RedirectLogistration.propTypes = {
|
||||
@@ -102,8 +91,6 @@ RedirectLogistration.propTypes = {
|
||||
optionalFields: PropTypes.shape({}),
|
||||
redirectToRecommendationsPage: PropTypes.bool,
|
||||
userId: PropTypes.number,
|
||||
registrationEmbedded: PropTypes.bool,
|
||||
host: PropTypes.string,
|
||||
};
|
||||
|
||||
export default RedirectLogistration;
|
||||
|
||||
@@ -7,7 +7,7 @@ const configuration = {
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
|
||||
ENABLE_POPULAR_AND_TRENDING_RECOMMENDATIONS: process.env.ENABLE_POPULAR_AND_TRENDING_RECOMMENDATIONS || false,
|
||||
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
|
||||
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || true,
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
|
||||
// Links
|
||||
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
|
||||
|
||||
@@ -27,13 +27,8 @@ export const FORBIDDEN_STATE = 'forbidden';
|
||||
export const EMBEDDED = 'embedded';
|
||||
|
||||
// Regex
|
||||
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
|
||||
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
|
||||
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
|
||||
+ '|\\[(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 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.
|
||||
|
||||
@@ -23,8 +23,9 @@ import ForgotPasswordAlert from './ForgotPasswordAlert';
|
||||
import messages from './messages';
|
||||
import BaseContainer from '../base-container';
|
||||
import { FormGroup } from '../common-components';
|
||||
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
||||
import { DEFAULT_STATE, LOGIN_PAGE } from '../data/constants';
|
||||
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||
import { VALID_EMAIL_REGEX } from '../register/RegistrationFields/EmailField/constants';
|
||||
|
||||
const ForgotPasswordPage = (props) => {
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from '@edx/paragon';
|
||||
import { ChevronLeft } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Navigate, useNavigate } from 'react-router-dom';
|
||||
|
||||
import BaseContainer from '../base-container';
|
||||
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
|
||||
@@ -36,6 +36,7 @@ const Logistration = (props) => {
|
||||
} = tpaProviders;
|
||||
const { formatMessage } = useIntl();
|
||||
const [institutionLogin, setInstitutionLogin] = useState(false);
|
||||
const [key, setKey] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
|
||||
|
||||
@@ -69,7 +70,7 @@ const Logistration = (props) => {
|
||||
if (tabKey === LOGIN_PAGE) {
|
||||
props.backupRegistrationForm();
|
||||
}
|
||||
navigate(updatePathWithQueryParams(tabKey));
|
||||
setKey(tabKey);
|
||||
};
|
||||
|
||||
const tabTitle = (
|
||||
@@ -121,6 +122,9 @@ const Logistration = (props) => {
|
||||
<Tab title={formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
|
||||
</Tabs>
|
||||
))}
|
||||
{ key && (
|
||||
<Navigate to={updatePathWithQueryParams(key)} replace />
|
||||
)}
|
||||
<div id="main-content" className="main-content">
|
||||
{selectedPage === LOGIN_PAGE
|
||||
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
import React, {
|
||||
useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
getCountryList, getLocale, useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Form, FormGroup, StatefulButton } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { PasswordField } from '../../common-components';
|
||||
import { getThirdPartyAuthContext } from '../../common-components/data/actions';
|
||||
import { fieldDescriptionSelector } from '../../common-components/data/selectors';
|
||||
import {
|
||||
DEFAULT_STATE, LETTER_REGEX, NUMBER_REGEX, REDIRECT,
|
||||
} from '../../data/constants';
|
||||
import { getAllPossibleQueryParams, setCookie } from '../../data/utils';
|
||||
import ConfigurableRegistrationForm from '../components/ConfigurableRegistrationForm';
|
||||
import RegistrationFailure from '../components/RegistrationFailure';
|
||||
import {
|
||||
clearRegistertionBackendError,
|
||||
clearUsernameSuggestions,
|
||||
fetchRealtimeValidations,
|
||||
registerNewUser,
|
||||
} from '../data/actions';
|
||||
import {
|
||||
FORM_SUBMISSION_ERROR,
|
||||
} from '../data/constants';
|
||||
import { registrationErrorSelector, validationsSelector } from '../data/selectors';
|
||||
import messages from '../messages';
|
||||
import { EmailField, UsernameField } from '../RegistrationFields';
|
||||
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from '../RegistrationFields/CountryField/constants';
|
||||
import validateCountryField from '../RegistrationFields/CountryField/validator';
|
||||
import {
|
||||
emailRegex,
|
||||
getSuggestionForInvalidEmail,
|
||||
validateEmailAddress,
|
||||
} from '../RegistrationFields/EmailField/validator';
|
||||
import { urlRegex } from '../RegistrationFields/NameField/constants';
|
||||
import { VALID_USERNAME_REGEX } from '../RegistrationFields/UsernameField/constants';
|
||||
|
||||
const EmbeddableRegistrationPage = (props) => {
|
||||
const {
|
||||
backendCountryCode,
|
||||
backendValidations,
|
||||
fieldDescriptions,
|
||||
registrationError,
|
||||
registrationErrorCode,
|
||||
registrationResult,
|
||||
submitState,
|
||||
usernameSuggestions,
|
||||
validationApiRateLimited,
|
||||
// Actions
|
||||
getRegistrationDataFromBackend,
|
||||
validateFromBackend,
|
||||
clearBackendError,
|
||||
} = props;
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
const countryList = useMemo(() => getCountryList(getLocale()), []);
|
||||
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||
const { cta, host } = queryParams;
|
||||
const flags = {
|
||||
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
|
||||
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
|
||||
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
|
||||
};
|
||||
|
||||
const [formFields, setFormFields] = useState({
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
});
|
||||
const [configurableFormFields, setConfigurableFormFields] = useState({
|
||||
marketingEmailsOptIn: true,
|
||||
});
|
||||
const [errors, setErrors] = useState({
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
});
|
||||
const [emailSuggestion, setEmailSuggestion] = useState({ suggestion: '', type: '' });
|
||||
const [errorCode, setErrorCode] = useState({ type: '', count: 0 });
|
||||
const [formStartTime, setFormStartTime] = useState(null);
|
||||
const [, setFocusedField] = useState(null);
|
||||
|
||||
const buttonLabel = cta ? formatMessage(messages['create.account.cta.button'], { label: cta }) : formatMessage(messages['create.account.for.free.button']);
|
||||
|
||||
useEffect(() => {
|
||||
if (!formStartTime) {
|
||||
sendPageEvent('login_and_registration', 'register');
|
||||
const payload = { ...queryParams, is_register_page: true };
|
||||
getRegistrationDataFromBackend(payload);
|
||||
setFormStartTime(Date.now());
|
||||
}
|
||||
}, [formStartTime, getRegistrationDataFromBackend, queryParams]);
|
||||
|
||||
useEffect(() => {
|
||||
if (backendValidations) {
|
||||
setErrors(prevErrors => ({ ...prevErrors, ...backendValidations }));
|
||||
}
|
||||
}, [backendValidations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (registrationErrorCode) {
|
||||
setErrorCode(prevState => ({ type: registrationErrorCode, count: prevState.count + 1 }));
|
||||
}
|
||||
}, [registrationErrorCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (backendCountryCode && backendCountryCode !== configurableFormFields?.country?.countryCode) {
|
||||
let countryCode = '';
|
||||
let countryDisplayValue = '';
|
||||
|
||||
const selectedCountry = countryList.find(
|
||||
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()),
|
||||
);
|
||||
if (selectedCountry) {
|
||||
countryCode = selectedCountry[COUNTRY_CODE_KEY];
|
||||
countryDisplayValue = selectedCountry[COUNTRY_DISPLAY_KEY];
|
||||
}
|
||||
setConfigurableFormFields(prevState => (
|
||||
{
|
||||
...prevState,
|
||||
country: {
|
||||
countryCode, displayValue: countryDisplayValue,
|
||||
},
|
||||
}
|
||||
));
|
||||
}
|
||||
}, [backendCountryCode, countryList]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
/**
|
||||
* 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(prevState => ({ ...prevState, username: ' ' }));
|
||||
}
|
||||
}, [usernameSuggestions, formFields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (registrationResult.success) {
|
||||
// Optimizely registration conversion event
|
||||
window.optimizely = window.optimizely || [];
|
||||
window.optimizely.push({
|
||||
type: 'event',
|
||||
eventName: 'authn-registration-conversion',
|
||||
});
|
||||
|
||||
// We probably don't need this cookie because this fires the same event as
|
||||
// above for optimizely using GTM.
|
||||
setCookie(getConfig().REGISTER_CONVERSION_COOKIE_NAME, true);
|
||||
// This is used by the "User Retention Rate Event" on GTM
|
||||
setCookie('authn-returning-user');
|
||||
|
||||
// Fire GTM event used for integration with impact.com
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({
|
||||
event: 'ImpactRegistrationEvent',
|
||||
});
|
||||
|
||||
window.parent.postMessage({
|
||||
action: REDIRECT,
|
||||
redirectUrl: encodeURIComponent(getConfig().POST_REGISTRATION_REDIRECT_URL),
|
||||
}, host);
|
||||
}
|
||||
}, [registrationResult, host]);
|
||||
|
||||
const validateInput = (fieldName, value, payload, shouldValidateFromBackend, shouldSetErrors = true) => {
|
||||
let fieldError = '';
|
||||
|
||||
switch (fieldName) {
|
||||
case 'name':
|
||||
if (value && value.match(urlRegex)) {
|
||||
fieldError = formatMessage(messages['name.validation.message']);
|
||||
} else if (value && !payload.username.trim() && shouldValidateFromBackend) {
|
||||
validateFromBackend(payload);
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
if (value.length <= 2) {
|
||||
fieldError = 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 = formatMessage(messages['email.invalid.format.error']);
|
||||
setEmailSuggestion({
|
||||
suggestion: getSuggestionForInvalidEmail(domainName, username),
|
||||
type: 'error',
|
||||
});
|
||||
} else {
|
||||
const response = validateEmailAddress(value, username, domainName);
|
||||
if (response.hasError) {
|
||||
fieldError = formatMessage(messages['email.invalid.format.error']);
|
||||
delete response.hasError;
|
||||
}
|
||||
setEmailSuggestion({ ...response });
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'username':
|
||||
if (!value.match(VALID_USERNAME_REGEX)) {
|
||||
fieldError = formatMessage(messages['username.format.validation.message']);
|
||||
}
|
||||
break;
|
||||
case 'password':
|
||||
if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) {
|
||||
fieldError = formatMessage(messages['password.validation.message']);
|
||||
}
|
||||
break;
|
||||
case 'country':
|
||||
if (flags.showConfigurableEdxFields || flags.showConfigurableRegistrationFields) {
|
||||
const {
|
||||
countryCode, displayValue, error,
|
||||
} = validateCountryField(value.trim(), countryList, formatMessage(messages['empty.country.field.error']));
|
||||
fieldError = error;
|
||||
setConfigurableFormFields(prevState => ({ ...prevState, 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 = formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (shouldSetErrors && fieldError) {
|
||||
setErrors(prevErrors => ({
|
||||
...prevErrors,
|
||||
[fieldName]: fieldError,
|
||||
}));
|
||||
}
|
||||
return fieldError;
|
||||
};
|
||||
|
||||
const isFormValid = (payload) => {
|
||||
const fieldErrors = { ...errors };
|
||||
let isValid = true;
|
||||
Object.keys(payload).forEach(key => {
|
||||
if (!payload[key]) {
|
||||
fieldErrors[key] = formatMessage(messages[`empty.${key}.field.error`]);
|
||||
}
|
||||
if (fieldErrors[key]) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (flags.showConfigurableEdxFields) {
|
||||
if (!configurableFormFields.country.displayValue) {
|
||||
fieldErrors.country = 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] = formatMessage(messages['empty.country.field.error']);
|
||||
} else if (!configurableFormFields[key]) {
|
||||
fieldErrors[key] = fieldDescriptions[key].error_message;
|
||||
}
|
||||
if (fieldErrors[key]) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
setErrors({ ...fieldErrors });
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (event, fieldName, suggestion = '') => {
|
||||
event.preventDefault();
|
||||
setErrors(prevErrors => ({ ...prevErrors, [fieldName]: '' }));
|
||||
switch (fieldName) {
|
||||
case 'email':
|
||||
setFormFields(prevState => ({ ...prevState, email: emailSuggestion.suggestion }));
|
||||
setEmailSuggestion({ suggestion: '', type: '' });
|
||||
break;
|
||||
case 'username':
|
||||
setFormFields(prevState => ({ ...prevState, username: suggestion }));
|
||||
props.resetUsernameSuggestions();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailSuggestionClosed = () => setEmailSuggestion({ suggestion: '', type: '' });
|
||||
|
||||
const handleUsernameSuggestionClosed = () => props.resetUsernameSuggestions();
|
||||
|
||||
const handleOnChange = (event) => {
|
||||
const { name } = event.target;
|
||||
let value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
||||
if (registrationError[name]) {
|
||||
clearBackendError(name);
|
||||
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||
}
|
||||
if (name === 'username') {
|
||||
if (value.length > 30) {
|
||||
return;
|
||||
}
|
||||
if (value.startsWith(' ')) {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
||||
};
|
||||
|
||||
const handleOnBlur = (event) => {
|
||||
const { name, value } = event.target;
|
||||
|
||||
if (name === 'name') {
|
||||
validateInput(
|
||||
name,
|
||||
value,
|
||||
{ name: formFields.name, username: formFields.username, form_field_key: name },
|
||||
!validationApiRateLimited,
|
||||
false,
|
||||
);
|
||||
}
|
||||
if (name === 'email') {
|
||||
validateInput(name, value, null, !validationApiRateLimited, false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnFocus = (event) => {
|
||||
const { name, value } = event.target;
|
||||
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||
clearBackendError(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(prevState => ({ ...prevState, [name]: '' }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
|
||||
let payload = { ...formFields };
|
||||
if (!isFormValid(payload)) {
|
||||
setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(configurableFormFields).forEach((fieldName) => {
|
||||
if (fieldName === 'country') {
|
||||
payload[fieldName] = configurableFormFields[fieldName].countryCode;
|
||||
} else {
|
||||
payload[fieldName] = configurableFormFields[fieldName];
|
||||
}
|
||||
});
|
||||
// Don't send the marketing email opt-in value if the flag is turned off
|
||||
if (!flags.showMarketingEmailOptInCheckbox) {
|
||||
delete payload.marketingEmailsOptIn;
|
||||
}
|
||||
let isValid = true;
|
||||
Object.entries(payload).forEach(([key, value]) => {
|
||||
if (validateInput(key, value, payload, false, true) !== '') {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
if (!isValid) {
|
||||
setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
|
||||
return;
|
||||
}
|
||||
payload = snakeCaseObject(payload);
|
||||
payload.totalRegistrationTime = totalRegistrationTime;
|
||||
|
||||
// add query params to the payload
|
||||
payload = { ...payload, ...queryParams };
|
||||
props.registerNewUser(payload);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}</title>
|
||||
</Helmet>
|
||||
<div
|
||||
className="mw-xs mt-3 w-100 m-auto pt-4 main-content"
|
||||
>
|
||||
<RegistrationFailure
|
||||
errorCode={errorCode.type}
|
||||
failureCount={errorCode.count}
|
||||
/>
|
||||
<Form id="registration-form" name="registration-form">
|
||||
<FormGroup
|
||||
name="name"
|
||||
value={formFields.name}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={errors.name}
|
||||
helpText={[formatMessage(messages['help.text.name'])]}
|
||||
floatingLabel={formatMessage(messages['registration.fullname.label'])}
|
||||
/>
|
||||
<EmailField
|
||||
name="email"
|
||||
value={formFields.email}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
handleSuggestionClick={(e) => handleSuggestionClick(e, 'email')}
|
||||
handleOnClose={handleEmailSuggestionClosed}
|
||||
emailSuggestion={emailSuggestion}
|
||||
errorMessage={errors.email}
|
||||
helpText={[formatMessage(messages['help.text.email'])]}
|
||||
floatingLabel={formatMessage(messages['registration.email.label'])}
|
||||
/>
|
||||
<UsernameField
|
||||
name="username"
|
||||
spellCheck="false"
|
||||
value={formFields.username}
|
||||
handleBlur={handleOnBlur}
|
||||
handleChange={handleOnChange}
|
||||
handleFocus={handleOnFocus}
|
||||
handleSuggestionClick={handleSuggestionClick}
|
||||
handleUsernameSuggestionClose={handleUsernameSuggestionClosed}
|
||||
usernameSuggestions={usernameSuggestions}
|
||||
errorMessage={errors.username}
|
||||
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
|
||||
floatingLabel={formatMessage(messages['registration.username.label'])}
|
||||
/>
|
||||
<PasswordField
|
||||
name="password"
|
||||
value={formFields.password}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={errors.password}
|
||||
floatingLabel={formatMessage(messages['registration.password.label'])}
|
||||
/>
|
||||
<ConfigurableRegistrationForm
|
||||
countryList={countryList}
|
||||
email={formFields.email}
|
||||
fieldErrors={errors}
|
||||
registrationEmbedded
|
||||
formFields={configurableFormFields}
|
||||
setFieldErrors={setErrors}
|
||||
setFormFields={setConfigurableFormFields}
|
||||
setFocusedField={setFocusedField}
|
||||
fieldDescriptions={fieldDescriptions}
|
||||
/>
|
||||
<StatefulButton
|
||||
id="register-user"
|
||||
name="register-user"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="register-button mt-4 mb-4"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: buttonLabel,
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const registerPageState = state.register;
|
||||
return {
|
||||
backendCountryCode: registerPageState.backendCountryCode,
|
||||
backendValidations: validationsSelector(state),
|
||||
fieldDescriptions: fieldDescriptionSelector(state),
|
||||
registrationError: registerPageState.registrationError,
|
||||
registrationErrorCode: registrationErrorSelector(state),
|
||||
registrationResult: registerPageState.registrationResult,
|
||||
submitState: registerPageState.submitState,
|
||||
validationApiRateLimited: registerPageState.validationApiRateLimited,
|
||||
usernameSuggestions: registerPageState.usernameSuggestions,
|
||||
};
|
||||
};
|
||||
|
||||
EmbeddableRegistrationPage.propTypes = {
|
||||
backendCountryCode: PropTypes.string,
|
||||
backendValidations: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
}),
|
||||
fieldDescriptions: PropTypes.shape({}),
|
||||
registrationError: PropTypes.shape({}),
|
||||
registrationErrorCode: PropTypes.string,
|
||||
registrationResult: PropTypes.shape({
|
||||
redirectUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
}),
|
||||
submitState: PropTypes.string,
|
||||
usernameSuggestions: PropTypes.arrayOf(PropTypes.string),
|
||||
validationApiRateLimited: PropTypes.bool,
|
||||
// Actions
|
||||
clearBackendError: PropTypes.func.isRequired,
|
||||
getRegistrationDataFromBackend: PropTypes.func.isRequired,
|
||||
registerNewUser: PropTypes.func.isRequired,
|
||||
resetUsernameSuggestions: PropTypes.func.isRequired,
|
||||
validateFromBackend: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
EmbeddableRegistrationPage.defaultProps = {
|
||||
backendCountryCode: '',
|
||||
backendValidations: null,
|
||||
fieldDescriptions: {},
|
||||
registrationError: {},
|
||||
registrationErrorCode: '',
|
||||
registrationResult: null,
|
||||
submitState: DEFAULT_STATE,
|
||||
usernameSuggestions: [],
|
||||
validationApiRateLimited: false,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
clearBackendError: clearRegistertionBackendError,
|
||||
getRegistrationDataFromBackend: getThirdPartyAuthContext,
|
||||
resetUsernameSuggestions: clearUsernameSuggestions,
|
||||
validateFromBackend: fetchRealtimeValidations,
|
||||
registerNewUser,
|
||||
},
|
||||
)(EmbeddableRegistrationPage);
|
||||
134
src/register/RegistrationFields/CountryField/CountryField.jsx
Normal file
134
src/register/RegistrationFields/CountryField/CountryField.jsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormAutosuggest, FormAutosuggestOption, FormControlFeedback } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, COUNTRY_FIELD_LABEL } from './constants';
|
||||
import validateCountryField from './validator';
|
||||
import { clearRegistertionBackendError } from '../../data/actions';
|
||||
import messages from '../../messages';
|
||||
|
||||
const CountryField = (props) => {
|
||||
const {
|
||||
countryList,
|
||||
selectedCountry,
|
||||
onChangeHandler,
|
||||
handleErrorChange,
|
||||
onFocusHandler,
|
||||
} = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const backendCountryCode = useSelector(state => state.register.backendCountryCode);
|
||||
|
||||
useEffect(() => {
|
||||
if (backendCountryCode && backendCountryCode !== selectedCountry?.countryCode) {
|
||||
let countryCode = '';
|
||||
let countryDisplayValue = '';
|
||||
|
||||
const countryVal = countryList.find(
|
||||
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()),
|
||||
);
|
||||
if (countryVal) {
|
||||
countryCode = countryVal[COUNTRY_CODE_KEY];
|
||||
countryDisplayValue = countryVal[COUNTRY_DISPLAY_KEY];
|
||||
}
|
||||
onChangeHandler(
|
||||
{ target: { name: COUNTRY_FIELD_LABEL } },
|
||||
{ countryCode, displayValue: countryDisplayValue },
|
||||
);
|
||||
}
|
||||
}, [backendCountryCode, countryList]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleOnBlur = (event) => {
|
||||
// Do not run validations when drop-down arrow is clicked
|
||||
if (event.relatedTarget && event.relatedTarget.className.includes('pgn__form-autosuggest__icon-button')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { value } = event.target;
|
||||
|
||||
const { countryCode, displayValue, error } = validateCountryField(
|
||||
value.trim(), countryList, formatMessage(messages['empty.country.field.error']),
|
||||
);
|
||||
|
||||
onChangeHandler({ target: { name: COUNTRY_FIELD_LABEL } }, { countryCode, displayValue });
|
||||
handleErrorChange(COUNTRY_FIELD_LABEL, error);
|
||||
// onBlurHandler(event);
|
||||
};
|
||||
|
||||
const handleSelected = (value) => {
|
||||
handleOnBlur({ target: { name: COUNTRY_FIELD_LABEL, value } });
|
||||
};
|
||||
|
||||
const handleOnFocus = (event) => {
|
||||
handleErrorChange(COUNTRY_FIELD_LABEL, '');
|
||||
dispatch(clearRegistertionBackendError(COUNTRY_FIELD_LABEL));
|
||||
onFocusHandler(event);
|
||||
};
|
||||
|
||||
const handleOnChange = (value) => {
|
||||
onChangeHandler({ target: { name: COUNTRY_FIELD_LABEL } }, { countryCode: '', displayValue: value });
|
||||
};
|
||||
|
||||
const getCountryList = () => countryList.map((country) => (
|
||||
<FormAutosuggestOption key={country[COUNTRY_CODE_KEY]}>
|
||||
{country[COUNTRY_DISPLAY_KEY]}
|
||||
</FormAutosuggestOption>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<FormAutosuggest
|
||||
floatingLabel={formatMessage(messages['registration.country.label'])}
|
||||
aria-label="form autosuggest"
|
||||
name="country"
|
||||
value={selectedCountry.displayValue || ''}
|
||||
onSelected={(value) => handleSelected(value)}
|
||||
onFocus={(e) => handleOnFocus(e)}
|
||||
onBlur={(e) => handleOnBlur(e)}
|
||||
onChange={(value) => handleOnChange(value)}
|
||||
>
|
||||
{getCountryList()}
|
||||
</FormAutosuggest>
|
||||
{props.errorMessage !== '' && (
|
||||
<FormControlFeedback
|
||||
key="error"
|
||||
className="form-text-size"
|
||||
hasIcon={false}
|
||||
feedback-for="country"
|
||||
type="invalid"
|
||||
>
|
||||
{props.errorMessage}
|
||||
</FormControlFeedback>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CountryField.propTypes = {
|
||||
countryList: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
code: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
}),
|
||||
).isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onChangeHandler: PropTypes.func.isRequired,
|
||||
handleErrorChange: PropTypes.func.isRequired,
|
||||
onFocusHandler: PropTypes.func.isRequired,
|
||||
selectedCountry: PropTypes.shape({
|
||||
displayValue: PropTypes.string,
|
||||
countryCode: PropTypes.string,
|
||||
}),
|
||||
};
|
||||
|
||||
CountryField.defaultProps = {
|
||||
errorMessage: null,
|
||||
selectedCountry: {
|
||||
value: '',
|
||||
},
|
||||
};
|
||||
|
||||
export default CountryField;
|
||||
@@ -0,0 +1,3 @@
|
||||
export const COUNTRY_FIELD_LABEL = 'country';
|
||||
export const COUNTRY_CODE_KEY = 'code';
|
||||
export const COUNTRY_DISPLAY_KEY = 'name';
|
||||
28
src/register/RegistrationFields/CountryField/validator.js
Normal file
28
src/register/RegistrationFields/CountryField/validator.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from './constants';
|
||||
|
||||
const 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) => (
|
||||
// When translations are applied, extra space added in country value, so we should trim that.
|
||||
country[COUNTRY_DISPLAY_KEY].toLowerCase().trim() === normalizedValue
|
||||
|| country[COUNTRY_CODE_KEY].toLowerCase().trim() === normalizedValue
|
||||
),
|
||||
);
|
||||
if (selectedCountry) {
|
||||
countryCode = selectedCountry[COUNTRY_CODE_KEY];
|
||||
displayValue = selectedCountry[COUNTRY_DISPLAY_KEY];
|
||||
error = '';
|
||||
}
|
||||
}
|
||||
return { error, countryCode, displayValue };
|
||||
};
|
||||
|
||||
export default validateCountryField;
|
||||
123
src/register/RegistrationFields/EmailField/EmailField.jsx
Normal file
123
src/register/RegistrationFields/EmailField/EmailField.jsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Icon } from '@edx/paragon';
|
||||
import { Close, Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CONFIRM_EMAIL_FIELD_LABEL, EMAIL_FIELD_LABEL } from './constants';
|
||||
import validateEmail from './validator';
|
||||
import { FormGroup } from '../../../common-components';
|
||||
import { backupRegistrationFormBegin, clearRegistertionBackendError, fetchRealtimeValidations } from '../../data/actions';
|
||||
import messages from '../../messages';
|
||||
|
||||
const EmailField = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
handleChange,
|
||||
handleErrorChange,
|
||||
confirmEmailValue,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
registrationFormData: backedUpFormData,
|
||||
validationApiRateLimited,
|
||||
} = useSelector(state => state.register);
|
||||
|
||||
const [emailSuggestion, setEmailSuggestion] = useState({ ...backedUpFormData?.emailSuggestion });
|
||||
|
||||
const handleOnBlur = (e) => {
|
||||
const { value } = e.target;
|
||||
const { fieldError, confirmEmailError, suggestion } = validateEmail(value, confirmEmailValue, formatMessage);
|
||||
|
||||
handleErrorChange(CONFIRM_EMAIL_FIELD_LABEL, confirmEmailError);
|
||||
dispatch(backupRegistrationFormBegin({
|
||||
...backedUpFormData,
|
||||
emailSuggestion: { ...suggestion },
|
||||
}));
|
||||
setEmailSuggestion(suggestion);
|
||||
|
||||
if (fieldError) {
|
||||
handleErrorChange(EMAIL_FIELD_LABEL, fieldError);
|
||||
} else if (!validationApiRateLimited) {
|
||||
dispatch(fetchRealtimeValidations({ email: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnFocus = () => {
|
||||
handleErrorChange(EMAIL_FIELD_LABEL, '');
|
||||
dispatch(clearRegistertionBackendError(EMAIL_FIELD_LABEL));
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (event) => {
|
||||
event.preventDefault();
|
||||
handleErrorChange(EMAIL_FIELD_LABEL, '');
|
||||
handleChange({ target: { name: EMAIL_FIELD_LABEL, value: emailSuggestion.suggestion } });
|
||||
setEmailSuggestion({ suggestion: '', type: '' });
|
||||
};
|
||||
|
||||
const handleSuggestionClosed = () => setEmailSuggestion({ suggestion: '', type: '' });
|
||||
|
||||
const renderEmailFeedback = () => {
|
||||
if (emailSuggestion.type === 'error') {
|
||||
return (
|
||||
<Alert variant="danger" className="email-suggestion-alert-error mt-1" icon={Error}>
|
||||
<span className="email-suggestion__text">
|
||||
{formatMessage(messages['did.you.mean.alert.text'])}{' '}
|
||||
<Alert.Link
|
||||
href="#"
|
||||
name="email"
|
||||
onClick={handleSuggestionClick}
|
||||
>
|
||||
{emailSuggestion.suggestion}
|
||||
</Alert.Link>?
|
||||
<Icon src={Close} className="email-suggestion__close" onClick={handleSuggestionClosed} tabIndex="0" />
|
||||
</span>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span id="email-warning" className="small">
|
||||
{formatMessage(messages['did.you.mean.alert.text'])}:{' '}
|
||||
<Alert.Link
|
||||
href="#"
|
||||
name="email"
|
||||
className="email-suggestion-alert-warning"
|
||||
onClick={handleSuggestionClick}
|
||||
>
|
||||
{emailSuggestion.suggestion}
|
||||
</Alert.Link>?
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
borderClass={emailSuggestion.type === 'warning' ? 'yellow-border' : ''}
|
||||
maxLength={254} // Limit per RFCs is 254
|
||||
{...props}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
>
|
||||
{emailSuggestion.suggestion ? renderEmailFeedback() : null}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
EmailField.defaultProps = {
|
||||
errorMessage: '',
|
||||
confirmEmailValue: null,
|
||||
};
|
||||
|
||||
EmailField.propTypes = {
|
||||
errorMessage: PropTypes.string,
|
||||
value: PropTypes.string.isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
handleErrorChange: PropTypes.func.isRequired,
|
||||
confirmEmailValue: PropTypes.string,
|
||||
};
|
||||
|
||||
export default EmailField;
|
||||
139
src/register/RegistrationFields/EmailField/constants.js
Normal file
139
src/register/RegistrationFields/EmailField/constants.js
Normal file
@@ -0,0 +1,139 @@
|
||||
export const EMAIL_FIELD_LABEL = 'email';
|
||||
export const CONFIRM_EMAIL_FIELD_LABEL = 'confirm_email';
|
||||
|
||||
export const COMMON_EMAIL_PROVIDERS = [
|
||||
'hotmail.com', 'yahoo.com', 'outlook.com', 'live.com', 'gmail.com',
|
||||
];
|
||||
|
||||
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
|
||||
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
|
||||
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
|
||||
+ '|\\[(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 DEFAULT_SERVICE_PROVIDER_DOMAINS = ['yahoo', 'hotmail', 'live', 'outlook', 'gmail'];
|
||||
|
||||
export const DEFAULT_TOP_LEVEL_DOMAINS = [
|
||||
'aaa', 'aarp', 'abarth', 'abb', 'abbott', 'abbvie', 'abc', 'able', 'abogado', 'abudhabi', 'ac', 'academy',
|
||||
'accenture', 'accountant', 'accountants', 'aco', 'active', 'actor', 'ad', 'adac', 'ads', 'adult', 'ae', 'aeg', 'aero',
|
||||
'aetna', 'af', 'afamilycompany', 'afl', 'africa', 'ag', 'agakhan', 'agency', 'ai', 'aig', 'aigo', 'airbus', 'airforce',
|
||||
'airtel', 'akdn', 'al', 'alfaromeo', 'alibaba', 'alipay', 'allfinanz', 'allstate', 'ally', 'alsace', 'alstom', 'am',
|
||||
'amazon', 'americanexpress', 'americanfamily', 'amex', 'amfam', 'amica', 'amsterdam', 'an', 'analytics', 'android',
|
||||
'anquan', 'anz', 'ao', 'aol', 'apartments', 'app', 'apple', 'aq', 'aquarelle', 'ar', 'arab', 'aramco', 'archi', 'army',
|
||||
'arpa', 'art', 'arte', 'as', 'asda', 'asia', 'associates', 'at', 'athleta', 'attorney', 'au', 'auction', 'audi',
|
||||
'audible', 'audio', 'auspost', 'author', 'auto', 'autos', 'avianca', 'aw', 'aws', 'ax', 'axa', 'az', 'azure', 'ba',
|
||||
'baby', 'baidu', 'banamex', 'bananarepublic', 'band', 'bank', 'bar', 'barcelona', 'barclaycard', 'barclays',
|
||||
'barefoot', 'bargains', 'baseball', 'basketball', 'bauhaus', 'bayern', 'bb', 'bbc', 'bbt', 'bbva', 'bcg', 'bcn', 'bd',
|
||||
'be', 'beats', 'beauty', 'beer', 'bentley', 'berlin', 'best', 'bestbuy', 'bet', 'bf', 'bg', 'bh', 'bharti', 'bi',
|
||||
'bible', 'bid', 'bike', 'bing', 'bingo', 'bio', 'biz', 'bj', 'bl', 'black', 'blackfriday', 'blanco', 'blockbuster',
|
||||
'blog', 'bloomberg', 'blue', 'bm', 'bms', 'bmw', 'bn', 'bnl', 'bnpparibas', 'bo', 'boats', 'boehringer', 'bofa', 'bom',
|
||||
'bond', 'boo', 'book', 'booking', 'boots', 'bosch', 'bostik', 'boston', 'bot', 'boutique', 'box', 'bq', 'br',
|
||||
'bradesco', 'bridgestone', 'broadway', 'broker', 'brother', 'brussels', 'bs', 'bt', 'budapest', 'bugatti', 'build',
|
||||
'builders', 'business', 'buy', 'buzz', 'bv', 'bw', 'by', 'bz', 'bzh', 'ca', 'cab', 'cafe', 'cal', 'call',
|
||||
'calvinklein', 'cam', 'camera', 'camp', 'cancerresearch', 'canon', 'capetown', 'capital', 'capitalone', 'car',
|
||||
'caravan', 'cards', 'care', 'career', 'careers', 'cars', 'cartier', 'casa', 'case', 'caseih', 'cash', 'casino', 'cat',
|
||||
'catering', 'catholic', 'cba', 'cbn', 'cbre', 'cbs', 'cc', 'cd', 'ceb', 'center', 'ceo', 'cern', 'cf', 'cfa', 'cfd',
|
||||
'cg', 'ch', 'chanel', 'channel', 'charity', 'chase', 'chat', 'cheap', 'chintai', 'chloe', 'christmas', 'chrome',
|
||||
'chrysler', 'church', 'ci', 'cipriani', 'circle', 'cisco', 'citadel', 'citi', 'citic', 'city', 'cityeats', 'ck', 'cl',
|
||||
'claims', 'cleaning', 'click', 'clinic', 'clinique', 'clothing', 'cloud', 'club', 'clubmed', 'cm', 'cn', 'co', 'coach',
|
||||
'codes', 'coffee', 'college', 'cologne', 'com', 'comcast', 'commbank', 'community', 'company', 'compare', 'computer',
|
||||
'comsec', 'condos', 'construction', 'consulting', 'contact', 'contractors', 'cooking', 'cookingchannel', 'cool', 'coop',
|
||||
'corsica', 'country', 'coupon', 'coupons', 'courses', 'cpa', 'cr', 'credit', 'creditcard', 'creditunion', 'cricket',
|
||||
'crown', 'crs', 'cruise', 'cruises', 'csc', 'cu', 'cuisinella', 'cv', 'cw', 'cx', 'cy', 'cymru', 'cyou', 'cz', 'dabur',
|
||||
'dad', 'dance', 'data', 'date', 'dating', 'datsun', 'day', 'dclk', 'dds', 'de', 'deal', 'dealer', 'deals', 'degree',
|
||||
'delivery', 'dell', 'deloitte', 'delta', 'democrat', 'dental', 'dentist', 'desi', 'design', 'dev', 'dhl', 'diamonds',
|
||||
'diet', 'digital', 'direct', 'directory', 'discount', 'discover', 'dish', 'diy', 'dj', 'dk', 'dm', 'dnp', 'do', 'docs',
|
||||
'doctor', 'dodge', 'dog', 'doha', 'domains', 'doosan', 'dot', 'download', 'drive', 'dtv', 'dubai', 'duck', 'dunlop',
|
||||
'duns', 'dupont', 'durban', 'dvag', 'dvr', 'dz', 'earth', 'eat', 'ec', 'eco', 'edeka', 'edu', 'education', 'ee', 'eg',
|
||||
'eh', 'email', 'emerck', 'energy', 'engineer', 'engineering', 'enterprises', 'epost', 'epson', 'equipment', 'er',
|
||||
'ericsson', 'erni', 'es', 'esq', 'estate', 'esurance', 'et', 'etisalat', 'eu', 'eurovision', 'eus', 'events', 'everbank',
|
||||
'exchange', 'expert', 'exposed', 'express', 'extraspace', 'fage', 'fail', 'fairwinds', 'faith', 'family', 'fan', 'fans',
|
||||
'farm', 'farmers', 'fashion', 'fast', 'fedex', 'feedback', 'ferrari', 'ferrero', 'fi', 'fiat', 'fidelity', 'fido', 'film',
|
||||
'final', 'finance', 'financial', 'fire', 'firestone', 'firmdale', 'fish', 'fishing', 'fit', 'fitness', 'fj', 'fk',
|
||||
'flickr', 'flights', 'flir', 'florist', 'flowers', 'flsmidth', 'fly', 'fm', 'fo', 'foo', 'food', 'foodnetwork', 'football',
|
||||
'ford', 'forex', 'forsale', 'forum', 'foundation', 'fox', 'fr', 'free', 'fresenius', 'frl', 'frogans', 'frontdoor',
|
||||
'frontier', 'ftr', 'fujitsu', 'fujixerox', 'fun', 'fund', 'furniture', 'futbol', 'fyi', 'ga', 'gal', 'gallery', 'gallo',
|
||||
'gallup', 'game', 'games', 'gap', 'garden', 'gay', 'gb', 'gbiz', 'gd', 'gdn', 'ge', 'gea', 'gent', 'genting', 'george',
|
||||
'gf', 'gg', 'ggee', 'gh', 'gi', 'gift', 'gifts', 'gives', 'giving', 'gl', 'glade', 'glass', 'gle', 'global', 'globo',
|
||||
'gm', 'gmail', 'gmbh', 'gmo', 'gmx', 'gn', 'godaddy', 'gold', 'goldpoint', 'golf', 'goo', 'goodhands', 'goodyear', 'goog',
|
||||
'google', 'gop', 'got', 'gov', 'gp', 'gq', 'gr', 'grainger', 'graphics', 'gratis', 'green', 'gripe', 'grocery', 'group',
|
||||
'gs', 'gt', 'gu', 'guardian', 'gucci', 'guge', 'guide', 'guitars', 'guru', 'gw', 'gy', 'hair', 'hamburg', 'hangout',
|
||||
'haus', 'hbo', 'hdfc', 'hdfcbank', 'health', 'healthcare', 'help', 'helsinki', 'here', 'hermes', 'hgtv', 'hiphop',
|
||||
'hisamitsu', 'hitachi', 'hiv', 'hk', 'hkt', 'hm', 'hn', 'hockey', 'holdings', 'holiday', 'homedepot', 'homegoods',
|
||||
'homes', 'homesense', 'honda', 'honeywell', 'horse', 'hospital', 'host', 'hosting', 'hot', 'hoteles', 'hotels', 'hotmail',
|
||||
'house', 'how', 'hr', 'hsbc', 'ht', 'htc', 'hu', 'hughes', 'hyatt', 'hyundai', 'ibm', 'icbc', 'ice', 'icu', 'id', 'ie',
|
||||
'ieee', 'ifm', 'iinet', 'ikano', 'il', 'im', 'imamat', 'imdb', 'immo', 'immobilien', 'in', 'inc', 'industries', 'infiniti',
|
||||
'info', 'ing', 'ink', 'institute', 'insurance', 'insure', 'int', 'intel', 'international', 'intuit', 'investments',
|
||||
'io', 'ipiranga', 'iq', 'ir', 'irish', 'is', 'iselect', 'ismaili', 'ist', 'istanbul', 'it', 'itau', 'itv', 'iveco', 'iwc',
|
||||
'jaguar', 'java', 'jcb', 'jcp', 'je', 'jeep', 'jetzt', 'jewelry', 'jio', 'jlc', 'jll', 'jm', 'jmp', 'jnj', 'jo',
|
||||
'jobs', 'joburg', 'jot', 'joy', 'jp', 'jpmorgan', 'jprs', 'juegos', 'juniper', 'kaufen', 'kddi', 'ke', 'kerryhotels',
|
||||
'kerrylogistics', 'kerryproperties', 'kfh', 'kg', 'kh', 'ki', 'kia', 'kim', 'kinder', 'kindle', 'kitchen', 'kiwi', 'km',
|
||||
'kn', 'koeln', 'komatsu', 'kosher', 'kp', 'kpmg', 'kpn', 'kr', 'krd', 'kred', 'kuokgroup', 'kw', 'ky', 'kyoto', 'kz',
|
||||
'la', 'lacaixa', 'ladbrokes', 'lamborghini', 'lamer', 'lancaster', 'lancia', 'lancome', 'land', 'landrover', 'lanxess',
|
||||
'lasalle', 'lat', 'latino', 'latrobe', 'law', 'lawyer', 'lb', 'lc', 'lds', 'lease', 'leclerc', 'lefrak', 'legal',
|
||||
'lego', 'lexus', 'lgbt', 'li', 'liaison', 'lidl', 'life', 'lifeinsurance', 'lifestyle', 'lighting', 'like', 'lilly',
|
||||
'limited', 'limo', 'lincoln', 'linde', 'link', 'lipsy', 'live', 'living', 'lixil', 'lk', 'llc', 'llp', 'loan', 'loans',
|
||||
'locker', 'locus', 'loft', 'lol', 'london', 'lotte', 'lotto', 'love', 'lpl', 'lplfinancial', 'lr', 'ls', 'lt', 'ltd',
|
||||
'ltda', 'lu', 'lundbeck', 'lupin', 'luxe', 'luxury', 'lv', 'ly', 'ma', 'macys', 'madrid', 'maif', 'maison', 'makeup',
|
||||
'man', 'management', 'mango', 'map', 'market', 'marketing', 'markets', 'marriott', 'marshalls', 'maserati', 'mattel',
|
||||
'mba', 'mc', 'mcd', 'mcdonalds', 'mckinsey', 'md', 'me', 'med', 'media', 'meet', 'melbourne', 'meme', 'memorial', 'men',
|
||||
'menu', 'meo', 'merckmsd', 'metlife', 'mf', 'mg', 'mh', 'miami', 'microsoft', 'mil', 'mini', 'mint', 'mit', 'mitsubishi',
|
||||
'mk', 'ml', 'mlb', 'mls', 'mm', 'mma', 'mn', 'mo', 'mobi', 'mobile', 'mobily', 'moda', 'moe', 'moi', 'mom', 'monash',
|
||||
'money', 'monster', 'montblanc', 'mopar', 'mormon', 'mortgage', 'moscow', 'moto', 'motorcycles', 'mov', 'movie', 'movistar',
|
||||
'mp', 'mq', 'mr', 'ms', 'msd', 'mt', 'mtn', 'mtpc', 'mtr', 'mu', 'museum', 'mutual', 'mutuelle', 'mv', 'mw', 'mx', 'my',
|
||||
'mz', 'na', 'nab', 'nadex', 'nagoya', 'name', 'nationwide', 'natura', 'navy', 'nba', 'nc', 'ne', 'nec', 'net', 'netbank',
|
||||
'netflix', 'network', 'neustar', 'new', 'newholland', 'news', 'next', 'nextdirect', 'nexus', 'nf', 'nfl', 'ng', 'ngo', 'nhk',
|
||||
'ni', 'nico', 'nike', 'nikon', 'ninja', 'nissan', 'nissay', 'nl', 'no', 'nokia', 'northwesternmutual', 'norton', 'now',
|
||||
'nowruz', 'nowtv', 'np', 'nr', 'nra', 'nrw', 'ntt', 'nu', 'nyc', 'nz', 'obi', 'observer', 'off', 'office', 'okinawa',
|
||||
'olayan', 'olayangroup', 'oldnavy', 'ollo', 'om', 'omega', 'one', 'ong', 'onl', 'online', 'onyourside', 'ooo', 'open',
|
||||
'oracle', 'orange', 'org', 'organic', 'orientexpress', 'origins', 'osaka', 'otsuka', 'ott', 'ovh', 'pa', 'page',
|
||||
'pamperedchef', 'panasonic', 'panerai', 'paris', 'pars', 'partners', 'parts', 'party', 'passagens', 'pay', 'pccw', 'pe',
|
||||
'pet', 'pf', 'pfizer', 'pg', 'ph', 'pharmacy', 'phd', 'philips', 'phone', 'photo', 'photography', 'photos', 'physio',
|
||||
'piaget', 'pics', 'pictet', 'pictures', 'pid', 'pin', 'ping', 'pink', 'pioneer', 'pizza', 'pk', 'pl', 'place', 'play',
|
||||
'playstation', 'plumbing', 'plus', 'pm', 'pn', 'pnc', 'pohl', 'poker', 'politie', 'porn', 'post', 'pr', 'pramerica',
|
||||
'praxi', 'press', 'prime', 'pro', 'prod', 'productions', 'prof', 'progressive', 'promo', 'properties', 'property',
|
||||
'protection', 'pru', 'prudential', 'ps', 'pt', 'pub', 'pw', 'pwc', 'py', 'qa', 'qpon', 'quebec', 'quest', 'qvc',
|
||||
'racing', 'radio', 'raid', 're', 'read', 'realestate', 'realtor', 'realty', 'recipes', 'red', 'redstone', 'redumbrella',
|
||||
'rehab', 'reise', 'reisen', 'reit', 'reliance', 'ren', 'rent', 'rentals', 'repair', 'report', 'republican', 'rest',
|
||||
'restaurant', 'review', 'reviews', 'rexroth', 'rich', 'richardli', 'ricoh', 'rightathome', 'ril', 'rio', 'rip', 'rmit',
|
||||
'ro', 'rocher', 'rocks', 'rodeo', 'rogers', 'room', 'rs', 'rsvp', 'ru', 'rugby', 'ruhr', 'run', 'rw', 'rwe', 'ryukyu',
|
||||
'sa', 'saarland', 'safe', 'safety', 'sakura', 'sale', 'salon', 'samsclub', 'samsung', 'sandvik', 'sandvikcoromant',
|
||||
'sanofi', 'sap', 'sapo', 'sarl', 'sas', 'save', 'saxo', 'sb', 'sbi', 'sbs', 'sc', 'sca', 'scb', 'schaeffler', 'schmidt',
|
||||
'scholarships', 'school', 'schule', 'schwarz', 'science', 'scjohnson', 'scor', 'scot', 'sd', 'se', 'search', 'seat',
|
||||
'secure', 'security', 'seek', 'select', 'sener', 'services', 'ses', 'seven', 'sew', 'sex', 'sexy', 'sfr', 'sg', 'sh',
|
||||
'shangrila', 'sharp', 'shaw', 'shell', 'shia', 'shiksha', 'shoes', 'shop', 'shopping', 'shouji', 'show', 'showtime',
|
||||
'shriram', 'si', 'silk', 'sina', 'singles', 'site', 'sj', 'sk', 'ski', 'skin', 'sky', 'skype', 'sl', 'sling', 'sm',
|
||||
'smart', 'smile', 'sn', 'sncf', 'so', 'soccer', 'social', 'softbank', 'software', 'sohu', 'solar', 'solutions', 'song',
|
||||
'sony', 'soy', 'spa', 'space', 'spiegel', 'sport', 'spot', 'spreadbetting', 'sr', 'srl', 'srt', 'ss', 'st', 'stada',
|
||||
'staples', 'star', 'starhub', 'statebank', 'statefarm', 'statoil', 'stc', 'stcgroup', 'stockholm', 'storage', 'store',
|
||||
'stream', 'studio', 'study', 'style', 'su', 'sucks', 'supplies', 'supply', 'support', 'surf', 'surgery', 'suzuki', 'sv',
|
||||
'swatch', 'swiftcover', 'swiss', 'sx', 'sy', 'sydney', 'symantec', 'systems', 'sz', 'tab', 'taipei', 'talk', 'taobao',
|
||||
'target', 'tatamotors', 'tatar', 'tattoo', 'tax', 'taxi', 'tc', 'tci', 'td', 'tdk', 'team', 'tech', 'technology', 'tel',
|
||||
'telecity', 'telefonica', 'temasek', 'tennis', 'teva', 'tf', 'tg', 'th', 'thd', 'theater', 'theatre', 'tiaa', 'tickets',
|
||||
'tienda', 'tiffany', 'tips', 'tires', 'tirol', 'tj', 'tjmaxx', 'tjx', 'tk', 'tkmaxx', 'tl', 'tm', 'tmall', 'tn', 'to',
|
||||
'today', 'tokyo', 'tools', 'top', 'toray', 'toshiba', 'total', 'tours', 'town', 'toyota', 'toys', 'tp', 'tr', 'trade',
|
||||
'trading', 'training', 'travel', 'travelchannel', 'travelers', 'travelersinsurance', 'trust', 'trv', 'tt', 'tube', 'tui',
|
||||
'tunes', 'tushu', 'tv', 'tvs', 'tw', 'tz', 'ua', 'ubank', 'ubs', 'uconnect', 'ug', 'uk', 'um', 'unicom', 'university',
|
||||
'uno', 'uol', 'ups', 'us', 'uy', 'uz', 'va', 'vacations', 'vana', 'vanguard', 'vc', 've', 'vegas', 'ventures', 'verisign',
|
||||
'versicherung', 'vet', 'vg', 'vi', 'viajes', 'video', 'vig', 'viking', 'villas', 'vin', 'vip', 'virgin', 'visa', 'vision',
|
||||
'vista', 'vistaprint', 'viva', 'vivo', 'vlaanderen', 'vn', 'vodka', 'volkswagen', 'volvo', 'vote', 'voting', 'voto',
|
||||
'voyage', 'vu', 'vuelos', 'wales', 'walmart', 'walter', 'wang', 'wanggou', 'warman', 'watch', 'watches', 'weather',
|
||||
'weatherchannel', 'webcam', 'weber', 'website', 'wed', 'wedding', 'weibo', 'weir', 'wf', 'whoswho', 'wien', 'wiki',
|
||||
'williamhill', 'win', 'windows', 'wine', 'winners', 'wme', 'wolterskluwer', 'woodside', 'work', 'works', 'world', 'wow',
|
||||
'ws', 'wtc', 'wtf', 'xbox', 'xerox', 'xfinity', 'xihuan', 'xin', '测试', 'कॉम', 'परीक्षा', 'セール', '佛山', 'ಭಾರತ', '慈善',
|
||||
'集团', '在线', '한국', 'ଭାରତ', '大众汽车', '点看', 'คอม', 'ভাৰত', 'ভারত', '八卦', 'ישראל\u200e', 'موقع\u200e', 'বাংলা', '公益',
|
||||
'公司', '香格里拉', '网站', '移动', '我爱你', 'москва', 'испытание', 'қаз', 'католик', 'онлайн', 'сайт', '联通', 'срб', 'бг',
|
||||
'бел', 'קום\u200e', '时尚', '微博', '테스트', '淡马锡', 'ファッション', 'орг', 'नेट', 'ストア', 'アマゾン', '삼성', 'சிங்கப்பூர்', '商标',
|
||||
'商店', '商城', 'дети', 'мкд', 'טעסט\u200e', 'ею', 'ポイント', '新闻', '工行', '家電', 'كوم\u200e', '中文网', '中信', '中国',
|
||||
'中國', '娱乐', '谷歌', 'భారత్', 'ලංකා', '電訊盈科', '购物', '測試', 'クラウド', 'ભારત', '通販', 'भारतम्', 'भारत', 'भारोत', 'آزمایشی\u200e',
|
||||
'பரிட்சை', '网店', 'संगठन', '餐厅', '网络', 'ком', 'укр', '香港', '亚马逊', '诺基亚', '食品', 'δοκιμή', '飞利浦', 'إختبار\u200e',
|
||||
'台湾', '台灣', '手表', '手机', 'мон', 'الجزائر\u200e', 'عمان\u200e', 'ارامكو\u200e', 'ایران\u200e', 'العليان\u200e',
|
||||
'اتصالات\u200e', 'امارات\u200e', 'بازار\u200e', 'موريتانيا\u200e', 'پاکستان\u200e', 'الاردن\u200e', 'موبايلي\u200e',
|
||||
'بارت\u200e', 'بھارت\u200e', 'المغرب\u200e', 'ابوظبي\u200e', 'البحرين\u200e', 'السعودية\u200e', 'ڀارت\u200e',
|
||||
'كاثوليك\u200e', 'سودان\u200e', 'همراه\u200e', 'عراق\u200e', 'مليسيا\u200e', '澳門', '닷컴', '政府', 'شبكة\u200e',
|
||||
'بيتك\u200e', 'عرب\u200e', 'გე', '机构', '组织机构', '健康', 'ไทย', 'سورية\u200e', '招聘', 'рус', 'рф', '珠宝',
|
||||
'تونس\u200e', '大拿', 'ລາວ', 'みんな', 'グーグル', 'ευ', 'ελ', '世界', '書籍', 'ഭാരതം', 'ਭਾਰਤ', '网址', '닷넷', 'コム',
|
||||
'天主教', '游戏', 'vermögensberater', 'vermögensberatung', '企业', '信息', '嘉里大酒店', '嘉里', 'مصر\u200e',
|
||||
'قطر\u200e', '广东', 'இலங்கை', 'இந்தியா', 'հայ', '新加坡', 'فلسطين\u200e', 'テスト', '政务', 'xperia', 'xxx',
|
||||
'xyz', 'yachts', 'yahoo', 'yamaxun', 'yandex', 'ye', 'yodobashi', 'yoga', 'yokohama', 'you', 'youtube', 'yt',
|
||||
'yun', 'za', 'zappos', 'zara', 'zero', 'zip', 'zippo', 'zm', 'zone', 'zuerich', 'zw',
|
||||
];
|
||||
125
src/register/RegistrationFields/EmailField/validator.js
Normal file
125
src/register/RegistrationFields/EmailField/validator.js
Normal file
@@ -0,0 +1,125 @@
|
||||
import { distance } from 'fastest-levenshtein';
|
||||
|
||||
import {
|
||||
COMMON_EMAIL_PROVIDERS,
|
||||
DEFAULT_SERVICE_PROVIDER_DOMAINS,
|
||||
DEFAULT_TOP_LEVEL_DOMAINS, VALID_EMAIL_REGEX,
|
||||
} from './constants';
|
||||
import messages from '../../messages';
|
||||
|
||||
export const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||
|
||||
const 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 const 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 const 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;
|
||||
};
|
||||
|
||||
const validateEmail = (value, confirmEmailValue, formatMessage) => {
|
||||
let fieldError = '';
|
||||
let confirmEmailError = '';
|
||||
let emailSuggestion = {};
|
||||
|
||||
if (!value) {
|
||||
fieldError = formatMessage(messages['empty.email.field.error']);
|
||||
} else if (value.length <= 2) {
|
||||
fieldError = 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 = formatMessage(messages['email.invalid.format.error']);
|
||||
emailSuggestion = {
|
||||
suggestion: getSuggestionForInvalidEmail(domainName, username),
|
||||
type: 'error',
|
||||
};
|
||||
} else {
|
||||
const response = validateEmailAddress(value, username, domainName);
|
||||
if (response.hasError) {
|
||||
fieldError = formatMessage(messages['email.invalid.format.error']);
|
||||
delete response.hasError;
|
||||
}
|
||||
emailSuggestion = { ...response };
|
||||
|
||||
if (confirmEmailValue && value !== confirmEmailValue) {
|
||||
confirmEmailError = formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { fieldError, confirmEmailError, suggestion: emailSuggestion };
|
||||
};
|
||||
|
||||
export default validateEmail;
|
||||
59
src/register/RegistrationFields/NameField/NameField.jsx
Normal file
59
src/register/RegistrationFields/NameField/NameField.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { NAME_FIELD_LABEL } from './constants';
|
||||
import validateName from './validator';
|
||||
import { FormGroup } from '../../../common-components';
|
||||
import { clearRegistertionBackendError, fetchRealtimeValidations } from '../../data/actions';
|
||||
|
||||
const NameField = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
|
||||
|
||||
const {
|
||||
handleErrorChange,
|
||||
shouldFetchUsernameSuggestions,
|
||||
} = props;
|
||||
|
||||
const handleOnBlur = (e) => {
|
||||
const { value } = e.target;
|
||||
const fieldError = validateName(value, formatMessage);
|
||||
if (fieldError) {
|
||||
handleErrorChange(NAME_FIELD_LABEL, fieldError);
|
||||
} else if (shouldFetchUsernameSuggestions && !validationApiRateLimited) {
|
||||
dispatch(fetchRealtimeValidations({ name: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnFocus = () => {
|
||||
handleErrorChange(NAME_FIELD_LABEL, '');
|
||||
dispatch(clearRegistertionBackendError(NAME_FIELD_LABEL));
|
||||
};
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
{...props}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
NameField.defaultProps = {
|
||||
errorMessage: '',
|
||||
shouldFetchUsernameSuggestions: false,
|
||||
};
|
||||
|
||||
NameField.propTypes = {
|
||||
errorMessage: PropTypes.string,
|
||||
shouldFetchUsernameSuggestions: PropTypes.bool,
|
||||
value: PropTypes.string.isRequired,
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
handleErrorChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default NameField;
|
||||
5
src/register/RegistrationFields/NameField/constants.js
Normal file
5
src/register/RegistrationFields/NameField/constants.js
Normal file
@@ -0,0 +1,5 @@
|
||||
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
|
||||
|
||||
export const urlRegex = new RegExp(INVALID_NAME_REGEX);
|
||||
|
||||
export const NAME_FIELD_LABEL = 'name';
|
||||
14
src/register/RegistrationFields/NameField/validator.js
Normal file
14
src/register/RegistrationFields/NameField/validator.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { urlRegex } from './constants';
|
||||
import messages from '../../messages';
|
||||
|
||||
const validateName = (value, formatMessage) => {
|
||||
let fieldError;
|
||||
if (!value.trim()) {
|
||||
fieldError = formatMessage(messages['empty.name.field.error']);
|
||||
} else if (value && value.match(urlRegex)) {
|
||||
fieldError = formatMessage(messages['name.validation.message']);
|
||||
}
|
||||
return fieldError;
|
||||
};
|
||||
|
||||
export default validateName;
|
||||
150
src/register/RegistrationFields/UsernameField/UsernameField.jsx
Normal file
150
src/register/RegistrationFields/UsernameField/UsernameField.jsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Icon, IconButton } from '@edx/paragon';
|
||||
import { Close } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { USERNAME_FIELD_LABEL } from './constants';
|
||||
import validateUsername from './validator';
|
||||
import { FormGroup } from '../../../common-components';
|
||||
import {
|
||||
clearRegistertionBackendError,
|
||||
clearUsernameSuggestions,
|
||||
fetchRealtimeValidations,
|
||||
} from '../../data/actions';
|
||||
import messages from '../../messages';
|
||||
|
||||
const UsernameField = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const {
|
||||
value,
|
||||
errorMessage,
|
||||
handleChange,
|
||||
handleErrorChange,
|
||||
} = props;
|
||||
|
||||
let className = '';
|
||||
let suggestedUsernameDiv = null;
|
||||
let iconButton = null;
|
||||
const { usernameSuggestions, validationApiRateLimited } = useSelector(state => state.register);
|
||||
|
||||
/**
|
||||
* 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 && !value) {
|
||||
handleChange({ target: { name: USERNAME_FIELD_LABEL, value: ' ' } });
|
||||
}
|
||||
}, [handleChange, usernameSuggestions, value]);
|
||||
|
||||
const handleOnBlur = (event) => {
|
||||
const { value: username } = event.target;
|
||||
const fieldError = validateUsername(username, formatMessage);
|
||||
if (fieldError) {
|
||||
handleErrorChange(USERNAME_FIELD_LABEL, fieldError);
|
||||
} else if (!validationApiRateLimited) {
|
||||
dispatch(fetchRealtimeValidations({ username: value }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnChange = (event) => {
|
||||
let username = event.target.value;
|
||||
if (username.length > 30) {
|
||||
return;
|
||||
}
|
||||
if (event.target.value.startsWith(' ')) {
|
||||
username = username.trim();
|
||||
}
|
||||
handleChange({ target: { name: USERNAME_FIELD_LABEL, value: username } });
|
||||
};
|
||||
|
||||
const handleOnFocus = (event) => {
|
||||
const username = event.target.value;
|
||||
dispatch(clearUsernameSuggestions());
|
||||
// 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 (username === ' ') {
|
||||
handleChange({ target: { name: USERNAME_FIELD_LABEL, value: '' } });
|
||||
}
|
||||
handleErrorChange(USERNAME_FIELD_LABEL, '');
|
||||
dispatch(clearRegistertionBackendError(USERNAME_FIELD_LABEL));
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (event, suggestion = '') => {
|
||||
event.preventDefault();
|
||||
handleErrorChange(USERNAME_FIELD_LABEL, ''); // clear error
|
||||
handleChange({ target: { name: USERNAME_FIELD_LABEL, value: suggestion } }); // to set suggestion as value
|
||||
dispatch(clearUsernameSuggestions());
|
||||
};
|
||||
|
||||
const handleUsernameSuggestionClose = () => {
|
||||
handleChange({ target: { name: USERNAME_FIELD_LABEL, value: '' } }); // to remove space in field
|
||||
dispatch(clearUsernameSuggestions());
|
||||
};
|
||||
|
||||
const suggestedUsernames = () => (
|
||||
<div className={className}>
|
||||
<span className="text-gray username-suggestion--label">{formatMessage(messages['registration.username.suggestion.label'])}</span>
|
||||
<div className="username-scroll-suggested--form-field">
|
||||
{usernameSuggestions.map((username, index) => (
|
||||
<Button
|
||||
type="button"
|
||||
name="username"
|
||||
variant="outline-dark"
|
||||
className="username-suggestions--chip data-hj-suppress"
|
||||
autoComplete={props.autoComplete}
|
||||
key={`suggestion-${index.toString()}`}
|
||||
onClick={(e) => handleSuggestionClick(e, username)}
|
||||
>
|
||||
{username}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{iconButton}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (usernameSuggestions.length > 0 && errorMessage && value === ' ') {
|
||||
className = 'username-suggestions__error';
|
||||
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
|
||||
suggestedUsernameDiv = suggestedUsernames();
|
||||
} else if (usernameSuggestions.length > 0 && value === ' ') {
|
||||
className = 'username-suggestions d-flex align-items-center';
|
||||
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
|
||||
suggestedUsernameDiv = suggestedUsernames();
|
||||
} else if (usernameSuggestions.length > 0 && errorMessage) {
|
||||
suggestedUsernameDiv = suggestedUsernames();
|
||||
}
|
||||
return (
|
||||
<FormGroup
|
||||
{...props}
|
||||
handleChange={handleOnChange}
|
||||
handleFocus={handleOnFocus}
|
||||
handleBlur={handleOnBlur}
|
||||
>
|
||||
{suggestedUsernameDiv}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
UsernameField.defaultProps = {
|
||||
errorMessage: '',
|
||||
autoComplete: null,
|
||||
};
|
||||
|
||||
UsernameField.propTypes = {
|
||||
handleChange: PropTypes.func.isRequired,
|
||||
handleErrorChange: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
autoComplete: PropTypes.string,
|
||||
};
|
||||
|
||||
export default UsernameField;
|
||||
@@ -0,0 +1,3 @@
|
||||
export const USERNAME_FIELD_LABEL = 'username';
|
||||
|
||||
export const VALID_USERNAME_REGEX = /^[a-zA-Z0-9_-]*$/i;
|
||||
16
src/register/RegistrationFields/UsernameField/validator.js
Normal file
16
src/register/RegistrationFields/UsernameField/validator.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { VALID_USERNAME_REGEX } from './constants';
|
||||
import messages from '../../messages';
|
||||
|
||||
export const usernameRegex = new RegExp(VALID_USERNAME_REGEX, 'i');
|
||||
|
||||
const validateUsername = (value, formatMessage) => {
|
||||
let fieldError = '';
|
||||
if (!value || value.length <= 1 || value.length > 30) {
|
||||
fieldError = formatMessage(messages['username.validation.message']);
|
||||
} else if (!usernameRegex.test(value)) {
|
||||
fieldError = formatMessage(messages['username.format.validation.message']);
|
||||
}
|
||||
return fieldError;
|
||||
};
|
||||
|
||||
export default validateUsername;
|
||||
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/EmailField';
|
||||
export { default as UsernameField } from './UsernameField/UsernameField';
|
||||
export { default as CountryField } from './CountryField/CountryField';
|
||||
export { default as HonorCode } from './HonorCode';
|
||||
export { default as TermsOfService } from './TermsOfService';
|
||||
@@ -1,94 +1,79 @@
|
||||
import React, {
|
||||
useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
getCountryList, getLocale, useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Spinner, StatefulButton } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import ConfigurableRegistrationForm from './ConfigurableRegistrationForm';
|
||||
import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm';
|
||||
import {
|
||||
backupRegistrationFormBegin,
|
||||
clearRegistertionBackendError,
|
||||
clearUsernameSuggestions,
|
||||
fetchRealtimeValidations,
|
||||
registerNewUser,
|
||||
setUserPipelineDataLoaded,
|
||||
} from './data/actions';
|
||||
import {
|
||||
COUNTRY_CODE_KEY,
|
||||
COUNTRY_DISPLAY_KEY,
|
||||
FIELDS,
|
||||
FORM_SUBMISSION_ERROR,
|
||||
TPA_AUTHENTICATION_FAILURE,
|
||||
} from './data/constants';
|
||||
import { registrationErrorSelector, validationsSelector } from './data/selectors';
|
||||
import {
|
||||
getSuggestionForInvalidEmail, validateCountryField, validateEmailAddress,
|
||||
} from './data/utils';
|
||||
import { getBackendValidations } from './data/selectors';
|
||||
import { isFormValid, prepareRegistrationPayload } from './data/utils';
|
||||
import messages from './messages';
|
||||
import RegistrationFailure from './RegistrationFailure';
|
||||
import { EmailField, UsernameField } from './registrationFields';
|
||||
import ThirdPartyAuth from './ThirdPartyAuth';
|
||||
import RegistrationFailure from './components/RegistrationFailure';
|
||||
import { EmailField, UsernameField } from './RegistrationFields';
|
||||
import NameField from './RegistrationFields/NameField/NameField';
|
||||
import ThirdPartyAuth from './components/ThirdPartyAuth';
|
||||
import {
|
||||
FormGroup, InstitutionLogistration, PasswordField, RedirectLogistration, ThirdPartyAuthAlert,
|
||||
InstitutionLogistration, PasswordField, RedirectLogistration, ThirdPartyAuthAlert,
|
||||
} from '../common-components';
|
||||
import { getThirdPartyAuthContext } from '../common-components/data/actions';
|
||||
import {
|
||||
fieldDescriptionSelector, optionalFieldsSelector, thirdPartyAuthContextSelector,
|
||||
} from '../common-components/data/selectors';
|
||||
import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../common-components/data/actions';
|
||||
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
||||
import {
|
||||
COMPLETE_STATE, DEFAULT_STATE,
|
||||
INVALID_NAME_REGEX, LETTER_REGEX, NUMBER_REGEX, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX,
|
||||
COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
import {
|
||||
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie,
|
||||
getAllPossibleQueryParams, getTpaHint, getTpaProvider, setCookie,
|
||||
} from '../data/utils';
|
||||
|
||||
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||
const urlRegex = new RegExp(INVALID_NAME_REGEX);
|
||||
|
||||
const RegistrationPage = (props) => {
|
||||
const {
|
||||
backedUpFormData,
|
||||
backendCountryCode,
|
||||
backendValidations,
|
||||
fieldDescriptions,
|
||||
handleInstitutionLogin,
|
||||
institutionLogin,
|
||||
optionalFields,
|
||||
registrationFormData: backedUpFormData,
|
||||
registrationError,
|
||||
registrationErrorCode,
|
||||
registrationError: {
|
||||
errorCode: registrationErrorCode,
|
||||
} = {},
|
||||
registrationResult,
|
||||
shouldBackupState,
|
||||
userPipelineDataLoaded,
|
||||
submitState,
|
||||
validations,
|
||||
} = useSelector(state => state.register);
|
||||
|
||||
const {
|
||||
fieldDescriptions,
|
||||
optionalFields,
|
||||
thirdPartyAuthApiStatus,
|
||||
thirdPartyAuthContext,
|
||||
usernameSuggestions,
|
||||
validationApiRateLimited,
|
||||
// Actions
|
||||
backupFormState,
|
||||
setUserPipelineDetailsLoaded,
|
||||
getRegistrationDataFromBackend,
|
||||
userPipelineDataLoaded,
|
||||
validateFromBackend,
|
||||
clearBackendError,
|
||||
} = useSelector(state => state.commonComponents);
|
||||
|
||||
const {
|
||||
handleInstitutionLogin,
|
||||
institutionLogin,
|
||||
} = props;
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
const countryList = useMemo(() => getCountryList(getLocale()), []);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const backendValidations = useMemo(
|
||||
() => getBackendValidations(registrationError, validations), [registrationError, validations],
|
||||
);
|
||||
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||
const registrationEmbedded = isHostAvailableInQueryParams();
|
||||
const { cta, host } = queryParams;
|
||||
const tpaHint = useMemo(() => getTpaHint(), []);
|
||||
const flags = {
|
||||
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
|
||||
@@ -99,35 +84,14 @@ const RegistrationPage = (props) => {
|
||||
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 [autoSubmitRegisterForm, setAutoSubmitRegisterForm] = useState(false);
|
||||
const [errorCode, setErrorCode] = useState({ type: '', count: 0 });
|
||||
const [formStartTime, setFormStartTime] = useState(null);
|
||||
const [focusedField, setFocusedField] = useState(null);
|
||||
|
||||
const {
|
||||
providers, currentProvider, secondaryProviders, finishAuthUrl,
|
||||
} = thirdPartyAuthContext;
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
const buttonLabel = cta ? formatMessage(messages['create.account.cta.button'], { label: cta }) : formatMessage(messages['create.account.for.free.button']);
|
||||
|
||||
/**
|
||||
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
|
||||
*/
|
||||
const checkTOSandHonorCodeFields = () => {
|
||||
if (Object.keys(fieldDescriptions).includes(FIELDS.HONOR_CODE)) {
|
||||
setConfigurableFormFields(prevState => ({
|
||||
...prevState,
|
||||
[FIELDS.HONOR_CODE]: true,
|
||||
}));
|
||||
}
|
||||
if (Object.keys(fieldDescriptions).includes(FIELDS.TERMS_OF_SERVICE)) {
|
||||
setConfigurableFormFields(prevState => ({
|
||||
...prevState,
|
||||
[FIELDS.TERMS_OF_SERVICE]: true,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the userPipelineDetails data in formFields for only first time
|
||||
@@ -138,7 +102,6 @@ const RegistrationPage = (props) => {
|
||||
if (errorMessage) {
|
||||
setErrorCode(prevState => ({ type: TPA_AUTHENTICATION_FAILURE, count: prevState.count + 1 }));
|
||||
} else if (autoSubmitRegForm) {
|
||||
checkTOSandHonorCodeFields();
|
||||
setAutoSubmitRegisterForm(true);
|
||||
}
|
||||
if (pipelineUserDetails && Object.keys(pipelineUserDetails).length !== 0) {
|
||||
@@ -146,13 +109,12 @@ const RegistrationPage = (props) => {
|
||||
setFormFields(prevState => ({
|
||||
...prevState, name, username, email,
|
||||
}));
|
||||
setUserPipelineDetailsLoaded(true);
|
||||
dispatch(setUserPipelineDataLoaded(true));
|
||||
}
|
||||
}
|
||||
}, [ // eslint-disable-line react-hooks/exhaustive-deps
|
||||
thirdPartyAuthContext,
|
||||
userPipelineDataLoaded,
|
||||
setUserPipelineDetailsLoaded,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -162,24 +124,24 @@ const RegistrationPage = (props) => {
|
||||
if (tpaHint) {
|
||||
payload.tpa_hint = tpaHint;
|
||||
}
|
||||
getRegistrationDataFromBackend(payload);
|
||||
dispatch(getRegistrationDataFromBackend(payload));
|
||||
setFormStartTime(Date.now());
|
||||
}
|
||||
}, [formStartTime, getRegistrationDataFromBackend, queryParams, tpaHint]);
|
||||
}, [dispatch, formStartTime, queryParams, tpaHint]);
|
||||
|
||||
/**
|
||||
* Backup the registration form in redux when register page is toggled.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (shouldBackupState) {
|
||||
backupFormState({
|
||||
dispatch(backupRegistrationFormBegin({
|
||||
...backedUpFormData,
|
||||
configurableFormFields: { ...configurableFormFields },
|
||||
formFields: { ...formFields },
|
||||
emailSuggestion: { ...emailSuggestion },
|
||||
errors: { ...errors },
|
||||
});
|
||||
}));
|
||||
}
|
||||
}, [shouldBackupState, configurableFormFields, formFields, errors, emailSuggestion, backupFormState]);
|
||||
}, [shouldBackupState, configurableFormFields, formFields, errors, dispatch, backedUpFormData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (backendValidations) {
|
||||
@@ -193,39 +155,6 @@ const RegistrationPage = (props) => {
|
||||
}
|
||||
}, [registrationErrorCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (backendCountryCode && backendCountryCode !== configurableFormFields?.country?.countryCode) {
|
||||
let countryCode = '';
|
||||
let countryDisplayValue = '';
|
||||
|
||||
const selectedCountry = countryList.find(
|
||||
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()),
|
||||
);
|
||||
if (selectedCountry) {
|
||||
countryCode = selectedCountry[COUNTRY_CODE_KEY];
|
||||
countryDisplayValue = selectedCountry[COUNTRY_DISPLAY_KEY];
|
||||
}
|
||||
setConfigurableFormFields(prevState => (
|
||||
{
|
||||
...prevState,
|
||||
country: {
|
||||
countryCode, displayValue: countryDisplayValue,
|
||||
},
|
||||
}
|
||||
));
|
||||
}
|
||||
}, [backendCountryCode, countryList]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
/**
|
||||
* 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(prevState => ({ ...prevState, username: ' ' }));
|
||||
}
|
||||
}, [usernameSuggestions, formFields]);
|
||||
|
||||
useEffect(() => {
|
||||
if (registrationResult.success) {
|
||||
// Optimizely registration conversion event
|
||||
@@ -249,221 +178,22 @@ const RegistrationPage = (props) => {
|
||||
}
|
||||
}, [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 = formatMessage(messages['empty.name.field.error']);
|
||||
} else if (value && value.match(urlRegex)) {
|
||||
fieldError = formatMessage(messages['name.validation.message']);
|
||||
} else if (value && !payload.username.trim() && shouldValidateFromBackend) {
|
||||
validateFromBackend(payload);
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
if (!value) {
|
||||
fieldError = formatMessage(messages['empty.email.field.error']);
|
||||
} else if (value.length <= 2) {
|
||||
fieldError = 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 = formatMessage(messages['email.invalid.format.error']);
|
||||
setEmailSuggestion({
|
||||
suggestion: getSuggestionForInvalidEmail(domainName, username),
|
||||
type: 'error',
|
||||
});
|
||||
} else {
|
||||
const response = validateEmailAddress(value, username, domainName);
|
||||
if (response.hasError) {
|
||||
fieldError = 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 = formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'username':
|
||||
if (!value || value.length <= 1 || value.length > 30) {
|
||||
fieldError = formatMessage(messages['username.validation.message']);
|
||||
} else if (!value.match(/^[a-zA-Z0-9_-]*$/i)) {
|
||||
fieldError = 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 = 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, formatMessage(messages['empty.country.field.error']));
|
||||
fieldError = error;
|
||||
countryFieldCode = countryCode;
|
||||
setConfigurableFormFields(prevState => ({ ...prevState, 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 = formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (setError) {
|
||||
setErrors(prevErrors => ({
|
||||
...prevErrors,
|
||||
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] = formatMessage(messages[`empty.${key}.field.error`]);
|
||||
}
|
||||
if (fieldErrors[key]) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (flags.showConfigurableEdxFields) {
|
||||
if (!configurableFormFields.country.displayValue) {
|
||||
fieldErrors.country = 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] = formatMessage(messages['empty.country.field.error']);
|
||||
} else if (!configurableFormFields[key]) {
|
||||
fieldErrors[key] = fieldDescriptions[key].error_message;
|
||||
}
|
||||
if (fieldErrors[key]) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (focusedField) {
|
||||
fieldErrors[focusedField] = focusedFieldError;
|
||||
}
|
||||
setErrors({ ...fieldErrors });
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (event, fieldName, suggestion = '') => {
|
||||
event.preventDefault();
|
||||
setErrors(prevErrors => ({ ...prevErrors, [fieldName]: '' }));
|
||||
switch (fieldName) {
|
||||
case 'email':
|
||||
setFormFields(prevState => ({ ...prevState, email: emailSuggestion.suggestion }));
|
||||
setEmailSuggestion({ suggestion: '', type: '' });
|
||||
break;
|
||||
case 'username':
|
||||
setFormFields(prevState => ({ ...prevState, username: suggestion }));
|
||||
props.resetUsernameSuggestions();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEmailSuggestionClosed = () => setEmailSuggestion({ suggestion: '', type: '' });
|
||||
|
||||
const handleUsernameSuggestionClosed = () => props.resetUsernameSuggestions();
|
||||
|
||||
const handleOnChange = (event) => {
|
||||
const { name } = event.target;
|
||||
let value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
||||
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
||||
if (registrationError[name]) {
|
||||
clearBackendError(name);
|
||||
dispatch(clearRegistertionBackendError(name));
|
||||
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||
}
|
||||
if (name === 'username') {
|
||||
if (value.length > 30) {
|
||||
return;
|
||||
}
|
||||
if (value.startsWith(' ')) {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
||||
};
|
||||
|
||||
const handleOnBlur = (event) => {
|
||||
const { name, value } = event.target;
|
||||
if (registrationEmbedded) {
|
||||
if (name === 'name') {
|
||||
validateInput(
|
||||
name,
|
||||
value,
|
||||
{ name: formFields.name, username: formFields.username, form_field_key: name },
|
||||
!validationApiRateLimited,
|
||||
false,
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
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(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||
clearBackendError(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(prevState => ({ ...prevState, [name]: '' }));
|
||||
}
|
||||
const handleErrorChange = (fieldName, error) => {
|
||||
if (fieldName) {
|
||||
setErrors(prevErrors => ({
|
||||
...prevErrors,
|
||||
[fieldName]: error,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -476,42 +206,33 @@ const RegistrationPage = (props) => {
|
||||
payload.social_auth_provider = currentProvider;
|
||||
}
|
||||
|
||||
const { fieldError: focusedFieldError, countryFieldCode } = focusedField ? (
|
||||
validateInput(
|
||||
focusedField,
|
||||
(focusedField in fieldDescriptions || ['country', 'marketingEmailsOptIn'].includes(focusedField)) ? (
|
||||
configurableFormFields[focusedField]
|
||||
) : formFields[focusedField],
|
||||
payload,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
) : '';
|
||||
// Validating form data before submitting
|
||||
const { isValid, fieldErrors } = isFormValid(
|
||||
payload,
|
||||
errors,
|
||||
configurableFormFields,
|
||||
fieldDescriptions,
|
||||
formatMessage,
|
||||
);
|
||||
setErrors({ ...fieldErrors });
|
||||
|
||||
if (!isFormValid(payload, focusedFieldError)) {
|
||||
// returning if not valid
|
||||
if (!isValid) {
|
||||
setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(configurableFormFields).forEach((fieldName) => {
|
||||
if (fieldName === 'country') {
|
||||
payload[fieldName] = focusedField === 'country' ? countryFieldCode : configurableFormFields[fieldName].countryCode;
|
||||
} else {
|
||||
payload[fieldName] = configurableFormFields[fieldName];
|
||||
}
|
||||
});
|
||||
// Preparing payload for submission
|
||||
payload = prepareRegistrationPayload(
|
||||
payload,
|
||||
configurableFormFields,
|
||||
flags.showMarketingEmailOptInCheckbox,
|
||||
totalRegistrationTime,
|
||||
queryParams);
|
||||
|
||||
// Don't send the marketing email opt-in value if the flag is turned off
|
||||
if (!flags.showMarketingEmailOptInCheckbox) {
|
||||
delete payload.marketingEmailsOptIn;
|
||||
}
|
||||
|
||||
payload = snakeCaseObject(payload);
|
||||
payload.totalRegistrationTime = totalRegistrationTime;
|
||||
|
||||
// add query params to the payload
|
||||
payload = { ...payload, ...queryParams };
|
||||
props.registerNewUser(payload);
|
||||
// making register call
|
||||
console.log('register payload', payload);
|
||||
dispatch(registerNewUser(payload));
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
@@ -540,12 +261,10 @@ const RegistrationPage = (props) => {
|
||||
<title>{formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}</title>
|
||||
</Helmet>
|
||||
<RedirectLogistration
|
||||
host={host}
|
||||
success={registrationResult.success}
|
||||
redirectUrl={registrationResult.redirectUrl}
|
||||
finishAuthUrl={finishAuthUrl}
|
||||
optionalFields={optionalFields}
|
||||
registrationEmbedded={registrationEmbedded}
|
||||
redirectToProgressiveProfilingPage={
|
||||
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && Object.keys(optionalFields).includes('fields')
|
||||
}
|
||||
@@ -556,10 +275,7 @@ const RegistrationPage = (props) => {
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={classNames(
|
||||
'mw-xs mt-3',
|
||||
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded },
|
||||
)}
|
||||
className="mw-xs mt-3"
|
||||
>
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={currentProvider}
|
||||
@@ -572,12 +288,12 @@ const RegistrationPage = (props) => {
|
||||
context={{ provider: currentProvider, errorMessage: thirdPartyAuthContext.errorMessage }}
|
||||
/>
|
||||
<Form id="registration-form" name="registration-form">
|
||||
<FormGroup
|
||||
<NameField
|
||||
name="name"
|
||||
value={formFields.name}
|
||||
shouldFetchUsernameSuggestions={!formFields.username.trim()}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
handleErrorChange={handleErrorChange}
|
||||
errorMessage={errors.name}
|
||||
helpText={[formatMessage(messages['help.text.name'])]}
|
||||
floatingLabel={formatMessage(messages['registration.fullname.label'])}
|
||||
@@ -585,12 +301,9 @@ const RegistrationPage = (props) => {
|
||||
<EmailField
|
||||
name="email"
|
||||
value={formFields.email}
|
||||
confirmEmailValue={configurableFormFields?.confirm_email}
|
||||
handleErrorChange={handleErrorChange}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
handleSuggestionClick={(e) => handleSuggestionClick(e, 'email')}
|
||||
handleOnClose={handleEmailSuggestionClosed}
|
||||
emailSuggestion={emailSuggestion}
|
||||
errorMessage={errors.email}
|
||||
helpText={[formatMessage(messages['help.text.email'])]}
|
||||
floatingLabel={formatMessage(messages['registration.email.label'])}
|
||||
@@ -599,12 +312,8 @@ const RegistrationPage = (props) => {
|
||||
name="username"
|
||||
spellCheck="false"
|
||||
value={formFields.username}
|
||||
handleBlur={handleOnBlur}
|
||||
handleChange={handleOnChange}
|
||||
handleFocus={handleOnFocus}
|
||||
handleSuggestionClick={handleSuggestionClick}
|
||||
handleUsernameSuggestionClose={handleUsernameSuggestionClosed}
|
||||
usernameSuggestions={usernameSuggestions}
|
||||
handleErrorChange={handleErrorChange}
|
||||
errorMessage={errors.username}
|
||||
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
|
||||
floatingLabel={formatMessage(messages['registration.username.label'])}
|
||||
@@ -614,21 +323,18 @@ const RegistrationPage = (props) => {
|
||||
name="password"
|
||||
value={formFields.password}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
handleErrorChange={handleErrorChange}
|
||||
errorMessage={errors.password}
|
||||
floatingLabel={formatMessage(messages['registration.password.label'])}
|
||||
/>
|
||||
)}
|
||||
<ConfigurableRegistrationForm
|
||||
countryList={countryList}
|
||||
email={formFields.email}
|
||||
fieldErrors={errors}
|
||||
registrationEmbedded={registrationEmbedded}
|
||||
formFields={configurableFormFields}
|
||||
setFieldErrors={setErrors}
|
||||
setFormFields={setConfigurableFormFields}
|
||||
setFocusedField={setFocusedField}
|
||||
autoSubmitRegisterForm={autoSubmitRegisterForm}
|
||||
fieldDescriptions={fieldDescriptions}
|
||||
/>
|
||||
<StatefulButton
|
||||
@@ -639,21 +345,19 @@ const RegistrationPage = (props) => {
|
||||
className="register-button mt-4 mb-4"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: buttonLabel,
|
||||
default: formatMessage(messages['create.account.for.free.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
{!registrationEmbedded && (
|
||||
<ThirdPartyAuth
|
||||
currentProvider={currentProvider}
|
||||
providers={providers}
|
||||
secondaryProviders={secondaryProviders}
|
||||
handleInstitutionLogin={handleInstitutionLogin}
|
||||
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
||||
/>
|
||||
)}
|
||||
<ThirdPartyAuth
|
||||
currentProvider={currentProvider}
|
||||
providers={providers}
|
||||
secondaryProviders={secondaryProviders}
|
||||
handleInstitutionLogin={handleInstitutionLogin}
|
||||
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
@@ -678,139 +382,15 @@ const RegistrationPage = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const registerPageState = state.register;
|
||||
return {
|
||||
backedUpFormData: registerPageState.registrationFormData,
|
||||
backendCountryCode: registerPageState.backendCountryCode,
|
||||
backendValidations: validationsSelector(state),
|
||||
fieldDescriptions: fieldDescriptionSelector(state),
|
||||
optionalFields: optionalFieldsSelector(state),
|
||||
registrationError: registerPageState.registrationError,
|
||||
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,
|
||||
}),
|
||||
fieldDescriptions: PropTypes.shape({}),
|
||||
institutionLogin: PropTypes.bool,
|
||||
optionalFields: PropTypes.shape({}),
|
||||
registrationError: PropTypes.shape({}),
|
||||
registrationErrorCode: PropTypes.string,
|
||||
registrationResult: PropTypes.shape({
|
||||
redirectUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
}),
|
||||
shouldBackupState: PropTypes.bool,
|
||||
submitState: PropTypes.string,
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
thirdPartyAuthContext: PropTypes.shape({
|
||||
autoSubmitRegForm: PropTypes.bool,
|
||||
countryCode: PropTypes.string,
|
||||
currentProvider: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
finishAuthUrl: PropTypes.string,
|
||||
pipelineUserDetails: PropTypes.shape({
|
||||
email: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
firstName: PropTypes.string,
|
||||
lastName: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
}),
|
||||
platformName: PropTypes.string,
|
||||
providers: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
secondaryProviders: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
}),
|
||||
usernameSuggestions: PropTypes.arrayOf(PropTypes.string),
|
||||
userPipelineDataLoaded: PropTypes.bool,
|
||||
validationApiRateLimited: PropTypes.bool,
|
||||
// Actions
|
||||
backupFormState: PropTypes.func.isRequired,
|
||||
clearBackendError: PropTypes.func.isRequired,
|
||||
getRegistrationDataFromBackend: PropTypes.func.isRequired,
|
||||
handleInstitutionLogin: PropTypes.func,
|
||||
registerNewUser: PropTypes.func.isRequired,
|
||||
resetUsernameSuggestions: PropTypes.func.isRequired,
|
||||
setUserPipelineDetailsLoaded: PropTypes.func.isRequired,
|
||||
validateFromBackend: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
RegistrationPage.defaultProps = {
|
||||
backedUpFormData: {
|
||||
configurableFormFields: {
|
||||
marketingEmailsOptIn: true,
|
||||
},
|
||||
formFields: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
},
|
||||
errors: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
},
|
||||
emailSuggestion: {
|
||||
suggestion: '', type: '',
|
||||
},
|
||||
},
|
||||
backendCountryCode: '',
|
||||
backendValidations: null,
|
||||
fieldDescriptions: {},
|
||||
handleInstitutionLogin: null,
|
||||
institutionLogin: false,
|
||||
optionalFields: {},
|
||||
registrationError: {},
|
||||
registrationErrorCode: '',
|
||||
registrationResult: null,
|
||||
shouldBackupState: false,
|
||||
submitState: DEFAULT_STATE,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
autoSubmitRegForm: false,
|
||||
countryCode: null,
|
||||
currentProvider: null,
|
||||
errorMessage: null,
|
||||
finishAuthUrl: null,
|
||||
pipelineUserDetails: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
},
|
||||
usernameSuggestions: [],
|
||||
userPipelineDataLoaded: false,
|
||||
validationApiRateLimited: false,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
backupFormState: backupRegistrationFormBegin,
|
||||
clearBackendError: clearRegistertionBackendError,
|
||||
getRegistrationDataFromBackend: getThirdPartyAuthContext,
|
||||
resetUsernameSuggestions: clearUsernameSuggestions,
|
||||
validateFromBackend: fetchRealtimeValidations,
|
||||
registerNewUser,
|
||||
setUserPipelineDetailsLoaded: setUserPipelineDataLoaded,
|
||||
},
|
||||
)(RegistrationPage);
|
||||
export default RegistrationPage;
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FIELDS } from './data/constants';
|
||||
import { validateCountryField } from './data/utils';
|
||||
import messages from './messages';
|
||||
import { HonorCode, TermsOfService } from './registrationFields';
|
||||
import CountryField from './registrationFields/CountryField';
|
||||
import { FormFieldRenderer } from '../field-renderer';
|
||||
import { FormFieldRenderer } from '../../field-renderer';
|
||||
import { FIELDS } from '../data/constants';
|
||||
import messages from '../messages';
|
||||
import { HonorCode, TermsOfService } from '../RegistrationFields';
|
||||
import CountryField from '../RegistrationFields/CountryField/CountryField';
|
||||
|
||||
/**
|
||||
* Fields on registration page that are not the default required fields (name, email, username, password).
|
||||
@@ -26,17 +25,18 @@ import { FormFieldRenderer } from '../field-renderer';
|
||||
const ConfigurableRegistrationForm = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
countryList,
|
||||
email,
|
||||
fieldDescriptions,
|
||||
fieldErrors,
|
||||
formFields,
|
||||
setFieldErrors,
|
||||
setFocusedField,
|
||||
setFormFields,
|
||||
registrationEmbedded,
|
||||
autoSubmitRegistrationForm,
|
||||
} = props;
|
||||
|
||||
const countryList = useMemo(() => getCountryList(getLocale()), []);
|
||||
|
||||
let showTermsOfServiceAndHonorCode = false;
|
||||
let showCountryField = false;
|
||||
|
||||
@@ -54,6 +54,35 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (autoSubmitRegistrationForm) {
|
||||
if (Object.keys(fieldDescriptions).includes(FIELDS.HONOR_CODE)) {
|
||||
setFormFields(prevState => ({
|
||||
...prevState,
|
||||
[FIELDS.HONOR_CODE]: true,
|
||||
}));
|
||||
}
|
||||
if (Object.keys(fieldDescriptions).includes(FIELDS.TERMS_OF_SERVICE)) {
|
||||
setFormFields(prevState => ({
|
||||
...prevState,
|
||||
[FIELDS.TERMS_OF_SERVICE]: true,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, [autoSubmitRegistrationForm]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleErrorChange = (fieldName, error) => {
|
||||
if (fieldName) {
|
||||
setFieldErrors(prevErrors => ({
|
||||
...prevErrors,
|
||||
[fieldName]: error,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handleOnChange = (event, countryValue = null) => {
|
||||
const { name } = event.target;
|
||||
let value;
|
||||
@@ -71,14 +100,7 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
const handleOnBlur = (event) => {
|
||||
const { name, value } = event.target;
|
||||
let error = '';
|
||||
if (name === 'country') {
|
||||
const countryValidation = validateCountryField(
|
||||
value.trim(), countryList, formatMessage(messages['empty.country.field.error']),
|
||||
);
|
||||
const { countryCode, displayValue } = countryValidation;
|
||||
error = countryValidation.error;
|
||||
setFormFields(prevState => ({ ...prevState, country: { countryCode, displayValue } }));
|
||||
} else if (!value || !value.trim()) {
|
||||
if ((!value || !value.trim()) && fieldDescriptions[name]?.error_message) {
|
||||
error = fieldDescriptions[name].error_message;
|
||||
} else if (name === 'confirm_email' && value !== email) {
|
||||
error = formatMessage(messages['email.do.not.match']);
|
||||
@@ -86,16 +108,12 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
if (registrationEmbedded) {
|
||||
return;
|
||||
}
|
||||
setFocusedField(null);
|
||||
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: error }));
|
||||
};
|
||||
|
||||
const handleOnFocus = (event) => {
|
||||
const { name } = event.target;
|
||||
setFieldErrors(prevErrors => ({ ...prevErrors, [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) {
|
||||
@@ -158,6 +176,7 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
selectedCountry={formFields.country}
|
||||
errorMessage={fieldErrors.country || ''}
|
||||
onChangeHandler={handleOnChange}
|
||||
handleErrorChange={handleErrorChange}
|
||||
onBlurHandler={handleOnBlur}
|
||||
onFocusHandler={handleOnFocus}
|
||||
/>
|
||||
@@ -203,7 +222,6 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
};
|
||||
|
||||
ConfigurableRegistrationForm.propTypes = {
|
||||
countryList: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
fieldDescriptions: PropTypes.shape({}),
|
||||
fieldErrors: PropTypes.shape({
|
||||
@@ -218,14 +236,15 @@ ConfigurableRegistrationForm.propTypes = {
|
||||
marketingEmailsOptIn: PropTypes.bool,
|
||||
}).isRequired,
|
||||
setFieldErrors: PropTypes.func.isRequired,
|
||||
setFocusedField: PropTypes.func.isRequired,
|
||||
setFormFields: PropTypes.func.isRequired,
|
||||
registrationEmbedded: PropTypes.bool,
|
||||
autoSubmitRegistrationForm: PropTypes.bool,
|
||||
};
|
||||
|
||||
ConfigurableRegistrationForm.defaultProps = {
|
||||
fieldDescriptions: {},
|
||||
registrationEmbedded: false,
|
||||
autoSubmitRegistrationForm: false,
|
||||
};
|
||||
|
||||
export default ConfigurableRegistrationForm;
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
INTERNAL_SERVER_ERROR,
|
||||
TPA_AUTHENTICATION_FAILURE,
|
||||
TPA_SESSION_EXPIRED,
|
||||
} from './data/constants';
|
||||
import messages from './messages';
|
||||
import { windowScrollTo } from '../data/utils';
|
||||
} from '../data/constants';
|
||||
import messages from '../messages';
|
||||
import { windowScrollTo } from '../../data/utils';
|
||||
|
||||
const RegistrationFailureMessage = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
@@ -5,14 +5,14 @@ import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import messages from './messages';
|
||||
import messages from '../messages';
|
||||
import {
|
||||
RenderInstitutionButton,
|
||||
SocialAuthProviders,
|
||||
} from '../common-components';
|
||||
} from '../../common-components';
|
||||
import {
|
||||
PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
} from '../../data/constants';
|
||||
|
||||
/**
|
||||
* This component renders the Single sign-on (SSO) buttons for the providers passed.
|
||||
@@ -19,10 +19,13 @@ export const backupRegistrationFormBegin = (data) => ({
|
||||
});
|
||||
|
||||
// Validate fields from the backend
|
||||
export const fetchRealtimeValidations = (formPayload) => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.BASE,
|
||||
payload: { formPayload },
|
||||
});
|
||||
export const fetchRealtimeValidations = (formPayload) => {
|
||||
console.log('test fetchRealtimeValidations');
|
||||
return {
|
||||
type: REGISTER_FORM_VALIDATIONS.BASE,
|
||||
payload: { formPayload },
|
||||
};
|
||||
};
|
||||
|
||||
export const fetchRealtimeValidationsBegin = () => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.BEGIN,
|
||||
|
||||
@@ -39,137 +39,4 @@ export const EDUCATION_LEVELS = [
|
||||
|
||||
export const GENDER_OPTIONS = ['', 'f', 'm', 'o'];
|
||||
|
||||
export const COMMON_EMAIL_PROVIDERS = [
|
||||
'hotmail.com', 'yahoo.com', 'outlook.com', 'live.com', 'gmail.com',
|
||||
];
|
||||
|
||||
export const DEFAULT_SERVICE_PROVIDER_DOMAINS = ['yahoo', 'hotmail', 'live', 'outlook', 'gmail'];
|
||||
|
||||
export const DEFAULT_TOP_LEVEL_DOMAINS = [
|
||||
'aaa', 'aarp', 'abarth', 'abb', 'abbott', 'abbvie', 'abc', 'able', 'abogado', 'abudhabi', 'ac', 'academy',
|
||||
'accenture', 'accountant', 'accountants', 'aco', 'active', 'actor', 'ad', 'adac', 'ads', 'adult', 'ae', 'aeg', 'aero',
|
||||
'aetna', 'af', 'afamilycompany', 'afl', 'africa', 'ag', 'agakhan', 'agency', 'ai', 'aig', 'aigo', 'airbus', 'airforce',
|
||||
'airtel', 'akdn', 'al', 'alfaromeo', 'alibaba', 'alipay', 'allfinanz', 'allstate', 'ally', 'alsace', 'alstom', 'am',
|
||||
'amazon', 'americanexpress', 'americanfamily', 'amex', 'amfam', 'amica', 'amsterdam', 'an', 'analytics', 'android',
|
||||
'anquan', 'anz', 'ao', 'aol', 'apartments', 'app', 'apple', 'aq', 'aquarelle', 'ar', 'arab', 'aramco', 'archi', 'army',
|
||||
'arpa', 'art', 'arte', 'as', 'asda', 'asia', 'associates', 'at', 'athleta', 'attorney', 'au', 'auction', 'audi',
|
||||
'audible', 'audio', 'auspost', 'author', 'auto', 'autos', 'avianca', 'aw', 'aws', 'ax', 'axa', 'az', 'azure', 'ba',
|
||||
'baby', 'baidu', 'banamex', 'bananarepublic', 'band', 'bank', 'bar', 'barcelona', 'barclaycard', 'barclays',
|
||||
'barefoot', 'bargains', 'baseball', 'basketball', 'bauhaus', 'bayern', 'bb', 'bbc', 'bbt', 'bbva', 'bcg', 'bcn', 'bd',
|
||||
'be', 'beats', 'beauty', 'beer', 'bentley', 'berlin', 'best', 'bestbuy', 'bet', 'bf', 'bg', 'bh', 'bharti', 'bi',
|
||||
'bible', 'bid', 'bike', 'bing', 'bingo', 'bio', 'biz', 'bj', 'bl', 'black', 'blackfriday', 'blanco', 'blockbuster',
|
||||
'blog', 'bloomberg', 'blue', 'bm', 'bms', 'bmw', 'bn', 'bnl', 'bnpparibas', 'bo', 'boats', 'boehringer', 'bofa', 'bom',
|
||||
'bond', 'boo', 'book', 'booking', 'boots', 'bosch', 'bostik', 'boston', 'bot', 'boutique', 'box', 'bq', 'br',
|
||||
'bradesco', 'bridgestone', 'broadway', 'broker', 'brother', 'brussels', 'bs', 'bt', 'budapest', 'bugatti', 'build',
|
||||
'builders', 'business', 'buy', 'buzz', 'bv', 'bw', 'by', 'bz', 'bzh', 'ca', 'cab', 'cafe', 'cal', 'call',
|
||||
'calvinklein', 'cam', 'camera', 'camp', 'cancerresearch', 'canon', 'capetown', 'capital', 'capitalone', 'car',
|
||||
'caravan', 'cards', 'care', 'career', 'careers', 'cars', 'cartier', 'casa', 'case', 'caseih', 'cash', 'casino', 'cat',
|
||||
'catering', 'catholic', 'cba', 'cbn', 'cbre', 'cbs', 'cc', 'cd', 'ceb', 'center', 'ceo', 'cern', 'cf', 'cfa', 'cfd',
|
||||
'cg', 'ch', 'chanel', 'channel', 'charity', 'chase', 'chat', 'cheap', 'chintai', 'chloe', 'christmas', 'chrome',
|
||||
'chrysler', 'church', 'ci', 'cipriani', 'circle', 'cisco', 'citadel', 'citi', 'citic', 'city', 'cityeats', 'ck', 'cl',
|
||||
'claims', 'cleaning', 'click', 'clinic', 'clinique', 'clothing', 'cloud', 'club', 'clubmed', 'cm', 'cn', 'co', 'coach',
|
||||
'codes', 'coffee', 'college', 'cologne', 'com', 'comcast', 'commbank', 'community', 'company', 'compare', 'computer',
|
||||
'comsec', 'condos', 'construction', 'consulting', 'contact', 'contractors', 'cooking', 'cookingchannel', 'cool', 'coop',
|
||||
'corsica', 'country', 'coupon', 'coupons', 'courses', 'cpa', 'cr', 'credit', 'creditcard', 'creditunion', 'cricket',
|
||||
'crown', 'crs', 'cruise', 'cruises', 'csc', 'cu', 'cuisinella', 'cv', 'cw', 'cx', 'cy', 'cymru', 'cyou', 'cz', 'dabur',
|
||||
'dad', 'dance', 'data', 'date', 'dating', 'datsun', 'day', 'dclk', 'dds', 'de', 'deal', 'dealer', 'deals', 'degree',
|
||||
'delivery', 'dell', 'deloitte', 'delta', 'democrat', 'dental', 'dentist', 'desi', 'design', 'dev', 'dhl', 'diamonds',
|
||||
'diet', 'digital', 'direct', 'directory', 'discount', 'discover', 'dish', 'diy', 'dj', 'dk', 'dm', 'dnp', 'do', 'docs',
|
||||
'doctor', 'dodge', 'dog', 'doha', 'domains', 'doosan', 'dot', 'download', 'drive', 'dtv', 'dubai', 'duck', 'dunlop',
|
||||
'duns', 'dupont', 'durban', 'dvag', 'dvr', 'dz', 'earth', 'eat', 'ec', 'eco', 'edeka', 'edu', 'education', 'ee', 'eg',
|
||||
'eh', 'email', 'emerck', 'energy', 'engineer', 'engineering', 'enterprises', 'epost', 'epson', 'equipment', 'er',
|
||||
'ericsson', 'erni', 'es', 'esq', 'estate', 'esurance', 'et', 'etisalat', 'eu', 'eurovision', 'eus', 'events', 'everbank',
|
||||
'exchange', 'expert', 'exposed', 'express', 'extraspace', 'fage', 'fail', 'fairwinds', 'faith', 'family', 'fan', 'fans',
|
||||
'farm', 'farmers', 'fashion', 'fast', 'fedex', 'feedback', 'ferrari', 'ferrero', 'fi', 'fiat', 'fidelity', 'fido', 'film',
|
||||
'final', 'finance', 'financial', 'fire', 'firestone', 'firmdale', 'fish', 'fishing', 'fit', 'fitness', 'fj', 'fk',
|
||||
'flickr', 'flights', 'flir', 'florist', 'flowers', 'flsmidth', 'fly', 'fm', 'fo', 'foo', 'food', 'foodnetwork', 'football',
|
||||
'ford', 'forex', 'forsale', 'forum', 'foundation', 'fox', 'fr', 'free', 'fresenius', 'frl', 'frogans', 'frontdoor',
|
||||
'frontier', 'ftr', 'fujitsu', 'fujixerox', 'fun', 'fund', 'furniture', 'futbol', 'fyi', 'ga', 'gal', 'gallery', 'gallo',
|
||||
'gallup', 'game', 'games', 'gap', 'garden', 'gay', 'gb', 'gbiz', 'gd', 'gdn', 'ge', 'gea', 'gent', 'genting', 'george',
|
||||
'gf', 'gg', 'ggee', 'gh', 'gi', 'gift', 'gifts', 'gives', 'giving', 'gl', 'glade', 'glass', 'gle', 'global', 'globo',
|
||||
'gm', 'gmail', 'gmbh', 'gmo', 'gmx', 'gn', 'godaddy', 'gold', 'goldpoint', 'golf', 'goo', 'goodhands', 'goodyear', 'goog',
|
||||
'google', 'gop', 'got', 'gov', 'gp', 'gq', 'gr', 'grainger', 'graphics', 'gratis', 'green', 'gripe', 'grocery', 'group',
|
||||
'gs', 'gt', 'gu', 'guardian', 'gucci', 'guge', 'guide', 'guitars', 'guru', 'gw', 'gy', 'hair', 'hamburg', 'hangout',
|
||||
'haus', 'hbo', 'hdfc', 'hdfcbank', 'health', 'healthcare', 'help', 'helsinki', 'here', 'hermes', 'hgtv', 'hiphop',
|
||||
'hisamitsu', 'hitachi', 'hiv', 'hk', 'hkt', 'hm', 'hn', 'hockey', 'holdings', 'holiday', 'homedepot', 'homegoods',
|
||||
'homes', 'homesense', 'honda', 'honeywell', 'horse', 'hospital', 'host', 'hosting', 'hot', 'hoteles', 'hotels', 'hotmail',
|
||||
'house', 'how', 'hr', 'hsbc', 'ht', 'htc', 'hu', 'hughes', 'hyatt', 'hyundai', 'ibm', 'icbc', 'ice', 'icu', 'id', 'ie',
|
||||
'ieee', 'ifm', 'iinet', 'ikano', 'il', 'im', 'imamat', 'imdb', 'immo', 'immobilien', 'in', 'inc', 'industries', 'infiniti',
|
||||
'info', 'ing', 'ink', 'institute', 'insurance', 'insure', 'int', 'intel', 'international', 'intuit', 'investments',
|
||||
'io', 'ipiranga', 'iq', 'ir', 'irish', 'is', 'iselect', 'ismaili', 'ist', 'istanbul', 'it', 'itau', 'itv', 'iveco', 'iwc',
|
||||
'jaguar', 'java', 'jcb', 'jcp', 'je', 'jeep', 'jetzt', 'jewelry', 'jio', 'jlc', 'jll', 'jm', 'jmp', 'jnj', 'jo',
|
||||
'jobs', 'joburg', 'jot', 'joy', 'jp', 'jpmorgan', 'jprs', 'juegos', 'juniper', 'kaufen', 'kddi', 'ke', 'kerryhotels',
|
||||
'kerrylogistics', 'kerryproperties', 'kfh', 'kg', 'kh', 'ki', 'kia', 'kim', 'kinder', 'kindle', 'kitchen', 'kiwi', 'km',
|
||||
'kn', 'koeln', 'komatsu', 'kosher', 'kp', 'kpmg', 'kpn', 'kr', 'krd', 'kred', 'kuokgroup', 'kw', 'ky', 'kyoto', 'kz',
|
||||
'la', 'lacaixa', 'ladbrokes', 'lamborghini', 'lamer', 'lancaster', 'lancia', 'lancome', 'land', 'landrover', 'lanxess',
|
||||
'lasalle', 'lat', 'latino', 'latrobe', 'law', 'lawyer', 'lb', 'lc', 'lds', 'lease', 'leclerc', 'lefrak', 'legal',
|
||||
'lego', 'lexus', 'lgbt', 'li', 'liaison', 'lidl', 'life', 'lifeinsurance', 'lifestyle', 'lighting', 'like', 'lilly',
|
||||
'limited', 'limo', 'lincoln', 'linde', 'link', 'lipsy', 'live', 'living', 'lixil', 'lk', 'llc', 'llp', 'loan', 'loans',
|
||||
'locker', 'locus', 'loft', 'lol', 'london', 'lotte', 'lotto', 'love', 'lpl', 'lplfinancial', 'lr', 'ls', 'lt', 'ltd',
|
||||
'ltda', 'lu', 'lundbeck', 'lupin', 'luxe', 'luxury', 'lv', 'ly', 'ma', 'macys', 'madrid', 'maif', 'maison', 'makeup',
|
||||
'man', 'management', 'mango', 'map', 'market', 'marketing', 'markets', 'marriott', 'marshalls', 'maserati', 'mattel',
|
||||
'mba', 'mc', 'mcd', 'mcdonalds', 'mckinsey', 'md', 'me', 'med', 'media', 'meet', 'melbourne', 'meme', 'memorial', 'men',
|
||||
'menu', 'meo', 'merckmsd', 'metlife', 'mf', 'mg', 'mh', 'miami', 'microsoft', 'mil', 'mini', 'mint', 'mit', 'mitsubishi',
|
||||
'mk', 'ml', 'mlb', 'mls', 'mm', 'mma', 'mn', 'mo', 'mobi', 'mobile', 'mobily', 'moda', 'moe', 'moi', 'mom', 'monash',
|
||||
'money', 'monster', 'montblanc', 'mopar', 'mormon', 'mortgage', 'moscow', 'moto', 'motorcycles', 'mov', 'movie', 'movistar',
|
||||
'mp', 'mq', 'mr', 'ms', 'msd', 'mt', 'mtn', 'mtpc', 'mtr', 'mu', 'museum', 'mutual', 'mutuelle', 'mv', 'mw', 'mx', 'my',
|
||||
'mz', 'na', 'nab', 'nadex', 'nagoya', 'name', 'nationwide', 'natura', 'navy', 'nba', 'nc', 'ne', 'nec', 'net', 'netbank',
|
||||
'netflix', 'network', 'neustar', 'new', 'newholland', 'news', 'next', 'nextdirect', 'nexus', 'nf', 'nfl', 'ng', 'ngo', 'nhk',
|
||||
'ni', 'nico', 'nike', 'nikon', 'ninja', 'nissan', 'nissay', 'nl', 'no', 'nokia', 'northwesternmutual', 'norton', 'now',
|
||||
'nowruz', 'nowtv', 'np', 'nr', 'nra', 'nrw', 'ntt', 'nu', 'nyc', 'nz', 'obi', 'observer', 'off', 'office', 'okinawa',
|
||||
'olayan', 'olayangroup', 'oldnavy', 'ollo', 'om', 'omega', 'one', 'ong', 'onl', 'online', 'onyourside', 'ooo', 'open',
|
||||
'oracle', 'orange', 'org', 'organic', 'orientexpress', 'origins', 'osaka', 'otsuka', 'ott', 'ovh', 'pa', 'page',
|
||||
'pamperedchef', 'panasonic', 'panerai', 'paris', 'pars', 'partners', 'parts', 'party', 'passagens', 'pay', 'pccw', 'pe',
|
||||
'pet', 'pf', 'pfizer', 'pg', 'ph', 'pharmacy', 'phd', 'philips', 'phone', 'photo', 'photography', 'photos', 'physio',
|
||||
'piaget', 'pics', 'pictet', 'pictures', 'pid', 'pin', 'ping', 'pink', 'pioneer', 'pizza', 'pk', 'pl', 'place', 'play',
|
||||
'playstation', 'plumbing', 'plus', 'pm', 'pn', 'pnc', 'pohl', 'poker', 'politie', 'porn', 'post', 'pr', 'pramerica',
|
||||
'praxi', 'press', 'prime', 'pro', 'prod', 'productions', 'prof', 'progressive', 'promo', 'properties', 'property',
|
||||
'protection', 'pru', 'prudential', 'ps', 'pt', 'pub', 'pw', 'pwc', 'py', 'qa', 'qpon', 'quebec', 'quest', 'qvc',
|
||||
'racing', 'radio', 'raid', 're', 'read', 'realestate', 'realtor', 'realty', 'recipes', 'red', 'redstone', 'redumbrella',
|
||||
'rehab', 'reise', 'reisen', 'reit', 'reliance', 'ren', 'rent', 'rentals', 'repair', 'report', 'republican', 'rest',
|
||||
'restaurant', 'review', 'reviews', 'rexroth', 'rich', 'richardli', 'ricoh', 'rightathome', 'ril', 'rio', 'rip', 'rmit',
|
||||
'ro', 'rocher', 'rocks', 'rodeo', 'rogers', 'room', 'rs', 'rsvp', 'ru', 'rugby', 'ruhr', 'run', 'rw', 'rwe', 'ryukyu',
|
||||
'sa', 'saarland', 'safe', 'safety', 'sakura', 'sale', 'salon', 'samsclub', 'samsung', 'sandvik', 'sandvikcoromant',
|
||||
'sanofi', 'sap', 'sapo', 'sarl', 'sas', 'save', 'saxo', 'sb', 'sbi', 'sbs', 'sc', 'sca', 'scb', 'schaeffler', 'schmidt',
|
||||
'scholarships', 'school', 'schule', 'schwarz', 'science', 'scjohnson', 'scor', 'scot', 'sd', 'se', 'search', 'seat',
|
||||
'secure', 'security', 'seek', 'select', 'sener', 'services', 'ses', 'seven', 'sew', 'sex', 'sexy', 'sfr', 'sg', 'sh',
|
||||
'shangrila', 'sharp', 'shaw', 'shell', 'shia', 'shiksha', 'shoes', 'shop', 'shopping', 'shouji', 'show', 'showtime',
|
||||
'shriram', 'si', 'silk', 'sina', 'singles', 'site', 'sj', 'sk', 'ski', 'skin', 'sky', 'skype', 'sl', 'sling', 'sm',
|
||||
'smart', 'smile', 'sn', 'sncf', 'so', 'soccer', 'social', 'softbank', 'software', 'sohu', 'solar', 'solutions', 'song',
|
||||
'sony', 'soy', 'spa', 'space', 'spiegel', 'sport', 'spot', 'spreadbetting', 'sr', 'srl', 'srt', 'ss', 'st', 'stada',
|
||||
'staples', 'star', 'starhub', 'statebank', 'statefarm', 'statoil', 'stc', 'stcgroup', 'stockholm', 'storage', 'store',
|
||||
'stream', 'studio', 'study', 'style', 'su', 'sucks', 'supplies', 'supply', 'support', 'surf', 'surgery', 'suzuki', 'sv',
|
||||
'swatch', 'swiftcover', 'swiss', 'sx', 'sy', 'sydney', 'symantec', 'systems', 'sz', 'tab', 'taipei', 'talk', 'taobao',
|
||||
'target', 'tatamotors', 'tatar', 'tattoo', 'tax', 'taxi', 'tc', 'tci', 'td', 'tdk', 'team', 'tech', 'technology', 'tel',
|
||||
'telecity', 'telefonica', 'temasek', 'tennis', 'teva', 'tf', 'tg', 'th', 'thd', 'theater', 'theatre', 'tiaa', 'tickets',
|
||||
'tienda', 'tiffany', 'tips', 'tires', 'tirol', 'tj', 'tjmaxx', 'tjx', 'tk', 'tkmaxx', 'tl', 'tm', 'tmall', 'tn', 'to',
|
||||
'today', 'tokyo', 'tools', 'top', 'toray', 'toshiba', 'total', 'tours', 'town', 'toyota', 'toys', 'tp', 'tr', 'trade',
|
||||
'trading', 'training', 'travel', 'travelchannel', 'travelers', 'travelersinsurance', 'trust', 'trv', 'tt', 'tube', 'tui',
|
||||
'tunes', 'tushu', 'tv', 'tvs', 'tw', 'tz', 'ua', 'ubank', 'ubs', 'uconnect', 'ug', 'uk', 'um', 'unicom', 'university',
|
||||
'uno', 'uol', 'ups', 'us', 'uy', 'uz', 'va', 'vacations', 'vana', 'vanguard', 'vc', 've', 'vegas', 'ventures', 'verisign',
|
||||
'versicherung', 'vet', 'vg', 'vi', 'viajes', 'video', 'vig', 'viking', 'villas', 'vin', 'vip', 'virgin', 'visa', 'vision',
|
||||
'vista', 'vistaprint', 'viva', 'vivo', 'vlaanderen', 'vn', 'vodka', 'volkswagen', 'volvo', 'vote', 'voting', 'voto',
|
||||
'voyage', 'vu', 'vuelos', 'wales', 'walmart', 'walter', 'wang', 'wanggou', 'warman', 'watch', 'watches', 'weather',
|
||||
'weatherchannel', 'webcam', 'weber', 'website', 'wed', 'wedding', 'weibo', 'weir', 'wf', 'whoswho', 'wien', 'wiki',
|
||||
'williamhill', 'win', 'windows', 'wine', 'winners', 'wme', 'wolterskluwer', 'woodside', 'work', 'works', 'world', 'wow',
|
||||
'ws', 'wtc', 'wtf', 'xbox', 'xerox', 'xfinity', 'xihuan', 'xin', '测试', 'कॉम', 'परीक्षा', 'セール', '佛山', 'ಭಾರತ', '慈善',
|
||||
'集团', '在线', '한국', 'ଭାରତ', '大众汽车', '点看', 'คอม', 'ভাৰত', 'ভারত', '八卦', 'ישראל\u200e', 'موقع\u200e', 'বাংলা', '公益',
|
||||
'公司', '香格里拉', '网站', '移动', '我爱你', 'москва', 'испытание', 'қаз', 'католик', 'онлайн', 'сайт', '联通', 'срб', 'бг',
|
||||
'бел', 'קום\u200e', '时尚', '微博', '테스트', '淡马锡', 'ファッション', 'орг', 'नेट', 'ストア', 'アマゾン', '삼성', 'சிங்கப்பூர்', '商标',
|
||||
'商店', '商城', 'дети', 'мкд', 'טעסט\u200e', 'ею', 'ポイント', '新闻', '工行', '家電', 'كوم\u200e', '中文网', '中信', '中国',
|
||||
'中國', '娱乐', '谷歌', 'భారత్', 'ලංකා', '電訊盈科', '购物', '測試', 'クラウド', 'ભારત', '通販', 'भारतम्', 'भारत', 'भारोत', 'آزمایشی\u200e',
|
||||
'பரிட்சை', '网店', 'संगठन', '餐厅', '网络', 'ком', 'укр', '香港', '亚马逊', '诺基亚', '食品', 'δοκιμή', '飞利浦', 'إختبار\u200e',
|
||||
'台湾', '台灣', '手表', '手机', 'мон', 'الجزائر\u200e', 'عمان\u200e', 'ارامكو\u200e', 'ایران\u200e', 'العليان\u200e',
|
||||
'اتصالات\u200e', 'امارات\u200e', 'بازار\u200e', 'موريتانيا\u200e', 'پاکستان\u200e', 'الاردن\u200e', 'موبايلي\u200e',
|
||||
'بارت\u200e', 'بھارت\u200e', 'المغرب\u200e', 'ابوظبي\u200e', 'البحرين\u200e', 'السعودية\u200e', 'ڀارت\u200e',
|
||||
'كاثوليك\u200e', 'سودان\u200e', 'همراه\u200e', 'عراق\u200e', 'مليسيا\u200e', '澳門', '닷컴', '政府', 'شبكة\u200e',
|
||||
'بيتك\u200e', 'عرب\u200e', 'გე', '机构', '组织机构', '健康', 'ไทย', 'سورية\u200e', '招聘', 'рус', 'рф', '珠宝',
|
||||
'تونس\u200e', '大拿', 'ລາວ', 'みんな', 'グーグル', 'ευ', 'ελ', '世界', '書籍', 'ഭാരതം', 'ਭਾਰਤ', '网址', '닷넷', 'コム',
|
||||
'天主教', '游戏', 'vermögensberater', 'vermögensberatung', '企业', '信息', '嘉里大酒店', '嘉里', 'مصر\u200e',
|
||||
'قطر\u200e', '广东', 'இலங்கை', 'இந்தியா', 'հայ', '新加坡', 'فلسطين\u200e', 'テスト', '政务', 'xperia', 'xxx',
|
||||
'xyz', 'yachts', 'yahoo', 'yamaxun', 'yandex', 'ye', 'yodobashi', 'yoga', 'yokohama', 'you', 'youtube', 'yt',
|
||||
'yun', 'za', 'zappos', 'zara', 'zero', 'zip', 'zippo', 'zm', 'zone', 'zuerich', 'zw',
|
||||
];
|
||||
|
||||
export const COUNTRY_CODE_KEY = 'code';
|
||||
export const COUNTRY_DISPLAY_KEY = 'name';
|
||||
export const PASSWORD_FIELD_LABEL = 'password';
|
||||
|
||||
@@ -38,6 +38,7 @@ export function* handleNewUserRegistration(action) {
|
||||
}
|
||||
|
||||
export function* fetchRealtimeValidations(action) {
|
||||
console.log('test saga fetchRealtimeValidations');
|
||||
try {
|
||||
yield put(fetchRealtimeValidationsBegin());
|
||||
const { fieldValidations } = yield call(getFieldsValidations, action.payload.formPayload);
|
||||
|
||||
@@ -31,3 +31,21 @@ export const validationsSelector = createSelector(
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
export const getBackendValidations = (registrationError, validations) => {
|
||||
if (validations) {
|
||||
return validations.validationDecisions;
|
||||
}
|
||||
|
||||
if (Object.keys(registrationError).length > 0) {
|
||||
const fields = Object.keys(registrationError).filter((fieldName) => !(fieldName in ['errorCode', 'usernameSuggestions']));
|
||||
|
||||
const validationDecisions = {};
|
||||
fields.forEach(field => {
|
||||
validationDecisions[field] = registrationError[field][0].userMessage || '';
|
||||
});
|
||||
return validationDecisions;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,112 +1,79 @@
|
||||
import { distance } from 'fastest-levenshtein';
|
||||
import { snakeCaseObject } from '@edx/frontend-platform';
|
||||
|
||||
import {
|
||||
COMMON_EMAIL_PROVIDERS,
|
||||
COUNTRY_CODE_KEY,
|
||||
COUNTRY_DISPLAY_KEY,
|
||||
DEFAULT_SERVICE_PROVIDER_DOMAINS,
|
||||
DEFAULT_TOP_LEVEL_DOMAINS,
|
||||
} from './constants';
|
||||
import { LETTER_REGEX, NUMBER_REGEX } from '../../data/constants';
|
||||
import messages from '../messages';
|
||||
import { COUNTRY_FIELD_LABEL } from '../RegistrationFields/CountryField/constants';
|
||||
|
||||
function getLevenshteinSuggestion(word, knownWords, similarityThreshold = 4) {
|
||||
if (!word) {
|
||||
return null;
|
||||
export const validatePasswordField = (value, formatMessage) => {
|
||||
let fieldError = '';
|
||||
if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) {
|
||||
fieldError = formatMessage(messages['password.validation.message']);
|
||||
}
|
||||
return fieldError;
|
||||
};
|
||||
|
||||
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];
|
||||
export const isFormValid = (
|
||||
payload,
|
||||
errors,
|
||||
configurableFormFields,
|
||||
fieldDescriptions,
|
||||
formatMessage,
|
||||
) => {
|
||||
const fieldErrors = { ...errors };
|
||||
let isValid = true;
|
||||
Object.keys(payload).forEach(key => {
|
||||
if (!payload[key]) {
|
||||
fieldErrors[key] = formatMessage(messages[`empty.${key}.field.error`]);
|
||||
}
|
||||
}
|
||||
|
||||
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`;
|
||||
if (fieldErrors[key]) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (!configurableFormFields?.country?.displayValue) {
|
||||
fieldErrors.country = formatMessage(messages['empty.country.field.error']);
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
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';
|
||||
Object.keys(fieldDescriptions).forEach(key => {
|
||||
if (key === COUNTRY_FIELD_LABEL && !configurableFormFields.country.displayValue) {
|
||||
fieldErrors[key] = formatMessage(messages['empty.country.field.error']);
|
||||
} else if (!configurableFormFields[key]) {
|
||||
fieldErrors[key] = fieldDescriptions[key].error_message;
|
||||
}
|
||||
}
|
||||
|
||||
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) => (
|
||||
// When translations are applied, extra space added in country value, so we should trim that.
|
||||
country[COUNTRY_DISPLAY_KEY].toLowerCase().trim() === normalizedValue
|
||||
|| country[COUNTRY_CODE_KEY].toLowerCase().trim() === normalizedValue
|
||||
),
|
||||
);
|
||||
if (selectedCountry) {
|
||||
countryCode = selectedCountry[COUNTRY_CODE_KEY];
|
||||
displayValue = selectedCountry[COUNTRY_DISPLAY_KEY];
|
||||
error = '';
|
||||
if (fieldErrors[key]) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
return { isValid, fieldErrors };
|
||||
};
|
||||
|
||||
export const prepareRegistrationPayload = (
|
||||
initPayload,
|
||||
configurableFormFields,
|
||||
showMarketingEmailOptInCheckbox,
|
||||
totalRegistrationTime,
|
||||
queryParams,
|
||||
) => {
|
||||
let payload = { ...initPayload };
|
||||
Object.keys(configurableFormFields).forEach((fieldName) => {
|
||||
if (fieldName === 'country') {
|
||||
payload[fieldName] = configurableFormFields[fieldName].countryCode;
|
||||
} else {
|
||||
payload[fieldName] = configurableFormFields[fieldName];
|
||||
}
|
||||
});
|
||||
|
||||
// Don't send the marketing email opt-in value if the flag is turned off
|
||||
if (!showMarketingEmailOptInCheckbox) {
|
||||
delete payload.marketingEmailsOptIn;
|
||||
}
|
||||
return { error, countryCode, displayValue };
|
||||
}
|
||||
|
||||
payload = snakeCaseObject(payload);
|
||||
payload.totalRegistrationTime = totalRegistrationTime;
|
||||
|
||||
// add query params to the payload
|
||||
payload = { ...payload, ...queryParams };
|
||||
return payload;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export { default as RegistrationPage } from './RegistrationPage';
|
||||
export { default as EmbeddableRegistrationPage } from './EmbeddableRegistrationPage/EmbeddableRegistrationPage';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { storeName } from './data/selectors';
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormAutosuggest, FormAutosuggestOption, FormControlFeedback } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from '../data/constants';
|
||||
import messages from '../messages';
|
||||
|
||||
const CountryField = (props) => {
|
||||
const { countryList, selectedCountry } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const handleSelected = (value) => {
|
||||
if (props.onBlurHandler) { props.onBlurHandler({ target: { name: 'country', value } }); }
|
||||
};
|
||||
|
||||
const onBlurHandler = (event) => {
|
||||
// Do not run validations when drop-down arrow is clicked
|
||||
if (event.relatedTarget && event.relatedTarget.className.includes('pgn__form-autosuggest__icon-button')) {
|
||||
return;
|
||||
}
|
||||
if (props.onBlurHandler) { props.onBlurHandler(event); }
|
||||
};
|
||||
|
||||
const onFocusHandler = (event) => {
|
||||
if (props.onFocusHandler) { props.onFocusHandler(event); }
|
||||
};
|
||||
|
||||
const onChangeHandler = (value) => {
|
||||
if (props.onChangeHandler) {
|
||||
props.onChangeHandler({ target: { name: 'country' } }, { countryCode: '', displayValue: value });
|
||||
}
|
||||
};
|
||||
|
||||
const getCountryList = () => countryList.map((country) => (
|
||||
<FormAutosuggestOption key={country[COUNTRY_CODE_KEY]}>
|
||||
{country[COUNTRY_DISPLAY_KEY]}
|
||||
</FormAutosuggestOption>
|
||||
));
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<FormAutosuggest
|
||||
floatingLabel={formatMessage(messages['registration.country.label'])}
|
||||
aria-label="form autosuggest"
|
||||
name="country"
|
||||
value={selectedCountry.displayValue || ''}
|
||||
onSelected={(value) => handleSelected(value)}
|
||||
onFocus={(e) => onFocusHandler(e)}
|
||||
onBlur={(e) => onBlurHandler(e)}
|
||||
onChange={(value) => onChangeHandler(value)}
|
||||
>
|
||||
{getCountryList()}
|
||||
</FormAutosuggest>
|
||||
{props.errorMessage !== '' && (
|
||||
<FormControlFeedback
|
||||
key="error"
|
||||
className="form-text-size"
|
||||
hasIcon={false}
|
||||
feedback-for="country"
|
||||
type="invalid"
|
||||
>
|
||||
{props.errorMessage}
|
||||
</FormControlFeedback>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
CountryField.propTypes = {
|
||||
countryList: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
code: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
}),
|
||||
).isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
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 CountryField;
|
||||
@@ -1,81 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } 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 { formatMessage } = useIntl();
|
||||
const {
|
||||
emailSuggestion,
|
||||
handleSuggestionClick,
|
||||
handleOnClose,
|
||||
} = props;
|
||||
|
||||
const renderEmailFeedback = () => {
|
||||
if (emailSuggestion.type === 'error') {
|
||||
return (
|
||||
<Alert variant="danger" className="email-suggestion-alert-error mt-1" icon={Error}>
|
||||
<span className="email-suggestion__text">
|
||||
{formatMessage(messages['did.you.mean.alert.text'])}{' '}
|
||||
<Alert.Link
|
||||
href="#"
|
||||
name="email"
|
||||
onClick={handleSuggestionClick}
|
||||
>
|
||||
{emailSuggestion.suggestion}
|
||||
</Alert.Link>?<Icon src={Close} className="email-suggestion__close" onClick={handleOnClose} tabIndex="0" />
|
||||
</span>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span id="email-warning" className="small">
|
||||
{formatMessage(messages['did.you.mean.alert.text'])}:{' '}
|
||||
<Alert.Link
|
||||
href="#"
|
||||
name="email"
|
||||
className="email-suggestion-alert-warning"
|
||||
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,
|
||||
}),
|
||||
value: PropTypes.string.isRequired,
|
||||
handleOnClose: PropTypes.func.isRequired,
|
||||
handleSuggestionClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default EmailField;
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
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';
|
||||
|
||||
const UsernameField = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
handleSuggestionClick, handleUsernameSuggestionClose, usernameSuggestions, errorMessage,
|
||||
} = props;
|
||||
let className = '';
|
||||
let suggestedUsernameDiv = null;
|
||||
let iconButton = null;
|
||||
const suggestedUsernames = () => (
|
||||
<div className={className}>
|
||||
<span className="text-gray username-suggestion--label">{formatMessage(messages['registration.username.suggestion.label'])}</span>
|
||||
<div className="username-scroll-suggested--form-field">
|
||||
{usernameSuggestions.map((username, index) => (
|
||||
<Button
|
||||
type="button"
|
||||
name="username"
|
||||
variant="outline-dark"
|
||||
className="username-suggestions--chip data-hj-suppress"
|
||||
autoComplete={props.autoComplete}
|
||||
key={`suggestion-${index.toString()}`}
|
||||
onClick={(e) => handleSuggestionClick(e, 'username', username)}
|
||||
>
|
||||
{username}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{iconButton}
|
||||
</div>
|
||||
);
|
||||
if (usernameSuggestions.length > 0 && errorMessage && props.value === ' ') {
|
||||
className = 'username-suggestions__error';
|
||||
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
|
||||
suggestedUsernameDiv = suggestedUsernames();
|
||||
} else if (usernameSuggestions.length > 0 && props.value === ' ') {
|
||||
className = 'username-suggestions d-flex align-items-center';
|
||||
iconButton = <IconButton src={Close} iconAs={Icon} alt="Close" onClick={() => handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />;
|
||||
suggestedUsernameDiv = suggestedUsernames();
|
||||
} else if (usernameSuggestions.length > 0 && errorMessage) {
|
||||
suggestedUsernameDiv = suggestedUsernames();
|
||||
}
|
||||
return (
|
||||
<FormGroup {...props}>
|
||||
{suggestedUsernameDiv}
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
UsernameField.defaultProps = {
|
||||
usernameSuggestions: [],
|
||||
errorMessage: '',
|
||||
autoComplete: null,
|
||||
};
|
||||
|
||||
UsernameField.propTypes = {
|
||||
usernameSuggestions: PropTypes.arrayOf(string),
|
||||
handleSuggestionClick: PropTypes.func.isRequired,
|
||||
handleUsernameSuggestionClose: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
autoComplete: PropTypes.string,
|
||||
};
|
||||
|
||||
export default UsernameField;
|
||||
@@ -1,5 +0,0 @@
|
||||
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';
|
||||
707
src/register/tests/EmbeddableRegistrationPage.test.jsx
Normal file
707
src/register/tests/EmbeddableRegistrationPage.test.jsx
Normal file
@@ -0,0 +1,707 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
configure, getLocale, injectIntl, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import {
|
||||
PENDING_STATE,
|
||||
} from '../../data/constants';
|
||||
import {
|
||||
clearUsernameSuggestions,
|
||||
registerNewUser,
|
||||
} from '../data/actions';
|
||||
import {
|
||||
FIELDS,
|
||||
} from '../data/constants';
|
||||
import EmbeddableRegistrationPage from '../EmbeddableRegistrationPage/EmbeddableRegistrationPage';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendPageEvent: jest.fn(),
|
||||
sendTrackEvent: jest.fn(),
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||
getLocale: jest.fn(),
|
||||
}));
|
||||
|
||||
const IntlEmbedableRegistrationForm = injectIntl(EmbeddableRegistrationPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('RegistrationPage', () => {
|
||||
mergeConfig({
|
||||
PRIVACY_POLICY: 'https://privacy-policy.com',
|
||||
TOS_AND_HONOR_CODE: 'https://tos-and-honot-code.com',
|
||||
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME,
|
||||
});
|
||||
|
||||
let props = {};
|
||||
let store = {};
|
||||
const registrationFormData = {
|
||||
configurableFormFields: {
|
||||
marketingEmailsOptIn: true,
|
||||
},
|
||||
formFields: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
},
|
||||
emailSuggestion: {
|
||||
suggestion: '', type: '',
|
||||
},
|
||||
errors: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
},
|
||||
};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const initialState = {
|
||||
register: {
|
||||
registrationResult: { success: false, redirectUrl: '' },
|
||||
registrationError: {},
|
||||
registrationFormData,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(initialState);
|
||||
window.parent.postMessage = jest.fn();
|
||||
configure({
|
||||
loggingService: { logError: jest.fn() },
|
||||
config: {
|
||||
ENVIRONMENT: 'production',
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
|
||||
},
|
||||
messages: { 'es-419': {}, de: {}, 'en-us': {} },
|
||||
});
|
||||
props = {
|
||||
registrationResult: jest.fn(),
|
||||
handleInstitutionLogin: jest.fn(),
|
||||
institutionLogin: false,
|
||||
};
|
||||
window.location = { search: '' };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const populateRequiredFields = (registrationPage, payload, isThirdPartyAuth = false) => {
|
||||
registrationPage.find('input#name').simulate('change', { target: { value: payload.name, name: 'name' } });
|
||||
registrationPage.find('input#username').simulate('change', { target: { value: payload.username, name: 'username' } });
|
||||
registrationPage.find('input#email').simulate('change', { target: { value: payload.email, name: 'email' } });
|
||||
|
||||
registrationPage.find('input[name="country"]').simulate('change', { target: { value: payload.country, name: 'country' } });
|
||||
registrationPage.find('input[name="country"]').simulate('blur', { target: { value: payload.country, name: 'country' } });
|
||||
|
||||
if (!isThirdPartyAuth) {
|
||||
registrationPage.find('input#password').simulate('change', { target: { value: payload.password, name: 'password' } });
|
||||
}
|
||||
};
|
||||
|
||||
describe('Test Registration Page', () => {
|
||||
mergeConfig({
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: true,
|
||||
});
|
||||
|
||||
const emptyFieldValidation = {
|
||||
name: 'Enter your full name',
|
||||
username: 'Username must be between 2 and 30 characters',
|
||||
email: 'Enter your email',
|
||||
password: 'Password criteria has not been met',
|
||||
country: 'Select your country or region of residence',
|
||||
};
|
||||
|
||||
// ******** 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;
|
||||
window.location = { href: getConfig().BASE_URL, search: '?next=/course/demo-course-url' };
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@gmail.com',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
totalRegistrationTime: 0,
|
||||
next: '/course/demo-course-url',
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
populateRequiredFields(registrationPage, payload);
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
|
||||
it('should submit form with marketing email opt in value', () => {
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: 'true',
|
||||
});
|
||||
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@gmail.com',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
totalRegistrationTime: 0,
|
||||
marketing_emails_opt_in: true,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
populateRequiredFields(registrationPage, payload);
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not dispatch registerNewUser on empty form Submission', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).not.toHaveBeenCalledWith(registerNewUser({}));
|
||||
});
|
||||
|
||||
// // ******** test registration form validations ********
|
||||
|
||||
it('should show error messages for required fields on empty form submission', () => {
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(registrationPage.find('div[feedback-for="name"]').text()).toEqual(emptyFieldValidation.name);
|
||||
expect(registrationPage.find('div[feedback-for="username"]').text()).toEqual(emptyFieldValidation.username);
|
||||
expect(registrationPage.find('div[feedback-for="email"]').text()).toEqual(emptyFieldValidation.email);
|
||||
expect(registrationPage.find('div[feedback-for="password"]').text()).toContain(emptyFieldValidation.password);
|
||||
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.';
|
||||
expect(registrationPage.find('#validation-errors').first().text()).toEqual(alertBanner);
|
||||
});
|
||||
|
||||
it('should run validations for focused field on form submission', () => {
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('input[name="country"]').simulate('focus');
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(emptyFieldValidation.country);
|
||||
});
|
||||
|
||||
it('should update props with validations returned by registration api', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
registrationError: {
|
||||
username: [{ userMessage: 'It looks like this username is already taken' }],
|
||||
email: [{ userMessage: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account` }],
|
||||
},
|
||||
},
|
||||
});
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />)).find('EmbeddableRegistrationPage');
|
||||
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 remove space from the start of username', () => {
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('input#username').simulate('change', { target: { value: ' test-user', name: 'username' } });
|
||||
|
||||
expect(registrationPage.find('input#username').prop('value')).toEqual('test-user');
|
||||
});
|
||||
it('should run username and email frontend validations', () => {
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'test@2u.com',
|
||||
email: 'test@yopmail.test',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
totalRegistrationTime: 0,
|
||||
marketing_emails_opt_in: true,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
populateRequiredFields(registrationPage, payload);
|
||||
registrationPage.find('input[name="email"]').simulate('focus');
|
||||
registrationPage.find('input[name="email"]').simulate('blur', { target: { value: 'test@yopmail.test', name: 'email' } });
|
||||
expect(registrationPage.find('.email-suggestion__text').exists()).toBeTruthy();
|
||||
|
||||
registrationPage.find('input[name="email"]').simulate('focus');
|
||||
registrationPage.find('input[name="email"]').simulate('blur', { target: { value: 'asasasasas', name: 'email' } });
|
||||
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeTruthy();
|
||||
expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeTruthy();
|
||||
});
|
||||
it('should run email frontend validations when random string is input', () => {
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'testh@2u.com',
|
||||
email: 'as',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
totalRegistrationTime: 0,
|
||||
marketing_emails_opt_in: true,
|
||||
};
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
populateRequiredFields(registrationPage, payload);
|
||||
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeTruthy();
|
||||
});
|
||||
it('should run frontend validations for name field', () => {
|
||||
const payload = {
|
||||
name: 'https://localhost.com',
|
||||
username: 'test@2u.com',
|
||||
email: 'as',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
totalRegistrationTime: 0,
|
||||
marketing_emails_opt_in: true,
|
||||
};
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
populateRequiredFields(registrationPage, payload);
|
||||
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
expect(registrationPage.find('div[feedback-for="name"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should run frontend validations for password field', () => {
|
||||
const payload = {
|
||||
name: 'https://localhost.com',
|
||||
username: 'test@2u.com',
|
||||
email: 'as',
|
||||
password: 'as',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
totalRegistrationTime: 0,
|
||||
marketing_emails_opt_in: true,
|
||||
};
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
populateRequiredFields(registrationPage, payload);
|
||||
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
expect(registrationPage.find('div[feedback-for="password"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should click on email suggestion in case suggestion is avialable', () => {
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('input[name="email"]').simulate('focus');
|
||||
registrationPage.find('input[name="email"]').simulate('blur', { target: { value: 'test@gmail.co', name: 'email' } });
|
||||
|
||||
registrationPage.find('a.email-suggestion-alert-warning').simulate('click');
|
||||
expect(registrationPage.find('input#email').prop('value')).toEqual('test@gmail.com');
|
||||
});
|
||||
|
||||
it('should remove extra character if username is more than 30 character long', () => {
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('input#username').simulate('change', { target: { value: 'why_this_is_not_valid_username_', name: 'username' } });
|
||||
|
||||
expect(registrationPage.find('input#username').prop('value')).toEqual('');
|
||||
});
|
||||
|
||||
// // ******** test field focus in functionality ********
|
||||
|
||||
it('should clear field related error messages on input field Focus', () => {
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...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[name="country"]').simulate('focus');
|
||||
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should clear username suggestions when username field is focused in', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('input#username').simulate('focus');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
|
||||
});
|
||||
|
||||
it('should call backend api for username suggestions when input the name field', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
|
||||
registrationPage.find('input#name').simulate('focus');
|
||||
registrationPage.find('input#name').simulate('change', { target: { value: 'ahtesham', name: 'name' } });
|
||||
registrationPage.find('input#name').simulate('blur');
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
// // ******** test form buttons and fields ********
|
||||
|
||||
it('should match default button state', () => {
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
expect(registrationPage.find('button[type="submit"] span').first().text())
|
||||
.toEqual('Create an account for free');
|
||||
});
|
||||
|
||||
it('should match pending button state', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
submitState: PENDING_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
const button = registrationPage.find('button[type="submit"] span').first();
|
||||
|
||||
expect(button.find('.sr-only').text()).toEqual('pending');
|
||||
});
|
||||
|
||||
it('should display opt-in/opt-out checkbox', () => {
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: 'true',
|
||||
});
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
expect(registrationPage.find('div.form-field--checkbox').length).toEqual(1);
|
||||
|
||||
mergeConfig({
|
||||
MARKETING_EMAILS_OPT_IN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should show button label based on cta query params value', () => {
|
||||
const buttonLabel = 'Register';
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` };
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual(buttonLabel);
|
||||
});
|
||||
|
||||
it('should check registration conversion cookie', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
registrationResult: {
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderer.create(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
expect(document.cookie).toMatch(`${getConfig().REGISTER_CONVERSION_COOKIE_NAME}=true`);
|
||||
});
|
||||
|
||||
it('should show username suggestions in case of conflict with an existing username', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
usernameSuggestions: ['test_1', 'test_12', 'test_123'],
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
errors: {
|
||||
...registrationFormData.errors,
|
||||
username: 'It looks like this username is already taken',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
expect(registrationPage.find('button.username-suggestions--chip').length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should show username suggestions when full name is populated', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
usernameSuggestions: ['test_1', 'test_12', 'test_123'],
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
username: ' ',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
|
||||
|
||||
expect(registrationPage.find('button.username-suggestions--chip').length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should remove empty space from username field when it is focused', async () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
usernameSuggestions: ['test_1', 'test_12', 'test_123'],
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
username: ' ',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
|
||||
registrationPage.find('input#username').simulate('focus');
|
||||
await act(async () => { await expect(registrationPage.find('input#username').text()).toEqual(''); });
|
||||
});
|
||||
|
||||
it('should click on username suggestions when full name is populated', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
usernameSuggestions: ['test_1', 'test_12', 'test_123'],
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
username: ' ',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
|
||||
registrationPage.find('.username-suggestions--chip').first().simulate('click');
|
||||
expect(registrationPage.find('input#username').props().value).toEqual('test_1');
|
||||
});
|
||||
|
||||
it('should clear username suggestions when close icon is clicked', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
usernameSuggestions: ['test_1', 'test_12', 'test_123'],
|
||||
registrationFormData: {
|
||||
...registrationFormData,
|
||||
username: ' ',
|
||||
},
|
||||
},
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } });
|
||||
registrationPage.find('button.username-suggestions__close__button').at(0).simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions());
|
||||
});
|
||||
|
||||
// // ******** miscellaneous tests ********
|
||||
|
||||
it('should send page event when register page is rendered', () => {
|
||||
mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
|
||||
});
|
||||
|
||||
it('should update state from country code present in redux store', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
backendCountryCode: 'PK',
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
expect(registrationPage.find('input[name="country"]').props().value).toEqual('Pakistan');
|
||||
});
|
||||
|
||||
it('should set country in component state when form is translated used i18n', () => {
|
||||
getLocale.mockImplementation(() => ('ar-ae'));
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('input[name="country"]').simulate('click');
|
||||
registrationPage.find('button.dropdown-item').at(0).simulate('click', { target: { value: 'أفغانستان ', name: 'countryItem' } });
|
||||
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should clear the registation validation error on change event on field focused', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
registrationError: {
|
||||
errorCode: 'duplicate-email',
|
||||
email: [{ userMessage: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account` }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const clearBackendError = jest.fn();
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} {...clearBackendError} />));
|
||||
registrationPage.find('input#email').simulate('change', { target: { value: 'a@gmail.com', name: 'email' } });
|
||||
expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test Configurable Fields', () => {
|
||||
mergeConfig({
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: true,
|
||||
});
|
||||
|
||||
it('should render fields returned by backend', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
fieldDescriptions: {
|
||||
profession: { name: 'profession', type: 'text', label: 'Profession' },
|
||||
terms_of_service: {
|
||||
name: FIELDS.TERMS_OF_SERVICE,
|
||||
error_message: 'You must agree to the Terms and Service agreement of our site',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
expect(registrationPage.find('#profession').exists()).toBeTruthy();
|
||||
expect(registrationPage.find('#tos').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should submit form with fields returned by backend in payload', () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
fieldDescriptions: {
|
||||
profession: { name: 'profession', type: 'text', label: 'Profession' },
|
||||
},
|
||||
extendedProfile: ['profession'],
|
||||
},
|
||||
});
|
||||
|
||||
const payload = {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
password: 'password1',
|
||||
country: 'Pakistan',
|
||||
honor_code: true,
|
||||
profession: 'Engineer',
|
||||
totalRegistrationTime: 0,
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
|
||||
populateRequiredFields(registrationPage, payload);
|
||||
registrationPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } });
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||
});
|
||||
|
||||
it('should show error messages for required fields on empty form submission', () => {
|
||||
const professionError = 'Enter your profession';
|
||||
const countryError = 'Select your country or region of residence';
|
||||
const confirmEmailError = 'Enter your email';
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
fieldDescriptions: {
|
||||
profession: {
|
||||
name: 'profession', type: 'text', label: 'Profession', error_message: professionError,
|
||||
},
|
||||
confirm_email: {
|
||||
name: 'confirm_email', type: 'text', label: 'Confirm Email', error_message: confirmEmailError,
|
||||
},
|
||||
country: { name: 'country' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError);
|
||||
expect(registrationPage.find('div[feedback-for="country"]').text()).toEqual(countryError);
|
||||
expect(registrationPage.find('#confirm_email-error').last().text()).toEqual(confirmEmailError);
|
||||
});
|
||||
|
||||
it('should run validations for configurable focused field on form submission', () => {
|
||||
const professionError = 'Enter your profession';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
fieldDescriptions: {
|
||||
profession: {
|
||||
name: 'profession', type: 'text', label: 'Profession', error_message: professionError,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('input#profession').simulate('focus', { target: { value: '', name: 'profession' } });
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
|
||||
expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError);
|
||||
});
|
||||
|
||||
it('should not run country field validation when onBlur is fired by drop-down arrow icon click', () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlEmbedableRegistrationForm {...props} />));
|
||||
registrationPage.find('input[name="country"]').simulate('blur', {
|
||||
target: { value: '', name: 'country' },
|
||||
relatedTarget: { type: 'button', className: 'btn-icon pgn__form-autosuggest__icon-button' },
|
||||
});
|
||||
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 '../registrationFields/HonorCode';
|
||||
import HonorCode from '../RegistrationFields/HonorCode';
|
||||
|
||||
const IntlHonorCode = injectIntl(HonorCode);
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
import {
|
||||
FIELDS, FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
|
||||
} from '../data/constants';
|
||||
import RegistrationFailureMessage from '../RegistrationFailure';
|
||||
import RegistrationFailureMessage from '../components/RegistrationFailure';
|
||||
import RegistrationPage from '../RegistrationPage';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
@@ -105,6 +105,7 @@ describe('RegistrationPage', () => {
|
||||
register: {
|
||||
registrationResult: { success: false, redirectUrl: '' },
|
||||
registrationError: {},
|
||||
usernameSuggestions: [],
|
||||
registrationFormData,
|
||||
},
|
||||
commonComponents: {
|
||||
@@ -270,18 +271,6 @@ describe('RegistrationPage', () => {
|
||||
|
||||
// ******** test registration form validations ********
|
||||
|
||||
it('should not run validations on blur event when embedded variant is rendered', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: '?host=http://localhost/host-website' };
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
|
||||
registrationPage.find('input#username').simulate('blur', { target: { value: '', name: 'username' } });
|
||||
expect(registrationPage.find('div[feedback-for="username"]').exists()).toBeFalsy();
|
||||
|
||||
registrationPage.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' } });
|
||||
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should show error messages for required fields on empty form submission', () => {
|
||||
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
registrationPage.find('button.btn-brand').simulate('click');
|
||||
@@ -639,14 +628,6 @@ describe('RegistrationPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should show button label based on cta query params value', () => {
|
||||
const buttonLabel = 'Register';
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL, search: `?cta=${buttonLabel}` };
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual(buttonLabel);
|
||||
});
|
||||
|
||||
it('should display no password field when current provider is present', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -902,41 +883,6 @@ describe('RegistrationPage', () => {
|
||||
expect(registrationPage.find(`button#${ssoProvider.id}`).hasClass(`btn-tpa btn-${ssoProvider.id}`)).toEqual(true);
|
||||
});
|
||||
|
||||
it('should call the postMessage API when embedded variant is rendered', () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
mergeConfig({
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true,
|
||||
});
|
||||
|
||||
window.parent.postMessage = jest.fn();
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING), search: '?host=http://localhost/host-website' };
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
registrationResult: {
|
||||
success: true,
|
||||
},
|
||||
},
|
||||
commonComponents: {
|
||||
optionalFields: {
|
||||
extended_profile: {},
|
||||
fields: {
|
||||
level_of_education: { name: 'level_of_education', error_message: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const progressiveProfilingPage = mount(reduxWrapper(
|
||||
<IntlRegistrationPage {...props} />,
|
||||
));
|
||||
progressiveProfilingPage.update();
|
||||
expect(window.parent.postMessage).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should render icon if icon classes are missing in providers', () => {
|
||||
ssoProvider.iconClass = null;
|
||||
store = mockStore({
|
||||
@@ -1049,7 +995,9 @@ describe('RegistrationPage', () => {
|
||||
expect(store.dispatch).toHaveBeenCalledWith(setUserPipelineDataLoaded(true));
|
||||
});
|
||||
|
||||
it('should update state from country code present in redux store', () => {
|
||||
// TODO: should be moved to country fields tests
|
||||
it.skip('should update state from country code present in redux store', () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import TermsOfService from '../registrationFields/TermsOfService';
|
||||
import TermsOfService from '../RegistrationFields/TermsOfService';
|
||||
|
||||
const IntlTermsOfService = injectIntl(TermsOfService);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user