diff --git a/public/index.html b/public/index.html index e204398e6a..c582e3a690 100644 --- a/public/index.html +++ b/public/index.html @@ -1,7 +1,7 @@ - Authn | <%= process.env.SITE_NAME %> + <%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %> diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx index 52ab010182..7b0a08f3c7 100644 --- a/src/login/LoginPage.jsx +++ b/src/login/LoginPage.jsx @@ -166,6 +166,7 @@ class LoginPage extends React.Component { const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider; const isSocialAuthActive = !!providers.length && !currentProvider; const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN; + const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive); return ( <> @@ -183,7 +184,7 @@ class LoginPage extends React.Component { )} - {thirdPartyAuthApiStatus === PENDING_STATE ? ( + {thirdPartyAuthApiStatus === PENDING_STATE && isThirdPartyAuthActive ? ( ) : ( <> diff --git a/src/register/RegistrationFields/EmailField/EmailField.jsx b/src/register/RegistrationFields/EmailField/EmailField.jsx index 7de07acc17..7937eaab2a 100644 --- a/src/register/RegistrationFields/EmailField/EmailField.jsx +++ b/src/register/RegistrationFields/EmailField/EmailField.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -9,9 +9,9 @@ import PropTypes from 'prop-types'; import validateEmail from './validator'; import { FormGroup } from '../../../common-components'; import { - backupRegistrationFormBegin, clearRegistrationBackendError, fetchRealtimeValidations, + setEmailSuggestionInStore, } from '../../data/actions'; import messages from '../../messages'; @@ -44,6 +44,10 @@ const EmailField = (props) => { const [emailSuggestion, setEmailSuggestion] = useState({ ...backedUpFormData?.emailSuggestion }); + useEffect(() => { + setEmailSuggestion(backedUpFormData.emailSuggestion); + }, [backedUpFormData.emailSuggestion]); + const handleOnBlur = (e) => { const { value } = e.target; const { fieldError, confirmEmailError, suggestion } = validateEmail(value, confirmEmailValue, formatMessage); @@ -52,10 +56,7 @@ const EmailField = (props) => { handleErrorChange('confirm_email', confirmEmailError); } - dispatch(backupRegistrationFormBegin({ - ...backedUpFormData, - emailSuggestion: { ...suggestion }, - })); + dispatch(setEmailSuggestionInStore(suggestion)); setEmailSuggestion(suggestion); if (fieldError) { diff --git a/src/register/RegistrationFields/EmailField/EmailField.test.jsx b/src/register/RegistrationFields/EmailField/EmailField.test.jsx index 0d528cf6d7..349b8531ee 100644 --- a/src/register/RegistrationFields/EmailField/EmailField.test.jsx +++ b/src/register/RegistrationFields/EmailField/EmailField.test.jsx @@ -46,7 +46,14 @@ describe('EmailField', () => { ); const initialState = { - register: {}, + register: { + registrationFormData: { + emailSuggestion: { + suggestion: 'example@gmail.com', + type: 'warning', + }, + }, + }, }; beforeEach(() => { diff --git a/src/register/RegistrationFields/EmailField/validator.js b/src/register/RegistrationFields/EmailField/validator.js index c59b9e67a1..44fd2fa1c8 100644 --- a/src/register/RegistrationFields/EmailField/validator.js +++ b/src/register/RegistrationFields/EmailField/validator.js @@ -91,7 +91,7 @@ export const validateEmailAddress = (value, username, domainName) => { const validateEmail = (value, confirmEmailValue, formatMessage) => { let fieldError = ''; let confirmEmailError = ''; - let emailSuggestion = {}; + let emailSuggestion = { suggestion: '', type: '' }; if (!value) { fieldError = formatMessage(messages['empty.email.field.error']); diff --git a/src/register/RegistrationFields/NameField/validator.js b/src/register/RegistrationFields/NameField/validator.js index 6831be300e..752768013d 100644 --- a/src/register/RegistrationFields/NameField/validator.js +++ b/src/register/RegistrationFields/NameField/validator.js @@ -4,7 +4,7 @@ export const INVALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{ export const urlRegex = new RegExp(INVALID_NAME_REGEX); const validateName = (value, formatMessage) => { - let fieldError; + let fieldError = ''; if (!value.trim()) { fieldError = formatMessage(messages['empty.name.field.error']); } else if (value && value.match(urlRegex)) { diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index a553fedd9f..3f14f0abfe 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -19,6 +19,7 @@ import { backupRegistrationFormBegin, clearRegistrationBackendError, registerNewUser, + setEmailSuggestionInStore, setUserPipelineDataLoaded, } from './data/actions'; import { @@ -185,8 +186,8 @@ const RegistrationPage = (props) => { const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value; if (registrationError[name]) { dispatch(clearRegistrationBackendError(name)); - setErrors(prevErrors => ({ ...prevErrors, [name]: '' })); } + setErrors(prevErrors => ({ ...prevErrors, [name]: '' })); setFormFields(prevState => ({ ...prevState, [name]: value })); }; @@ -220,7 +221,7 @@ const RegistrationPage = (props) => { } // Validating form data before submitting - const { isValid, fieldErrors } = isFormValid( + const { isValid, fieldErrors, emailSuggestion } = isFormValid( payload, registrationEmbedded ? temporaryErrors : errors, configurableFormFields, @@ -228,6 +229,7 @@ const RegistrationPage = (props) => { formatMessage, ); setErrors({ ...fieldErrors }); + dispatch(setEmailSuggestionInStore(emailSuggestion)); // returning if not valid if (!isValid) { diff --git a/src/register/RegistrationPage.test.jsx b/src/register/RegistrationPage.test.jsx index 6bc2ce5a89..42d510718a 100644 --- a/src/register/RegistrationPage.test.jsx +++ b/src/register/RegistrationPage.test.jsx @@ -235,6 +235,53 @@ describe('RegistrationPage', () => { expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' })); }); + it('should display an error when form is submitted with an invalid email', () => { + jest.spyOn(global.Date, 'now').mockImplementation(() => 0); + const emailError = 'Enter a valid email address'; + + const formPayload = { + name: 'Petro', + username: 'petro_qa', + email: 'petro @example.com', + password: 'password1', + country: 'Ukraine', + honor_code: true, + totalRegistrationTime: 0, + }; + + store.dispatch = jest.fn(store.dispatch); + const registrationPage = mount(routerWrapper(reduxWrapper())); + populateRequiredFields(registrationPage, formPayload, true); + registrationPage.find('button.btn-brand').simulate('click'); + expect( + registrationPage.find('div[feedback-for="email"]').text(), + ).toEqual(emailError); + }); + + it('should display an error when form is submitted with an invalid username', () => { + jest.spyOn(global.Date, 'now').mockImplementation(() => 0); + const usernameError = 'Usernames can only contain letters (A-Z, a-z), numerals (0-9), ' + + 'underscores (_), and hyphens (-). Usernames cannot contain spaces'; + + const formPayload = { + name: 'Petro', + username: 'petro qa', + email: 'petro@example.com', + password: 'password1', + country: 'Ukraine', + honor_code: true, + totalRegistrationTime: 0, + }; + + store.dispatch = jest.fn(store.dispatch); + const registrationPage = mount(routerWrapper(reduxWrapper())); + populateRequiredFields(registrationPage, formPayload, true); + registrationPage.find('button.btn-brand').simulate('click'); + expect( + registrationPage.find('div[feedback-for="username"]').text(), + ).toEqual(usernameError); + }); + it('should submit form with marketing email opt in value', () => { mergeConfig({ MARKETING_EMAILS_OPT_IN: 'true', diff --git a/src/register/components/ConfigurableRegistrationForm.test.jsx b/src/register/components/ConfigurableRegistrationForm.test.jsx index 282a25eb5b..367da91bf3 100644 --- a/src/register/components/ConfigurableRegistrationForm.test.jsx +++ b/src/register/components/ConfigurableRegistrationForm.test.jsx @@ -11,6 +11,7 @@ import configureStore from 'redux-mock-store'; import ConfigurableRegistrationForm from './ConfigurableRegistrationForm'; import { FIELDS } from '../data/constants'; +import RegistrationPage from '../RegistrationPage'; jest.mock('@edx/frontend-platform/analytics', () => ({ sendPageEvent: jest.fn(), @@ -22,7 +23,20 @@ jest.mock('@edx/frontend-platform/i18n', () => ({ })); const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm); +const IntlRegistrationPage = injectIntl(RegistrationPage); const mockStore = configureStore(); +const populateRequiredFields = (registrationPage, payload, isThirdPartyAuth = false) => { + registrationPage.find('input#name').simulate('change', { target: { value: payload.name, name: 'name' } }); + registrationPage.find('input#username').simulate('change', { target: { value: payload.username, name: 'username' } }); + registrationPage.find('input#email').simulate('change', { target: { value: payload.email, name: 'email' } }); + + registrationPage.find('input[name="country"]').simulate('change', { target: { value: payload.country, name: 'country' } }); + registrationPage.find('input[name="country"]').simulate('blur', { target: { value: payload.country, name: 'country' } }); + + if (!isThirdPartyAuth) { + registrationPage.find('input#password').simulate('change', { target: { value: payload.password, name: 'password' } }); + } +}; jest.mock('react-router-dom', () => { const mockNavigation = jest.fn(); @@ -182,5 +196,52 @@ describe('ConfigurableRegistrationForm', () => { [FIELDS.TERMS_OF_SERVICE]: true, }); }); + + it('should show error if email and confirm email fields do not match on submit click', () => { + const formPayload = { + name: 'Petro', + username: 'petro_qa', + email: 'petro@example.com', + password: 'password1', + country: 'Ukraine', + honor_code: true, + totalRegistrationTime: 0, + }; + + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + fieldDescriptions: { + confirm_email: { + name: 'confirm_email', type: 'text', label: 'Confirm Email', + }, + country: { name: 'country' }, + }, + }, + }); + const registrationPage = mount(routerWrapper(reduxWrapper( + , + ))); + + populateRequiredFields(registrationPage, formPayload, true); + registrationPage.find('input#confirm_email').simulate( + 'change', { target: { value: 'test2@gmail.com', name: 'confirm_email' } }, + ); + + const button = registrationPage.find('button.btn-brand'); + button.simulate('click'); + + registrationPage.update(); + + const confirmEmailErrorElement = registrationPage.find('div#confirm_email-error'); + expect(confirmEmailErrorElement.text()).toEqual('The email addresses do not match.'); + + const validationErrors = registrationPage.find('#validation-errors'); + const firstValidationErrorText = validationErrors.first().text(); + expect(firstValidationErrorText).toContain( + "We couldn't create your account.Please check your responses and try again.", + ); + }); }); }); diff --git a/src/register/components/ThirdPartyAuth.jsx b/src/register/components/ThirdPartyAuth.jsx index 3a8682488c..0d447287d3 100644 --- a/src/register/components/ThirdPartyAuth.jsx +++ b/src/register/components/ThirdPartyAuth.jsx @@ -25,6 +25,7 @@ const ThirdPartyAuth = (props) => { const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider; const isSocialAuthActive = !!providers.length && !currentProvider; const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN; + const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive); return ( <> @@ -34,7 +35,7 @@ const ThirdPartyAuth = (props) => { )} - {thirdPartyAuthApiStatus === PENDING_STATE ? ( + {thirdPartyAuthApiStatus === PENDING_STATE && isThirdPartyAuthActive ? ( ) : ( <> diff --git a/src/register/data/actions.js b/src/register/data/actions.js index d1316ed783..908d8d3c9a 100644 --- a/src/register/data/actions.js +++ b/src/register/data/actions.js @@ -6,6 +6,7 @@ export const REGISTER_NEW_USER = new AsyncActionType('REGISTRATION', 'REGISTER_N export const REGISTER_CLEAR_USERNAME_SUGGESTIONS = 'REGISTRATION_CLEAR_USERNAME_SUGGESTIONS'; export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERROR'; export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE'; +export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS'; export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED'; // Backup registration form @@ -37,6 +38,12 @@ export const fetchRealtimeValidationsFailure = () => ({ type: REGISTER_FORM_VALIDATIONS.FAILURE, }); +// Set email field frontend validations +export const setEmailSuggestionInStore = (emailSuggestion) => ({ + type: REGISTER_SET_EMAIL_SUGGESTIONS, + payload: { emailSuggestion }, +}); + // Register export const registerNewUser = registrationInfo => ({ type: REGISTER_NEW_USER.BASE, diff --git a/src/register/data/reducers.js b/src/register/data/reducers.js index e2cc00956a..82c836c118 100644 --- a/src/register/data/reducers.js +++ b/src/register/data/reducers.js @@ -3,7 +3,9 @@ import { REGISTER_CLEAR_USERNAME_SUGGESTIONS, REGISTER_FORM_VALIDATIONS, REGISTER_NEW_USER, - REGISTER_SET_COUNTRY_CODE, REGISTER_SET_USER_PIPELINE_DATA_LOADED, + REGISTER_SET_COUNTRY_CODE, + REGISTER_SET_EMAIL_SUGGESTIONS, + REGISTER_SET_USER_PIPELINE_DATA_LOADED, REGISTRATION_CLEAR_BACKEND_ERROR, } from './actions'; import { @@ -119,6 +121,15 @@ const reducer = (state = defaultState, action = {}) => { userPipelineDataLoaded: value, }; } + case REGISTER_SET_EMAIL_SUGGESTIONS: { + return { + ...state, + registrationFormData: { + ...state.registrationFormData, + emailSuggestion: action.payload.emailSuggestion, + }, + }; + } default: return { ...state, diff --git a/src/register/data/tests/reducers.test.js b/src/register/data/tests/reducers.test.js index a4bb32bac3..3e2270ab93 100644 --- a/src/register/data/tests/reducers.test.js +++ b/src/register/data/tests/reducers.test.js @@ -7,6 +7,7 @@ import { REGISTER_FORM_VALIDATIONS, REGISTER_NEW_USER, REGISTER_SET_COUNTRY_CODE, + REGISTER_SET_EMAIL_SUGGESTIONS, REGISTER_SET_USER_PIPELINE_DATA_LOADED, REGISTRATION_CLEAR_BACKEND_ERROR, } from '../actions'; @@ -64,6 +65,29 @@ describe('Registration Reducer Tests', () => { }, ); }); + + it('should set email suggestions', () => { + const emailSuggestion = { + type: 'test type', + suggestion: 'test suggestion', + }; + const action = { + type: REGISTER_SET_EMAIL_SUGGESTIONS, + payload: { emailSuggestion }, + }; + + expect(reducer(defaultState, action)).toEqual( + { + ...defaultState, + registrationFormData: { + ...defaultState.registrationFormData, + emailSuggestion: { + type: 'test type', suggestion: 'test suggestion', + }, + }, + }); + }); + it('should set redirect url dashboard on registration success action', () => { const payload = { redirectUrl: `${getConfig().BASE_URL}${DEFAULT_REDIRECT_URL}`, diff --git a/src/register/data/utils.js b/src/register/data/utils.js index ca060512be..36ce469ab5 100644 --- a/src/register/data/utils.js +++ b/src/register/data/utils.js @@ -2,6 +2,9 @@ import { getConfig, snakeCaseObject } from '@edx/frontend-platform'; import { LETTER_REGEX, NUMBER_REGEX } from '../../data/constants'; import messages from '../messages'; +import validateEmail from '../RegistrationFields/EmailField/validator'; +import validateName from '../RegistrationFields/NameField/validator'; +import validateUsername from '../RegistrationFields/UsernameField/validator'; /** * It validates the password field value @@ -35,12 +38,38 @@ export const isFormValid = ( ) => { const fieldErrors = { ...errors }; let isValid = true; + let emailSuggestion = { suggestion: '', type: '' }; Object.keys(payload).forEach(key => { - if (!payload[key]) { - fieldErrors[key] = formatMessage(messages[`empty.${key}.field.error`]); - } - if (fieldErrors[key]) { - isValid = false; + switch (key) { + case 'name': + fieldErrors.name = validateName(payload.name, formatMessage); + if (fieldErrors.name) { isValid = false; } + break; + case 'email': { + const { + fieldError, confirmEmailError, suggestion, + } = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage); + if (fieldError) { + fieldErrors.email = fieldError; + isValid = false; + } + if (confirmEmailError) { + fieldErrors.confirm_email = confirmEmailError; + isValid = false; + } + emailSuggestion = suggestion; + break; + } + case 'username': + fieldErrors.username = validateUsername(payload.username, formatMessage); + if (fieldErrors.username) { isValid = false; } + break; + case 'password': + fieldErrors.password = validatePasswordField(payload.password, formatMessage); + if (fieldErrors.password) { isValid = false; } + break; + default: + break; } }); @@ -56,12 +85,10 @@ export const isFormValid = ( } else if (!configurableFormFields[key]) { fieldErrors[key] = fieldDescriptions[key].error_message; } - if (fieldErrors[key]) { - isValid = false; - } + if (fieldErrors[key]) { isValid = false; } }); - return { isValid, fieldErrors }; + return { isValid, fieldErrors, emailSuggestion }; }; /** diff --git a/src/sass/_style.scss b/src/sass/_style.scss index d15d71cd3d..c62fc3e759 100644 --- a/src/sass/_style.scss +++ b/src/sass/_style.scss @@ -11,7 +11,6 @@ // ---------------------------- // #COLORS // ---------------------------- -$font-blue: #126f9a; $white: #FFFFFF; // social platforms @@ -105,10 +104,10 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15); font-size: 14px; background-color: $white; - border: 1px solid $font-blue; + border: 1px solid $primary; width: 224px; height: 36px; - color: $font-blue; + color: $primary; .btn-tpa__image-icon{ background-color: transparent; @@ -133,7 +132,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15); } .btn-tpa__font-container { - background-color: $font-blue; + background-color: $primary; color: $white; font-size: 11px;