Compare commits
218 Commits
temp-main
...
INF-rebase
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1b3a9006e | ||
|
|
ddcd4dbe08 | ||
|
|
d55ddbeb0f | ||
|
|
ff3fce99db | ||
|
|
157c302384 | ||
|
|
f2a905d373 | ||
|
|
e984a0b07b | ||
|
|
7150d4562a | ||
|
|
451056866f | ||
|
|
76f0cc54d9 | ||
|
|
fb70f7a1c2 | ||
|
|
b664150b4d | ||
|
|
da5a2e31b6 | ||
|
|
486d0bfd37 | ||
|
|
9332fc113a | ||
|
|
181e837ca4 | ||
|
|
735a9afc3c | ||
|
|
319c48f1c8 | ||
|
|
fbd73bfbfe | ||
|
|
27a63cf406 | ||
|
|
7ea351f6a0 | ||
|
|
61e8c254d7 | ||
|
|
3a08e790c3 | ||
|
|
b4c5171886 | ||
|
|
7b83c416f8 | ||
|
|
a3c261bb13 | ||
|
|
98d03aa29f | ||
|
|
f5d5e2fd02 | ||
|
|
490bf27ed1 | ||
|
|
780acac2fd | ||
|
|
2ea763701d | ||
|
|
e2d9ba5857 | ||
|
|
747d656f0a | ||
|
|
8638ed5cf4 | ||
|
|
ca2e7f554a | ||
|
|
f3f14fb3e7 | ||
|
|
6c8b3835b6 | ||
|
|
0e94124d74 | ||
|
|
af7edd8a3f | ||
|
|
9323f119c8 | ||
|
|
38a1924c6a | ||
|
|
8cbe6ce02e | ||
|
|
a53334d3bf | ||
|
|
2d7303009f | ||
|
|
dfcb94a831 | ||
|
|
520dd6ed6b | ||
|
|
0172c79dd9 | ||
|
|
5d4abcbab3 | ||
|
|
1b32dbfa19 | ||
|
|
f7d9bdb5b5 | ||
|
|
063ec80cde | ||
|
|
3cbb134c3a | ||
|
|
059a60d1c8 | ||
|
|
c88d701271 | ||
|
|
33b98b356b | ||
|
|
1f81699af4 | ||
|
|
13aa77fc70 | ||
|
|
66531831b7 | ||
|
|
846d3f0662 | ||
|
|
c9987eb2f4 | ||
|
|
a2ad9d5248 | ||
|
|
cca87bd16a | ||
|
|
206c4c887b | ||
|
|
09dc21eb0e | ||
|
|
945af2bdfd | ||
|
|
0dca6f3fdc | ||
|
|
189a67c9dc | ||
|
|
3dceb63b9c | ||
|
|
f78e84ee0a | ||
|
|
9385174b93 | ||
|
|
2d27da2391 | ||
|
|
86ed8e2361 | ||
|
|
f5a6ece6b1 | ||
|
|
78413be34a | ||
|
|
88866a39c1 | ||
|
|
0d71e31ffb | ||
|
|
dc9699c033 | ||
|
|
00a0e27062 | ||
|
|
38dd2944b8 | ||
|
|
f4708ed274 | ||
|
|
6839afcf3c | ||
|
|
cb7300441c | ||
|
|
244b9e68e6 | ||
|
|
1cd9c58c1a | ||
|
|
5d481a93c7 | ||
|
|
438d1fcfa7 | ||
|
|
bc912ce139 | ||
|
|
ab1c2d5379 | ||
|
|
c109f6e771 | ||
|
|
8976647190 | ||
|
|
cb051a83ad | ||
|
|
1b8aec5709 | ||
|
|
9a68e95fcc | ||
|
|
c90980afb0 | ||
|
|
abb8ae5085 | ||
|
|
8bb7462098 | ||
|
|
b4a5397ba1 | ||
|
|
a43c620dc4 | ||
|
|
93d11b8485 | ||
|
|
68e13d4daf | ||
|
|
f6617935e3 | ||
|
|
5f4591c046 | ||
|
|
ae52a8cb65 | ||
|
|
b8df66ad23 | ||
|
|
923776ab96 | ||
|
|
f170f5e3f0 | ||
|
|
730875ceb2 | ||
|
|
c04ed9aa43 | ||
|
|
812350d24a | ||
|
|
6879bacb89 | ||
|
|
9b2b0f2019 | ||
|
|
87884f2d91 | ||
|
|
3e20fcae57 | ||
|
|
173896811d | ||
|
|
7af4a08bd9 | ||
|
|
6c12b3b034 | ||
|
|
5304085cd8 | ||
|
|
3cd9ae130c | ||
|
|
28ad2c2cf6 | ||
|
|
3e889df109 | ||
|
|
354c73bb2a | ||
|
|
76a5a5dffa | ||
|
|
5efe9d8344 | ||
|
|
cd2003921b | ||
|
|
52c6efc34d | ||
|
|
7339aec7c2 | ||
|
|
584a84a99c | ||
|
|
7e4ab1c74c | ||
|
|
025870a3b9 | ||
|
|
13d89cb3a0 | ||
|
|
5a1e2e6c97 | ||
|
|
6f1cf29a60 | ||
|
|
8dc77d5db6 | ||
|
|
159f1ae30e | ||
|
|
e2e626552f | ||
|
|
40a1f4ce6b | ||
|
|
308d7c62e4 | ||
|
|
0bc78da55d | ||
|
|
bb9fcd91c0 | ||
|
|
6527caea54 | ||
|
|
a52912e35b | ||
|
|
6479382b90 | ||
|
|
4ce36bb12c | ||
|
|
4cc7723984 | ||
|
|
3c3d359d4e | ||
|
|
cccbf3a9d1 | ||
|
|
0fa00290da | ||
|
|
4a3fd2ee8e | ||
|
|
b18caa2da0 | ||
|
|
48d7cb386a | ||
|
|
bdf9cab869 | ||
|
|
be02dabf40 | ||
|
|
c535fb9d24 | ||
|
|
5ca86f9183 | ||
|
|
8ab8d09b97 | ||
|
|
286c70d50f | ||
|
|
8939e5b91f | ||
|
|
bc9f7b3bce | ||
|
|
fd0bcb9e5f | ||
|
|
98e0167ef1 | ||
|
|
8091085f45 | ||
|
|
cd5abd1d9c | ||
|
|
2a88f435b9 | ||
|
|
fe1e9c5629 | ||
|
|
0e363ca724 | ||
|
|
c874638bd1 | ||
|
|
e5c3b1ed41 | ||
|
|
8a27b8cc37 | ||
|
|
2a9dbe9d30 | ||
|
|
62508e3bc7 | ||
|
|
ceb489753b | ||
|
|
5035a07e0a | ||
|
|
f086a165e2 | ||
|
|
9239df3620 | ||
|
|
009125c3ef | ||
|
|
b69ed6e422 | ||
|
|
07ee2392e9 | ||
|
|
2bfce01772 | ||
|
|
1477ed33d7 | ||
|
|
c4f1a97316 | ||
|
|
47b0501e1c | ||
|
|
046fbeab01 | ||
|
|
27ea509989 | ||
|
|
27f0508e6e | ||
|
|
c53fedf7a1 | ||
|
|
0f1a5e9aef | ||
|
|
6cb4b799b7 | ||
|
|
439b9161b5 | ||
|
|
e496bb62c5 | ||
|
|
c1e63da778 | ||
|
|
ecf4c3ae53 | ||
|
|
2428b4c389 | ||
|
|
099fe8d717 | ||
|
|
4755540be8 | ||
|
|
9a30f053c7 | ||
|
|
6b983e18d3 | ||
|
|
327210192c | ||
|
|
0d603b5fa1 | ||
|
|
efaa83a1bc | ||
|
|
bd63bb1f15 | ||
|
|
5754c2961a | ||
|
|
dcbd644a25 | ||
|
|
52e438652c | ||
|
|
d8947a4c0a | ||
|
|
03d1666c2c | ||
|
|
3782503983 | ||
|
|
b219fe3683 | ||
|
|
90f650ce3e | ||
|
|
6f325c20c3 | ||
|
|
de12dfbf9e | ||
|
|
c663f6fa30 | ||
|
|
dba93333fd | ||
|
|
611af07326 | ||
|
|
564ec70d9e | ||
|
|
65e95a4d1b | ||
|
|
cf2b50005b | ||
|
|
faf4ff8488 | ||
|
|
7d64220852 |
5
.env
5
.env
@@ -16,6 +16,9 @@ SITE_NAME=null
|
||||
INFO_EMAIL=''
|
||||
# ***** Cookies *****
|
||||
USER_RETENTION_COOKIE_NAME=null
|
||||
# ***** Cohesion Keys *****
|
||||
COHESION_WRITE_KEY=''
|
||||
COHESION_SOURCE_KEY=''
|
||||
# ***** Links *****
|
||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
|
||||
@@ -41,3 +44,5 @@ BANNER_IMAGE_EXTRA_SMALL=''
|
||||
# ***** Miscellaneous *****
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -25,6 +25,9 @@ ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
|
||||
# ***** Cookies *****
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
# ***** Cohesion Keys *****
|
||||
COHESION_WRITE_KEY=''
|
||||
COHESION_SOURCE_KEY=''
|
||||
# ***** Links *****
|
||||
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
|
||||
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
||||
@@ -41,3 +44,5 @@ APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -18,3 +18,6 @@ SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
COHESION_WRITE_KEY=''
|
||||
COHESION_SOURCE_KEY=''
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
13
.eslintrc.js
13
.eslintrc.js
@@ -1,7 +1,7 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
|
||||
module.exports = createConfig('eslint', {
|
||||
const config = createConfig('eslint', {
|
||||
rules: {
|
||||
// Temporarily update the 'indent', 'template-curly-spacing' and
|
||||
// 'no-multiple-empty-lines' rules since they are causing eslint
|
||||
@@ -50,3 +50,14 @@ module.exports = createConfig('eslint', {
|
||||
'function-paren-newline': 'off',
|
||||
},
|
||||
});
|
||||
|
||||
config.settings = {
|
||||
'import/resolver': {
|
||||
node: {
|
||||
paths: ['src', 'node_modules'],
|
||||
extensions: ['.js', '.jsx'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = config;
|
||||
|
||||
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Adding new check for github-actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
autoupdate:
|
||||
name: autoupdate
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: docker://chinthakagodawita/autoupdate-action:v1
|
||||
env:
|
||||
|
||||
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -10,18 +10,15 @@ on:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
@@ -42,7 +39,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run Code Coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
14
README.rst
14
README.rst
@@ -29,7 +29,13 @@ Getting Started
|
||||
Installation
|
||||
============
|
||||
|
||||
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
|
||||
`Tutor`_ is currently recommended as a development environment for your new MFE. Please refer to the `relevant tutor-mfe documentation`_ to get started using it.
|
||||
|
||||
.. _Tutor: https://github.com/overhangio/tutor
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
|
||||
|
||||
Devstack (Deprecated) instructions
|
||||
==================================
|
||||
|
||||
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
|
||||
|
||||
@@ -51,7 +57,7 @@ This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see
|
||||
Environment Variables/Setup Notes
|
||||
=================================
|
||||
|
||||
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
|
||||
|
||||
The authentication micro-frontend also requires the following additional variable:
|
||||
|
||||
@@ -142,13 +148,13 @@ Furthermore, there are several edX-specific environment variables that enable in
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
For more information see the document: `Micro-frontend applications in Open
|
||||
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
edX <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
|
||||
|
||||
How To Contribute
|
||||
=================
|
||||
|
||||
Contributions are very welcome, and strongly encouraged! We've
|
||||
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
|
||||
put together `some documentation that describes our contribution process <https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html>`_.
|
||||
|
||||
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ metadata:
|
||||
icon: 'Article'
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: group:2u-infinity
|
||||
type: 'service'
|
||||
|
||||
@@ -3,7 +3,7 @@ Enable Social Auth Locally
|
||||
|
||||
Please follow the steps below to enable social auth (SSO) locally.
|
||||
|
||||
1. Follow `Enabling Third Party Authentication <https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/tpa/index.html>`_ for backend configuration.
|
||||
1. Follow `Enabling Third Party Authentication <https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/tpa/index.html>`_ for backend configuration.
|
||||
|
||||
2. Authn has a component for rendering Social Auth providers at frontend which goes through each provider.
|
||||
|
||||
|
||||
60
example.env.config.js
Normal file
60
example.env.config.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Authn MFE is now able to handle JS-based configuration!
|
||||
|
||||
For the time being, the `.env.*` files are still made available when cloning down this repo or pulling from
|
||||
the master branch. To switch to using `env.config.js`, make a copy of `example.env.config.js` and configure as needed.
|
||||
|
||||
For testing with Jest Snapshot, there is a mock in `/src/setupTest.jsx` for `getConfig` that will need to be
|
||||
uncommented.
|
||||
|
||||
Note: having both .env and env.config.js files will follow a predictable order, in which non-empty values in the
|
||||
JS-based config will overwrite the .env environment variables.
|
||||
|
||||
frontend-platform's getConfig loads configuration in the following sequence:
|
||||
- .env file config
|
||||
- optional handlers (commonly used to merge MFE-specific config in via additional process.env variables)
|
||||
- env.config.js file config
|
||||
- runtime config
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
NODE_ENV: 'development',
|
||||
NODE_PATH: './src',
|
||||
PORT: 1999,
|
||||
ACCESS_TOKEN_COOKIE_NAME: 'edx-jwt-cookie-header-payload',
|
||||
BASE_URL: 'http://localhost:1999',
|
||||
CREDENTIALS_BASE_URL: 'http://localhost:18150',
|
||||
CSRF_TOKEN_API_PATH: '/csrf/api/v1/token',
|
||||
ECOMMERCE_BASE_URL: 'http://localhost:18130',
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME: 'openedx-language-preference',
|
||||
LMS_BASE_URL: 'http://localhost:18000',
|
||||
LOGIN_URL: 'http://localhost:1999/login',
|
||||
LOGOUT_URL: 'http://localhost:18000/logout',
|
||||
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
|
||||
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
|
||||
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
|
||||
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
|
||||
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
|
||||
ORDER_HISTORY_URL: 'http://localhost:1996/orders',
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT: 'http://localhost:18000/login_refresh',
|
||||
SEGMENT_KEY: '',
|
||||
SITE_NAME: 'Your Platform Name Here',
|
||||
INFO_EMAIL: 'info@example.com',
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: 'true',
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: 'true',
|
||||
SESSION_COOKIE_DOMAIN: 'localhost',
|
||||
USER_INFO_COOKIE_NAME: 'edx-user-info',
|
||||
LOGIN_ISSUE_SUPPORT_LINK: 'http://localhost:18000/login-issue-support-url',
|
||||
TOS_AND_HONOR_CODE: 'http://localhost:18000/honor',
|
||||
TOS_LINK: 'http://localhost:18000/tos',
|
||||
PRIVACY_POLICY: 'http://localhost:18000/privacy',
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/welcome',
|
||||
BANNER_IMAGE_LARGE: '',
|
||||
BANNER_IMAGE_MEDIUM: '',
|
||||
BANNER_IMAGE_SMALL: '',
|
||||
BANNER_IMAGE_EXTRA_SMALL: '',
|
||||
APP_ID: '',
|
||||
MFE_CONFIG_API_URL: '',
|
||||
ZENDESK_KEY: '',
|
||||
ZENDESK_LOGO_URL: '',
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
|
||||
|
||||
nick: Authn MFE
|
||||
oeps: {}
|
||||
owner: openedx/2u-infinity
|
||||
openedx-release:
|
||||
ref: master
|
||||
23659
package-lock.json
generated
23659
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
51
package.json
51
package.json
@@ -13,8 +13,10 @@
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"dev": "PUBLIC_PATH=/authn/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
||||
},
|
||||
"husky": {
|
||||
@@ -33,53 +35,56 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-platform": "^8.0.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@openedx/paragon": "^22.1.1",
|
||||
"@openedx/frontend-plugin-framework": "^1.3.0",
|
||||
"@openedx/paragon": "^23.4.2",
|
||||
"@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",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"algoliasearch": "^4.14.3",
|
||||
"algoliasearch-helper": "^3.14.0",
|
||||
"algoliasearch-helper": "^3.26.0",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.38.1",
|
||||
"core-js": "3.43.0",
|
||||
"fastest-levenshtein": "1.0.16",
|
||||
"form-urlencoded": "6.1.5",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-loading-skeleton": "3.4.0",
|
||||
"react-loading-skeleton": "3.5.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-router": "6.26.1",
|
||||
"react-router-dom": "6.26.1",
|
||||
"react-router": "6.30.1",
|
||||
"react-router-dom": "6.30.1",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.2.1",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"redux-mock-store": "1.5.5",
|
||||
"redux-saga": "1.3.0",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.14.1",
|
||||
"reselect": "4.1.8",
|
||||
"universal-cookie": "4.0.4"
|
||||
"reselect": "5.1.1",
|
||||
"universal-cookie": "7.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@openedx/frontend-build": "^14.0.3",
|
||||
"babel-plugin-formatjs": "10.5.16",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"@openedx/frontend-build": "^14.6.1",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"babel-plugin-formatjs": "10.5.39",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"glob": "7.2.3",
|
||||
"history": "5.3.0",
|
||||
"husky": "7.0.4",
|
||||
"jest": "29.7.0",
|
||||
"react-test-renderer": "^17.0.2"
|
||||
"husky": "9.1.7",
|
||||
"jest": "30.0.4",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"ts-jest": "^29.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon"/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
|
||||
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer">
|
||||
</script>
|
||||
<% if (process.env.OPTIMIZELY_URL) { %>
|
||||
<script
|
||||
src="<%= process.env.OPTIMIZELY_URL %>"
|
||||
></script>
|
||||
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script
|
||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
||||
></script>
|
||||
<% } %>
|
||||
<title>
|
||||
<%= (process.env.SITE_NAME && process.env.SITE_NAME !='null' ) ?
|
||||
'Authentication | ' + process.env.SITE_NAME : 'Authentication' %>
|
||||
</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href="<%=htmlWebpackPlugin.options.FAVICON_URL%>"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<script defer src="https://www.edx.org/beam-wrapper.js" ></script>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
|
||||
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
<% if (process.env.OPTIMIZELY_URL) { %>
|
||||
<script src="<%= process.env.OPTIMIZELY_URL %>"></script>
|
||||
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"></script>
|
||||
<% } %>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -6,7 +6,7 @@ import { Helmet } from 'react-helmet';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
|
||||
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, RouteTracker, UnAuthOnlyRoute,
|
||||
} from './common-components';
|
||||
import configureStore from './data/configureStore';
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { updatePathWithQueryParams } from './data/utils';
|
||||
import { ForgotPasswordPage } from './forgot-password';
|
||||
import Logistration from './logistration/Logistration';
|
||||
import MainAppSlot from './plugin-slots/MainAppSlot';
|
||||
import { ProgressiveProfiling } from './progressive-profiling';
|
||||
import { RecommendationsPage } from './recommendations';
|
||||
import { RegistrationPage } from './register';
|
||||
@@ -36,7 +37,6 @@ const MainApp = () => (
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
{getConfig().ZENDESK_KEY && <Zendesk />}
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||
<Route
|
||||
@@ -57,6 +57,8 @@ const MainApp = () => (
|
||||
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
|
||||
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
|
||||
</Routes>
|
||||
<RouteTracker />
|
||||
<MainAppSlot />
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
|
||||
22
src/cohesion/constants.js
Normal file
22
src/cohesion/constants.js
Normal file
@@ -0,0 +1,22 @@
|
||||
export const PAGE_TYPES = {
|
||||
ACCOUNT_CREATION: 'account-creation',
|
||||
SIGN_IN: 'sign-in',
|
||||
};
|
||||
|
||||
export const ELEMENT_TYPES = {
|
||||
BUTTON: 'BUTTON',
|
||||
};
|
||||
|
||||
export const EVENT_TYPES = { ElementClicked: 'redventures.usertracking.v3.ElementClicked' };
|
||||
|
||||
export const ELEMENT_TEXT = {
|
||||
CREATE_ACCOUNT: 'create-account',
|
||||
OPT_IN_TEXT: 'I agree that edx may send me marketing messages',
|
||||
SIGN_IN: 'Sign In',
|
||||
};
|
||||
|
||||
export const ELEMENT_NAME = {
|
||||
SIGN_IN: PAGE_TYPES.SIGN_IN,
|
||||
OPT_OUT: 'opt-out',
|
||||
CREATE_ACCOUNT: 'Create an account for free',
|
||||
};
|
||||
6
src/cohesion/data/actions.js
Normal file
6
src/cohesion/data/actions.js
Normal file
@@ -0,0 +1,6 @@
|
||||
export const SET_COHESION_EVENT_ELEMENT_STATES = 'SET_COHESION_EVENT_ELEMENT_STATES';
|
||||
|
||||
export const setCohesionEventStates = (eventData) => ({
|
||||
type: SET_COHESION_EVENT_ELEMENT_STATES,
|
||||
payload: eventData,
|
||||
});
|
||||
17
src/cohesion/data/reducers.js
Normal file
17
src/cohesion/data/reducers.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { SET_COHESION_EVENT_ELEMENT_STATES } from './actions';
|
||||
|
||||
export const storeName = 'cohesion';
|
||||
|
||||
export const defaultState = {
|
||||
eventData: {},
|
||||
};
|
||||
|
||||
export const reducer = (state = defaultState, action = {}) => {
|
||||
if (action.type === SET_COHESION_EVENT_ELEMENT_STATES) {
|
||||
return {
|
||||
...state,
|
||||
eventData: action.payload,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
24
src/cohesion/trackers.js
Normal file
24
src/cohesion/trackers.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { EVENT_TYPES } from './constants';
|
||||
|
||||
/**
|
||||
* Tracks cohesion events by setting the page type and tracking a click event.
|
||||
*
|
||||
* @param {string} pageType - The type of page where the event occurred.
|
||||
* @param {string} elementType - The type of the web element (e.g., 'BUTTON', 'LINK').
|
||||
* @param {string} webElementText - The text content of the web element.
|
||||
* @param {string} webElementName - The name of the web element.
|
||||
*/
|
||||
const trackCohesionEvent = (eventData) => {
|
||||
window.chsn_pageType = eventData.pageType;
|
||||
const webElement = {
|
||||
elementType: eventData.elementType,
|
||||
text: eventData.webElementText,
|
||||
name: eventData.webElementName,
|
||||
};
|
||||
window.tagular?.('beam', {
|
||||
'@type': EVENT_TYPES.ElementClicked,
|
||||
webElement,
|
||||
});
|
||||
};
|
||||
|
||||
export default trackCohesionEvent;
|
||||
6
src/cohesion/utils.js
Normal file
6
src/cohesion/utils.js
Normal file
@@ -0,0 +1,6 @@
|
||||
const mockTagular = () => {
|
||||
const getTagular = jest.fn();
|
||||
window.tagular = getTagular;
|
||||
};
|
||||
|
||||
export default mockTagular;
|
||||
@@ -1,11 +1,15 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import trackCohesionEvent from '../cohesion/trackers';
|
||||
import {
|
||||
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
|
||||
} from '../data/constants';
|
||||
import setCookie from '../data/utils/cookies';
|
||||
import { redirectWithDelay } from '../data/utils/dataUtils';
|
||||
|
||||
const RedirectLogistration = (props) => {
|
||||
const {
|
||||
@@ -20,10 +24,16 @@ const RedirectLogistration = (props) => {
|
||||
userId,
|
||||
registrationEmbedded,
|
||||
host,
|
||||
currectProvider,
|
||||
} = props;
|
||||
const cohesionEventData = useSelector(state => state.cohesion.eventData);
|
||||
let finalRedirectUrl = '';
|
||||
|
||||
if (success) {
|
||||
// This event is used by cohesion upon successful login and registration
|
||||
if (!currectProvider) {
|
||||
trackCohesionEvent(cohesionEventData);
|
||||
}
|
||||
// If we're in a third party auth pipeline, we must complete the pipeline
|
||||
// once user has successfully logged in. Otherwise, redirect to the specified redirect url.
|
||||
// Note: For multiple enterprise use case, we need to make sure that user first visits the
|
||||
@@ -75,8 +85,7 @@ const RedirectLogistration = (props) => {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
window.location.href = finalRedirectUrl;
|
||||
redirectWithDelay(finalRedirectUrl);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -94,6 +103,7 @@ RedirectLogistration.defaultProps = {
|
||||
userId: null,
|
||||
registrationEmbedded: false,
|
||||
host: '',
|
||||
currectProvider: '',
|
||||
};
|
||||
|
||||
RedirectLogistration.propTypes = {
|
||||
@@ -108,6 +118,7 @@ RedirectLogistration.propTypes = {
|
||||
userId: PropTypes.number,
|
||||
registrationEmbedded: PropTypes.bool,
|
||||
host: PropTypes.string,
|
||||
currectProvider: PropTypes.string,
|
||||
};
|
||||
|
||||
export default RedirectLogistration;
|
||||
|
||||
15
src/common-components/RouteTracker.jsx
Normal file
15
src/common-components/RouteTracker.jsx
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
const RouteTracker = () => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.tagular?.('pageView');
|
||||
}, [location]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default RouteTracker;
|
||||
@@ -9,22 +9,35 @@ import { Login } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
import { ELEMENT_TYPES, PAGE_TYPES } from '../cohesion/constants';
|
||||
import trackCohesionEvent from '../cohesion/trackers';
|
||||
import {
|
||||
LOGIN_PAGE, REGISTER_PAGE, SUPPORTED_ICON_CLASSES,
|
||||
} from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
import { redirectWithDelay } from '../data/utils/dataUtils';
|
||||
|
||||
const SocialAuthProviders = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { referrer, socialAuthProviders } = props;
|
||||
const registrationFields = useSelector(state => state.register.registrationFormData);
|
||||
|
||||
function handleSubmit(e) {
|
||||
function handleSubmit(e, providerName) {
|
||||
e.preventDefault();
|
||||
const eventData = {
|
||||
pageType: referrer === LOGIN_PAGE ? PAGE_TYPES.SIGN_IN : PAGE_TYPES.ACCOUNT_CREATION,
|
||||
elementType: ELEMENT_TYPES.BUTTON,
|
||||
webElementText: providerName,
|
||||
webElementName: providerName.toLowerCase(),
|
||||
};
|
||||
// This event is used by cohesion upon successful login
|
||||
trackCohesionEvent(eventData);
|
||||
|
||||
if (referrer === REGISTER_PAGE) {
|
||||
setCookie('marketingEmailsOptIn', registrationFields?.configurableFormFields?.marketingEmailsOptIn);
|
||||
}
|
||||
const url = e.currentTarget.dataset.providerUrl;
|
||||
window.location.href = getConfig().LMS_BASE_URL + url;
|
||||
redirectWithDelay(getConfig().LMS_BASE_URL + url);
|
||||
}
|
||||
|
||||
const socialAuth = socialAuthProviders.map((provider, index) => (
|
||||
@@ -34,7 +47,7 @@ const SocialAuthProviders = (props) => {
|
||||
type="button"
|
||||
className={`btn-social btn-${provider.id} ${index % 2 === 0 ? 'mr-3' : ''}`}
|
||||
data-provider-url={referrer === LOGIN_PAGE ? provider.loginUrl : provider.registerUrl}
|
||||
onClick={handleSubmit}
|
||||
onClick={(event) => handleSubmit(event, provider?.name)}
|
||||
>
|
||||
{provider.iconImage ? (
|
||||
<div aria-hidden="true">
|
||||
|
||||
@@ -13,9 +13,15 @@ export const getThirdPartyAuthContextBegin = () => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
|
||||
});
|
||||
|
||||
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({
|
||||
export const getThirdPartyAuthContextSuccess = (
|
||||
fieldDescriptions,
|
||||
optionalFields,
|
||||
thirdPartyAuthContext,
|
||||
countriesCodesList) => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
|
||||
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
|
||||
payload: {
|
||||
fieldDescriptions, optionalFields, thirdPartyAuthContext, countriesCodesList,
|
||||
},
|
||||
});
|
||||
|
||||
export const getThirdPartyAuthContextFailure = () => ({
|
||||
|
||||
@@ -77,3 +77,7 @@ export const progressiveProfilingFields = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FIELD_LABELS = {
|
||||
COUNTRY: 'country',
|
||||
};
|
||||
|
||||
@@ -35,6 +35,7 @@ const reducer = (state = defaultState, action = {}) => {
|
||||
optionalFields: action.payload.optionalFields,
|
||||
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
countriesCodesList: action.payload.countriesCodesList,
|
||||
};
|
||||
}
|
||||
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from './actions';
|
||||
import { progressiveProfilingFields, registerFields } from './constants';
|
||||
import {
|
||||
getCountryList,
|
||||
getThirdPartyAuthContext,
|
||||
} from './service';
|
||||
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
|
||||
@@ -20,6 +21,7 @@ export function* fetchThirdPartyAuthContext(action) {
|
||||
const {
|
||||
fieldDescriptions, optionalFields, thirdPartyAuthContext,
|
||||
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
||||
const countriesCodesList = (yield call(getCountryList)) || [];
|
||||
|
||||
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
|
||||
// hard code country field, level of education and gender fields
|
||||
@@ -28,9 +30,15 @@ export function* fetchThirdPartyAuthContext(action) {
|
||||
registerFields,
|
||||
progressiveProfilingFields,
|
||||
thirdPartyAuthContext,
|
||||
countriesCodesList,
|
||||
));
|
||||
} else {
|
||||
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
|
||||
yield put(getThirdPartyAuthContextSuccess(
|
||||
fieldDescriptions,
|
||||
optionalFields,
|
||||
thirdPartyAuthContext,
|
||||
countriesCodesList,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
yield put(getThirdPartyAuthContextFailure());
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { FIELD_LABELS } from './constants';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function getThirdPartyAuthContext(urlParams) {
|
||||
@@ -23,3 +26,28 @@ export async function getThirdPartyAuthContext(urlParams) {
|
||||
thirdPartyAuthContext: data.contextData || {},
|
||||
};
|
||||
}
|
||||
|
||||
function extractCountryList(data) {
|
||||
return data?.fields
|
||||
.find(({ name }) => name === FIELD_LABELS.COUNTRY)
|
||||
?.options?.map(({ value }) => (value)) || [];
|
||||
}
|
||||
|
||||
export async function getCountryList() {
|
||||
try {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
isPublic: true,
|
||||
};
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(
|
||||
`${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`,
|
||||
requestConfig,
|
||||
);
|
||||
return extractCountryList(data);
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@ import * as api from '../service';
|
||||
|
||||
const { loggingService } = initializeMockLogging();
|
||||
|
||||
jest.mock('../service', () => ({
|
||||
getCountryList: jest.fn(),
|
||||
getThirdPartyAuthContext: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('fetchThirdPartyAuthContext', () => {
|
||||
const params = {
|
||||
payload: { urlParams: {} },
|
||||
@@ -31,6 +36,7 @@ describe('fetchThirdPartyAuthContext', () => {
|
||||
thirdPartyAuthContext: data,
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
countriesCodesList: [],
|
||||
}));
|
||||
|
||||
const dispatched = [];
|
||||
@@ -44,7 +50,7 @@ describe('fetchThirdPartyAuthContext', () => {
|
||||
expect(dispatched).toEqual([
|
||||
actions.getThirdPartyAuthContextBegin(),
|
||||
setCountryFromThirdPartyAuthContext(),
|
||||
actions.getThirdPartyAuthContextSuccess({}, {}, data),
|
||||
actions.getThirdPartyAuthContextSuccess({}, {}, data, []),
|
||||
]);
|
||||
getThirdPartyAuthContext.mockClear();
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ export { default as RedirectLogistration } from './RedirectLogistration';
|
||||
export { default as registerIcons } from './RegisterFaIcons';
|
||||
export { default as EmbeddedRegistrationRoute } from './EmbeddedRegistrationRoute';
|
||||
export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute';
|
||||
export { default as RouteTracker } from './RouteTracker';
|
||||
export { default as NotFoundPage } from './NotFoundPage';
|
||||
export { default as SocialAuthProviders } from './SocialAuthProviders';
|
||||
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { combineReducers } from 'redux';
|
||||
|
||||
import { reducer as cohesionReducer, storeName as cohesionStoreName } from '../cohesion/data/reducers';
|
||||
import {
|
||||
reducer as commonComponentsReducer,
|
||||
storeName as commonComponentsStoreName,
|
||||
@@ -31,6 +32,7 @@ const createRootReducer = () => combineReducers({
|
||||
[commonComponentsStoreName]: commonComponentsReducer,
|
||||
[forgotPasswordStoreName]: forgotPasswordReducer,
|
||||
[resetPasswordStoreName]: resetPasswordReducer,
|
||||
[cohesionStoreName]: cohesionReducer,
|
||||
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
|
||||
});
|
||||
export default createRootReducer;
|
||||
|
||||
@@ -81,3 +81,9 @@ export const isHostAvailableInQueryParams = () => {
|
||||
const queryParams = getAllPossibleQueryParams();
|
||||
return 'host' in queryParams;
|
||||
};
|
||||
|
||||
export const redirectWithDelay = (redirectUrl) => {
|
||||
setTimeout(() => {
|
||||
window.location.href = redirectUrl;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
@@ -1,27 +1,36 @@
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import React, { StrictMode } from 'react';
|
||||
|
||||
import {
|
||||
APP_INIT_ERROR, APP_READY, initialize, mergeConfig, subscribe,
|
||||
} from '@edx/frontend-platform';
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import configuration from './config';
|
||||
import messages from './i18n';
|
||||
import MainApp from './MainApp';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
<MainApp />,
|
||||
document.getElementById('root'),
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<MainApp />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
subscribe(APP_INIT_ERROR, (error) => {
|
||||
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ErrorPage message={error.message} />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
initialize({
|
||||
|
||||
@@ -1,6 +1,2 @@
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@openedx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
|
||||
@import "sass/style";
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { Alert, Hyperlink } from '@openedx/paragon';
|
||||
import { CheckCircle, Error } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -36,19 +36,17 @@ const AccountActivationMessage = ({ messageType }) => {
|
||||
break;
|
||||
}
|
||||
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
|
||||
const supportLink = (
|
||||
<Alert.Link href={getConfig().ACTIVATION_EMAIL_SUPPORT_LINK}>
|
||||
{formatMessage(messages['account.activation.support.link'])}
|
||||
</Alert.Link>
|
||||
const supportEmail = (
|
||||
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
|
||||
);
|
||||
|
||||
heading = formatMessage(messages[`account.${activationOrConfirmation}.error.message.title`]);
|
||||
activationMessage = (
|
||||
<FormattedMessage
|
||||
id="account.activation.error.message"
|
||||
defaultMessage="Something went wrong, please {supportLink} to resolve this issue."
|
||||
defaultMessage="Something went wrong, please contact {supportEmail} to resolve this issue."
|
||||
description="Account activation error message"
|
||||
values={{ supportLink }}
|
||||
values={{ supportEmail }}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -10,19 +11,23 @@ import PropTypes from 'prop-types';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import messages from './messages';
|
||||
import trackCohesionEvent from '../cohesion/trackers';
|
||||
import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants';
|
||||
import { updatePathWithQueryParams } from '../data/utils';
|
||||
import { redirectWithDelay } from '../data/utils/dataUtils';
|
||||
import useMobileResponsive from '../data/utils/useMobileResponsive';
|
||||
|
||||
const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
|
||||
const isMobileView = useMobileResponsive();
|
||||
const [redirectToResetPasswordPage, setRedirectToResetPasswordPage] = useState(false);
|
||||
const cohesionEventData = useSelector(state => state.cohesion.eventData);
|
||||
const handlers = {
|
||||
handleToggleOff: () => {
|
||||
if (variant === 'block') {
|
||||
setRedirectToResetPasswordPage(true);
|
||||
} else {
|
||||
window.location.href = redirectUrl || getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
trackCohesionEvent(cohesionEventData);
|
||||
redirectWithDelay(redirectUrl || getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
|
||||
import LoginFailureMessage from './LoginFailure';
|
||||
import messages from './messages';
|
||||
import {
|
||||
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
|
||||
} from '../cohesion/constants';
|
||||
import { setCohesionEventStates } from '../cohesion/data/actions';
|
||||
import {
|
||||
FormGroup,
|
||||
InstitutionLogistration,
|
||||
@@ -31,9 +35,7 @@ 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 { DEFAULT_STATE, PENDING_STATE, RESET_PAGE } from '../data/constants';
|
||||
import {
|
||||
getActivationStatus,
|
||||
getAllPossibleQueryParams,
|
||||
@@ -72,6 +74,7 @@ const LoginPage = (props) => {
|
||||
getTPADataFromBackend,
|
||||
} = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
const activationMsgType = getActivationStatus();
|
||||
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||
|
||||
@@ -87,7 +90,6 @@ const LoginPage = (props) => {
|
||||
useEffect(() => {
|
||||
if (loginResult.success) {
|
||||
trackLoginSuccess();
|
||||
|
||||
// Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
|
||||
removeCookie('ssoPipelineRedirectionDone');
|
||||
}
|
||||
@@ -152,6 +154,15 @@ const LoginPage = (props) => {
|
||||
|
||||
const handleSubmit = (event) => {
|
||||
event.preventDefault();
|
||||
const eventData = {
|
||||
pageType: PAGE_TYPES.SIGN_IN,
|
||||
elementType: ELEMENT_TYPES.BUTTON,
|
||||
webElementText: ELEMENT_TEXT.SIGN_IN,
|
||||
webElementName: ELEMENT_NAME.SIGN_IN,
|
||||
};
|
||||
|
||||
dispatch(setCohesionEventStates(eventData));
|
||||
|
||||
if (showResetPasswordSuccessBanner) {
|
||||
props.dismissPasswordResetBanner();
|
||||
}
|
||||
@@ -217,6 +228,7 @@ const LoginPage = (props) => {
|
||||
success={loginResult.success}
|
||||
redirectUrl={loginResult.redirectUrl}
|
||||
finishAuthUrl={finishAuthUrl}
|
||||
currentProvider={currentProvider}
|
||||
/>
|
||||
<div className="mw-xs mt-3 mb-2">
|
||||
<LoginFailureMessage
|
||||
|
||||
@@ -95,11 +95,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Your account could not be activated',
|
||||
description: 'Account Activation error message title',
|
||||
},
|
||||
'account.activation.support.link': {
|
||||
id: 'account.activation.support.link',
|
||||
defaultMessage: 'contact support',
|
||||
description: 'Link text used in account activation error message to go to learner help center',
|
||||
},
|
||||
// Email Confirmation Strings
|
||||
'account.confirmation.success.message.title': {
|
||||
id: 'account.confirmation.success.message.title',
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('AccountActivationMessage', () => {
|
||||
);
|
||||
|
||||
const expectedMessage = 'Your account could not be activated'
|
||||
+ 'Something went wrong, please contact support to resolve this issue.';
|
||||
+ 'Something went wrong, please contact to resolve this issue.';
|
||||
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
@@ -121,7 +121,7 @@ describe('EmailConfirmationMessage', () => {
|
||||
</IntlProvider>,
|
||||
);
|
||||
const expectedMessage = 'Your email could not be confirmed'
|
||||
+ 'Something went wrong, please contact support to resolve this issue.';
|
||||
+ 'Something went wrong, please contact to resolve this issue.';
|
||||
expect(screen.getByText(
|
||||
'',
|
||||
{ selector: '#account-activation-message' },
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
fireEvent, render, screen,
|
||||
fireEvent, render, screen, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import mockTagular from '../../cohesion/utils';
|
||||
import { RESET_PAGE } from '../../data/constants';
|
||||
import ChangePasswordPrompt from '../ChangePasswordPrompt';
|
||||
|
||||
const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt);
|
||||
const mockedNavigator = jest.fn();
|
||||
const mockStore = configureStore();
|
||||
mockTagular();
|
||||
|
||||
const eventData = {
|
||||
pageType: 'test-page',
|
||||
elementType: 'test-element-type',
|
||||
webElementText: 'test-element-text',
|
||||
webElementName: 'test-element-name',
|
||||
};
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...(jest.requireActual('react-router-dom')),
|
||||
@@ -21,8 +33,14 @@ jest.mock('react-router-dom', () => ({
|
||||
|
||||
describe('ChangePasswordPromptTests', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const initialState = {
|
||||
cohesion: { eventData: {} },
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
store = mockStore(initialState);
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
@@ -31,38 +49,56 @@ describe('ChangePasswordPromptTests', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('[nudge modal] should redirect to next url when user clicks close button', () => {
|
||||
it('[nudge modal] should redirect to next url when user clicks close button', async () => {
|
||||
const dashboardUrl = getConfig().BASE_URL.concat('/dashboard');
|
||||
props = {
|
||||
variant: 'nudge',
|
||||
redirectUrl: dashboardUrl,
|
||||
};
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<IntlChangePasswordPrompt {...props} />
|
||||
</MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<IntlChangePasswordPrompt {...props} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Close'));
|
||||
expect(window.location.href).toBe(dashboardUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(dashboardUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('[block modal] should redirect to reset password page when user clicks outside modal', async () => {
|
||||
props = {
|
||||
variant: 'block',
|
||||
};
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<IntlChangePasswordPrompt {...props} />
|
||||
</MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<IntlChangePasswordPrompt {...props} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
render, screen,
|
||||
} from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import {
|
||||
ACCOUNT_LOCKED_OUT,
|
||||
@@ -25,13 +27,27 @@ import LoginFailureMessage from '../LoginFailure';
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthService: jest.fn(),
|
||||
}));
|
||||
const mockStore = configureStore();
|
||||
|
||||
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
|
||||
|
||||
const eventData = {
|
||||
pageType: 'test-page',
|
||||
elementType: 'test-element-type',
|
||||
webElementText: 'test-element-text',
|
||||
webElementName: 'test-element-name',
|
||||
};
|
||||
|
||||
describe('LoginFailureMessage', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const initialState = {
|
||||
cohesion: { eventData: {} },
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
store = mockStore(initialState);
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
@@ -298,11 +314,19 @@ describe('LoginFailureMessage', () => {
|
||||
errorCount: 0,
|
||||
};
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -323,12 +347,20 @@ describe('LoginFailureMessage', () => {
|
||||
errorCode: REQUIRE_PASSWORD_CHANGE,
|
||||
errorCount: 0,
|
||||
};
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { act } from 'react-dom/test-utils';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import mockTagular from '../../cohesion/utils';
|
||||
import {
|
||||
APP_NAME, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE,
|
||||
} from '../../data/constants';
|
||||
@@ -25,6 +26,7 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthService: jest.fn(),
|
||||
}));
|
||||
mockTagular();
|
||||
|
||||
const IntlLoginPage = injectIntl(LoginPage);
|
||||
const mockStore = configureStore();
|
||||
@@ -58,6 +60,7 @@ describe('LoginPage', () => {
|
||||
register: {
|
||||
validationApiRateLimited: false,
|
||||
},
|
||||
cohesion: { eventData: {} },
|
||||
};
|
||||
|
||||
const secondaryProviders = {
|
||||
@@ -512,7 +515,7 @@ describe('LoginPage', () => {
|
||||
|
||||
// ******** test redirection ********
|
||||
|
||||
it('should redirect to url returned by login endpoint after successful authentication', () => {
|
||||
it('should redirect to url returned by login endpoint after successful authentication', async () => {
|
||||
const dashboardURL = 'https://test.com/testing-dashboard/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -528,10 +531,12 @@ describe('LoginPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(dashboardURL);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(dashboardURL);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('should redirect to finishAuthUrl upon successful login via SSO', () => {
|
||||
it('should redirect to finishAuthUrl upon successful login via SSO', async () => {
|
||||
const authCompleteUrl = '/auth/complete/google-oauth2/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -555,10 +560,12 @@ describe('LoginPage', () => {
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('should redirect to social auth provider url on SSO button click', () => {
|
||||
it('should redirect to social auth provider url on SSO button click', async () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
@@ -579,10 +586,12 @@ describe('LoginPage', () => {
|
||||
'',
|
||||
{ selector: '#oa2-apple-id' },
|
||||
));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('should redirect to finishAuthUrl upon successful authentication via SSO', () => {
|
||||
it('should redirect to finishAuthUrl upon successful authentication via SSO', async () => {
|
||||
const finishAuthUrl = '/auth/complete/google-oauth2/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -603,7 +612,9 @@ describe('LoginPage', () => {
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
// ******** test hinted third party auth ********
|
||||
|
||||
@@ -70,6 +70,7 @@ const Logistration = (props) => {
|
||||
if (tabKey === currentTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement', app_name: APP_NAME });
|
||||
props.clearThirdPartyAuthContextErrorMessage();
|
||||
if (tabKey === LOGIN_PAGE) {
|
||||
|
||||
@@ -69,6 +69,7 @@ describe('Logistration', () => {
|
||||
usernameSuggestions: [],
|
||||
validationApiRateLimited: false,
|
||||
},
|
||||
cohesion: { eventData: {} },
|
||||
commonComponents: {
|
||||
thirdPartyAuthContext: {
|
||||
providers: [],
|
||||
|
||||
29
src/plugin-slots/MainAppSlot/MainAppSlot.test.jsx
Normal file
29
src/plugin-slots/MainAppSlot/MainAppSlot.test.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import MainAppSlot from './index';
|
||||
|
||||
jest.mock('@openedx/frontend-plugin-framework', () => ({
|
||||
PluginSlot: jest.fn(() => null),
|
||||
}));
|
||||
|
||||
describe('MainAppSlot', () => {
|
||||
it('renders without crashing', () => {
|
||||
render(<MainAppSlot />);
|
||||
});
|
||||
|
||||
it('renders a PluginSlot component', () => {
|
||||
render(<MainAppSlot />);
|
||||
expect(PluginSlot).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('passes the correct id prop to PluginSlot', () => {
|
||||
render(<MainAppSlot />);
|
||||
expect(PluginSlot).toHaveBeenCalledWith({ id: 'main_app_slot' }, {});
|
||||
});
|
||||
|
||||
it('does not render any children', () => {
|
||||
const { container } = render(<MainAppSlot />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
41
src/plugin-slots/MainAppSlot/README.md
Normal file
41
src/plugin-slots/MainAppSlot/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Main App Slot
|
||||
|
||||
### Slot ID: `main_app_slot`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used for adding content at the root level.
|
||||
|
||||
## Example
|
||||
|
||||
The following `env.config.jsx` will render a component at the MFE root level.
|
||||
|
||||

|
||||
|
||||
```js
|
||||
import {
|
||||
DIRECT_PLUGIN,
|
||||
PLUGIN_OPERATIONS,
|
||||
} from "@openedx/frontend-plugin-framework";
|
||||
import { ExampleComponent } from "@openedx/frontend-plugin-example";
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
main_app_slot: {
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: "example-component",
|
||||
type: DIRECT_PLUGIN,
|
||||
priority: 60,
|
||||
RenderWidget: ExampleComponent,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
BIN
src/plugin-slots/MainAppSlot/images/main_app_slot.png
Normal file
BIN
src/plugin-slots/MainAppSlot/images/main_app_slot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
7
src/plugin-slots/MainAppSlot/index.jsx
Normal file
7
src/plugin-slots/MainAppSlot/index.jsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const MainAppSlot = () => (
|
||||
<PluginSlot id="main_app_slot" />
|
||||
);
|
||||
|
||||
export default MainAppSlot;
|
||||
3
src/plugin-slots/README.md
Normal file
3
src/plugin-slots/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# `frontend-app-authn` Plugin Slots
|
||||
|
||||
- [`main_app_slot`](./MainAppSlot/)
|
||||
@@ -6,11 +6,12 @@ import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platfor
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
fireEvent, render, screen,
|
||||
fireEvent, render, screen, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import mockTagular from '../../cohesion/utils';
|
||||
import {
|
||||
APP_NAME,
|
||||
AUTHN_PROGRESSIVE_PROFILING,
|
||||
@@ -25,6 +26,7 @@ import ProgressiveProfiling from '../ProgressiveProfiling';
|
||||
|
||||
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
|
||||
const mockStore = configureStore();
|
||||
mockTagular();
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendPageEvent: jest.fn(),
|
||||
@@ -55,6 +57,13 @@ jest.mock('react-router-dom', () => {
|
||||
};
|
||||
});
|
||||
|
||||
const eventData = {
|
||||
pageType: 'test-page',
|
||||
elementType: 'test-element-type',
|
||||
webElementText: 'test-element-text',
|
||||
webElementName: 'test-element-name',
|
||||
};
|
||||
|
||||
describe('ProgressiveProfilingTests', () => {
|
||||
let store = {};
|
||||
|
||||
@@ -252,6 +261,9 @@ describe('ProgressiveProfilingTests', () => {
|
||||
...initialState.welcomePage,
|
||||
success: true,
|
||||
},
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const nextButton = container.querySelector('button.btn-brand');
|
||||
@@ -278,13 +290,18 @@ describe('ProgressiveProfilingTests', () => {
|
||||
...initialState.welcomePage,
|
||||
success: true,
|
||||
},
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
|
||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const nextButton = container.querySelector('button.btn-brand');
|
||||
expect(nextButton.textContent).toEqual('Submit');
|
||||
|
||||
expect(window.location.href).toEqual(redirectUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toEqual(redirectUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -399,7 +416,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
expect(window.location.href).toBe(DASHBOARD_URL);
|
||||
});
|
||||
|
||||
it('should redirect to provided redirect url', () => {
|
||||
it('should redirect to provided redirect url', async () => {
|
||||
const redirectUrl = 'https://redirect-test.com';
|
||||
delete window.location;
|
||||
window.location = {
|
||||
@@ -421,12 +438,17 @@ describe('ProgressiveProfilingTests', () => {
|
||||
...initialState.welcomePage,
|
||||
success: true,
|
||||
},
|
||||
cohesion: {
|
||||
eventData,
|
||||
},
|
||||
});
|
||||
|
||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||
const submitButton = screen.getByText('Submit');
|
||||
fireEvent.click(submitButton);
|
||||
expect(window.location.href).toBe(redirectUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(redirectUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import algoliasearchHelper from 'algoliasearch-helper';
|
||||
|
||||
import mockedRecommendedProducts from './mockedData';
|
||||
|
||||
@@ -11,6 +11,12 @@ import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import {
|
||||
InstitutionLogistration,
|
||||
PasswordField,
|
||||
RedirectLogistration,
|
||||
ThirdPartyAuthAlert,
|
||||
} from '../common-components';
|
||||
import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm';
|
||||
import RegistrationFailure from './components/RegistrationFailure';
|
||||
import {
|
||||
@@ -34,22 +40,20 @@ import {
|
||||
import messages from './messages';
|
||||
import { EmailField, NameField, UsernameField } from './RegistrationFields';
|
||||
import {
|
||||
InstitutionLogistration,
|
||||
PasswordField,
|
||||
RedirectLogistration,
|
||||
ThirdPartyAuthAlert,
|
||||
} from '../common-components';
|
||||
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
|
||||
} from '../cohesion/constants';
|
||||
import { setCohesionEventStates } from '../cohesion/data/actions';
|
||||
import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../common-components/data/actions';
|
||||
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
||||
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
|
||||
import {
|
||||
APP_NAME, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
|
||||
APP_NAME, COMPLETE_STATE, PENDING_STATE,
|
||||
REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
import {
|
||||
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, removeCookie, setCookie,
|
||||
} from '../data/utils';
|
||||
import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
|
||||
|
||||
/**
|
||||
* Main Registration Page component
|
||||
*/
|
||||
@@ -89,6 +93,7 @@ const RegistrationPage = (props) => {
|
||||
const providers = useSelector(state => state.commonComponents.thirdPartyAuthContext.providers);
|
||||
const secondaryProviders = useSelector(state => state.commonComponents.thirdPartyAuthContext.secondaryProviders);
|
||||
const pipelineUserDetails = useSelector(state => state.commonComponents.thirdPartyAuthContext.pipelineUserDetails);
|
||||
const countriesCodesList = useSelector(state => state.commonComponents.countriesCodesList);
|
||||
|
||||
const backendValidations = useSelector(getBackendValidations);
|
||||
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||
@@ -268,6 +273,14 @@ const RegistrationPage = (props) => {
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const eventData = {
|
||||
pageType: PAGE_TYPES.ACCOUNT_CREATION,
|
||||
elementType: ELEMENT_TYPES.BUTTON,
|
||||
webElementText: ELEMENT_TEXT.CREATE_ACCOUNT,
|
||||
webElementName: ELEMENT_NAME.CREATE_ACCOUNT,
|
||||
};
|
||||
|
||||
dispatch(setCohesionEventStates(eventData));
|
||||
registerUser();
|
||||
};
|
||||
|
||||
@@ -302,6 +315,7 @@ const RegistrationPage = (props) => {
|
||||
redirectToProgressiveProfilingPage={
|
||||
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
|
||||
}
|
||||
currentProvider={currentProvider}
|
||||
/>
|
||||
{(autoSubmitRegForm && !errorCode.type)
|
||||
|| (!autoGeneratedUsernameExpVariation && !(
|
||||
@@ -379,6 +393,7 @@ const RegistrationPage = (props) => {
|
||||
setFormFields={setConfigurableFormFields}
|
||||
autoSubmitRegisterForm={autoSubmitRegForm}
|
||||
fieldDescriptions={fieldDescriptions}
|
||||
countriesCodesList={countriesCodesList}
|
||||
/>
|
||||
<StatefulButton
|
||||
id="register-user"
|
||||
|
||||
@@ -6,7 +6,7 @@ import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'
|
||||
import {
|
||||
configure, getLocale, injectIntl, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { mockNavigate, BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
setUserPipelineDataLoaded,
|
||||
} from './data/actions';
|
||||
import { INTERNAL_SERVER_ERROR } from './data/constants';
|
||||
import mockTagular from '../cohesion/utils';
|
||||
import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
|
||||
import useAutoGeneratedUsernameExperimentVariation
|
||||
from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||
@@ -37,6 +38,7 @@ jest.mock('./data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariati
|
||||
|
||||
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
||||
const mockStore = configureStore();
|
||||
mockTagular();
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const mockNavigation = jest.fn();
|
||||
@@ -106,6 +108,7 @@ describe('RegistrationPage', () => {
|
||||
usernameSuggestions: [],
|
||||
|
||||
},
|
||||
cohesion: { eventData: {} },
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext,
|
||||
@@ -505,7 +508,7 @@ describe('RegistrationPage', () => {
|
||||
expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`);
|
||||
});
|
||||
|
||||
it('should redirect to url returned in registration result after successful account creation', () => {
|
||||
it('should redirect to url returned in registration result after successful account creation', async () => {
|
||||
const dashboardURL = 'https://test.com/testing-dashboard/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -520,10 +523,12 @@ describe('RegistrationPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(window.location.href).toBe(dashboardURL);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(dashboardURL);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('should redirect to dashboard if features flags are configured but no optional fields are configured', () => {
|
||||
it('should redirect to dashboard if features flags are configured but no optional fields are configured', async () => {
|
||||
mergeConfig({
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true,
|
||||
});
|
||||
@@ -547,7 +552,9 @@ describe('RegistrationPage', () => {
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(window.location.href).toBe(dashboardUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(dashboardUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('should redirect to progressive profiling page if optional fields are configured', () => {
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
|
||||
} from '../../cohesion/constants';
|
||||
import trackCohesionEvent from '../../cohesion/trackers';
|
||||
import { FormFieldRenderer } from '../../field-renderer';
|
||||
import { backupRegistrationFormBegin } from '../data/actions';
|
||||
import { FIELDS } from '../data/constants';
|
||||
@@ -33,6 +37,7 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
setFieldErrors,
|
||||
setFormFields,
|
||||
autoSubmitRegistrationForm,
|
||||
countriesCodesList,
|
||||
} = props;
|
||||
const dispatch = useDispatch();
|
||||
|
||||
@@ -41,10 +46,6 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
confused and unable to create an account. So we added the United States entry in the dropdown list.
|
||||
*/
|
||||
|
||||
const countryList = useMemo(() => (
|
||||
getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]).filter(country => country.code !== 'RU')
|
||||
), []);
|
||||
|
||||
let showTermsOfServiceAndHonorCode = false;
|
||||
let showCountryField = false;
|
||||
|
||||
@@ -78,6 +79,16 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
}
|
||||
}, [autoSubmitRegistrationForm]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const removeDisabledCountries = useCallback((countryList) => {
|
||||
if (!countriesCodesList.length) {
|
||||
return countryList;
|
||||
}
|
||||
return countryList.filter(({ code }) => countriesCodesList.find(x => x === code));
|
||||
}, [countriesCodesList]);
|
||||
|
||||
const countryList = useMemo(() => removeDisabledCountries(
|
||||
getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }])), [removeDisabledCountries]);
|
||||
|
||||
const handleErrorChange = (fieldName, error) => {
|
||||
if (fieldName) {
|
||||
setFieldErrors(prevErrors => ({
|
||||
@@ -100,6 +111,15 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
}
|
||||
// setting marketingEmailsOptIn state for SSO authentication flow for register API call
|
||||
if (name === 'marketingEmailsOptIn') {
|
||||
if (!value) {
|
||||
const cohesionEventData = {
|
||||
pageType: PAGE_TYPES.ACCOUNT_CREATION,
|
||||
elementType: ELEMENT_TYPES.BUTTON,
|
||||
webElementText: ELEMENT_TEXT.OPT_IN_TEXT,
|
||||
webElementName: ELEMENT_NAME.OPT_OUT,
|
||||
};
|
||||
trackCohesionEvent(cohesionEventData);
|
||||
}
|
||||
dispatch(backupRegistrationFormBegin({
|
||||
...backedUpFormData,
|
||||
configurableFormFields: {
|
||||
@@ -249,11 +269,16 @@ ConfigurableRegistrationForm.propTypes = {
|
||||
setFieldErrors: PropTypes.func.isRequired,
|
||||
setFormFields: PropTypes.func.isRequired,
|
||||
autoSubmitRegistrationForm: PropTypes.bool,
|
||||
countriesCodesList: PropTypes.arrayOf(PropTypes.shape({
|
||||
code: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
})),
|
||||
};
|
||||
|
||||
ConfigurableRegistrationForm.defaultProps = {
|
||||
fieldDescriptions: {},
|
||||
autoSubmitRegistrationForm: false,
|
||||
countriesCodesList: [],
|
||||
};
|
||||
|
||||
export default ConfigurableRegistrationForm;
|
||||
|
||||
@@ -99,6 +99,7 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
registrationFormData,
|
||||
usernameSuggestions: [],
|
||||
},
|
||||
cohesion: { eventData: {} },
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext,
|
||||
@@ -191,6 +192,7 @@ describe('ConfigurableRegistrationForm', () => {
|
||||
},
|
||||
},
|
||||
autoSubmitRegistrationForm: true,
|
||||
countriesCodesList: [{ code: 'AX', name: 'Åland Islands' }, { code: 'AL', name: 'Albania' }],
|
||||
};
|
||||
|
||||
render(routerWrapper(reduxWrapper(
|
||||
|
||||
@@ -99,6 +99,7 @@ describe('RegistrationFailure', () => {
|
||||
registrationFormData,
|
||||
usernameSuggestions: [],
|
||||
},
|
||||
cohesion: { eventData: {} },
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext,
|
||||
|
||||
@@ -5,10 +5,11 @@ import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
configure, getLocale, injectIntl, IntlProvider,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import mockTagular from '../../../cohesion/utils';
|
||||
import {
|
||||
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../../../data/constants';
|
||||
@@ -26,6 +27,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
getLocale: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||
mockTagular();
|
||||
|
||||
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
||||
const mockStore = configureStore();
|
||||
@@ -98,6 +100,7 @@ describe('ThirdPartyAuth', () => {
|
||||
registrationFormData,
|
||||
usernameSuggestions: [],
|
||||
},
|
||||
cohesion: { eventData: {} },
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext,
|
||||
@@ -339,7 +342,7 @@ describe('ThirdPartyAuth', () => {
|
||||
expect(headingElement).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should redirect to social auth provider url on SSO button click', () => {
|
||||
it('should redirect to social auth provider url on SSO button click', async () => {
|
||||
const registerUrl = '/auth/login/apple-id/?auth_entry=register&next=/dashboard';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -365,10 +368,12 @@ describe('ThirdPartyAuth', () => {
|
||||
const ssoButton = container.querySelector('button#oa2-apple-id');
|
||||
fireEvent.click(ssoButton);
|
||||
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
it('should redirect to finishAuthUrl upon successful registration via SSO', () => {
|
||||
it('should redirect to finishAuthUrl upon successful registration via SSO', async () => {
|
||||
const authCompleteUrl = '/auth/complete/google-oauth2/';
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
@@ -391,7 +396,9 @@ describe('ThirdPartyAuth', () => {
|
||||
window.location = { href: getConfig().BASE_URL };
|
||||
|
||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
|
||||
await waitFor(() => {
|
||||
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
|
||||
}, { timeout: 1100 });
|
||||
});
|
||||
|
||||
// ******** test alert messages ********
|
||||
|
||||
@@ -2,19 +2,19 @@
|
||||
.layout {
|
||||
display: flex;
|
||||
|
||||
@include media-breakpoint-down('lg') {
|
||||
@media (--pgn-size-breakpoint-max-width-lg) {
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up('xl') {
|
||||
@media (--pgn-size-breakpoint-min-width-xl) {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
@include media-breakpoint-up('xl') {
|
||||
@media (--pgn-size-breakpoint-min-width-xl) {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 50vw;
|
||||
@@ -47,7 +47,7 @@
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
|
||||
@include media-breakpoint-down('xl') {
|
||||
@media (--pgn-size-breakpoint-max-width-xl) {
|
||||
font-size: 3.75rem;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 700;
|
||||
|
||||
@include media-breakpoint-down('xl') {
|
||||
@media (-pgn-size-breakpoint-max-width-xl) {
|
||||
font-size: 1.375rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
@@ -72,7 +72,7 @@
|
||||
}
|
||||
|
||||
.large-screen-left-container {
|
||||
@include media-breakpoint-down('xl') {
|
||||
@media (-pgn-size-breakpoint-max-width-xl) {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
@@ -87,43 +87,43 @@
|
||||
height: 0.25rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700,
|
||||
$brand-700 20%,
|
||||
$brand 20%,
|
||||
var(--pgn-color-brand-700),
|
||||
var(--pgn-color-brand-700) 20%,
|
||||
var(--pgn-color-brand-base) 20%,
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
@include media-breakpoint-only('md') {
|
||||
@media (--pgn-size-breakpoint-min-width-md) and (--pgn-size-breakpoint-max-width-md) {
|
||||
.medium-screen-top-stripe {
|
||||
display: flex;
|
||||
height: 0.5rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700,
|
||||
$brand-700 10%,
|
||||
$brand 10%,
|
||||
$brand 90%,
|
||||
$primary-700 90%,
|
||||
$primary-700 100%,
|
||||
var(--pgn-color-brand-700),
|
||||
var(--pgn-color-brand-700) 10%,
|
||||
var(--pgn-color-brand-base) 10%,
|
||||
var(--pgn-color-brand-base) 90%,
|
||||
var(--pgn-color-primary-700) 90%,
|
||||
var(--pgn-color-primary-700) 100%,
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-only('lg') {
|
||||
@media (--pgn-size-breakpoint-min-width-lg) and (--pgn-size-breakpoint-max-width-lg){
|
||||
.medium-screen-top-stripe {
|
||||
display: flex;
|
||||
height: 0.5rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700 10%,
|
||||
$brand 10%,
|
||||
$brand 65%,
|
||||
$primary-700 65%,
|
||||
$primary-700 75%,
|
||||
$accent-a 75%,
|
||||
$accent-a 75%
|
||||
var(--pgn-color-brand-700) 10%,
|
||||
var(--pgn-color-brand-base) 10%,
|
||||
var(--pgn-color-brand-base) 65%,
|
||||
var(--pgn-color-primary-700) 65%,
|
||||
var(--pgn-color-primary-700) 75%,
|
||||
var(--pgn-color-accent-a) 75%,
|
||||
var(--pgn-color-accent-a) 75%
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@@ -131,20 +131,20 @@
|
||||
|
||||
.extra-large-screen-top-stripe { display: none; }
|
||||
|
||||
@include media-breakpoint-up('xl') {
|
||||
@media (--pgn-size-breakpoint-min-width-xl) {
|
||||
.extra-large-screen-top-stripe {
|
||||
display: flex;
|
||||
height: 0.5rem;
|
||||
background-image: linear-gradient(
|
||||
102.02deg,
|
||||
$brand-700 10%,
|
||||
$brand 10%,
|
||||
$brand 45%,
|
||||
$primary-700 45%,
|
||||
$primary-700 55%,
|
||||
$accent-a 55%,
|
||||
$accent-a 75%,
|
||||
$info-200 75%,
|
||||
var(--pgn-color-brand-700) 10%,
|
||||
var(--pgn-color-brand-base) 10%,
|
||||
var(--pgn-color-brand-base) 45%,
|
||||
var(--pgn-color-primary-700) 45%,
|
||||
var(--pgn-color-primary-700) 55%,
|
||||
var(--pgn-color-accent-a) 55%,
|
||||
var(--pgn-color-accent-a) 75%,
|
||||
var(--pgn-color-info-200) 75%,
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
@@ -152,24 +152,24 @@
|
||||
|
||||
.large-screen-svg-light,
|
||||
.large-screen-svg-primary {
|
||||
fill: $light-200;
|
||||
fill: var(--pgn-color-light-200);
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.large-screen-svg-primary {
|
||||
fill: $primary-400;
|
||||
fill: var(--pgn-color-primary-400);
|
||||
}
|
||||
|
||||
.medium-screen-svg-light,
|
||||
.medium-screen-svg-primary {
|
||||
fill: $light-200;
|
||||
fill: var(--pgn-color-light-200);
|
||||
overflow: inherit;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.medium-screen-svg-primary {
|
||||
fill: $primary-400;
|
||||
fill: var(--pgn-color-primary-400);
|
||||
}
|
||||
|
||||
[dir=rtl]{
|
||||
@@ -184,20 +184,20 @@
|
||||
.small-yellow-line {
|
||||
width: 80px;
|
||||
height: 0;
|
||||
border: 2px solid $accent-b;
|
||||
border: 2px solid var(--pgn-color-accent-b);
|
||||
transform: rotate(102.02deg);
|
||||
}
|
||||
|
||||
.medium-yellow-line {
|
||||
width: 120px;
|
||||
height: 0;
|
||||
border: 3px solid $accent-b;
|
||||
border: 3px solid var(--pgn-color-accent-b);
|
||||
transform: rotate(102.02deg);
|
||||
}
|
||||
|
||||
.large-yellow-line {
|
||||
width: 240px;
|
||||
height: 0;
|
||||
border: 3px solid $accent-b;
|
||||
border: 3px solid var(--pgn-color-accent-b);
|
||||
transform: rotate(102.02deg);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 700;
|
||||
|
||||
@include media-breakpoint-down('md') {
|
||||
@media (--pgn-size-breakpoint-max-width-md) {
|
||||
line-height: 1.5rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
@@ -64,52 +64,52 @@ $header-height: 104px;
|
||||
}
|
||||
|
||||
&.light {
|
||||
background-color: $white;
|
||||
background-color: var(--pgn-color-white);
|
||||
|
||||
.title {
|
||||
color: $black;
|
||||
color: var(--pgn-color-black);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: $gray-700;
|
||||
color: var(--pgn-color-gray-700);
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: $light-500;
|
||||
color: $black;
|
||||
background-color: var(--pgn-color-light-500);
|
||||
color: var(--pgn-color-black);
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
color: $gray-700;
|
||||
color: var(--pgn-color-gray-700);
|
||||
}
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background-color: $primary-500;
|
||||
background-color: var(--pgn-color-primary-500);
|
||||
|
||||
.pgn__card-header-title-md {
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
|
||||
.pgn__card-header-subtitle-md {
|
||||
color: $light-200;
|
||||
color: var(--pgn-color-light-200);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: $light-200;
|
||||
color: var(--pgn-color-light-200);
|
||||
}
|
||||
|
||||
.badge {
|
||||
background-color: $dark-200;
|
||||
color: $white;
|
||||
background-color: var(--pgn-color-dark-200);
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
color: $light-200;
|
||||
color: var(--pgn-color-light-200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ $card-gap: 24px;
|
||||
.recommendations-container__card-list {
|
||||
gap: $card-gap $card-gap;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
@media (-pgn-size-breakpoint-max-width-sm) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
@@ -11,15 +11,15 @@ $card-gap: 24px;
|
||||
flex: 0 1 100%;
|
||||
cursor: pointer;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
@media (--pgn-size-breakpoint-min-width-sm) {
|
||||
flex: 0 1 calc(50% - #{$card-gap - 12});
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
@media (--pgn-size-breakpoint-min-width-md) {
|
||||
flex: 0 1 calc(33.333% - #{$card-gap - 8});
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
@media (--pgn-size-breakpoint-min-width-lg) {
|
||||
flex: 0 1 calc(25% - #{$card-gap - 6});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,21 +23,21 @@
|
||||
}
|
||||
|
||||
.alert-link {
|
||||
color: $primary !important;
|
||||
color: var(--pgn-color-primary-base) !important;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: $info-700 !important;
|
||||
color: var(--pgn-color-info-700) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.email-suggestion-alert-warning {
|
||||
color: $info-500 !important;
|
||||
color: var(--pgn-color-info-500) !important;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
color: $info-700 !important;
|
||||
color: var(--pgn-color-info-700) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
line-height: 24px;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
color: $primary-700;
|
||||
color: var(--pgn-color-primary-700);
|
||||
}
|
||||
|
||||
.username-suggestion--label {
|
||||
@@ -99,7 +99,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
@media (--pgn-size-breakpoint-max-width-sm) {
|
||||
.username-scroll-suggested--form-field {
|
||||
width: 15rem;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
@extend .pt-4;
|
||||
padding-top: calc(var(--pgn-spacing-spacer-base) * 1.5) !important;
|
||||
min-width: 464px !important;
|
||||
}
|
||||
|
||||
@@ -80,15 +80,15 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
.alert-link {
|
||||
font-weight: normal;
|
||||
text-decoration: underline;
|
||||
color: $info-300 !important;
|
||||
color: var(--pgn-color-info-300) !important;
|
||||
|
||||
&:hover {
|
||||
color: $info-500 !important;
|
||||
color: var(--pgn-color-info-500) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.form-control {
|
||||
background-color: $white !important;
|
||||
background-color: var(--pgn-color-white) !important;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5;
|
||||
height: 2.75rem;
|
||||
@@ -103,11 +103,11 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 14px;
|
||||
|
||||
background-color: $white;
|
||||
border: 1px solid $primary;
|
||||
background-color: var(--pgn-color-white);
|
||||
border: 1px solid var(--pgn-color-primary-base);
|
||||
width: 224px;
|
||||
height: 36px;
|
||||
color: $primary;
|
||||
color: var(--pgn-color-primary-base);
|
||||
|
||||
.btn-tpa__image-icon{
|
||||
background-color: transparent;
|
||||
@@ -132,8 +132,8 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-tpa__font-container {
|
||||
background-color: $primary;
|
||||
color: $white;
|
||||
background-color: var(--pgn-color-primary-base);
|
||||
color: var(--pgn-color-white);
|
||||
font-size: 11px;
|
||||
|
||||
margin-left: -6px;
|
||||
@@ -143,7 +143,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.btn-oa2-facebook {
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
border-color: $facebook-blue;
|
||||
background-color: $facebook-blue;
|
||||
|
||||
@@ -151,12 +151,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
&:focus {
|
||||
background-color: $facebook-focus-blue;
|
||||
border: 1px solid $facebook-focus-blue;
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-google-oauth2 {
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
border-color: $google-blue;
|
||||
background-color: $google-blue;
|
||||
|
||||
@@ -171,12 +171,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
&:focus {
|
||||
background-color: $google-focus-blue;
|
||||
border: 1px solid $google-focus-blue;
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-apple-id {
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
border-color: $apple-black;
|
||||
background-color: $apple-black;
|
||||
font-size: 16px;
|
||||
@@ -190,12 +190,12 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
&:focus {
|
||||
background-color: $apple-focus-black;
|
||||
border: 1px solid $apple-focus-black;
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-oa2-azuread-oauth2 {
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
border-color: $microsoft-black;
|
||||
background-color: $microsoft-black;
|
||||
|
||||
@@ -203,7 +203,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
&:focus {
|
||||
background-color: $microsoft-focus-black;
|
||||
border: 1px solid $microsoft-focus-black;
|
||||
color: $white;
|
||||
color: var(--pgn-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,9 +214,8 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.institute-icon {
|
||||
@extend .mr-1;
|
||||
@extend .text-gray;
|
||||
|
||||
margin: calc(var(--pgn-spacing-spacer-base) * 0.25) !important;
|
||||
color: var(--pgn-color-gray-base) !important;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.25rem;
|
||||
height: 18px;
|
||||
@@ -232,7 +231,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.invalid-feedback {
|
||||
color: $red;
|
||||
color: var(--pgn-color-red);
|
||||
}
|
||||
|
||||
.full-vertical-height {
|
||||
@@ -290,22 +289,22 @@ select.form-control {
|
||||
|
||||
#password-requirement-left {
|
||||
opacity: 1;
|
||||
@extend .x-small;
|
||||
font-size: var(--pgn-typography-font-size-xs) !important;
|
||||
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
|
||||
right: 0.2rem !important;
|
||||
.tooltip-inner {
|
||||
background: $white;
|
||||
background: var(--pgn-color-white);
|
||||
display: block;
|
||||
color: $gray-500;
|
||||
color: var(--pgn-color-gray-500);
|
||||
}
|
||||
.arrow::before {
|
||||
border-left-color: $white;
|
||||
border-left-color: var(--pgn-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
#password-requirement-top {
|
||||
@extend .x-small;
|
||||
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
|
||||
font-size: var(--pgn-typography-font-size-xs) !important;
|
||||
filter: drop-shadow(var(--pgn-elevation-box-shadow-level-2)) drop-shadow(var(--pgn-elevation-box-shadow-level-2)) !important;
|
||||
opacity: 1;
|
||||
width: 90%;
|
||||
bottom: 10px !important;
|
||||
@@ -314,30 +313,30 @@ select.form-control {
|
||||
|
||||
.tooltip-inner {
|
||||
min-width: 464px !important;
|
||||
background: $white;
|
||||
background: var(--pgn-color-white);
|
||||
display: block;
|
||||
color: $gray-500;
|
||||
color: var(--pgn-color-gray-500);
|
||||
}
|
||||
.arrow::before {
|
||||
border-top-color: $white;
|
||||
border-top-color: var(--pgn-color-white);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.yellow-border {
|
||||
border: 2px solid $accent-b;
|
||||
border: 2px solid var(--pgn-color-accent-b);
|
||||
}
|
||||
|
||||
.institutions__heading {
|
||||
color: $primary-700;
|
||||
color: var(--pgn-color-primary-700);
|
||||
}
|
||||
|
||||
.logistration-button {
|
||||
color: $gray-700;
|
||||
color: var(--pgn-color-gray-700);
|
||||
}
|
||||
|
||||
.logistration-button:hover{
|
||||
color: $gray-700;
|
||||
color: var(--pgn-color-gray-700);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
@@ -352,7 +351,7 @@ select.form-control {
|
||||
width: 2.3rem;
|
||||
}
|
||||
.has-floating-label {
|
||||
color: $gray-500;
|
||||
color: var(--pgn-color-gray-500);
|
||||
}
|
||||
|
||||
.pgn__form-control-floating-label .pgn__form-control-floating-label-content {
|
||||
@@ -366,7 +365,7 @@ select.form-control {
|
||||
|
||||
.form-group__form-field .form-control:focus ~ .pgn__form-control-floating-label .pgn__form-control-floating-label-content {
|
||||
font-size: 16px;
|
||||
color: $primary-700;
|
||||
color: var(--pgn-color-primary-700);
|
||||
}
|
||||
|
||||
.form-group__form-field .form-control:not([value='']):not(:focus) ~
|
||||
@@ -444,14 +443,14 @@ select.form-control {
|
||||
}
|
||||
|
||||
.table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: $light-200;
|
||||
background-color: var(--pgn-color-light-200);
|
||||
}
|
||||
|
||||
.institutions--provider-link {
|
||||
font-weight: normal;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5rem;
|
||||
color: $primary-700
|
||||
color: var(--pgn-color-primary-700)
|
||||
}
|
||||
|
||||
.pgn__form-control-decorator-trailing {
|
||||
|
||||
26
webpack.dev.config.js
Normal file
26
webpack.dev.config.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const path = require('path');
|
||||
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const config = createConfig('webpack-dev');
|
||||
|
||||
config.resolve.modules = [
|
||||
path.resolve(__dirname, './src'),
|
||||
'node_modules',
|
||||
];
|
||||
|
||||
config.module.rules[0].exclude = /node_modules\/(?!(fastest-levenshtein|@edx))/;
|
||||
|
||||
config.plugins.push(
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.resolve(__dirname, './public/robots.txt'),
|
||||
to: path.resolve(__dirname, './dist/robots.txt'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
module.exports = config;
|
||||
@@ -1,7 +1,26 @@
|
||||
const path = require('path');
|
||||
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
const CopyPlugin = require('copy-webpack-plugin');
|
||||
|
||||
const config = createConfig('webpack-prod');
|
||||
|
||||
config.resolve.modules = [
|
||||
path.resolve(__dirname, './src'),
|
||||
'node_modules',
|
||||
];
|
||||
|
||||
config.module.rules[0].exclude = /node_modules\/(?!(fastest-levenshtein|@edx))/;
|
||||
|
||||
config.plugins.push(
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: path.resolve(__dirname, './public/robots.txt'),
|
||||
to: path.resolve(__dirname, './dist/robots.txt'),
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
module.exports = config;
|
||||
|
||||
Reference in New Issue
Block a user