diff --git a/src/common-components/PasswordField.jsx b/src/common-components/PasswordField.jsx index 543d84cb92..7d7210c53f 100644 --- a/src/common-components/PasswordField.jsx +++ b/src/common-components/PasswordField.jsx @@ -138,7 +138,7 @@ const PasswordField = (props) => { {props.errorMessage !== '' && ( {props.errorMessage} - {formatMessage(messages['password.sr.only.helping.text'])} + {props.showScreenReaderText && {formatMessage(messages['password.sr.only.helping.text'])}} )} @@ -153,6 +153,7 @@ PasswordField.defaultProps = { handleChange: () => {}, handleErrorChange: null, showRequirements: true, + showScreenReaderText: true, autoComplete: null, }; @@ -168,6 +169,7 @@ PasswordField.propTypes = { showRequirements: PropTypes.bool, value: PropTypes.string.isRequired, autoComplete: PropTypes.string, + showScreenReaderText: PropTypes.bool, }; export default PasswordField; diff --git a/src/register/components/ThirdPartyAuth.jsx b/src/common-components/ThirdPartyAuth.jsx similarity index 62% rename from src/register/components/ThirdPartyAuth.jsx rename to src/common-components/ThirdPartyAuth.jsx index 3a8682488c..6393cd5963 100644 --- a/src/register/components/ThirdPartyAuth.jsx +++ b/src/common-components/ThirdPartyAuth.jsx @@ -2,17 +2,22 @@ import React from 'react'; import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { + Hyperlink, Icon, +} from '@edx/paragon'; +import { Institution } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; import Skeleton from 'react-loading-skeleton'; +import messages from './messages'; +import { + ENTERPRISE_LOGIN_URL, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE, +} from '../data/constants'; + import { RenderInstitutionButton, SocialAuthProviders, -} from '../../common-components'; -import { - PENDING_STATE, REGISTER_PAGE, -} from '../../data/constants'; -import messages from '../messages'; +} from './index'; /** * This component renders the Single sign-on (SSO) buttons for the providers passed. @@ -20,19 +25,33 @@ import messages from '../messages'; const ThirdPartyAuth = (props) => { const { formatMessage } = useIntl(); const { - providers, secondaryProviders, currentProvider, handleInstitutionLogin, thirdPartyAuthApiStatus, + providers, + secondaryProviders, + currentProvider, + handleInstitutionLogin, + thirdPartyAuthApiStatus, + isLoginPage, } = props; const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider; const isSocialAuthActive = !!providers.length && !currentProvider; const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN; + const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL; return ( <> {((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive) && (
- {formatMessage(messages['registration.other.options.heading'])} + {isLoginPage + ? formatMessage(messages['login.other.options.heading']) + : formatMessage(messages['registration.other.options.heading'])}
)} + {(isLoginPage && !isEnterpriseLoginDisabled && isSocialAuthActive) && ( + + + {formatMessage(messages['enterprise.login.btn.text'])} + + )} {thirdPartyAuthApiStatus === PENDING_STATE ? ( @@ -41,12 +60,15 @@ const ThirdPartyAuth = (props) => { {(isEnterpriseLoginDisabled && isInstitutionAuthActive) && ( )} {isSocialAuthActive && (
- +
)} @@ -59,7 +81,8 @@ ThirdPartyAuth.defaultProps = { currentProvider: null, providers: [], secondaryProviders: [], - thirdPartyAuthApiStatus: 'pending', + thirdPartyAuthApiStatus: PENDING_STATE, + isLoginPage: false, }; ThirdPartyAuth.propTypes = { @@ -86,6 +109,7 @@ ThirdPartyAuth.propTypes = { }), ), thirdPartyAuthApiStatus: PropTypes.string, + isLoginPage: PropTypes.bool, }; export default ThirdPartyAuth; diff --git a/src/common-components/messages.jsx b/src/common-components/messages.jsx index 1bf52e539e..08e88b8f0f 100644 --- a/src/common-components/messages.jsx +++ b/src/common-components/messages.jsx @@ -112,6 +112,26 @@ const messages = defineMessages({ description: 'Select ticket form', defaultMessage: 'Please choose your request type:', }, + 'registration.other.options.heading': { + id: 'registration.other.options.heading', + defaultMessage: 'Or register with:', + description: 'A message that appears above third party auth providers i.e saml, google, facebook etc', + }, + 'institution.login.button': { + id: 'institution.login.button', + defaultMessage: 'Institution/campus credentials', + description: 'shows institutions list', + }, + 'login.other.options.heading': { + id: 'login.other.options.heading', + defaultMessage: 'Or sign in with:', + description: 'Text that appears above other sign in options like social auth buttons', + }, + 'enterprise.login.btn.text': { + id: 'enterprise.login.btn.text', + defaultMessage: 'Company or school credentials', + description: 'Company or school login link text.', + }, }); export default messages; diff --git a/src/login/AccountActivationMessage.jsx b/src/login/AccountActivationMessage.jsx index 2832ddf8ab..3648219912 100644 --- a/src/login/AccountActivationMessage.jsx +++ b/src/login/AccountActivationMessage.jsx @@ -9,29 +9,30 @@ import PropTypes from 'prop-types'; import { ACCOUNT_ACTIVATION_MESSAGE } from './data/constants'; import messages from './messages'; -const AccountActivationMessage = (props) => { +const AccountActivationMessage = ({ messageType }) => { const { formatMessage } = useIntl(); - const { messageType } = props; - const variant = messageType === ACCOUNT_ACTIVATION_MESSAGE.ERROR ? 'danger' : messageType; - - const activationOrVerification = getConfig().MARKETING_EMAILS_OPT_IN ? 'confirmation' : 'activation'; - let activationMessage; - let heading; + if (!messageType) { + return null; + } + const variant = messageType === ACCOUNT_ACTIVATION_MESSAGE.ERROR ? 'danger' : messageType; + const activationOrConfirmation = getConfig().MARKETING_EMAILS_OPT_IN ? 'confirmation' : 'activation'; const iconMapping = { [ACCOUNT_ACTIVATION_MESSAGE.SUCCESS]: CheckCircle, [ACCOUNT_ACTIVATION_MESSAGE.ERROR]: Error, }; + let activationMessage; + let heading; switch (messageType) { case ACCOUNT_ACTIVATION_MESSAGE.SUCCESS: { - heading = formatMessage(messages[`account.${activationOrVerification}.success.message.title`]); - activationMessage = {formatMessage(messages[`account.${activationOrVerification}.success.message`])}; + heading = formatMessage(messages[`account.${activationOrConfirmation}.success.message.title`]); + activationMessage = {formatMessage(messages[`account.${activationOrConfirmation}.success.message`])}; break; } case ACCOUNT_ACTIVATION_MESSAGE.INFO: { - activationMessage = formatMessage(messages[`account.${activationOrVerification}.info.message`]); + activationMessage = formatMessage(messages[`account.${activationOrConfirmation}.info.message`]); break; } case ACCOUNT_ACTIVATION_MESSAGE.ERROR: { @@ -41,7 +42,7 @@ const AccountActivationMessage = (props) => { ); - heading = formatMessage(messages[`account.${activationOrVerification}.error.message.title`]); + heading = formatMessage(messages[`account.${activationOrConfirmation}.error.message.title`]); activationMessage = ( { return activationMessage ? ( @@ -70,7 +71,11 @@ const AccountActivationMessage = (props) => { }; AccountActivationMessage.propTypes = { - messageType: PropTypes.string.isRequired, + messageType: PropTypes.string, +}; + +AccountActivationMessage.defaultProps = { + messageType: null, }; export default AccountActivationMessage; diff --git a/src/login/LoginFailure.jsx b/src/login/LoginFailure.jsx index 4e630b5f4a..4d3338253b 100644 --- a/src/login/LoginFailure.jsx +++ b/src/login/LoginFailure.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { getConfig } from '@edx/frontend-platform'; import { getAuthService } from '@edx/frontend-platform/auth'; @@ -23,22 +23,35 @@ import { TPA_AUTHENTICATION_FAILURE, } from './data/constants'; import messages from './messages'; +import { windowScrollTo } from '../data/utils'; const LoginFailureMessage = (props) => { const { formatMessage } = useIntl(); - const { context, errorCode } = props.loginError; - const authService = getAuthService(); - let errorList; + const { + context, + errorCode, + errorCount, // This is used to trigger the useEffect, facilitating the scrolling to the top. + } = props; + + useEffect(() => { + windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }); + }, [errorCode, errorCount]); + + if (!errorCode) { + return null; + } + let resetLink = ( {formatMessage(messages['login.incorrect.credentials.error.reset.link.text'])} ); + let errorMessage; switch (errorCode) { case NON_COMPLIANT_PASSWORD_EXCEPTION: { - errorList = ( + errorMessage = ( <> {formatMessage(messages['non.compliant.password.title'])}

{formatMessage(messages['non.compliant.password.message'])}

@@ -47,7 +60,7 @@ const LoginFailureMessage = (props) => { break; } case FORBIDDEN_REQUEST: - errorList =

{formatMessage(messages['login.rate.limit.reached.message'])}

; + errorMessage =

{formatMessage(messages['login.rate.limit.reached.message'])}

; break; case INACTIVE_USER: { const supportLink = ( @@ -55,7 +68,7 @@ const LoginFailureMessage = (props) => { {formatMessage(messages['contact.support.link'], { platformName: context.platformName })} ); - errorList = ( + errorMessage = (

{ check your spam folders or {supportLink}." values={{ lineBreak:
, - email: {props.loginError.email}, + email: {context.email}, supportLink, }} /> @@ -79,7 +92,7 @@ const LoginFailureMessage = (props) => { {formatMessage(messages['tpa.account.link'], { provider: context.provider })} ); - errorList = ( + errorMessage = (

{ break; } case INVALID_FORM: - errorList =

{formatMessage(messages['login.form.invalid.error.message'])}

; + errorMessage =

{formatMessage(messages['login.form.invalid.error.message'])}

; break; case FAILED_LOGIN_ATTEMPT: { resetLink = ( @@ -100,7 +113,7 @@ const LoginFailureMessage = (props) => { {formatMessage(messages['login.incorrect.credentials.error.before.account.blocked.text'])} ); - errorList = ( + errorMessage = ( <>

{ break; } case ACCOUNT_LOCKED_OUT: { - errorList = ( + errorMessage = ( <>

{formatMessage(messages['account.locked.out.message.1'])}

@@ -141,9 +154,9 @@ const LoginFailureMessage = (props) => { } case INCORRECT_EMAIL_PASSWORD: if (context.failureCount <= 1) { - errorList =

{formatMessage(messages['login.incorrect.credentials.error'])}

; + errorMessage =

{formatMessage(messages['login.incorrect.credentials.error'])}

; } else if (context.failureCount === 2) { - errorList = ( + errorMessage = (

{ } return ( ); case REQUIRE_PASSWORD_CHANGE: return ; case TPA_AUTHENTICATION_FAILURE: - errorList = ( -

{formatMessage(messages['login.tpa.authentication.failure'], { - platform_name: getConfig().SITE_NAME, - lineBreak:
, - errorMessage: context.errorMessage, - })} + errorMessage = ( +

+ {formatMessage(messages['login.tpa.authentication.failure'], { + platform_name: getConfig().SITE_NAME, + lineBreak:
, + errorMessage: context.errorMessage, + })}

); break; case INTERNAL_SERVER_ERROR: default: - errorList =

{formatMessage(messages['internal.server.error.message'])}

; + errorMessage =

{formatMessage(messages['internal.server.error.message'])}

; break; } return ( {formatMessage(messages['login.failure.header.title'])} - { errorList } + { errorMessage } ); }; LoginFailureMessage.defaultProps = { - loginError: { - redirectUrl: null, - errorCode: null, - errorMessage: null, - }, + context: {}, }; LoginFailureMessage.propTypes = { - loginError: PropTypes.shape({ - context: PropTypes.shape({ - supportLink: PropTypes.string, - platformName: PropTypes.string, - tpaHint: PropTypes.string, - provider: PropTypes.string, - allowedDomain: PropTypes.string, - remainingAttempts: PropTypes.number, - failureCount: PropTypes.number, - errorMessage: PropTypes.string, - }), + context: PropTypes.shape({ + supportLink: PropTypes.string, + platformName: PropTypes.string, + tpaHint: PropTypes.string, + provider: PropTypes.string, + allowedDomain: PropTypes.string, + remainingAttempts: PropTypes.number, + failureCount: PropTypes.number, + errorMessage: PropTypes.string, email: PropTypes.string, - errorCode: PropTypes.string, redirectUrl: PropTypes.string, }), + errorCode: PropTypes.string.isRequired, + errorCount: PropTypes.number.isRequired, }; export default LoginFailureMessage; diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx index e3fdd29f2f..3dd4eec005 100644 --- a/src/login/LoginPage.jsx +++ b/src/login/LoginPage.jsx @@ -1,13 +1,12 @@ -import React from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { connect } from 'react-redux'; import { getConfig } from '@edx/frontend-platform'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { injectIntl } from '@edx/frontend-platform/i18n'; +import { injectIntl, useIntl } from '@edx/frontend-platform/i18n'; import { - Form, Hyperlink, Icon, StatefulButton, + Form, StatefulButton, } from '@edx/paragon'; -import { Institution } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import Skeleton from 'react-loading-skeleton'; @@ -15,21 +14,26 @@ import { Link } from 'react-router-dom'; import AccountActivationMessage from './AccountActivationMessage'; import { - loginRemovePasswordResetBanner, loginRequest, loginRequestFailure, loginRequestReset, setLoginFormData, + backupLoginFormBegin, + dismissPasswordResetBanner, + loginRequest, } from './data/actions'; import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants'; -import { loginErrorSelector, loginFormDataSelector, loginRequestSelector } from './data/selectors'; import LoginFailureMessage from './LoginFailure'; import messages from './messages'; import { - FormGroup, InstitutionLogistration, PasswordField, RedirectLogistration, - RenderInstitutionButton, SocialAuthProviders, ThirdPartyAuthAlert, + FormGroup, + InstitutionLogistration, + PasswordField, + RedirectLogistration, + ThirdPartyAuthAlert, } from '../common-components'; import { getThirdPartyAuthContext } from '../common-components/data/actions'; import { thirdPartyAuthContextSelector } from '../common-components/data/selectors'; import EnterpriseSSO from '../common-components/EnterpriseSSO'; +import ThirdPartyAuth from '../common-components/ThirdPartyAuth'; import { - DEFAULT_STATE, ENTERPRISE_LOGIN_URL, PENDING_STATE, RESET_PAGE, + DEFAULT_STATE, PENDING_STATE, RESET_PAGE, } from '../data/constants'; import { getActivationStatus, @@ -37,359 +41,281 @@ import { getTpaHint, getTpaProvider, updatePathWithQueryParams, - windowScrollTo, } from '../data/utils'; import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess'; -class LoginPage extends React.Component { - constructor(props, context) { - super(props, context); - this.state = { - password: this.props.loginFormData.password, - emailOrUsername: this.props.loginFormData.emailOrUsername, - errors: { - emailOrUsername: this.props.loginFormData.errors.emailOrUsername, - password: this.props.loginFormData.errors.password, - }, - isSubmitted: false, - }; - this.queryParams = getAllPossibleQueryParams(); - this.tpaHint = getTpaHint(); - } +const LoginPage = (props) => { + const { + backedUpFormData, + loginErrorCode, + loginErrorContext, + loginResult, + shouldBackupState, + thirdPartyAuthContext: { + providers, + currentProvider, + secondaryProviders, + finishAuthUrl, + platformName, + errorMessage: thirdPartyErrorMessage, + }, + thirdPartyAuthApiStatus, + institutionLogin, + showResetPasswordSuccessBanner, + submitState, + // Actions + backupFormState, + handleInstitutionLogin, + getTPADataFromBackend, + } = props; + const { formatMessage } = useIntl(); + const activationMsgType = getActivationStatus(); + const queryParams = useMemo(() => getAllPossibleQueryParams(), []); + + const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields }); + const [errorCode, setErrorCode] = useState({ type: '', count: 0, context: {} }); + const [errors, setErrors] = useState({ ...backedUpFormData.errors }); + const tpaHint = getTpaHint(); - componentDidMount() { + useEffect(() => { sendPageEvent('login_and_registration', 'login'); - const payload = { ...this.queryParams }; + }, []); - if (this.tpaHint) { - payload.tpa_hint = this.tpaHint; + useEffect(() => { + const payload = { ...queryParams }; + if (tpaHint) { + payload.tpa_hint = tpaHint; } - this.props.getThirdPartyAuthContext(payload); - this.props.loginRequestReset(); - } - - shouldComponentUpdate(nextProps) { - if (nextProps.loginFormData && this.props.loginFormData !== nextProps.loginFormData) { - // Ensuring browser's autofill user credentials get filled and their state persists in the redux store. - const nextState = { - emailOrUsername: nextProps.loginFormData.emailOrUsername || this.state.emailOrUsername, - password: nextProps.loginFormData.password || this.state.password, - }; - this.setState({ - ...nextProps.loginFormData, - ...nextState, + getTPADataFromBackend(payload); + }, [getTPADataFromBackend, queryParams, tpaHint]); + /** + * Backup the login form in redux when login page is toggled. + */ + useEffect(() => { + if (shouldBackupState) { + backupFormState({ + formFields: { ...formFields }, + errors: { ...errors }, }); - return false; } - return true; - } + }, [shouldBackupState, formFields, errors, backupFormState]); - componentWillUnmount() { - if (this.props.resetPassword) { - this.props.loginRemovePasswordResetBanner(); + useEffect(() => { + if (loginErrorCode) { + setErrorCode(prevState => ({ + type: loginErrorCode, + count: prevState.count + 1, + context: { ...loginErrorContext }, + })); } - } + }, [loginErrorCode, loginErrorContext]); - getEnterPriseLoginURL() { - return getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL; - } + useEffect(() => { + if (thirdPartyErrorMessage) { + setErrorCode((prevState) => ({ + type: TPA_AUTHENTICATION_FAILURE, + count: prevState.count + 1, + context: { + errorMessage: thirdPartyErrorMessage, + }, + })); + } + }, [thirdPartyErrorMessage]); + + const validateFormFields = (payload) => { + const { emailOrUsername, password } = payload; + const fieldErrors = { ...errors }; - handleSubmit = (e) => { - e.preventDefault(); - if (this.props.resetPassword) { - this.props.loginRemovePasswordResetBanner(); + if (emailOrUsername === '') { + fieldErrors.emailOrUsername = formatMessage(messages['email.validation.message']); + } else if (emailOrUsername.length < 3) { + fieldErrors.emailOrUsername = formatMessage(messages['username.or.email.format.validation.less.chars.message']); + } + if (password === '') { + fieldErrors.password = formatMessage(messages['password.validation.message']); } - this.setState({ isSubmitted: true }); - const { emailOrUsername, password } = this.state; - const emailValidationError = this.validateEmail(emailOrUsername); - const passwordValidationError = this.validatePassword(password); - if (emailValidationError !== '' || passwordValidationError !== '') { - this.props.setLoginFormData({ - errors: { - emailOrUsername: emailValidationError, - password: passwordValidationError, - }, - }); - this.props.loginRequestFailure({ - errorCode: INVALID_FORM, - }); + return { ...fieldErrors }; + }; + + const handleSubmit = (event) => { + event.preventDefault(); + if (showResetPasswordSuccessBanner) { + props.dismissPasswordResetBanner(); + } + + const formData = { ...formFields }; + const validationErrors = validateFormFields(formData); + if (validationErrors.emailOrUsername || validationErrors.password) { + setErrors({ ...validationErrors }); + setErrorCode(prevState => ({ type: INVALID_FORM, count: prevState.count + 1, context: {} })); return; } + // add query params to the payload const payload = { - email_or_username: emailOrUsername, password, ...this.queryParams, + email_or_username: formData.emailOrUsername, + password: formData.password, + ...queryParams, }; - this.props.loginRequest(payload); + props.loginRequest(payload); }; - handleOnFocus = (e) => { - const { errors } = this.state; - errors[e.target.name] = ''; - this.props.setLoginFormData({ - errors, - }); + const handleOnChange = (event) => { + const { name, value } = event.target; + setFormFields(prevState => ({ ...prevState, [name]: value })); }; - handleOnBlur = (e) => { - const payload = { - [e.target.name]: e.target.value, - }; - this.props.setLoginFormData(payload); + const handleOnFocus = (event) => { + const { name } = event.target; + setErrors(prevErrors => ({ ...prevErrors, [name]: '' })); }; - - handleForgotPasswordLinkClickEvent = () => { + const trackForgotPasswordLinkClick = () => { sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' }); }; - validateEmail(email) { - const { errors } = this.state; + const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders); - if (email === '') { - errors.emailOrUsername = this.props.intl.formatMessage(messages['email.validation.message']); - } else if (email.length < 3) { - errors.emailOrUsername = this.props.intl.formatMessage(messages['username.or.email.format.validation.less.chars.message']); - } else { - errors.emailOrUsername = ''; + if (tpaHint) { + if (thirdPartyAuthApiStatus === PENDING_STATE) { + return ; } - return errors.emailOrUsername; - } - validatePassword(password) { - const { errors } = this.state; - errors.password = password.length > 0 ? '' : this.props.intl.formatMessage(messages['password.validation.message']); + if (skipHintedLogin) { + window.location.href = getConfig().LMS_BASE_URL + provider.loginUrl; + return null; + } - return errors.password; + if (provider) { + return ; + } } - renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl) { - const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider; - const isSocialAuthActive = !!providers.length && !currentProvider; - const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN; - + if (institutionLogin) { return ( - <> - {(isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive)) - && ( -
- {intl.formatMessage(messages['login.other.options.heading'])} -
- )} - - {(!isEnterpriseLoginDisabled && isSocialAuthActive) && ( - - - {intl.formatMessage(messages['enterprise.login.btn.text'])} - - )} - - {thirdPartyAuthApiStatus === PENDING_STATE ? ( - - ) : ( - <> - {(isEnterpriseLoginDisabled && isInstitutionAuthActive) && ( - - )} - {isSocialAuthActive && ( -
- -
- )} - - )} - + ); } - - renderForm( - currentProvider, - providers, - secondaryProviders, - thirdPartyAuthContext, - thirdPartyAuthApiStatus, - submitState, - intl, - ) { - const activationMsgType = getActivationStatus(); - if (this.props.institutionLogin) { - return ( - + + {formatMessage(messages['login.page.title'], { siteName: getConfig().SITE_NAME })} + + +
+ - ); - } - const tpaAuthenticationError = {}; - if (thirdPartyAuthContext.errorMessage) { - tpaAuthenticationError.context = { - errorMessage: thirdPartyAuthContext.errorMessage, - }; - tpaAuthenticationError.errorCode = TPA_AUTHENTICATION_FAILURE; - } - - return ( - <> - - {intl.formatMessage(messages['login.page.title'], - { siteName: getConfig().SITE_NAME })} - - - + -
- } +
+ - {this.props.loginError ? : null} - {thirdPartyAuthContext.errorMessage ? : null} - {submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null} - {activationMsgType && } - {this.props.resetPassword && !this.props.loginError ? : null} - - this.setState({ emailOrUsername: e.target.value, isSubmitted: false })} - handleFocus={this.handleOnFocus} - handleBlur={this.handleOnBlur} - errorMessage={this.state.errors.emailOrUsername} - floatingLabel={intl.formatMessage(messages['login.user.identity.label'])} - /> - this.setState({ password: e.target.value, isSubmitted: false })} - handleFocus={this.handleOnFocus} - handleBlur={this.handleOnBlur} - errorMessage={this.state.errors.password} - floatingLabel={intl.formatMessage(messages['login.password.label'])} - /> - e.preventDefault()} - /> - - {intl.formatMessage(messages['forgot.password'])} - - {this.renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl)} - -
- - ); - } - - render() { - const { - intl, submitState, thirdPartyAuthContext, thirdPartyAuthApiStatus, - } = this.props; - const { currentProvider, providers, secondaryProviders } = this.props.thirdPartyAuthContext; - - if (this.tpaHint) { - if (thirdPartyAuthApiStatus === PENDING_STATE) { - return ; - } - const { provider, skipHintedLogin } = getTpaProvider(this.tpaHint, providers, secondaryProviders); - if (skipHintedLogin) { - window.location.href = getConfig().LMS_BASE_URL + provider.loginUrl; - return null; - } - return provider ? () : this.renderForm( - currentProvider, - providers, - secondaryProviders, - thirdPartyAuthContext, - thirdPartyAuthApiStatus, - submitState, - intl, - ); - } - return this.renderForm( - currentProvider, - providers, - secondaryProviders, - thirdPartyAuthContext, - thirdPartyAuthApiStatus, - submitState, - intl, - ); - } -} + + event.preventDefault()} + /> + + {formatMessage(messages['forgot.password'])} + + + +
+ + ); +}; -LoginPage.defaultProps = { - loginResult: null, - loginError: null, - loginFormData: { - emailOrUsername: '', - password: '', - errors: { - emailOrUsername: '', - password: '', - }, - }, - resetPassword: false, - submitState: DEFAULT_STATE, - thirdPartyAuthApiStatus: 'pending', - thirdPartyAuthContext: { - currentProvider: null, - errorMessage: null, - finishAuthUrl: null, - providers: [], - secondaryProviders: [], - }, +const mapStateToProps = state => { + const loginPageState = state.login; + return { + backedUpFormData: loginPageState.loginFormData, + loginErrorCode: loginPageState.loginErrorCode, + loginErrorContext: loginPageState.loginErrorContext, + loginResult: loginPageState.loginResult, + shouldBackupState: loginPageState.shouldBackupState, + showResetPasswordSuccessBanner: loginPageState.showResetPasswordSuccessBanner, + submitState: loginPageState.submitState, + thirdPartyAuthContext: thirdPartyAuthContextSelector(state), + thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus, + }; }; LoginPage.propTypes = { - getThirdPartyAuthContext: PropTypes.func.isRequired, - intl: PropTypes.shape({ - formatMessage: PropTypes.func, - }).isRequired, - loginError: PropTypes.shape({}), - loginRequest: PropTypes.func.isRequired, - loginRequestFailure: PropTypes.func.isRequired, - loginRequestReset: PropTypes.func.isRequired, - setLoginFormData: PropTypes.func.isRequired, - loginRemovePasswordResetBanner: PropTypes.func.isRequired, + backedUpFormData: PropTypes.shape({ + formFields: PropTypes.shape({}), + errors: PropTypes.shape({}), + }), + loginErrorCode: PropTypes.string, + loginErrorContext: PropTypes.shape({ + email: PropTypes.string, + redirectUrl: PropTypes.string, + context: PropTypes.shape({}), + }), loginResult: PropTypes.shape({ redirectUrl: PropTypes.string, success: PropTypes.bool, }), - loginFormData: PropTypes.shape({ - emailOrUsername: PropTypes.string, - password: PropTypes.string, - errors: PropTypes.shape({ - emailOrUsername: PropTypes.string, - password: PropTypes.string, - }), - }), - resetPassword: PropTypes.bool, + shouldBackupState: PropTypes.bool, + showResetPasswordSuccessBanner: PropTypes.bool, submitState: PropTypes.string, thirdPartyAuthApiStatus: PropTypes.string, + institutionLogin: PropTypes.bool.isRequired, thirdPartyAuthContext: PropTypes.shape({ currentProvider: PropTypes.string, errorMessage: PropTypes.string, @@ -398,34 +324,45 @@ LoginPage.propTypes = { secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})), finishAuthUrl: PropTypes.string, }), - institutionLogin: PropTypes.bool.isRequired, + // Actions + backupFormState: PropTypes.func.isRequired, + dismissPasswordResetBanner: PropTypes.func.isRequired, + loginRequest: PropTypes.func.isRequired, + getTPADataFromBackend: PropTypes.func.isRequired, handleInstitutionLogin: PropTypes.func.isRequired, }; -const mapStateToProps = state => { - const loginResult = loginRequestSelector(state); - const thirdPartyAuthContext = thirdPartyAuthContextSelector(state); - const loginError = loginErrorSelector(state); - const loginFormData = loginFormDataSelector(state); - return { - submitState: state.login.submitState, - thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus, - loginError, - loginResult, - thirdPartyAuthContext, - loginFormData, - resetPassword: state.login.resetPassword, - }; +LoginPage.defaultProps = { + backedUpFormData: { + formFields: { + emailOrUsername: '', password: '', + }, + errors: { + emailOrUsername: '', password: '', + }, + }, + loginErrorCode: null, + loginErrorContext: {}, + loginResult: {}, + shouldBackupState: false, + showResetPasswordSuccessBanner: false, + submitState: DEFAULT_STATE, + thirdPartyAuthApiStatus: PENDING_STATE, + thirdPartyAuthContext: { + currentProvider: null, + errorMessage: null, + finishAuthUrl: null, + providers: [], + secondaryProviders: [], + }, }; export default connect( mapStateToProps, { - getThirdPartyAuthContext, + backupFormState: backupLoginFormBegin, + dismissPasswordResetBanner, loginRequest, - loginRequestFailure, - loginRequestReset, - setLoginFormData, - loginRemovePasswordResetBanner, + getTPADataFromBackend: getThirdPartyAuthContext, }, )(injectIntl(LoginPage)); diff --git a/src/login/data/actions.js b/src/login/data/actions.js index d9a547f2ae..c9b1dddef9 100644 --- a/src/login/data/actions.js +++ b/src/login/data/actions.js @@ -1,8 +1,18 @@ import { AsyncActionType } from '../../data/utils'; +export const BACKUP_LOGIN_DATA = new AsyncActionType('LOGIN', 'BACKUP_LOGIN_DATA'); export const LOGIN_REQUEST = new AsyncActionType('LOGIN', 'REQUEST'); -export const LOGIN_PERSIST_FORM_DATA = 'LOGIN_PERSIST_FORM_DATA'; -export const LOGIN_REMOVE_PASSWORD_RESET_BANNER = 'LOGIN_REMOVE_PASSWORD_RESET_BANNER'; +export const DISMISS_PASSWORD_RESET_BANNER = 'DISMISS_PASSWORD_RESET_BANNER'; + +// Backup login form data +export const backupLoginForm = () => ({ + type: BACKUP_LOGIN_DATA.BASE, +}); + +export const backupLoginFormBegin = (data) => ({ + type: BACKUP_LOGIN_DATA.BEGIN, + payload: { ...data }, +}); // Login export const loginRequest = creds => ({ @@ -24,15 +34,6 @@ export const loginRequestFailure = (loginError) => ({ payload: { loginError }, }); -export const loginRequestReset = () => ({ - type: LOGIN_REQUEST.RESET, -}); - -export const setLoginFormData = (formData) => ({ - type: LOGIN_PERSIST_FORM_DATA, - payload: { formData }, -}); - -export const loginRemovePasswordResetBanner = () => ({ - type: LOGIN_REMOVE_PASSWORD_RESET_BANNER, +export const dismissPasswordResetBanner = () => ({ + type: DISMISS_PASSWORD_RESET_BANNER, }); diff --git a/src/login/data/reducers.js b/src/login/data/reducers.js index dc7069b505..d15d4497a1 100644 --- a/src/login/data/reducers.js +++ b/src/login/data/reducers.js @@ -1,64 +1,69 @@ -import { LOGIN_PERSIST_FORM_DATA, LOGIN_REMOVE_PASSWORD_RESET_BANNER, LOGIN_REQUEST } from './actions'; +import { + BACKUP_LOGIN_DATA, + DISMISS_PASSWORD_RESET_BANNER, + LOGIN_REQUEST, +} from './actions'; import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants'; import { RESET_PASSWORD } from '../../reset-password'; export const defaultState = { - loginError: null, + loginErrorCode: '', + loginErrorContext: {}, loginResult: {}, - resetPassword: false, loginFormData: { - password: '', - emailOrUsername: '', + formFields: { + emailOrUsername: '', password: '', + }, errors: { - emailOrUsername: '', - password: '', + emailOrUsername: '', password: '', }, }, + shouldBackupState: false, + showResetPasswordSuccessBanner: false, + submitState: DEFAULT_STATE, }; const reducer = (state = defaultState, action = {}) => { switch (action.type) { + case BACKUP_LOGIN_DATA.BASE: + return { + ...state, + shouldBackupState: true, + }; + case BACKUP_LOGIN_DATA.BEGIN: + return { + ...defaultState, + loginFormData: { ...action.payload }, + }; case LOGIN_REQUEST.BEGIN: return { ...state, + showResetPasswordSuccessBanner: false, submitState: PENDING_STATE, - resetPassword: false, }; case LOGIN_REQUEST.SUCCESS: return { ...state, loginResult: action.payload, }; - case LOGIN_REQUEST.FAILURE: + case LOGIN_REQUEST.FAILURE: { + const { email, loginError, redirectUrl } = action.payload; return { ...state, - loginError: action.payload.loginError, + loginErrorCode: loginError.errorCode, + loginErrorContext: { ...loginError.context, email, redirectUrl }, submitState: DEFAULT_STATE, }; - case LOGIN_REQUEST.RESET: - return { - ...state, - loginError: null, - }; + } case RESET_PASSWORD.SUCCESS: return { ...state, - resetPassword: true, + showResetPasswordSuccessBanner: true, }; - case LOGIN_PERSIST_FORM_DATA: { - const { formData } = action.payload; - return { - ...state, - loginFormData: { - ...state.loginFormData, - ...formData, - }, - }; - } - case LOGIN_REMOVE_PASSWORD_RESET_BANNER: { + case DISMISS_PASSWORD_RESET_BANNER: { return { ...state, - resetPassword: false, + showResetPasswordSuccessBanner: false, }; } default: diff --git a/src/login/data/selectors.js b/src/login/data/selectors.js deleted file mode 100644 index e060680843..0000000000 --- a/src/login/data/selectors.js +++ /dev/null @@ -1,20 +0,0 @@ -import { createSelector } from 'reselect'; - -export const storeName = 'login'; - -export const loginSelector = state => ({ ...state[storeName] }); - -export const loginRequestSelector = createSelector( - loginSelector, - login => login.loginResult, -); - -export const loginErrorSelector = createSelector( - loginSelector, - login => login.loginError, -); - -export const loginFormDataSelector = createSelector( - loginSelector, - login => login.loginFormData, -); diff --git a/src/login/data/tests/reducers.test.js b/src/login/data/tests/reducers.test.js index d9887fbc62..c691319577 100644 --- a/src/login/data/tests/reducers.test.js +++ b/src/login/data/tests/reducers.test.js @@ -1,57 +1,154 @@ -import { - LOGIN_PERSIST_FORM_DATA, LOGIN_REMOVE_PASSWORD_RESET_BANNER, -} from '../actions'; +import { getConfig } from '@edx/frontend-platform'; + +import { DEFAULT_REDIRECT_URL, DEFAULT_STATE, PENDING_STATE } from '../../../data/constants'; +import { RESET_PASSWORD } from '../../../reset-password'; +import { BACKUP_LOGIN_DATA, DISMISS_PASSWORD_RESET_BANNER, LOGIN_REQUEST } from '../actions'; import reducer from '../reducers'; describe('login reducer', () => { - it('should set loginFormData', () => { - const state = { - loginFormData: { - password: '', - emailOrUsername: '', - errors: { - emailOrUsername: '', - password: '', - }, + const defaultState = { + loginErrorCode: '', + loginErrorContext: {}, + loginResult: {}, + loginFormData: { + formFields: { + emailOrUsername: '', password: '', }, - resetPassword: false, - }; - const formData = { - password: 'johndoe', - emailOrUsername: 'john@gmail.com', + errors: { + emailOrUsername: '', password: '', + }, + }, + shouldBackupState: false, + showResetPasswordSuccessBanner: false, + submitState: DEFAULT_STATE, + }; + + it('should update state to show reset password success banner', () => { + const action = { + type: RESET_PASSWORD.SUCCESS, }; + + expect( + reducer(defaultState, action), + ).toEqual( + { + ...defaultState, + showResetPasswordSuccessBanner: true, + }, + ); + }); + + it('should set the flag which keeps the login form data in redux state', () => { const action = { - type: LOGIN_PERSIST_FORM_DATA, - payload: { formData }, + type: BACKUP_LOGIN_DATA.BASE, }; expect( - reducer(state, action), + reducer(defaultState, action), ).toEqual( { - loginFormData: { - ...state.loginFormData, - password: 'johndoe', - emailOrUsername: 'john@gmail.com', - }, - resetPassword: false, + ...defaultState, + shouldBackupState: true, }, ); }); - it('should set resetPassword', () => { - const state = { - resetPassword: true, + it('should backup the login form data', () => { + const payload = { + formFields: { + emailOrUsername: 'test@exmaple.com', + password: 'test1', + }, + errors: { + emailOrUsername: '', password: '', + }, }; const action = { - type: LOGIN_REMOVE_PASSWORD_RESET_BANNER, + type: BACKUP_LOGIN_DATA.BEGIN, + payload, + }; + + expect( + reducer(defaultState, action), + ).toEqual( + { + ...defaultState, + loginFormData: payload, + }, + ); + }); + + it('should update state to dismiss reset password banner', () => { + const action = { + type: DISMISS_PASSWORD_RESET_BANNER, }; expect( - reducer(state, action), + reducer(defaultState, action), ).toEqual( { - resetPassword: false, + ...defaultState, + showResetPasswordSuccessBanner: false, + }, + ); + }); + + it('should start the login request', () => { + const action = { + type: LOGIN_REQUEST.BEGIN, + }; + + expect(reducer(defaultState, action)).toEqual( + { + ...defaultState, + showResetPasswordSuccessBanner: false, + submitState: PENDING_STATE, + }, + ); + }); + + it('should set redirect url on login success action', () => { + const payload = { + redirectUrl: `${getConfig().BASE_URL}${DEFAULT_REDIRECT_URL}`, + success: true, + }; + const action = { + type: LOGIN_REQUEST.SUCCESS, + payload, + }; + + expect(reducer(defaultState, action)).toEqual( + { + ...defaultState, + loginResult: payload, + }, + ); + }); + + it('should set the error data on login request failure', () => { + const payload = { + loginError: { + success: false, + value: 'Email or password is incorrect.', + errorCode: 'incorrect-email-or-password', + context: { + failureCount: 0, + }, + }, + email: 'test@example.com', + redirectUrl: '', + }; + const action = { + type: LOGIN_REQUEST.FAILURE, + payload, + }; + + expect(reducer(defaultState, action)).toEqual( + { + ...defaultState, + loginErrorCode: payload.loginError.errorCode, + loginErrorContext: { ...payload.loginError.context, email: payload.email, redirectUrl: payload.redirectUrl }, + submitState: DEFAULT_STATE, }, ); }); diff --git a/src/login/index.js b/src/login/index.js index 769161d28c..4e73d45d44 100644 --- a/src/login/index.js +++ b/src/login/index.js @@ -1,4 +1,5 @@ +export const storeName = 'login'; + export { default as LoginPage } from './LoginPage'; export { default as reducer } from './data/reducers'; export { default as saga } from './data/sagas'; -export { storeName } from './data/selectors'; diff --git a/src/login/messages.jsx b/src/login/messages.jsx index 5d6581032c..dcf09c1b31 100644 --- a/src/login/messages.jsx +++ b/src/login/messages.jsx @@ -42,11 +42,6 @@ const messages = defineMessages({ defaultMessage: 'Choose your institution from the list below', description: 'Heading of the institutions list', }, - 'login.other.options.heading': { - id: 'login.other.options.heading', - defaultMessage: 'Or sign in with:', - description: 'Text that appears above other sign in options like social auth buttons', - }, 'non.compliant.password.title': { id: 'non.compliant.password.title', defaultMessage: 'We recently changed our password requirements', @@ -64,11 +59,6 @@ const messages = defineMessages({ defaultMessage: 'To protect your account, it\'s been temporarily locked. Try again in 30 minutes.', description: 'Part of message for when user account has been locked out after multiple failed login attempts', }, - 'enterprise.login.btn.text': { - id: 'enterprise.login.btn.text', - defaultMessage: 'Company or school credentials', - description: 'Company or school login link text.', - }, 'username.or.email.format.validation.less.chars.message': { id: 'username.or.email.format.validation.less.chars.message', defaultMessage: 'Username or email must have at least 3 characters.', diff --git a/src/login/tests/LoginFailure.test.jsx b/src/login/tests/LoginFailure.test.jsx index dc97fccb12..c77fafb172 100644 --- a/src/login/tests/LoginFailure.test.jsx +++ b/src/login/tests/LoginFailure.test.jsx @@ -7,6 +7,7 @@ import { import { MemoryRouter } from 'react-router-dom'; import { + ACCOUNT_LOCKED_OUT, ALLOWED_DOMAIN_LOGIN_ERROR, FAILED_LOGIN_ATTEMPT, FORBIDDEN_REQUEST, @@ -41,9 +42,8 @@ describe('LoginFailureMessage', () => { it('should match non compliant password error message', () => { props = { - loginError: { - errorCode: NON_COMPLIANT_PASSWORD_EXCEPTION, - }, + errorCode: NON_COMPLIANT_PASSWORD_EXCEPTION, + failureCount: 0, }; render( @@ -65,14 +65,13 @@ describe('LoginFailureMessage', () => { it('should match inactive user error message', () => { props = { - loginError: { + context: { email: 'text@example.com', - errorCode: INACTIVE_USER, - context: { - platformName: 'openedX', - supportLink: 'http://support.openedx.test', - }, + platformName: 'openedX', + supportLink: 'http://support.openedx.test', }, + errorCode: INACTIVE_USER, + failureCount: 0, }; render( @@ -95,15 +94,14 @@ describe('LoginFailureMessage', () => { it('test match failed login attempt error', () => { props = { - loginError: { + context: { email: 'text@example.com', - errorCode: FAILED_LOGIN_ATTEMPT, - context: { - remainingAttempts: 3, - allowedFailureAttempts: 6, - resetLink: '/reset', - }, + remainingAttempts: 3, + allowedFailureAttempts: 6, + resetLink: '/reset', }, + errorCode: FAILED_LOGIN_ATTEMPT, + failureCount: 0, }; render( @@ -123,14 +121,13 @@ describe('LoginFailureMessage', () => { it('test match failed login error first attempt', () => { props = { - loginError: { + context: { email: 'text@example.com', - errorCode: INCORRECT_EMAIL_PASSWORD, - context: { - failureCount: 1, - resetLink: '/reset', - }, + failureCount: 1, + resetLink: '/reset', }, + errorCode: INCORRECT_EMAIL_PASSWORD, + failureCount: 0, }; render( @@ -147,16 +144,34 @@ describe('LoginFailureMessage', () => { ).textContent).toBe(expectedMessage); }); + it('test match user account locked out', () => { + props = { + errorCode: ACCOUNT_LOCKED_OUT, + failureCount: 0, + }; + + render( + + + , + ); + + const expectedMessage = 'We couldn\'t sign you in.To protect your account, it\'s been temporarily locked. Try again in 30 minutes.To be on the safe side, you can reset your password before trying again.'; + expect(screen.getByText( + '', + { selector: '#login-failure-alert' }, + ).textContent).toBe(expectedMessage); + }); + it('test match failed login error second attempt', () => { props = { - loginError: { + context: { email: 'text@example.com', - errorCode: INCORRECT_EMAIL_PASSWORD, - context: { - failureCount: 2, - resetLink: '/reset', - }, + failureCount: 2, + resetLink: '/reset', }, + errorCode: INCORRECT_EMAIL_PASSWORD, + failureCount: 0, }; render( @@ -175,9 +190,8 @@ describe('LoginFailureMessage', () => { it('should match rate limit error message', () => { props = { - loginError: { - errorCode: FORBIDDEN_REQUEST, - }, + errorCode: FORBIDDEN_REQUEST, + failureCount: 0, }; render( @@ -196,9 +210,8 @@ describe('LoginFailureMessage', () => { it('should match internal server error message', () => { props = { - loginError: { - errorCode: INTERNAL_SERVER_ERROR, - }, + errorCode: INTERNAL_SERVER_ERROR, + failureCount: 0, }; render( @@ -217,9 +230,8 @@ describe('LoginFailureMessage', () => { it('should match invalid form error message', () => { props = { - loginError: { - errorCode: INVALID_FORM, - }, + errorCode: INVALID_FORM, + failureCount: 0, }; render( @@ -237,9 +249,8 @@ describe('LoginFailureMessage', () => { it('should match internal server of error message', () => { props = { - loginError: { - errorCode: 'invalid-error-code', - }, + errorCode: 'invalid-error-code', + failureCount: 0, }; render( @@ -257,12 +268,9 @@ describe('LoginFailureMessage', () => { it('should match tpa authentication failed error message', () => { props = { - loginError: { - errorCode: TPA_AUTHENTICATION_FAILURE, - context: { - errorMessage: 'An error occurred', - }, - }, + errorCode: TPA_AUTHENTICATION_FAILURE, + failureCount: 0, + context: { errorMessage: 'An error occurred' }, }; render( @@ -286,9 +294,8 @@ describe('LoginFailureMessage', () => { it('should show modal that nudges users to change password', () => { props = { - loginError: { - errorCode: NUDGE_PASSWORD_CHANGE, - }, + errorCode: NUDGE_PASSWORD_CHANGE, + failureCount: 0, }; render( @@ -313,9 +320,8 @@ describe('LoginFailureMessage', () => { it('should show modal that requires users to change password', () => { props = { - loginError: { - errorCode: REQUIRE_PASSWORD_CHANGE, - }, + errorCode: REQUIRE_PASSWORD_CHANGE, + failureCount: 0, }; render( @@ -341,15 +347,14 @@ describe('LoginFailureMessage', () => { it('should show message if staff user try to login through password', () => { props = { - loginError: { + context: { email: 'text@example.com', - errorCode: ALLOWED_DOMAIN_LOGIN_ERROR, - context: { - allowedDomain: 'test.com', - provider: 'Google', - tpaHint: 'google-auth2', - }, + allowedDomain: 'test.com', + provider: 'Google', + tpaHint: 'google-auth2', }, + errorCode: ALLOWED_DOMAIN_LOGIN_ERROR, + failureCount: 0, }; render( diff --git a/src/login/tests/LoginPage.test.jsx b/src/login/tests/LoginPage.test.jsx index cffbea42fa..1fc472a1ed 100644 --- a/src/login/tests/LoginPage.test.jsx +++ b/src/login/tests/LoginPage.test.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Provider } from 'react-redux'; import { getConfig, mergeConfig } from '@edx/frontend-platform'; -import { sendPageEvent } from '@edx/frontend-platform/analytics'; +import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { fireEvent, render, screen, waitFor, @@ -12,9 +12,7 @@ import { MemoryRouter } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE } from '../../data/constants'; -import { - loginRemovePasswordResetBanner, loginRequest, loginRequestFailure, loginRequestReset, setLoginFormData, -} from '../data/actions'; +import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions'; import { INTERNAL_SERVER_ERROR } from '../data/constants'; import LoginPage from '../LoginPage'; @@ -32,8 +30,8 @@ const mockStore = configureStore(); describe('LoginPage', () => { let props = {}; let store = {}; - let loginFormData = {}; + const emptyFieldValidation = { emailOrUsername: 'Enter your username or email', password: 'Enter your password' }; const reduxWrapper = children => ( @@ -83,14 +81,6 @@ describe('LoginPage', () => { handleInstitutionLogin: jest.fn(), institutionLogin: false, }; - loginFormData = { - emailOrUsername: '', - password: '', - errors: { - emailOrUsername: '', - password: '', - }, - }; }); // ******** test login form submission ******** @@ -103,18 +93,18 @@ describe('LoginPage', () => { fireEvent.change(screen.getByText( '', { selector: '#emailOrUsername' }, - ), { target: { value: 'test@example.com' } }); + ), { target: { value: 'test', name: 'emailOrUsername' } }); fireEvent.change(screen.getByText( '', { selector: '#password' }, - ), { target: { value: 'password' } }); + ), { target: { value: 'test-password', name: 'password' } }); fireEvent.click(screen.getByText( '', { selector: '.btn-brand' }, )); - expect(store.dispatch).toHaveBeenCalledWith(loginRequest({ email_or_username: 'test@example.com', password: 'password' })); + expect(store.dispatch).toHaveBeenCalledWith(loginRequest({ email_or_username: 'test', password: 'test-password' })); }); it('should not dispatch loginRequest on empty form submission', () => { @@ -128,21 +118,27 @@ describe('LoginPage', () => { expect(store.dispatch).not.toHaveBeenCalledWith(loginRequest({})); }); - // ******** test login form validations ******** + it('should dismiss reset password banner on form submission', () => { + store = mockStore({ + ...initialState, + login: { + ...initialState.login, + showResetPasswordSuccessBanner: true, + }, + }); - it('should match state on empty form submission', () => { store.dispatch = jest.fn(store.dispatch); - render(reduxWrapper()); fireEvent.click(screen.getByText( '', { selector: '.btn-brand' }, )); - expect(screen.getByText('Enter your username or email')).toBeDefined(); - expect(store.dispatch).toHaveBeenCalledWith(loginRequestFailure({ errorCode: 'invalid-form' })); + expect(store.dispatch).toHaveBeenCalledWith(dismissPasswordResetBanner()); }); + // ******** test login form validations ******** + it('should match state for invalid email (less than 3 characters), on form submission', () => { store.dispatch = jest.fn(store.dispatch); @@ -165,6 +161,37 @@ describe('LoginPage', () => { expect(screen.getByText('Username or email must have at least 3 characters.')).toBeDefined(); }); + it('should show error messages for required fields on empty form submission', () => { + const { container } = render(reduxWrapper()); + fireEvent.click(screen.getByText( + '', + { selector: '.btn-brand' }, + )); + + expect(container.querySelector('div[feedback-for="emailOrUsername"]').textContent).toEqual(emptyFieldValidation.emailOrUsername); + expect(container.querySelector('div[feedback-for="password"]').textContent).toEqual(emptyFieldValidation.password); + + const alertBanner = 'We couldn\'t sign you in.Please fill in the fields below.'; + expect(container.querySelector('#login-failure-alert').textContent).toEqual(alertBanner); + }); + + it('should run frontend validations for emailOrUsername field on form submission', () => { + const { container } = render(reduxWrapper()); + + fireEvent.change(screen.getByText( + '', + { selector: '#emailOrUsername' }, + ), { target: { value: 'te', name: 'emailOrUsername' } }); + + fireEvent.click(screen.getByText( + '', + { selector: '.btn-brand' }, + )); + + expect(container.querySelector('div[feedback-for="emailOrUsername"]').textContent).toEqual('Username or email must have at least 3 characters.'); + }); + + // ******** test field focus in functionality ******** it('should reset field related error messages on onFocus event', async () => { store.dispatch = jest.fn(store.dispatch); @@ -227,19 +254,13 @@ describe('LoginPage', () => { }); it('should show single sign on provider button', () => { - mergeConfig({ - DISABLE_ENTERPRISE_LOGIN: '', - }); - store = mockStore({ ...initialState, commonComponents: { ...initialState.commonComponents, thirdPartyAuthContext: { ...initialState.commonComponents.thirdPartyAuthContext, - providers: [{ - ...ssoProvider, - }], + providers: [ssoProvider], }, }, }); @@ -255,26 +276,13 @@ describe('LoginPage', () => { }); }); - it('should not display institution login option when no secondary providers are present', () => { - const { queryByText } = render(reduxWrapper()); - expect(queryByText('Use my university info')).toBeNull(); - }); - - it('should not show sign-in header and enterprise login once user authenticated through SSO', () => { - mergeConfig({ - DISABLE_ENTERPRISE_LOGIN: '', - }); - + it('should display sign-in header only when primary or secondary providers are available.', () => { store = mockStore({ ...initialState, commonComponents: { ...initialState.commonComponents, thirdPartyAuthContext: { ...initialState.commonComponents.thirdPartyAuthContext, - providers: [{ - ...ssoProvider, - }], - currentProvider: 'Apple', }, }, }); @@ -282,82 +290,52 @@ describe('LoginPage', () => { const { queryByText } = render(reduxWrapper()); expect(queryByText('Company or school credentials')).toBeNull(); expect(queryByText('Or sign in with:')).toBeNull(); + expect(queryByText('Institution/campus credentials')).toBeNull(); }); - it('should show sign-in header providers (ENABLE ENTERPRISE LOGIN)', () => { - mergeConfig({ - DISABLE_ENTERPRISE_LOGIN: '', - }); - + it('should hide sign-in header and enterprise login upon successful SSO authentication', () => { store = mockStore({ ...initialState, commonComponents: { ...initialState.commonComponents, thirdPartyAuthContext: { ...initialState.commonComponents.thirdPartyAuthContext, - providers: [{ - ...ssoProvider, - }], + providers: [ssoProvider], + secondaryProviders: [secondaryProviders], + currentProvider: 'Apple', }, }, }); const { queryByText } = render(reduxWrapper()); - expect(queryByText('Or sign in with:')).toBeDefined(); - expect(queryByText('Company or school credentials')).toBeDefined(); - expect(queryByText('Institution/campus credentials')).toBeDefined(); + expect(queryByText('Company or school credentials')).toBeNull(); + expect(queryByText('Or sign in with:')).toBeNull(); }); - it('should show sign-in header with providers (DISABLE ENTERPRISE LOGIN)', () => { - mergeConfig({ - DISABLE_ENTERPRISE_LOGIN: true, - }); + // ******** test enterprise login enabled scenarios ******** + it('should show sign-in header for enterprise login', () => { store = mockStore({ ...initialState, commonComponents: { ...initialState.commonComponents, thirdPartyAuthContext: { ...initialState.commonComponents.thirdPartyAuthContext, - providers: [{ - ...ssoProvider, - }], + providers: [ssoProvider], + secondaryProviders: [secondaryProviders], }, }, }); const { queryByText } = render(reduxWrapper()); expect(queryByText('Or sign in with:')).toBeDefined(); - expect(queryByText('Company or school credentials')).toBeNull(); - expect(queryByText('Institution/campus credentials')).toBeNull(); - - mergeConfig({ - DISABLE_ENTERPRISE_LOGIN: '', - }); + expect(queryByText('Company or school credentials')).toBeDefined(); + expect(queryByText('Institution/campus credentials')).toBeDefined(); }); - it('should not show sign-in header without Providers and secondary Providers (ENABLE ENTERPRISE LOGIN)', () => { - mergeConfig({ - DISABLE_ENTERPRISE_LOGIN: '', - }); - - store = mockStore({ - ...initialState, - commonComponents: { - ...initialState.commonComponents, - thirdPartyAuthContext: { - ...initialState.commonComponents.thirdPartyAuthContext, - }, - }, - }); - - const { queryByText } = render(reduxWrapper()); - expect(queryByText('Or sign in with:')).toBeNull(); - expect(queryByText('Company or school credentials')).toBeNull(); - expect(queryByText('Institution/campus credentials')).toBeNull(); - }); + // ******** test enterprise login disabled scenarios ******** - it('should not show sign-in header without Providers and secondary Providers (DISABLE ENTERPRISE LOGIN)', () => { + it('should show sign-in header for institution login if enterprise login is disabled', () => { mergeConfig({ DISABLE_ENTERPRISE_LOGIN: true, }); @@ -368,14 +346,16 @@ describe('LoginPage', () => { ...initialState.commonComponents, thirdPartyAuthContext: { ...initialState.commonComponents.thirdPartyAuthContext, + providers: [ssoProvider], + secondaryProviders: [secondaryProviders], }, }, }); const { queryByText } = render(reduxWrapper()); - expect(queryByText('Or sign in with:')).toBeNull(); + expect(queryByText('Or sign in with:')).toBeDefined(); expect(queryByText('Company or school credentials')).toBeNull(); - expect(queryByText('Institution/campus credentials')).toBeNull(); + expect(queryByText('Institution/campus credentials')).toBeDefined(); mergeConfig({ DISABLE_ENTERPRISE_LOGIN: '', @@ -409,23 +389,31 @@ describe('LoginPage', () => { }); }); - it('should show sign-in header with Providers and secondary Providers', () => { - mergeConfig({ - DISABLE_ENTERPRISE_LOGIN: true, + it('should not show sign-in header without primary or secondary providers', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + }, + }, }); + const { queryByText } = render(reduxWrapper()); + expect(queryByText('Or sign in with:')).toBeNull(); + expect(queryByText('Institution/campus credentials')).toBeNull(); + expect(queryByText('Company or school credentials')).toBeNull(); + }); + + it('should show enterprise login if even if only secondary providers are available', () => { store = mockStore({ ...initialState, commonComponents: { ...initialState.commonComponents, thirdPartyAuthContext: { ...initialState.commonComponents.thirdPartyAuthContext, - providers: [{ - ...ssoProvider, - }], - secondaryProviders: [{ - ...secondaryProviders, - }], + secondaryProviders: [secondaryProviders], }, }, }); @@ -442,13 +430,14 @@ describe('LoginPage', () => { // ******** test alert messages ******** - it('should match login error message', () => { - const errorMessage = 'An error has occurred. Try refreshing the page, or check your internet connection.'; + it('should match login internal server error message', () => { + const expectedMessage = 'We couldn\'t sign you in.' + + 'An error has occurred. Try refreshing the page, or check your internet connection.'; store = mockStore({ ...initialState, login: { ...initialState.login, - loginError: { errorCode: INTERNAL_SERVER_ERROR }, + loginErrorCode: INTERNAL_SERVER_ERROR, }, }); @@ -456,22 +445,7 @@ describe('LoginPage', () => { expect(screen.getByText( '', { selector: '#login-failure-alert' }, - ).textContent).toEqual(`We couldn't sign you in.${errorMessage}`); - }); - - it('should match account activation message', () => { - const activationMessage = 'Success! You have activated your account.' - + 'You will now receive email updates and alerts from us related ' - + 'to the courses you are enrolled in. Sign in to continue.'; - - delete window.location; - window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?account_activation_status=success' }; - - render(reduxWrapper()); - expect(screen.getByText( - '', - { selector: '#account-activation-message' }, - ).textContent).toEqual(activationMessage); + ).textContent).toEqual(`${expectedMessage}`); }); it('should match third party auth alert', () => { @@ -498,7 +472,7 @@ describe('LoginPage', () => { ).textContent).toEqual(expectedMessage); }); - it('should show tpa authentication fails error message', () => { + it('should show third party authentication failure message', () => { store = mockStore({ ...initialState, commonComponents: { @@ -510,7 +484,6 @@ describe('LoginPage', () => { }, }, }); - render(reduxWrapper()); expect(screen.getByText( '', @@ -524,7 +497,7 @@ describe('LoginPage', () => { ...initialState, login: { ...initialState.login, - loginError: { errorCode: 'invalid-form' }, + loginErrorCode: 'invalid-form', }, }); @@ -537,15 +510,15 @@ describe('LoginPage', () => { // ******** test redirection ******** - it('should redirect to url returned by login endpoint', () => { - const dashboardUrl = 'http://localhost:18000/enterprise/select/active/?success_url=/dashboard'; + it('should redirect to url returned by login endpoint after successful authentication', () => { + const dashboardURL = 'https://test.com/testing-dashboard/'; store = mockStore({ ...initialState, login: { ...initialState.login, loginResult: { success: true, - redirectUrl: dashboardUrl, + redirectUrl: dashboardURL, }, }, }); @@ -553,7 +526,7 @@ describe('LoginPage', () => { delete window.location; window.location = { href: getConfig().BASE_URL }; render(reduxWrapper()); - expect(window.location.href).toBe(dashboardUrl); + expect(window.location.href).toBe(dashboardURL); }); it('should redirect to finishAuthUrl upon successful login via SSO', () => { @@ -584,21 +557,13 @@ describe('LoginPage', () => { }); it('should redirect to social auth provider url on SSO button click', () => { - mergeConfig({ - DISABLE_ENTERPRISE_LOGIN: 'true', - }); - - const loginUrl = '/auth/login/apple-id/?auth_entry=login&next=/dashboard'; store = mockStore({ ...initialState, commonComponents: { ...initialState.commonComponents, thirdPartyAuthContext: { ...initialState.commonComponents.thirdPartyAuthContext, - providers: [{ - ...ssoProvider, - loginUrl, - }], + providers: [ssoProvider], }, }, }); @@ -612,11 +577,31 @@ describe('LoginPage', () => { '', { selector: '#oa2-apple-id' }, )); - expect(window.location.href).toBe(getConfig().LMS_BASE_URL + loginUrl); + expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl); + }); - mergeConfig({ - DISABLE_ENTERPRISE_LOGIN: '', + it('should redirect to finishAuthUrl upon successful authentication via SSO', () => { + const finishAuthUrl = '/auth/complete/google-oauth2/'; + store = mockStore({ + ...initialState, + login: { + ...initialState.login, + loginResult: { success: true, redirectUrl: '' }, + }, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + finishAuthUrl, + }, + }, }); + + delete window.location; + window.location = { href: getConfig().BASE_URL }; + + render(reduxWrapper()); + expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl); }); // ******** test hinted third party auth ******** @@ -648,6 +633,26 @@ describe('LoginPage', () => { )).toBeTruthy(); }); + it('should render the skeleton when third party status is pending', () => { + store = mockStore({ + ...initialState, + commonComponents: { + ...initialState.commonComponents, + thirdPartyAuthContext: { + ...initialState.commonComponents.thirdPartyAuthContext, + providers: [ssoProvider], + }, + thirdPartyAuthApiStatus: PENDING_STATE, + }, + }); + + delete window.location; + window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; + + const { container } = render(reduxWrapper()); + expect(container.querySelector('.react-loading-skeleton')).toBeTruthy(); + }); + it('should render tpa button for tpa_hint id matching one of the secondary providers', () => { secondaryProviders.skipHintedLogin = true; store = mockStore({ @@ -671,10 +676,6 @@ describe('LoginPage', () => { }); it('should render regular tpa button for invalid tpa_hint value', () => { - mergeConfig({ - DISABLE_ENTERPRISE_LOGIN: 'true', - }); - store = mockStore({ ...initialState, commonComponents: { @@ -698,7 +699,7 @@ describe('LoginPage', () => { }); }); - it('should render other ways to sign in button', () => { + it('should render "other ways to sign in" button on the tpa_hint page', () => { store = mockStore({ ...initialState, commonComponents: { @@ -720,10 +721,11 @@ describe('LoginPage', () => { ).textContent).toBeDefined(); }); - it('should render other ways to sign in button when public account creation disabled', () => { + it('should render other ways to sign in button when public account creation is disabled', () => { mergeConfig({ ALLOW_PUBLIC_ACCOUNT_CREATION: false, }); + store = mockStore({ ...initialState, commonComponents: { @@ -752,129 +754,80 @@ describe('LoginPage', () => { expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login'); }); - it('tests that form is in invalid state when it is submission', () => { + it('tests that form is in invalid state when it is submitted', () => { store = mockStore({ ...initialState, login: { ...initialState.login, - loginError: { errorCode: 'invalid-form' }, + shouldBackupState: true, }, }); - render(reduxWrapper()); - - fireEvent.change(screen.getByText( - '', - { selector: '#password' }, - ), { target: { value: 'password', name: 'password' } }); - fireEvent.click(screen.getByText( - '', - { selector: '.btn-brand' }, - )); - - expect(screen.getByText('Please fill in the fields below.')).toBeTruthy(); - }); - - it('should reset login form errors', () => { store.dispatch = jest.fn(store.dispatch); - render(reduxWrapper()); - - expect(store.dispatch).toHaveBeenCalledWith(loginRequestReset()); - }); - - // persists form data tests - - it('should set errors in redux store on submit form for invalid input', () => { - const formData = { - errors: { - emailOrUsername: 'Enter your username or email', - password: 'Enter your password', + expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin( + { + formFields: { + emailOrUsername: '', password: '', + }, + errors: { + emailOrUsername: '', password: '', + }, }, - }; - store.dispatch = jest.fn(store.dispatch); - render(reduxWrapper()); - - fireEvent.change(screen.getByText( - '', - { selector: '#emailOrUsername' }, - ), { target: { value: '' } }); - fireEvent.change(screen.getByText( - '', - { selector: '#password' }, - ), { target: { value: '' } }); - fireEvent.click(screen.getByText( - '', - { selector: '.btn-brand' }, - )); - - expect(store.dispatch).toHaveBeenCalledWith(setLoginFormData(formData)); - }); - - it('should set form data in redux store on onBlur', () => { - store.dispatch = jest.fn(store.dispatch); - - render(reduxWrapper()); - fireEvent.blur(screen.getByText( - '', - { selector: '#emailOrUsername' }, )); - expect(store.dispatch).toHaveBeenCalledWith(setLoginFormData({ emailOrUsername: '' })); }); - it('should clear form field errors in redux store on onFocus', () => { - store.dispatch = jest.fn(store.dispatch); - + it('should send track event when forgot password link is clicked', () => { render(reduxWrapper()); - fireEvent.focus(screen.getByText( - '', - { selector: '#emailOrUsername' }, + fireEvent.click(screen.getByText( + 'Forgot password', + { selector: '#forgot-password' }, )); - expect(store.dispatch).toHaveBeenCalledWith(setLoginFormData({ - errors: { - ...loginFormData.errors, - }, - })); + expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement' }); }); - it('should update form fields state if updated in redux store', async () => { - const { rerender } = render(reduxWrapper()); - + it('should backup the login form state when shouldBackupState is true', () => { store = mockStore({ ...initialState, login: { ...initialState.login, - loginFormData: { - emailOrUsername: 'john_doe', - password: 'password1', - }, + shouldBackupState: true, }, }); - rerender(( - - - - - )); - - expect(screen.getByDisplayValue('password1')).toBeDefined(); + store.dispatch = jest.fn(store.dispatch); + render(reduxWrapper()); + expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin( + { + formFields: { + emailOrUsername: '', password: '', + }, + errors: { + emailOrUsername: '', password: '', + }, + }, + )); }); - it('should update reset password value when unmount called', () => { + it('should update form fields state if updated in redux store', () => { store = mockStore({ ...initialState, login: { ...initialState.login, - resetPassword: true, + loginFormData: { + formFields: { + emailOrUsername: 'john_doe', password: 'test-password', + }, + errors: { + emailOrUsername: '', password: '', + }, + }, }, }); - store.dispatch = jest.fn(store.dispatch); - const { unmount } = render(reduxWrapper()); - unmount(); - - expect(store.dispatch).toHaveBeenCalledWith(loginRemovePasswordResetBanner()); + const { container } = render(reduxWrapper()); + expect(container.querySelector('input#emailOrUsername').value).toEqual('john_doe'); + expect(container.querySelector('input#password').value).toEqual('test-password'); }); }); diff --git a/src/logistration/Logistration.jsx b/src/logistration/Logistration.jsx index 5091531855..91bcce866e 100644 --- a/src/logistration/Logistration.jsx +++ b/src/logistration/Logistration.jsx @@ -25,6 +25,7 @@ import { getTpaHint, getTpaProvider, updatePathWithQueryParams, } from '../data/utils'; import { LoginPage } from '../login'; +import { backupLoginForm } from '../login/data/actions'; import { RegistrationPage } from '../register'; import { backupRegistrationForm } from '../register/data/actions'; @@ -70,6 +71,8 @@ const Logistration = (props) => { props.clearThirdPartyAuthContextErrorMessage(); if (tabKey === LOGIN_PAGE) { props.backupRegistrationForm(); + } else if (tabKey === REGISTER_PAGE) { + props.backupLoginForm(); } setKey(tabKey); }; @@ -150,6 +153,7 @@ const Logistration = (props) => { Logistration.propTypes = { selectedPage: PropTypes.string, + backupLoginForm: PropTypes.func.isRequired, backupRegistrationForm: PropTypes.func.isRequired, clearThirdPartyAuthContextErrorMessage: PropTypes.func.isRequired, tpaProviders: PropTypes.shape({ @@ -176,6 +180,7 @@ const mapStateToProps = state => ({ export default connect( mapStateToProps, { + backupLoginForm, backupRegistrationForm, clearThirdPartyAuthContextErrorMessage, }, diff --git a/src/logistration/Logistration.test.jsx b/src/logistration/Logistration.test.jsx index b5259c1173..16a94444c5 100644 --- a/src/logistration/Logistration.test.jsx +++ b/src/logistration/Logistration.test.jsx @@ -13,6 +13,7 @@ import { clearThirdPartyAuthContextErrorMessage } from '../common-components/dat import { COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE, } from '../data/constants'; +import { backupLoginForm } from '../login/data/actions'; import { backupRegistrationForm } from '../register/data/actions'; jest.mock('@edx/frontend-platform/analytics', () => ({ @@ -259,6 +260,12 @@ describe('Logistration', () => { fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]')); expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm()); }); + it('should fire action to backup login form on tab click', () => { + store.dispatch = jest.fn(store.dispatch); + const { container } = render(reduxWrapper()); + fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]')); + expect(store.dispatch).toHaveBeenCalledWith(backupLoginForm()); + }); it('should clear tpa context errorMessage tab click', () => { store.dispatch = jest.fn(store.dispatch); diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index aa6b5ddda8..7a854e8a59 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -14,7 +14,6 @@ import Skeleton from 'react-loading-skeleton'; import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm'; import RegistrationFailure from './components/RegistrationFailure'; -import ThirdPartyAuth from './components/ThirdPartyAuth'; import { backupRegistrationFormBegin, clearRegistrationBackendError, @@ -29,10 +28,14 @@ import { getBackendValidations, isFormValid, prepareRegistrationPayload } from ' import messages from './messages'; import { EmailField, NameField, UsernameField } from './RegistrationFields'; import { - InstitutionLogistration, PasswordField, RedirectLogistration, ThirdPartyAuthAlert, + InstitutionLogistration, + PasswordField, + RedirectLogistration, + ThirdPartyAuthAlert, } from '../common-components'; import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../common-components/data/actions'; import EnterpriseSSO from '../common-components/EnterpriseSSO'; +import ThirdPartyAuth from '../common-components/ThirdPartyAuth'; import { COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE, } from '../data/constants'; diff --git a/src/register/components/ConfigurableRegistrationForm.test.jsx b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx similarity index 98% rename from src/register/components/ConfigurableRegistrationForm.test.jsx rename to src/register/components/tests/ConfigurableRegistrationForm.test.jsx index f4a92da603..0b9837cacc 100644 --- a/src/register/components/ConfigurableRegistrationForm.test.jsx +++ b/src/register/components/tests/ConfigurableRegistrationForm.test.jsx @@ -9,10 +9,10 @@ import { fireEvent, render } from '@testing-library/react'; import { BrowserRouter as Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; -import ConfigurableRegistrationForm from './ConfigurableRegistrationForm'; -import { registerNewUser } from '../data/actions'; -import { FIELDS } from '../data/constants'; -import RegistrationPage from '../RegistrationPage'; +import { registerNewUser } from '../../data/actions'; +import { FIELDS } from '../../data/constants'; +import RegistrationPage from '../../RegistrationPage'; +import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm'; jest.mock('@edx/frontend-platform/analytics', () => ({ sendPageEvent: jest.fn(), diff --git a/src/register/components/RegistrationFailure.test.jsx b/src/register/components/tests/RegistrationFailure.test.jsx similarity index 97% rename from src/register/components/RegistrationFailure.test.jsx rename to src/register/components/tests/RegistrationFailure.test.jsx index 04c56f23ac..003cc966c6 100644 --- a/src/register/components/RegistrationFailure.test.jsx +++ b/src/register/components/tests/RegistrationFailure.test.jsx @@ -9,11 +9,11 @@ import { render, screen } from '@testing-library/react'; import { BrowserRouter as Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; -import RegistrationFailureMessage from './RegistrationFailure'; import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED, -} from '../data/constants'; -import RegistrationPage from '../RegistrationPage'; +} from '../../data/constants'; +import RegistrationPage from '../../RegistrationPage'; +import RegistrationFailureMessage from '../RegistrationFailure'; jest.mock('@edx/frontend-platform/analytics', () => ({ sendPageEvent: jest.fn(), diff --git a/src/register/components/ThirdPartyAuth.test.jsx b/src/register/components/tests/ThirdPartyAuth.test.jsx similarity index 99% rename from src/register/components/ThirdPartyAuth.test.jsx rename to src/register/components/tests/ThirdPartyAuth.test.jsx index 6cfa550cc2..917f10f94a 100644 --- a/src/register/components/ThirdPartyAuth.test.jsx +++ b/src/register/components/tests/ThirdPartyAuth.test.jsx @@ -11,8 +11,8 @@ import configureStore from 'redux-mock-store'; import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE, -} from '../../data/constants'; -import RegistrationPage from '../RegistrationPage'; +} from '../../../data/constants'; +import RegistrationPage from '../../RegistrationPage'; jest.mock('@edx/frontend-platform/analytics', () => ({ sendPageEvent: jest.fn(), diff --git a/src/register/data/tests/reducers.test.js b/src/register/data/tests/reducers.test.js index 0687e63762..90bef3fe62 100644 --- a/src/register/data/tests/reducers.test.js +++ b/src/register/data/tests/reducers.test.js @@ -65,6 +65,7 @@ describe('Registration Reducer Tests', () => { }, ); }); + it('should set redirect url dashboard on registration success action', () => { const payload = { redirectUrl: `${getConfig().BASE_URL}${DEFAULT_REDIRECT_URL}`, diff --git a/src/register/messages.jsx b/src/register/messages.jsx index 463a6a2b89..39d9e7f549 100644 --- a/src/register/messages.jsx +++ b/src/register/messages.jsx @@ -64,22 +64,12 @@ const messages = defineMessages({ defaultMessage: 'Create an account for free', description: 'Label text for registration form submission button', }, - 'registration.other.options.heading': { - id: 'registration.other.options.heading', - defaultMessage: 'Or register with:', - description: 'A message that appears above third party auth providers i.e saml, google, facebook etc', - }, 'create.account.cta.button': { id: 'create.account.cta.button', defaultMessage: '{label}', description: 'Label text for registration form submission button for those users who are landing through redirections', }, // Institution login - 'register.institution.login.button': { - id: 'register.institution.login.button', - defaultMessage: 'Institution/campus credentials', - description: 'shows institutions list', - }, 'register.institution.login.page.title': { id: 'register.institution.login.page.title', defaultMessage: 'Register with institution/campus credentials', diff --git a/src/reset-password/ResetPasswordSuccess.jsx b/src/reset-password/ResetPasswordSuccess.jsx index 81b9cba382..a3269df2ec 100644 --- a/src/reset-password/ResetPasswordSuccess.jsx +++ b/src/reset-password/ResetPasswordSuccess.jsx @@ -9,7 +9,7 @@ const ResetPasswordSuccess = () => { const { formatMessage } = useIntl(); return ( - + {formatMessage(messages['reset.password.success.heading'])}