From 8d6473f7accbf22ad62098db1bb254622924f1fc Mon Sep 17 00:00:00 2001 From: Syed Sajjad Hussain Shah Date: Tue, 29 Aug 2023 16:46:05 +0500 Subject: [PATCH] refactor: registration component refactoring --- src/common-components/PasswordField.jsx | 56 +- src/data/constants.js | 6 - src/forgot-password/ForgotPasswordPage.jsx | 3 +- .../CountryField/CountryField.jsx | 134 ++++ .../CountryField/constants.js | 3 + .../CountryField/validator.js | 28 + .../EmailField/EmailField.jsx | 125 ++++ .../EmailField/constants.js | 139 ++++ .../EmailField/validator.js | 125 ++++ .../HonorCode.jsx | 0 .../NameField/NameField.jsx | 59 ++ .../RegistrationFields/NameField/constants.js | 5 + .../RegistrationFields/NameField/validator.js | 14 + .../TermsOfService.jsx | 0 .../UsernameField/UsernameField.jsx | 152 +++++ .../UsernameField/constants.js | 3 + .../UsernameField/validator.js | 16 + src/register/RegistrationFields/index.js | 6 + src/register/RegistrationPage.jsx | 594 +++--------------- .../ConfigurableRegistrationForm.jsx | 66 +- .../{ => components}/RegistrationFailure.jsx | 6 +- .../{ => components}/ThirdPartyAuth.jsx | 6 +- src/register/data/constants.js | 135 +--- src/register/data/selectors.js | 42 +- src/register/data/utils.js | 165 ++--- .../registrationFields/CountryField.jsx | 95 --- .../registrationFields/EmailField.jsx | 81 --- .../registrationFields/UsernameField.jsx | 74 --- src/register/registrationFields/index.js | 5 - src/register/tests/RegistrationPage.test.jsx | 2 +- 30 files changed, 1096 insertions(+), 1049 deletions(-) create mode 100644 src/register/RegistrationFields/CountryField/CountryField.jsx create mode 100644 src/register/RegistrationFields/CountryField/constants.js create mode 100644 src/register/RegistrationFields/CountryField/validator.js create mode 100644 src/register/RegistrationFields/EmailField/EmailField.jsx create mode 100644 src/register/RegistrationFields/EmailField/constants.js create mode 100644 src/register/RegistrationFields/EmailField/validator.js rename src/register/{registrationFields => RegistrationFields}/HonorCode.jsx (100%) create mode 100644 src/register/RegistrationFields/NameField/NameField.jsx create mode 100644 src/register/RegistrationFields/NameField/constants.js create mode 100644 src/register/RegistrationFields/NameField/validator.js rename src/register/{registrationFields => RegistrationFields}/TermsOfService.jsx (100%) create mode 100644 src/register/RegistrationFields/UsernameField/UsernameField.jsx create mode 100644 src/register/RegistrationFields/UsernameField/constants.js create mode 100644 src/register/RegistrationFields/UsernameField/validator.js create mode 100644 src/register/RegistrationFields/index.js rename src/register/{ => components}/ConfigurableRegistrationForm.jsx (80%) rename src/register/{ => components}/RegistrationFailure.jsx (94%) rename src/register/{ => components}/ThirdPartyAuth.jsx (96%) delete mode 100644 src/register/registrationFields/CountryField.jsx delete mode 100644 src/register/registrationFields/EmailField.jsx delete mode 100644 src/register/registrationFields/UsernameField.jsx delete mode 100644 src/register/registrationFields/index.js diff --git a/src/common-components/PasswordField.jsx b/src/common-components/PasswordField.jsx index efc69635a3..c93cdb8dc6 100644 --- a/src/common-components/PasswordField.jsx +++ b/src/common-components/PasswordField.jsx @@ -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,80 @@ import PropTypes from 'prop-types'; import messages from './messages'; import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants'; +import { isHostAvailableInQueryParams } from '../data/utils'; +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 registrationEmbedded = isHostAvailableInQueryParams(); 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 (!registrationEmbedded && !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 = ( - + ); const ShowButton = ( - + ); + const placement = window.innerWidth < 768 ? 'top' : 'left'; const tooltip = ( @@ -89,6 +139,7 @@ PasswordField.defaultProps = { handleBlur: null, handleFocus: null, handleChange: () => {}, + handleErrorChange: null, showRequirements: true, autoComplete: null, }; @@ -100,6 +151,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, diff --git a/src/data/constants.js b/src/data/constants.js index 2370cbebe0..9b647b4804 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -26,14 +26,8 @@ export const FAILURE_STATE = 'failure'; 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. diff --git a/src/forgot-password/ForgotPasswordPage.jsx b/src/forgot-password/ForgotPasswordPage.jsx index 49770cdeb6..bdd2dcd436 100644 --- a/src/forgot-password/ForgotPasswordPage.jsx +++ b/src/forgot-password/ForgotPasswordPage.jsx @@ -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; diff --git a/src/register/RegistrationFields/CountryField/CountryField.jsx b/src/register/RegistrationFields/CountryField/CountryField.jsx new file mode 100644 index 0000000000..e940f189bb --- /dev/null +++ b/src/register/RegistrationFields/CountryField/CountryField.jsx @@ -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) => ( + + {country[COUNTRY_DISPLAY_KEY]} + + )); + + return ( +
+ handleSelected(value)} + onFocus={(e) => handleOnFocus(e)} + onBlur={(e) => handleOnBlur(e)} + onChange={(value) => handleOnChange(value)} + > + {getCountryList()} + + {props.errorMessage !== '' && ( + + {props.errorMessage} + + )} +
+ ); +}; + +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; diff --git a/src/register/RegistrationFields/CountryField/constants.js b/src/register/RegistrationFields/CountryField/constants.js new file mode 100644 index 0000000000..9817ad29c4 --- /dev/null +++ b/src/register/RegistrationFields/CountryField/constants.js @@ -0,0 +1,3 @@ +export const COUNTRY_FIELD_LABEL = 'country'; +export const COUNTRY_CODE_KEY = 'code'; +export const COUNTRY_DISPLAY_KEY = 'name'; diff --git a/src/register/RegistrationFields/CountryField/validator.js b/src/register/RegistrationFields/CountryField/validator.js new file mode 100644 index 0000000000..ebba8cf0bb --- /dev/null +++ b/src/register/RegistrationFields/CountryField/validator.js @@ -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; diff --git a/src/register/RegistrationFields/EmailField/EmailField.jsx b/src/register/RegistrationFields/EmailField/EmailField.jsx new file mode 100644 index 0000000000..8cfa6c436f --- /dev/null +++ b/src/register/RegistrationFields/EmailField/EmailField.jsx @@ -0,0 +1,125 @@ +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'; +import {isHostAvailableInQueryParams} from "../../../data/utils"; + +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 registrationEmbedded = isHostAvailableInQueryParams(); + + 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 (!registrationEmbedded && !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 ( + + + {formatMessage(messages['did.you.mean.alert.text'])}{' '} + + {emailSuggestion.suggestion} + ? + + + + ); + } + return ( + + {formatMessage(messages['did.you.mean.alert.text'])}:{' '} + + {emailSuggestion.suggestion} + ? + + ); + }; + + return ( + + {emailSuggestion.suggestion ? renderEmailFeedback() : null} + + ); +}; + +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; diff --git a/src/register/RegistrationFields/EmailField/constants.js b/src/register/RegistrationFields/EmailField/constants.js new file mode 100644 index 0000000000..1c953802f1 --- /dev/null +++ b/src/register/RegistrationFields/EmailField/constants.js @@ -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', +]; diff --git a/src/register/RegistrationFields/EmailField/validator.js b/src/register/RegistrationFields/EmailField/validator.js new file mode 100644 index 0000000000..477c9a83b3 --- /dev/null +++ b/src/register/RegistrationFields/EmailField/validator.js @@ -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; diff --git a/src/register/registrationFields/HonorCode.jsx b/src/register/RegistrationFields/HonorCode.jsx similarity index 100% rename from src/register/registrationFields/HonorCode.jsx rename to src/register/RegistrationFields/HonorCode.jsx diff --git a/src/register/RegistrationFields/NameField/NameField.jsx b/src/register/RegistrationFields/NameField/NameField.jsx new file mode 100644 index 0000000000..b5513bf077 --- /dev/null +++ b/src/register/RegistrationFields/NameField/NameField.jsx @@ -0,0 +1,59 @@ +import React 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 ( + + ); +}; + +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; diff --git a/src/register/RegistrationFields/NameField/constants.js b/src/register/RegistrationFields/NameField/constants.js new file mode 100644 index 0000000000..46405f6957 --- /dev/null +++ b/src/register/RegistrationFields/NameField/constants.js @@ -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'; diff --git a/src/register/RegistrationFields/NameField/validator.js b/src/register/RegistrationFields/NameField/validator.js new file mode 100644 index 0000000000..08d2b16fdf --- /dev/null +++ b/src/register/RegistrationFields/NameField/validator.js @@ -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; diff --git a/src/register/registrationFields/TermsOfService.jsx b/src/register/RegistrationFields/TermsOfService.jsx similarity index 100% rename from src/register/registrationFields/TermsOfService.jsx rename to src/register/RegistrationFields/TermsOfService.jsx diff --git a/src/register/RegistrationFields/UsernameField/UsernameField.jsx b/src/register/RegistrationFields/UsernameField/UsernameField.jsx new file mode 100644 index 0000000000..5098249727 --- /dev/null +++ b/src/register/RegistrationFields/UsernameField/UsernameField.jsx @@ -0,0 +1,152 @@ +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'; +import {isHostAvailableInQueryParams} from "../../../data/utils"; + +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); + const registrationEmbedded = isHostAvailableInQueryParams(); + + /** + * 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 (!registrationEmbedded && !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 = () => ( +
+ {formatMessage(messages['registration.username.suggestion.label'])} +
+ {usernameSuggestions.map((username, index) => ( + + ))} +
+ {iconButton} +
+ ); + + if (usernameSuggestions.length > 0 && errorMessage && value === ' ') { + className = 'username-suggestions__error'; + iconButton = 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 = handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />; + suggestedUsernameDiv = suggestedUsernames(); + } else if (usernameSuggestions.length > 0 && errorMessage) { + suggestedUsernameDiv = suggestedUsernames(); + } + return ( + + {suggestedUsernameDiv} + + ); +}; + +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; diff --git a/src/register/RegistrationFields/UsernameField/constants.js b/src/register/RegistrationFields/UsernameField/constants.js new file mode 100644 index 0000000000..39a629dffd --- /dev/null +++ b/src/register/RegistrationFields/UsernameField/constants.js @@ -0,0 +1,3 @@ +export const USERNAME_FIELD_LABEL = 'username'; + +export const VALID_USERNAME_REGEX = /^[a-zA-Z0-9_-]*$/i; diff --git a/src/register/RegistrationFields/UsernameField/validator.js b/src/register/RegistrationFields/UsernameField/validator.js new file mode 100644 index 0000000000..af92c42999 --- /dev/null +++ b/src/register/RegistrationFields/UsernameField/validator.js @@ -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; diff --git a/src/register/RegistrationFields/index.js b/src/register/RegistrationFields/index.js new file mode 100644 index 0000000000..10e3f2be51 --- /dev/null +++ b/src/register/RegistrationFields/index.js @@ -0,0 +1,6 @@ +export { default as NameField } from './NameField/NameField'; +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'; diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index 0fe7c8db99..44d3389bdb 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -1,91 +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, sendTrackEvent } 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 RegistrationFailure from './components/RegistrationFailure'; +import ThirdPartyAuth from './components/ThirdPartyAuth'; 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 { EmailField, NameField, UsernameField } from './RegistrationFields'; 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, } 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 backendValidations = useMemo( + () => getBackendValidations(registrationError, validations), [registrationError, validations], + ); + const { formatMessage } = useIntl(); - const countryList = useMemo(() => getCountryList(getLocale()), []); + const dispatch = useDispatch(); + const queryParams = useMemo(() => getAllPossibleQueryParams(), []); const registrationEmbedded = isHostAvailableInQueryParams(); const { cta, host } = queryParams; @@ -99,35 +87,21 @@ 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); + + // temporary error state for embedded experience because we dont want to show errors on blur + const [temporaryErrors, setTemporaryErrors] = useState({ ...backedUpFormData.errors }); 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, - })); - } - }; + const buttonLabel = cta + ? formatMessage(messages['create.account.cta.button'], { label: cta }) + : formatMessage(messages['create.account.for.free.button']); /** * Set the userPipelineDetails data in formFields for only first time @@ -138,7 +112,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 +119,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,30 +134,34 @@ 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) { - setErrors(prevErrors => ({ ...prevErrors, ...backendValidations })); + if (registrationEmbedded) { + setTemporaryErrors(prevErrors => ({ ...prevErrors, ...backendValidations })); + } else { + setErrors(prevErrors => ({ ...prevErrors, ...backendValidations })); + } } - }, [backendValidations]); + }, [backendValidations, registrationEmbedded]); useEffect(() => { if (registrationErrorCode) { @@ -193,39 +169,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) { sendTrackEvent('edx.bi.user.account.registered.client', {}); @@ -251,221 +194,31 @@ 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; - if (registrationError[name]) { - clearBackendError(name); + const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value; + if (registrationError[name] && !registrationEmbedded) { + 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; + console.log('errors', errors); + console.log('temporaryerrors', temporaryErrors); + const handleErrorChange = (fieldName, error) => { 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]: '' })); - } + console.log('handleErrorChange registrationEmbedded', fieldName, error); + setTemporaryErrors(prevErrors => ({ + ...prevErrors, + [fieldName]: error, + })); + } else { + console.log('handleErrorChange NORMAL', fieldName, error); + setErrors(prevErrors => ({ + ...prevErrors, + [fieldName]: error, + })); } }; @@ -478,42 +231,32 @@ 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, - ) - ) : ''; - - if (!isFormValid(payload, focusedFieldError)) { + // Validating form data before submitting + const { isValid, fieldErrors } = isFormValid( + payload, + registrationEmbedded ? temporaryErrors : errors, + configurableFormFields, + fieldDescriptions, + formatMessage, + ); + setErrors({ ...fieldErrors }); + + // 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]; - } - }); - - // 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; + // Preparing payload for submission + payload = prepareRegistrationPayload( + payload, + configurableFormFields, + flags.showMarketingEmailOptInCheckbox, + totalRegistrationTime, + queryParams); - // add query params to the payload - payload = { ...payload, ...queryParams }; - props.registerNewUser(payload); + // making register call + dispatch(registerNewUser(payload)); }; const handleSubmit = (e) => { @@ -575,12 +318,12 @@ const RegistrationPage = (props) => { context={{ provider: currentProvider, errorMessage: thirdPartyAuthContext.errorMessage }} />
- { handleSuggestionClick(e, 'email')} - handleOnClose={handleEmailSuggestionClosed} - emailSuggestion={emailSuggestion} errorMessage={errors.email} helpText={[formatMessage(messages['help.text.email'])]} floatingLabel={formatMessage(messages['registration.email.label'])} @@ -602,12 +342,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'])} @@ -617,21 +353,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'])} /> )} { ); }; -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({ - authenticatedUser: 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; diff --git a/src/register/ConfigurableRegistrationForm.jsx b/src/register/components/ConfigurableRegistrationForm.jsx similarity index 80% rename from src/register/ConfigurableRegistrationForm.jsx rename to src/register/components/ConfigurableRegistrationForm.jsx index 0c8e247f07..56777cd358 100644 --- a/src/register/ConfigurableRegistrationForm.jsx +++ b/src/register/components/ConfigurableRegistrationForm.jsx @@ -1,15 +1,13 @@ -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 { CountryField, HonorCode, TermsOfService } from '../RegistrationFields'; /** * Fields on registration page that are not the default required fields (name, email, username, password). @@ -26,17 +24,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 +53,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 +99,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 +107,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 +175,7 @@ const ConfigurableRegistrationForm = (props) => { selectedCountry={formFields.country} errorMessage={fieldErrors.country || ''} onChangeHandler={handleOnChange} + handleErrorChange={handleErrorChange} onBlurHandler={handleOnBlur} onFocusHandler={handleOnFocus} /> @@ -203,7 +221,6 @@ const ConfigurableRegistrationForm = (props) => { }; ConfigurableRegistrationForm.propTypes = { - countryList: PropTypes.arrayOf(PropTypes.shape({})).isRequired, email: PropTypes.string.isRequired, fieldDescriptions: PropTypes.shape({}), fieldErrors: PropTypes.shape({ @@ -218,14 +235,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; diff --git a/src/register/RegistrationFailure.jsx b/src/register/components/RegistrationFailure.jsx similarity index 94% rename from src/register/RegistrationFailure.jsx rename to src/register/components/RegistrationFailure.jsx index ba53761eb1..b71fb5fe7b 100644 --- a/src/register/RegistrationFailure.jsx +++ b/src/register/components/RegistrationFailure.jsx @@ -6,14 +6,14 @@ import { Alert } from '@edx/paragon'; import { Error } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; +import { windowScrollTo } from '../../data/utils'; import { FORBIDDEN_REQUEST, 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'; const RegistrationFailureMessage = (props) => { const { formatMessage } = useIntl(); diff --git a/src/register/ThirdPartyAuth.jsx b/src/register/components/ThirdPartyAuth.jsx similarity index 96% rename from src/register/ThirdPartyAuth.jsx rename to src/register/components/ThirdPartyAuth.jsx index 12b4540ce5..3a8682488c 100644 --- a/src/register/ThirdPartyAuth.jsx +++ b/src/register/components/ThirdPartyAuth.jsx @@ -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 { RenderInstitutionButton, SocialAuthProviders, -} from '../common-components'; +} from '../../common-components'; import { PENDING_STATE, REGISTER_PAGE, -} from '../data/constants'; +} from '../../data/constants'; +import messages from '../messages'; /** * This component renders the Single sign-on (SSO) buttons for the providers passed. diff --git a/src/register/data/constants.js b/src/register/data/constants.js index 34d75e4b4b..b5055bed40 100644 --- a/src/register/data/constants.js +++ b/src/register/data/constants.js @@ -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'; diff --git a/src/register/data/selectors.js b/src/register/data/selectors.js index 3914818545..5be72d99ac 100644 --- a/src/register/data/selectors.js +++ b/src/register/data/selectors.js @@ -1,33 +1,19 @@ -import { createSelector } from 'reselect'; - export const storeName = 'register'; -export const registerSelector = state => ({ ...state[storeName] }); - -export const registrationErrorSelector = createSelector( - registerSelector, - register => register.registrationError.errorCode, -); - -export const validationsSelector = createSelector( - registerSelector, - (register) => { - const { registrationError, validations } = register; - - if (validations) { - return validations.validationDecisions; - } +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'])); + 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; - } + const validationDecisions = {}; + fields.forEach(field => { + validationDecisions[field] = registrationError[field][0].userMessage || ''; + }); + return validationDecisions; + } - return null; - }, -); + return null; +}; diff --git a/src/register/data/utils.js b/src/register/data/utils.js index 190cb539bd..e5ffcacce0 100644 --- a/src/register/data/utils.js +++ b/src/register/data/utils.js @@ -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']); } - - 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 fieldError; +}; + +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; } - } - - 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 (!configurableFormFields?.country?.displayValue) { + fieldErrors.country = formatMessage(messages['empty.country.field.error']); + isValid = false; } - 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 (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]; + } + }); - if (!hasMultipleSubdomains && tldSuggestion) { - validation.hasError = true; + // Don't send the marketing email opt-in value if the flag is turned off + if (!showMarketingEmailOptInCheckbox) { + delete payload.marketingEmailsOptIn; } - return validation; -} - -export function validateCountryField(value, countryList, errorMessage) { - let countryCode = ''; - let displayValue = value; - let error = errorMessage; + payload = snakeCaseObject(payload); + payload.totalRegistrationTime = totalRegistrationTime; - 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 }; -} + // add query params to the payload + payload = { ...payload, ...queryParams }; + return payload; +}; diff --git a/src/register/registrationFields/CountryField.jsx b/src/register/registrationFields/CountryField.jsx deleted file mode 100644 index 9428bd1706..0000000000 --- a/src/register/registrationFields/CountryField.jsx +++ /dev/null @@ -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) => ( - - {country[COUNTRY_DISPLAY_KEY]} - - )); - - return ( -
- handleSelected(value)} - onFocus={(e) => onFocusHandler(e)} - onBlur={(e) => onBlurHandler(e)} - onChange={(value) => onChangeHandler(value)} - > - {getCountryList()} - - {props.errorMessage !== '' && ( - - {props.errorMessage} - - )} -
- ); -}; - -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; diff --git a/src/register/registrationFields/EmailField.jsx b/src/register/registrationFields/EmailField.jsx deleted file mode 100644 index 19f6d6ffa9..0000000000 --- a/src/register/registrationFields/EmailField.jsx +++ /dev/null @@ -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 ( - - - {formatMessage(messages['did.you.mean.alert.text'])}{' '} - - {emailSuggestion.suggestion} - ? - - - ); - } - return ( - - {formatMessage(messages['did.you.mean.alert.text'])}:{' '} - - {emailSuggestion.suggestion} - ? - - ); - }; - - return ( - - {emailSuggestion.suggestion ? renderEmailFeedback() : null} - - ); -}; - -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; diff --git a/src/register/registrationFields/UsernameField.jsx b/src/register/registrationFields/UsernameField.jsx deleted file mode 100644 index f30d55c818..0000000000 --- a/src/register/registrationFields/UsernameField.jsx +++ /dev/null @@ -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 = () => ( -
- {formatMessage(messages['registration.username.suggestion.label'])} -
- {usernameSuggestions.map((username, index) => ( - - ))} -
- {iconButton} -
- ); - if (usernameSuggestions.length > 0 && errorMessage && props.value === ' ') { - className = 'username-suggestions__error'; - iconButton = 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 = handleUsernameSuggestionClose()} variant="black" size="sm" className="username-suggestions__close__button" />; - suggestedUsernameDiv = suggestedUsernames(); - } else if (usernameSuggestions.length > 0 && errorMessage) { - suggestedUsernameDiv = suggestedUsernames(); - } - return ( - - {suggestedUsernameDiv} - - ); -}; - -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; diff --git a/src/register/registrationFields/index.js b/src/register/registrationFields/index.js deleted file mode 100644 index 4f4c45bad0..0000000000 --- a/src/register/registrationFields/index.js +++ /dev/null @@ -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'; diff --git a/src/register/tests/RegistrationPage.test.jsx b/src/register/tests/RegistrationPage.test.jsx index 06da54f6e3..31c2d39480 100644 --- a/src/register/tests/RegistrationPage.test.jsx +++ b/src/register/tests/RegistrationPage.test.jsx @@ -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', () => ({