diff --git a/package-lock.json b/package-lock.json index fc1eecf9cf..3d09753c9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,11 @@ "@fortawesome/free-brands-svg-icons": "6.7.2", "@fortawesome/free-solid-svg-icons": "6.7.2", "@fortawesome/react-fontawesome": "0.2.2", + "@openedx/frontend-plugin-framework": "^1.4.0", "@openedx/paragon": "^22.1.1", "@optimizely/react-sdk": "^2.9.1", "@redux-devtools/extension": "3.3.0", "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^8.0.1", "algoliasearch": "^4.14.3", "algoliasearch-helper": "^3.14.0", "classnames": "2.5.1", @@ -3477,6 +3477,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@openedx/frontend-plugin-framework": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.4.0.tgz", + "integrity": "sha512-cwwjvGjYnzQdyhw2PuMYfaZoZGclXTzYfR3NjZ8bSLkj0f7lRB64nMy6WUmBjTB6hjaD4vxMs0zahrU4IM7EWQ==", + "license": "AGPL-3.0", + "dependencies": { + "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", + "classnames": "^2.3.2", + "core-js": "3.37.1", + "react-redux": "7.2.9", + "redux": "4.2.1", + "regenerator-runtime": "0.14.1" + }, + "peerDependencies": { + "@edx/frontend-platform": "^7.0.0 || ^8.0.0", + "@openedx/paragon": "^21.0.0 || ^22.0.0", + "prop-types": "^15.8.0", + "react": "^17.0.0", + "react-dom": "^17.0.0", + "react-error-boundary": "^4.0.11" + } + }, + "node_modules/@openedx/frontend-plugin-framework/node_modules/core-js": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/@openedx/paragon": { "version": "22.13.0", "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.13.0.tgz", @@ -4125,34 +4158,6 @@ "react-dom": "<18.0.0" } }, - "node_modules/@testing-library/react-hooks": { - "version": "8.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "react-error-boundary": "^3.1.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0", - "react": "^16.9.0 || ^17.0.0", - "react-dom": "^16.9.0 || ^17.0.0", - "react-test-renderer": "^16.9.0 || ^17.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-test-renderer": { - "optional": true - } - } - }, "node_modules/@tootallnate/once": { "version": "2.0.0", "license": "MIT", @@ -13515,15 +13520,14 @@ } }, "node_modules/react-error-boundary": { - "version": "3.1.4", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", + "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.5" }, - "engines": { - "node": ">=10", - "npm": ">=6" - }, "peerDependencies": { "react": ">=16.13.1" } @@ -13855,7 +13859,7 @@ }, "node_modules/react-shallow-renderer": { "version": "16.15.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4.1.1", @@ -13906,7 +13910,7 @@ }, "node_modules/react-test-renderer": { "version": "17.0.2", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4.1.1", @@ -13920,7 +13924,7 @@ }, "node_modules/react-test-renderer/node_modules/react-is": { "version": "17.0.2", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/react-transition-group": { diff --git a/package.json b/package.json index 515c9fffac..6e8edc2d92 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,11 @@ "@fortawesome/free-brands-svg-icons": "6.7.2", "@fortawesome/free-solid-svg-icons": "6.7.2", "@fortawesome/react-fontawesome": "0.2.2", + "@openedx/frontend-plugin-framework": "^1.4.0", "@openedx/paragon": "^22.1.1", "@optimizely/react-sdk": "^2.9.1", "@redux-devtools/extension": "3.3.0", "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^8.0.1", "algoliasearch": "^4.14.3", "algoliasearch-helper": "^3.14.0", "classnames": "2.5.1", diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx index 6281572675..db0e98f3ea 100644 --- a/src/login/LoginPage.jsx +++ b/src/login/LoginPage.jsx @@ -1,26 +1,17 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { connect } from 'react-redux'; +import React, { + useCallback, useEffect, useMemo, useState, +} from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { getConfig } from '@edx/frontend-platform'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { injectIntl, useIntl } from '@edx/frontend-platform/i18n'; -import { - Form, StatefulButton, -} from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Form, StatefulButton } from '@openedx/paragon'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import Skeleton from 'react-loading-skeleton'; import { Link } from 'react-router-dom'; -import AccountActivationMessage from './AccountActivationMessage'; -import { - backupLoginFormBegin, - dismissPasswordResetBanner, - loginRequest, -} from './data/actions'; -import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants'; -import LoginFailureMessage from './LoginFailure'; -import messages from './messages'; import { FormGroup, InstitutionLogistration, @@ -28,13 +19,12 @@ import { RedirectLogistration, ThirdPartyAuthAlert, } from '../common-components'; +import AccountActivationMessage from './AccountActivationMessage'; 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, PENDING_STATE, RESET_PAGE, -} from '../data/constants'; +import { PENDING_STATE, RESET_PAGE } from '../data/constants'; import { getActivationStatus, getAllPossibleQueryParams, @@ -43,37 +33,57 @@ import { updatePathWithQueryParams, } from '../data/utils'; import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess'; +import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from './data/actions'; +import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants'; +import LoginFailureMessage from './LoginFailure'; +import messages from './messages'; -const LoginPage = (props) => { +const LoginPage = ({ + institutionLogin, + handleInstitutionLogin, +}) => { + const dispatch = useDispatch(); + const backupFormState = useCallback((data) => dispatch(backupLoginFormBegin(data)), [dispatch]); + const getTPADataFromBackend = useCallback(() => dispatch(getThirdPartyAuthContext()), [dispatch]); const { backedUpFormData, loginErrorCode, loginErrorContext, loginResult, shouldBackupState, - thirdPartyAuthContext: { - providers, - currentProvider, - secondaryProviders, - finishAuthUrl, - platformName, - errorMessage: thirdPartyErrorMessage, - }, - thirdPartyAuthApiStatus, - institutionLogin, showResetPasswordSuccessBanner, submitState, - // Actions - backupFormState, - handleInstitutionLogin, - getTPADataFromBackend, - } = props; + thirdPartyAuthContext, + thirdPartyAuthApiStatus, + } = useSelector((state) => ({ + backedUpFormData: state.login.loginFormData, + loginErrorCode: state.login.loginErrorCode, + loginErrorContext: state.login.loginErrorContext, + loginResult: state.login.loginResult, + shouldBackupState: state.login.shouldBackupState, + showResetPasswordSuccessBanner: state.login.showResetPasswordSuccessBanner, + submitState: state.login.submitState, + thirdPartyAuthContext: thirdPartyAuthContextSelector(state), + thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus, + })); + const { + providers, + currentProvider, + secondaryProviders, + finishAuthUrl, + platformName, + errorMessage: thirdPartyErrorMessage, + } = thirdPartyAuthContext; 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 [errorCode, setErrorCode] = useState({ + type: '', + count: 0, + context: {}, + }); const [errors, setErrors] = useState({ ...backedUpFormData.errors }); const tpaHint = getTpaHint(); @@ -87,7 +97,7 @@ const LoginPage = (props) => { payload.tpa_hint = tpaHint; } getTPADataFromBackend(payload); - }, [getTPADataFromBackend, queryParams, tpaHint]); + }, [queryParams, tpaHint, getTPADataFromBackend]); /** * Backup the login form in redux when login page is toggled. */ @@ -98,7 +108,7 @@ const LoginPage = (props) => { errors: { ...errors }, }); } - }, [shouldBackupState, formFields, errors, backupFormState]); + }, [backupFormState, shouldBackupState, formFields, errors]); useEffect(() => { if (loginErrorCode) { @@ -123,7 +133,10 @@ const LoginPage = (props) => { }, [thirdPartyErrorMessage]); const validateFormFields = (payload) => { - const { emailOrUsername, password } = payload; + const { + emailOrUsername, + password, + } = payload; const fieldErrors = { ...errors }; if (emailOrUsername === '') { @@ -141,14 +154,18 @@ const LoginPage = (props) => { const handleSubmit = (event) => { event.preventDefault(); if (showResetPasswordSuccessBanner) { - props.dismissPasswordResetBanner(); + dispatch(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: {} })); + setErrorCode(prevState => ({ + type: INVALID_FORM, + count: prevState.count + 1, + context: {}, + })); return; } @@ -158,23 +175,35 @@ const LoginPage = (props) => { password: formData.password, ...queryParams, }; - props.loginRequest(payload); + dispatch(loginRequest(payload)); }; const handleOnChange = (event) => { - const { name, value } = event.target; - setFormFields(prevState => ({ ...prevState, [name]: value })); + const { + name, + value, + } = event.target; + setFormFields(prevState => ({ + ...prevState, + [name]: value, + })); }; const handleOnFocus = (event) => { const { name } = event.target; - setErrors(prevErrors => ({ ...prevErrors, [name]: '' })); + setErrors(prevErrors => ({ + ...prevErrors, + [name]: '', + })); }; const trackForgotPasswordLinkClick = () => { sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' }); }; - const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders); + const { + provider, + skipHintedLogin, + } = getTpaProvider(tpaHint, providers, secondaryProviders); if (tpaHint) { if (thirdPartyAuthApiStatus === PENDING_STATE) { @@ -281,88 +310,9 @@ const LoginPage = (props) => { ); }; -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 = { - 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, - }), - 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, - platformName: PropTypes.string, - providers: PropTypes.arrayOf(PropTypes.shape({})), - secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})), - finishAuthUrl: PropTypes.string, - }), - // Actions - backupFormState: PropTypes.func.isRequired, - dismissPasswordResetBanner: PropTypes.func.isRequired, - loginRequest: PropTypes.func.isRequired, - getTPADataFromBackend: PropTypes.func.isRequired, handleInstitutionLogin: PropTypes.func.isRequired, }; -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, - { - backupFormState: backupLoginFormBegin, - dismissPasswordResetBanner, - loginRequest, - getTPADataFromBackend: getThirdPartyAuthContext, - }, -)(injectIntl(LoginPage)); +export default LoginPage; diff --git a/src/login/tests/LoginPage.test.jsx b/src/login/tests/LoginPage.test.jsx index 9c337bf25e..a337d4b851 100644 --- a/src/login/tests/LoginPage.test.jsx +++ b/src/login/tests/LoginPage.test.jsx @@ -43,6 +43,14 @@ describe('LoginPage', () => { const initialState = { login: { loginResult: { success: false, redirectUrl: '' }, + loginFormData: { + formFields: { + emailOrUsername: '', password: '', + }, + errors: { + emailOrUsername: '', password: '', + }, + }, }, commonComponents: { thirdPartyAuthApiStatus: null, diff --git a/src/logistration/Logistration.jsx b/src/logistration/Logistration.jsx index 9451aa2745..d93366ea11 100644 --- a/src/logistration/Logistration.jsx +++ b/src/logistration/Logistration.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { getConfig } from '@edx/frontend-platform'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; @@ -24,16 +24,20 @@ import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants'; import { getTpaHint, getTpaProvider, updatePathWithQueryParams, } from '../data/utils'; -import { LoginPage } from '../login'; import { backupLoginForm } from '../login/data/actions'; +import LoginPageSlot from '../plugin-slots/LoginPage'; import { RegistrationPage } from '../register'; import { backupRegistrationForm } from '../register/data/actions'; -const Logistration = (props) => { - const { selectedPage, tpaProviders } = props; +const Logistration = ({ + selectedPage, +}) => { const tpaHint = getTpaHint(); + const tpaProviders = useSelector(tpaProvidersSelector); + const dispatch = useDispatch(); const { - providers, secondaryProviders, + providers, + secondaryProviders, } = tpaProviders; const { formatMessage } = useIntl(); const [institutionLogin, setInstitutionLogin] = useState(false); @@ -45,7 +49,8 @@ const Logistration = (props) => { useEffect(() => { const authService = getAuthService(); if (authService) { - authService.getCsrfTokenService().getCsrfToken(getConfig().LMS_BASE_URL); + authService.getCsrfTokenService() + .getCsrfToken(getConfig().LMS_BASE_URL); } }); @@ -71,11 +76,11 @@ const Logistration = (props) => { return; } sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' }); - props.clearThirdPartyAuthContextErrorMessage(); + dispatch(clearThirdPartyAuthContextErrorMessage()); if (tabKey === LOGIN_PAGE) { - props.backupRegistrationForm(); + dispatch(backupRegistrationForm()); } else if (tabKey === REGISTER_PAGE) { - props.backupLoginForm(); + dispatch(backupLoginForm()); } setKey(tabKey); }; @@ -111,7 +116,7 @@ const Logistration = (props) => { {!institutionLogin && (