diff --git a/package-lock.json b/package-lock.json index 1c332ba1..a05b8b10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4691,6 +4691,31 @@ } } }, + "@loadable/component": { + "version": "5.14.1", + "resolved": "https://registry.npmjs.org/@loadable/component/-/component-5.14.1.tgz", + "integrity": "sha512-UQBZfZrp1FLTf8RNhljXNHFNY4QhAA1L2+GOEeABBFre9TD0aFyQh3Sai5QxcOfy+FTbjIfti5iHaNRR7yUzEQ==", + "requires": { + "@babel/runtime": "^7.7.7", + "hoist-non-react-statics": "^3.3.1", + "react-is": "^16.12.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.12.18", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.18.tgz", + "integrity": "sha512-BogPQ7ciE6SYAUPtlm9tWbgI9+2AgqSam6QivMgXgAT+fKbgppaj4ZX15MHeLC1PVF5sNk70huBu20XxWOs8Cg==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + } + } + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", diff --git a/package.json b/package.json index c6b456aa..8bdc7ec6 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@fortawesome/free-regular-svg-icons": "5.15.1", "@fortawesome/free-solid-svg-icons": "5.15.1", "@fortawesome/react-fontawesome": "0.1.13", + "@loadable/component": "^5.14.1", "babel-polyfill": "6.26.0", "classnames": "^2.2.6", "extract-react-intl-messages": "^4.1.1", diff --git a/src/reset-password/Spinner.jsx b/src/common-components/Spinner.jsx similarity index 100% rename from src/reset-password/Spinner.jsx rename to src/common-components/Spinner.jsx diff --git a/src/common-components/UnAuthOnlyRoute.jsx b/src/common-components/UnAuthOnlyRoute.jsx deleted file mode 100644 index f136f4f3..00000000 --- a/src/common-components/UnAuthOnlyRoute.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import { Route } from 'react-router-dom'; -import { AppContext } from '@edx/frontend-platform/react'; - -import { DEFAULT_REDIRECT_URL } from '../data/constants'; - -/** - * This wrapper redirects the requester to our default redirect url if they are - * already authenticated. - */ -const UnAuthOnlyRoute = (props) => { - const { authenticatedUser, config } = React.useContext(AppContext); - - if (authenticatedUser) { - global.location.href = config.LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); - return null; - } - - return ; -}; - -export default UnAuthOnlyRoute; diff --git a/src/common-components/UnAuthenticatedRoute.jsx b/src/common-components/UnAuthenticatedRoute.jsx new file mode 100644 index 00000000..2c9371f7 --- /dev/null +++ b/src/common-components/UnAuthenticatedRoute.jsx @@ -0,0 +1,36 @@ +/* eslint-disable react/prop-types */ +import React, { useEffect } from 'react'; +import { Route, useRouteMatch } from 'react-router-dom'; +import { AppContext } from '@edx/frontend-platform/react'; +import { sendPageEvent } from '@edx/frontend-platform/analytics'; + +import { DEFAULT_REDIRECT_URL } from '../data/constants'; + +/** + * This wrapper redirects the requester to our default redirect url if they are + * already authenticated. + */ +const UnAuthenticatedRoute = (props) => { + const { authenticatedUser, config } = React.useContext(AppContext); + const match = useRouteMatch({ + path: props.path, + exact: props.exact, + strict: props.strict, + sensitive: props.sensitive, + }); + + if (authenticatedUser) { + global.location.href = config.LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); + return null; + } + + useEffect(() => { + if (match) { + sendPageEvent('login_and_registration', props.path.replace('/', '')); + } + }, [match]); + + return ; +}; + +export default UnAuthenticatedRoute; diff --git a/src/common-components/index.jsx b/src/common-components/index.jsx index 20894220..65fc8d4b 100644 --- a/src/common-components/index.jsx +++ b/src/common-components/index.jsx @@ -1,7 +1,7 @@ export { default as HeaderLayout } from './HeaderLayout'; export { default as RedirectLogistration } from './RedirectLogistration'; export { default as registerIcons } from './RegisterFaIcons'; -export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute'; +export { default as UnAuthenticatedRoute } from './UnAuthenticatedRoute'; export { default as NotFoundPage } from './NotFoundPage'; export { default as SocialAuthProviders } from './SocialAuthProviders'; export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert'; @@ -12,3 +12,4 @@ export { default as APIFailureMessage } from './APIFailureMessage'; export { default as reducer } from './data/reducers'; export { default as saga } from './data/sagas'; export { storeName } from './data/selectors'; +export { default as Spinner } from './Spinner'; diff --git a/src/common-components/tests/UnAuthOnlyRoute.test.jsx b/src/common-components/tests/UnAuthenticatedRoute.test.jsx similarity index 58% rename from src/common-components/tests/UnAuthOnlyRoute.test.jsx rename to src/common-components/tests/UnAuthenticatedRoute.test.jsx index 3b0ad0cb..bfe34986 100644 --- a/src/common-components/tests/UnAuthOnlyRoute.test.jsx +++ b/src/common-components/tests/UnAuthenticatedRoute.test.jsx @@ -1,31 +1,36 @@ +/* eslint-disable react/prop-types */ import React from 'react'; import { mount } from 'enzyme'; import { BrowserRouter as Router, MemoryRouter, Switch } from 'react-router-dom'; import { getConfig } from '@edx/frontend-platform'; +import * as analytics from '@edx/frontend-platform/analytics'; -import { UnAuthOnlyRoute } from '..'; -import { DEFAULT_REDIRECT_URL, LOGIN_PAGE } from '../../data/constants'; +import { UnAuthenticatedRoute } from '..'; +import { DEFAULT_REDIRECT_URL, LOGIN_PAGE, REGISTER_PAGE } from '../../data/constants'; const RRD = require('react-router-dom'); // Just render plain div with its children -// eslint-disable-next-line react/prop-types RRD.BrowserRouter = ({ children }) =>
{ children }
; module.exports = RRD; +jest.mock('@edx/frontend-platform/analytics'); +analytics.sendPageEvent = jest.fn(); + const TestApp = () => (
- (Login Page)} /> + (Login Page)} /> + (Register Page)} />
); -describe('UnAuthOnlyRoute', () => { - const routerWrapper = () => ( - +describe('UnAuthenticatedRoute', () => { + const routerWrapper = (initialEntry) => ( + ); @@ -60,4 +65,14 @@ describe('UnAuthOnlyRoute', () => { expect(wrapper.find('span').text()).toBe('Login Page'); }); + + it('send page event when login page is rendered', () => { + mount(routerWrapper()); + expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login'); + }); + + it('send page event when register page is rendered', () => { + mount(routerWrapper(REGISTER_PAGE)); + expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register'); + }); }); diff --git a/src/forgot-password/ForgotPasswordPage.jsx b/src/forgot-password/ForgotPasswordPage.jsx index 3e5446dd..6142b76c 100644 --- a/src/forgot-password/ForgotPasswordPage.jsx +++ b/src/forgot-password/ForgotPasswordPage.jsx @@ -6,7 +6,6 @@ import { connect } from 'react-redux'; import { Redirect } from 'react-router-dom'; import { getConfig } from '@edx/frontend-platform'; -import { sendPageEvent } from '@edx/frontend-platform/analytics'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Alert, @@ -64,8 +63,6 @@ const ForgotPasswordPage = (props) => { return error; }; - sendPageEvent('login_and_registration', 'reset'); - return ( import('./ForgotPasswordPage.jsx'), { + fallback: , +}); + +const LoadableForgotPasswordPage = () => ; + +export default LoadableForgotPasswordPage; diff --git a/src/forgot-password/index.js b/src/forgot-password/index.js index 2d30ff06..5e3f59c4 100644 --- a/src/forgot-password/index.js +++ b/src/forgot-password/index.js @@ -1,4 +1,4 @@ -export { default } from './ForgotPasswordPage'; +export { default } from './LoadableForgotPasswordPage'; export { default as reducer } from './data/reducers'; export { FORGOT_PASSWORD } from './data/actions'; export { default as saga } from './data/sagas'; diff --git a/src/index.jsx b/src/index.jsx index c3b345f5..e88fb667 100755 --- a/src/index.jsx +++ b/src/index.jsx @@ -7,20 +7,19 @@ import { AppProvider, ErrorPage } from '@edx/frontend-platform/react'; import React from 'react'; import ReactDOM from 'react-dom'; import { Redirect, Route, Switch } from 'react-router-dom'; - import { messages as headerMessages } from '@edx/frontend-component-header'; import configureStore from './data/configureStore'; -import { RegistrationPage } from './register'; -import { LoginPage } from './login'; +import LoadableForgotPasswordPage from './forgot-password'; +import LoadableRegistrationPage from './register'; +import LoadableLoginPage from './login'; +import LoadableResetPasswordPage from './reset-password'; import { LOGIN_PAGE, PAGE_NOT_FOUND, REGISTER_PAGE, RESET_PAGE, PASSWORD_RESET_CONFIRM, } from './data/constants'; -import ForgotPasswordPage from './forgot-password'; import { - HeaderLayout, UnAuthOnlyRoute, registerIcons, NotFoundPage, + HeaderLayout, UnAuthenticatedRoute, registerIcons, NotFoundPage, } from './common-components'; -import ResetPasswordPage from './reset-password'; import appMessages from './i18n'; import './index.scss'; @@ -35,10 +34,10 @@ subscribe(APP_READY, () => { - - - - + + + + diff --git a/src/login/LoadableLoginPage.jsx b/src/login/LoadableLoginPage.jsx new file mode 100644 index 00000000..93782c61 --- /dev/null +++ b/src/login/LoadableLoginPage.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import loadable from '@loadable/component'; +import Spinner from '../common-components/Spinner'; + +const LoadableComponent = loadable(() => import('./LoginPage.jsx'), { + fallback: , +}); + +const LoadableLoginPage = () => ; + +export default LoadableLoginPage; diff --git a/src/login/LoginHelpLinks.jsx b/src/login/LoginHelpLinks.jsx index 31f179bc..46cbfff0 100644 --- a/src/login/LoginHelpLinks.jsx +++ b/src/login/LoginHelpLinks.jsx @@ -1,4 +1,5 @@ import React, { useState } from 'react'; +import { Link } from 'react-router-dom'; import PropTypes from 'prop-types'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; @@ -29,19 +30,17 @@ const LoginHelpLinks = (props) => { }; const forgotPasswordLink = () => ( - {intl.formatMessage(messages['forgot.password.link'])} - + ); const signUpLink = () => ( - - {intl.formatMessage(messages['register.link'])} - + {intl.formatMessage(messages['register.link'])} + ); const loginIssueSupportURL = (config) => (config.LOGIN_ISSUE_SUPPORT_LINK diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx index fddff73a..eb0ea683 100644 --- a/src/login/LoginPage.jsx +++ b/src/login/LoginPage.jsx @@ -1,5 +1,6 @@ import React from 'react'; import Skeleton from 'react-loading-skeleton'; +import { Link } from 'react-router-dom'; import { getConfig } from '@edx/frontend-platform'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; @@ -37,7 +38,6 @@ class LoginPage extends React.Component { constructor(props, context) { super(props, context); - sendPageEvent('login_and_registration', 'login'); this.state = { password: '', email: '', @@ -197,9 +197,13 @@ class LoginPage extends React.Component { ) : null}

{intl.formatMessage(messages['first.time.here'])} - - {intl.formatMessage(messages['create.an.account'])}. - + + {intl.formatMessage(messages['create.an.account'])} +


diff --git a/src/login/index.js b/src/login/index.js index 769161d2..8591800b 100644 --- a/src/login/index.js +++ b/src/login/index.js @@ -1,4 +1,4 @@ -export { default as LoginPage } from './LoginPage'; +export { default } from './LoadableLoginPage'; 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 d64a2dc7..9878fde8 100644 --- a/src/login/messages.jsx +++ b/src/login/messages.jsx @@ -48,7 +48,7 @@ const messages = defineMessages({ }, 'create.an.account': { id: 'create.an.account', - defaultMessage: 'Create an account', + defaultMessage: 'Create an account.', description: 'Message on button to return to register page', }, 'or.sign.in.with': { diff --git a/src/login/tests/LoginHelpLinks.test.jsx b/src/login/tests/LoginHelpLinks.test.jsx index e975aa1c..6adb7dab 100644 --- a/src/login/tests/LoginHelpLinks.test.jsx +++ b/src/login/tests/LoginHelpLinks.test.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { BrowserRouter as Router } from 'react-router-dom'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import * as analytics from '@edx/frontend-platform/analytics'; import { mount } from 'enzyme'; @@ -20,7 +21,9 @@ describe('LoginHelpLinks', () => { const reduxWrapper = children => ( - {children} + + {children} + ); diff --git a/src/login/tests/LoginPage.test.jsx b/src/login/tests/LoginPage.test.jsx index 6a892a77..525b2646 100644 --- a/src/login/tests/LoginPage.test.jsx +++ b/src/login/tests/LoginPage.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { Provider } from 'react-redux'; +import { BrowserRouter as Router } from 'react-router-dom'; import renderer from 'react-test-renderer'; import { mount } from 'enzyme'; import configureStore from 'redux-mock-store'; @@ -18,7 +19,6 @@ import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants'; jest.mock('@edx/frontend-platform/analytics'); analytics.sendTrackEvent = jest.fn(); -analytics.sendPageEvent = jest.fn(); const IntlLoginFailureMessage = injectIntl(LoginFailureMessage); const IntlLoginPage = injectIntl(LoginPage); @@ -61,7 +61,11 @@ describe('LoginPage', () => { const reduxWrapper = children => ( - {children} + + + {children} + + ); @@ -377,11 +381,6 @@ describe('LoginPage', () => { expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.register_form.toggled', { category: 'user-engagement' }); }); - it('send page event when login page is rendered', () => { - mount(reduxWrapper()); - expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login'); - }); - it('send tracking and page events when institutional button is clicked', () => { store = mockStore({ ...initialState, diff --git a/src/login/tests/__snapshots__/LoginPage.test.jsx.snap b/src/login/tests/__snapshots__/LoginPage.test.jsx.snap index 44fd63e5..7abff277 100644 --- a/src/login/tests/__snapshots__/LoginPage.test.jsx.snap +++ b/src/login/tests/__snapshots__/LoginPage.test.jsx.snap @@ -13,13 +13,11 @@ exports[`LoginPage should match TPA provider snapshot 1`] = `

First time here? - Create an account - . + Create an account.


First time here? - Create an account - . + Create an account.


First time here? - Create an account - . + Create an account.


First time here? - Create an account - . + Create an account.


First time here? - Create an account - . + Create an account.


import('./RegistrationPage.jsx'), { + fallback: , +}); + +const LoadableRegistrationPage = () => ; + +export default LoadableRegistrationPage; diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index 3fc92875..0794ad7e 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -1,4 +1,5 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import camelCase from 'lodash.camelcase'; import { connect } from 'react-redux'; @@ -6,7 +7,7 @@ import Skeleton from 'react-loading-skeleton'; import PropTypes from 'prop-types'; import { getConfig } from '@edx/frontend-platform'; -import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; +import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { injectIntl, intlShape, getCountryList, getLocale, FormattedMessage, } from '@edx/frontend-platform/i18n'; @@ -36,9 +37,7 @@ class RegistrationPage extends React.Component { constructor(props, context) { super(props, context); - sendPageEvent('login_and_registration', 'register'); this.intl = props.intl; - this.state = { email: '', name: '', @@ -436,9 +435,13 @@ class RegistrationPage extends React.Component { )}

{intl.formatMessage(messages['already.have.an.edx.account'])} - + {intl.formatMessage(messages['sign.in.hyperlink'])} - +


{intl.formatMessage(messages['create.a.new.account'])}

diff --git a/src/register/index.js b/src/register/index.js index 496c0f7b..0703e15e 100644 --- a/src/register/index.js +++ b/src/register/index.js @@ -1,4 +1,4 @@ -export { default as RegistrationPage } from './RegistrationPage'; +export { default } from './LoadableRegistrationPage'; export { default as reducer } from './data/reducers'; export { default as saga } from './data/sagas'; export { storeName } from './data/selectors'; diff --git a/src/register/tests/RegistrationPage.test.jsx b/src/register/tests/RegistrationPage.test.jsx index ae4fcc98..0c2995f1 100644 --- a/src/register/tests/RegistrationPage.test.jsx +++ b/src/register/tests/RegistrationPage.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { Provider } from 'react-redux'; +import { BrowserRouter as Router } from 'react-router-dom'; import renderer from 'react-test-renderer'; import { mount } from 'enzyme'; import configureStore from 'redux-mock-store'; @@ -18,7 +19,6 @@ import { fetchRealtimeValidations, registerNewUser } from '../data/actions'; jest.mock('@edx/frontend-platform/analytics'); analytics.sendTrackEvent = jest.fn(); -analytics.sendPageEvent = jest.fn(); const IntlRegistrationPage = injectIntl(RegistrationPage); const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage); @@ -76,7 +76,11 @@ describe('RegistrationPageTests', () => { const reduxWrapper = children => ( - {children} + + + {children} + + ); @@ -129,11 +133,6 @@ describe('RegistrationPageTests', () => { expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.login_form.toggled', { category: 'user-engagement' }); }); - it('send page event when register page is rendered', () => { - mount(reduxWrapper()); - expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register'); - }); - it('should show optional fields section on optional check enabled', () => { const registrationPage = mount(reduxWrapper()); registrationPage.find('input#optional').simulate('change', { target: { checked: true } }); diff --git a/src/register/tests/__snapshots__/RegistrationPage.test.jsx.snap b/src/register/tests/__snapshots__/RegistrationPage.test.jsx.snap index fbf4fe93..a24177be 100644 --- a/src/register/tests/__snapshots__/RegistrationPage.test.jsx.snap +++ b/src/register/tests/__snapshots__/RegistrationPage.test.jsx.snap @@ -13,10 +13,9 @@ exports[`RegistrationPageTests should match TPA provider snapshot 1`] = `

Already have an edX account? Sign in. @@ -1562,10 +1561,9 @@ exports[`RegistrationPageTests should match default section snapshot 1`] = `

Already have an edX account? Sign in. @@ -3072,10 +3070,9 @@ exports[`RegistrationPageTests should match pending button state snapshot 1`] =

Already have an edX account? Sign in. diff --git a/src/reset-password/LoadableResetPasswordPage.jsx b/src/reset-password/LoadableResetPasswordPage.jsx new file mode 100644 index 00000000..4f6b7831 --- /dev/null +++ b/src/reset-password/LoadableResetPasswordPage.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import loadable from '@loadable/component'; +import Spinner from '../common-components/Spinner'; + +const LoadableComponent = loadable(() => import('./ResetPasswordPage.jsx'), { + fallback: , +}); + +const LoadableResetPasswordPage = () => ; + +export default LoadableResetPasswordPage; diff --git a/src/reset-password/ResetPasswordPage.jsx b/src/reset-password/ResetPasswordPage.jsx index 1b46aeaa..5687cf4c 100644 --- a/src/reset-password/ResetPasswordPage.jsx +++ b/src/reset-password/ResetPasswordPage.jsx @@ -18,8 +18,8 @@ import ResetSuccessMessage from './ResetSuccess'; import { AuthnValidationFormGroup, APIFailureMessage, + Spinner, } from '../common-components'; -import Spinner from './Spinner'; import { API_RATELIMIT_ERROR, INTERNAL_SERVER_ERROR } from '../data/constants'; const ResetPasswordPage = (props) => { diff --git a/src/reset-password/index.js b/src/reset-password/index.js index 2116170a..335d1d21 100644 --- a/src/reset-password/index.js +++ b/src/reset-password/index.js @@ -1,4 +1,4 @@ -export { default } from './ResetPasswordPage'; +export { default } from './LoadableResetPasswordPage'; export { default as reducer } from './data/reducers'; export { RESET_PASSWORD } from './data/actions'; export { default as saga } from './data/sagas';