diff --git a/package-lock.json b/package-lock.json index b41cab14fe..c5409cbd8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9704,8 +9704,7 @@ "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, "decompress": { "version": "4.2.1", @@ -17665,6 +17664,16 @@ "cast-array": "~1.0.0", "object-filter": "~1.0.2", "query-string": "~2.4.1" + }, + "dependencies": { + "query-string": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz", + "integrity": "sha1-fbBmZCCAS6qSrp8miWKFWnYUPfs=", + "requires": { + "strict-uri-encode": "^1.0.0" + } + } } }, "make-dir": { @@ -20685,10 +20694,12 @@ "dev": true }, "query-string": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-2.4.2.tgz", - "integrity": "sha1-fbBmZCCAS6qSrp8miWKFWnYUPfs=", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", "strict-uri-encode": "^1.0.0" } }, diff --git a/package.json b/package.json index 9e146ad147..c5f00f3e37 100644 --- a/package.json +++ b/package.json @@ -35,25 +35,27 @@ }, "dependencies": { "@edx/brand": "npm:@edx/brand-openedx@1.1.0", + "@edx/frontend-component-cookie-policy-banner": "2.1.8", "@edx/frontend-component-header": "2.2.4", "@edx/frontend-platform": "1.8.4", - "@edx/frontend-component-cookie-policy-banner": "2.1.8", "@edx/paragon": "13.16.1", "@fortawesome/fontawesome-svg-core": "1.2.32", "@fortawesome/free-brands-svg-icons": "5.15.1", "@fortawesome/free-regular-svg-icons": "5.15.1", "@fortawesome/free-solid-svg-icons": "5.15.1", "@fortawesome/react-fontawesome": "0.1.13", - "core-js": "3.9.1", "classnames": "2.2.6", + "core-js": "3.9.1", "extract-react-intl-messages": "4.1.1", "form-urlencoded": "4.2.1", "formik": "2.2.6", "lodash.camelcase": "4.3.0", "lodash.snakecase": "4.1.1", "prop-types": "15.7.2", + "query-string": "5.1.1", "react": "16.14.0", "react-dom": "16.14.0", + "react-helmet": "6.1.0", "react-loading-skeleton": "2.1.1", "react-redux": "7.2.2", "react-router": "5.2.0", @@ -64,9 +66,8 @@ "redux-mock-store": "1.5.4", "redux-saga": "1.1.3", "redux-thunk": "2.3.0", - "reselect": "4.0.0", - "react-helmet": "6.1.0", - "regenerator-runtime": "0.13.7" + "regenerator-runtime": "0.13.7", + "reselect": "4.0.0" }, "devDependencies": { "@edx/frontend-build": "5.6.8", diff --git a/src/data/constants.js b/src/data/constants.js index 19e63d5632..a8e1342e72 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -24,3 +24,7 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+ + '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"' + ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})' + '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$'; + +// Query string parameters that can be passed to LMS to manage +// things like auto-enrollment upon login and registration. +export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next']; diff --git a/src/data/utils/dataUtils.js b/src/data/utils/dataUtils.js index 05bc875237..0963afa79b 100644 --- a/src/data/utils/dataUtils.js +++ b/src/data/utils/dataUtils.js @@ -1,5 +1,8 @@ // Utility functions +import * as QueryString from 'query-string'; +import { AUTH_PARAMS } from '../constants'; + export default function processLink(link) { let matches; link.replace(/(.*?)([^<]+)<\/a>(.*)/g, function () { // eslint-disable-line func-names @@ -40,3 +43,25 @@ export const processTpaHintURL = (params) => { } return tpaHint; }; + +export const updatePathWithQueryParams = (path) => { + const queryParams = window.location.search; + + if (!queryParams) { + return path; + } + + return `${path}${queryParams}`; +}; + +export const getAllPossibleQueryParam = () => { + const urlParams = QueryString.parse(document.location.search); + const params = {}; + Object.entries(urlParams).forEach(([key, value]) => { + if (AUTH_PARAMS.indexOf(key) > -1) { + params[key] = value; + } + }); + + return params; +}; diff --git a/src/data/utils/dataUtils.test.js b/src/data/utils/dataUtils.test.js index d37e31f2a7..b2dd3c582d 100644 --- a/src/data/utils/dataUtils.test.js +++ b/src/data/utils/dataUtils.test.js @@ -1,4 +1,5 @@ -import processLink from './dataUtils'; +import { LOGIN_PAGE } from '../constants'; +import processLink, { updatePathWithQueryParams } from './dataUtils'; describe('processLink', () => { it('should use the provided processLink function to', () => { @@ -12,3 +13,20 @@ describe('processLink', () => { expect(matches[2]).toEqual(expectedText); }); }); + +describe('updatePathWithQueryParams', () => { + it('should append query params into the path', () => { + const params = '?course_id=testCourseId'; + const expectedPath = `${LOGIN_PAGE}${params}`; + + Object.defineProperty(window, 'location', { + value: { + href: 'http://localhost/', + search: params, + }, + }); + const updatedPath = updatePathWithQueryParams(LOGIN_PAGE); + + expect(updatedPath).toEqual(expectedPath); + }); +}); diff --git a/src/data/utils/index.js b/src/data/utils/index.js index 96fe6c27c0..6719e5ef40 100644 --- a/src/data/utils/index.js +++ b/src/data/utils/index.js @@ -1,2 +1,8 @@ -export { default, getTpaProvider, processTpaHintURL } from './dataUtils'; +export { + default, + getTpaProvider, + processTpaHintURL, + updatePathWithQueryParams, + getAllPossibleQueryParam, +} from './dataUtils'; export { default as AsyncActionType } from './reduxUtils'; diff --git a/src/forgot-password/ForgotPasswordPage.jsx b/src/forgot-password/ForgotPasswordPage.jsx index 50e376d558..5c737a401e 100644 --- a/src/forgot-password/ForgotPasswordPage.jsx +++ b/src/forgot-password/ForgotPasswordPage.jsx @@ -28,6 +28,7 @@ import { import APIFailureMessage from '../common-components/APIFailureMessage'; import { INTERNAL_SERVER_ERROR, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants'; import LoginHelpLinks from '../login/LoginHelpLinks'; +import { updatePathWithQueryParams } from '../data/utils'; const ForgotPasswordPage = (props) => { const { intl, status } = props; @@ -92,7 +93,7 @@ const ForgotPasswordPage = (props) => { { siteName: getConfig().SITE_NAME })} - {status === 'complete' ? : null} + {status === 'complete' ? : null}
diff --git a/src/login/LoginHelpLinks.jsx b/src/login/LoginHelpLinks.jsx index 31f179bcf0..bba14ed588 100644 --- a/src/login/LoginHelpLinks.jsx +++ b/src/login/LoginHelpLinks.jsx @@ -14,6 +14,7 @@ import { RESET_PAGE, } from '../data/constants'; import messages from './messages'; +import { updatePathWithQueryParams } from '../data/utils'; const LoginHelpLinks = (props) => { const { intl, page } = props; @@ -31,7 +32,7 @@ const LoginHelpLinks = (props) => { const forgotPasswordLink = () => ( {intl.formatMessage(messages['forgot.password.link'])} @@ -39,7 +40,7 @@ const LoginHelpLinks = (props) => { ); const signUpLink = () => ( - + {intl.formatMessage(messages['register.link'])} ); diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx index 8a36880b0c..fed46eb022 100644 --- a/src/login/LoginPage.jsx +++ b/src/login/LoginPage.jsx @@ -32,7 +32,9 @@ import { DEFAULT_REDIRECT_URL, DEFAULT_STATE, LOGIN_PAGE, REGISTER_PAGE, ENTERPRISE_LOGIN_URL, PENDING_STATE, } from '../data/constants'; import { forgotPasswordResultSelector } from '../forgot-password'; -import { getTpaProvider, processTpaHintURL } from '../data/utils'; +import { + getTpaProvider, processTpaHintURL, updatePathWithQueryParams, getAllPossibleQueryParam, +} from '../data/utils'; class LoginPage extends React.Component { constructor(props, context) { @@ -90,16 +92,10 @@ class LoginPage extends React.Component { return; } - const params = (new URL(document.location)).searchParams; - const payload = { email, password }; - const next = params.get('next'); - const courseId = params.get('course_id'); - if (next) { - payload.next = next; - } - if (courseId) { - payload.course_id = courseId; - } + let payload = { email, password }; + const postParams = getAllPossibleQueryParam(); + + payload = { ...payload, ...postParams }; this.props.loginRequest(payload); } @@ -203,7 +199,11 @@ class LoginPage extends React.Component { ) : null}

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

diff --git a/src/register/RegistrationPage.jsx b/src/register/RegistrationPage.jsx index 4851432606..5d5dd119e9 100644 --- a/src/register/RegistrationPage.jsx +++ b/src/register/RegistrationPage.jsx @@ -31,7 +31,9 @@ import EnterpriseSSO from '../common-components/EnterpriseSSO'; import { DEFAULT_REDIRECT_URL, DEFAULT_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE, } from '../data/constants'; -import { getTpaProvider, processTpaHintURL } from '../data/utils'; +import { + getTpaProvider, processTpaHintURL, updatePathWithQueryParams, getAllPossibleQueryParam, +} from '../data/utils'; class RegistrationPage extends React.Component { constructor(props, context) { @@ -153,8 +155,7 @@ class RegistrationPage extends React.Component { handleSubmit = (e) => { e.preventDefault(); - const params = (new URL(document.location)).searchParams; - const payload = { + let payload = { name: this.state.name, username: this.state.username, email: this.state.email, @@ -168,14 +169,8 @@ class RegistrationPage extends React.Component { payload.password = this.state.password; } - const next = params.get('next'); - const courseId = params.get('course_id'); - if (next) { - payload.next = next; - } - if (courseId) { - payload.course_id = courseId; - } + const postParams = getAllPossibleQueryParam(); + payload = { ...payload, ...postParams }; let finalValidation = this.state.formValid; if (!this.state.formValid) { @@ -461,7 +456,11 @@ class RegistrationPage extends React.Component { )}

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