Compare commits
163 Commits
remove-com
...
sundass/IN
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c599f7eff | ||
|
|
ffb8695305 | ||
|
|
c04ed9aa43 | ||
|
|
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 | ||
|
|
b41fca3605 | ||
|
|
ac2548913f | ||
|
|
cd9b3bd084 | ||
|
|
efc07aac67 | ||
|
|
2d50ed224f | ||
|
|
d10f9b932b | ||
|
|
05aa85a5fb | ||
|
|
56bd6d835e | ||
|
|
afd4d24360 | ||
|
|
4898864416 | ||
|
|
739f94d624 | ||
|
|
1819edc9b7 | ||
|
|
ad0d75ab0d | ||
|
|
a90ebb7d4d | ||
|
|
f8290adab5 | ||
|
|
788a42b341 | ||
|
|
4f48e82959 | ||
|
|
99850574fb | ||
|
|
d66afe98f0 | ||
|
|
e2cdfce832 | ||
|
|
c1e63da778 | ||
|
|
6ffa45f0c1 | ||
|
|
a03ba3e3b3 | ||
|
|
e2a206caa5 | ||
|
|
3a963da819 | ||
|
|
ecf4c3ae53 | ||
|
|
4a65f0a84c | ||
|
|
2428b4c389 | ||
|
|
9688bd3699 | ||
|
|
c123815a55 | ||
|
|
182e669593 | ||
|
|
65533b8d58 | ||
|
|
45185dba70 | ||
|
|
444c4b4434 | ||
|
|
099fe8d717 | ||
|
|
4755540be8 | ||
|
|
9a30f053c7 | ||
|
|
d629d66bf2 | ||
|
|
9d46d68150 | ||
|
|
a4ed6a362e | ||
|
|
a1a0d3cd96 | ||
|
|
950c401e88 | ||
|
|
6b983e18d3 | ||
|
|
327210192c | ||
|
|
ce056c9ad2 | ||
|
|
3bd6e454d0 | ||
|
|
f52129a11e | ||
|
|
ea01050163 | ||
|
|
0d603b5fa1 | ||
|
|
c1ec9b6e99 | ||
|
|
2c509b00ac | ||
|
|
ef358fe741 | ||
|
|
56e0520d9c | ||
|
|
1f7b7f5c41 | ||
|
|
471fa75155 | ||
|
|
c89d16e529 | ||
|
|
fc02ab820a | ||
|
|
ac23cdcc7a | ||
|
|
02c4c5be29 | ||
|
|
3bd7d61e3a | ||
|
|
32ebc69c0e | ||
|
|
c98c3b16c5 | ||
|
|
287fe3adfe | ||
|
|
d4e7b7b371 | ||
|
|
ad78f068e0 | ||
|
|
d156de2e66 | ||
|
|
99bca1bd9b | ||
|
|
efaa83a1bc | ||
|
|
bd63bb1f15 | ||
|
|
5754c2961a | ||
|
|
dcbd644a25 | ||
|
|
52e438652c | ||
|
|
d8947a4c0a | ||
|
|
8efb22595c | ||
|
|
73e8913f90 | ||
|
|
03d1666c2c | ||
|
|
3782503983 | ||
|
|
b219fe3683 | ||
|
|
3ddaf795f2 | ||
|
|
90f650ce3e | ||
|
|
6f325c20c3 | ||
|
|
de12dfbf9e | ||
|
|
c663f6fa30 | ||
|
|
dba93333fd | ||
|
|
611af07326 | ||
|
|
564ec70d9e | ||
|
|
65e95a4d1b | ||
|
|
cf2b50005b | ||
|
|
faf4ff8488 | ||
|
|
7d64220852 | ||
|
|
a18df02d37 |
4
.env
4
.env
@@ -16,6 +16,9 @@ SITE_NAME=null
|
|||||||
INFO_EMAIL=''
|
INFO_EMAIL=''
|
||||||
# ***** Cookies *****
|
# ***** Cookies *****
|
||||||
USER_RETENTION_COOKIE_NAME=null
|
USER_RETENTION_COOKIE_NAME=null
|
||||||
|
# ***** Cohesion Keys *****
|
||||||
|
COHESION_WRITE_KEY=''
|
||||||
|
COHESION_SOURCE_KEY=''
|
||||||
# ***** Links *****
|
# ***** Links *****
|
||||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
|
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
|
||||||
@@ -23,6 +26,7 @@ POST_REGISTRATION_REDIRECT_URL=''
|
|||||||
SEARCH_CATALOG_URL=''
|
SEARCH_CATALOG_URL=''
|
||||||
# ***** Features flags *****
|
# ***** Features flags *****
|
||||||
DISABLE_ENTERPRISE_LOGIN=''
|
DISABLE_ENTERPRISE_LOGIN=''
|
||||||
|
ENABLE_AUTO_GENERATED_USERNAME=''
|
||||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
|
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
|
||||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
|
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
|
||||||
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
|
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
|
|||||||
# ***** Cookies *****
|
# ***** Cookies *****
|
||||||
SESSION_COOKIE_DOMAIN='localhost'
|
SESSION_COOKIE_DOMAIN='localhost'
|
||||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||||
|
# ***** Cohesion Keys *****
|
||||||
|
COHESION_WRITE_KEY=''
|
||||||
|
COHESION_SOURCE_KEY=''
|
||||||
# ***** Links *****
|
# ***** Links *****
|
||||||
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
|
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
|
||||||
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
||||||
|
|||||||
@@ -18,3 +18,5 @@ SEGMENT_KEY=''
|
|||||||
SITE_NAME='Your Platform Name Here'
|
SITE_NAME='Your Platform Name Here'
|
||||||
APP_ID=''
|
APP_ID=''
|
||||||
MFE_CONFIG_API_URL=''
|
MFE_CONFIG_API_URL=''
|
||||||
|
COHESION_WRITE_KEY=''
|
||||||
|
COHESION_SOURCE_KEY=''
|
||||||
|
|||||||
13
.eslintrc.js
13
.eslintrc.js
@@ -1,7 +1,7 @@
|
|||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
const { createConfig } = require('@openedx/frontend-build');
|
const { createConfig } = require('@openedx/frontend-build');
|
||||||
|
|
||||||
module.exports = createConfig('eslint', {
|
const config = createConfig('eslint', {
|
||||||
rules: {
|
rules: {
|
||||||
// Temporarily update the 'indent', 'template-curly-spacing' and
|
// Temporarily update the 'indent', 'template-curly-spacing' and
|
||||||
// 'no-multiple-empty-lines' rules since they are causing eslint
|
// 'no-multiple-empty-lines' rules since they are causing eslint
|
||||||
@@ -50,3 +50,14 @@ module.exports = createConfig('eslint', {
|
|||||||
'function-paren-newline': 'off',
|
'function-paren-newline': 'off',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
config.settings = {
|
||||||
|
'import/resolver': {
|
||||||
|
node: {
|
||||||
|
paths: ['src', 'node_modules'],
|
||||||
|
extensions: ['.js', '.jsx'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
|
|||||||
1
.github/CODEOWNERS
vendored
Normal file
1
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
* @openedx/2U-infinity
|
||||||
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"
|
||||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -25,5 +25,5 @@ Include a link to the sandbox for design changes or screenshot for before and af
|
|||||||
|
|
||||||
#### Post-merge Checklist
|
#### Post-merge Checklist
|
||||||
|
|
||||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-vanguards** to do it.
|
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-infinity** to do it.
|
||||||
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
autoupdate:
|
autoupdate:
|
||||||
name: autoupdate
|
name: autoupdate
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: docker://chinthakagodawita/autoupdate-action:v1
|
- uses: docker://chinthakagodawita/autoupdate-action:v1
|
||||||
env:
|
env:
|
||||||
|
|||||||
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@@ -10,17 +10,15 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v4
|
||||||
- name: Setup Nodejs Env
|
|
||||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
|
||||||
- name: Setup Nodejs
|
- name: Setup Nodejs
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VER }}
|
node-version-file: '.nvmrc'
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -41,4 +39,7 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Run Code Coverage
|
- name: Run Code Coverage
|
||||||
uses: codecov/codecov-action@v3
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
fail_ci_if_error: true
|
||||||
|
|||||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
version-check:
|
version-check:
|
||||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
# The following users are the owners of all frontend-app-authn files
|
# The following users are the owners of all frontend-app-authn files
|
||||||
* @openedx/2u-vanguards
|
* @openedx/2u-infinity
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ All community members are expected to follow the `Open edX Code of Conduct <http
|
|||||||
People
|
People
|
||||||
======
|
======
|
||||||
The assigned maintainers for this component and other project details may be
|
The assigned maintainers for this component and other project details may be
|
||||||
found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml``
|
found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-infinity>`_. Backstage pulls this data from the ``catalog-info.yaml``
|
||||||
file in this repo.
|
file in this repo.
|
||||||
|
|
||||||
Reporting Security Issues
|
Reporting Security Issues
|
||||||
|
|||||||
@@ -13,6 +13,6 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
openedx.org/arch-interest-groups: ""
|
openedx.org/arch-interest-groups: ""
|
||||||
spec:
|
spec:
|
||||||
owner: group:2u-vanguards
|
owner: group:2u-infinity
|
||||||
type: 'service'
|
type: 'service'
|
||||||
lifecycle: 'production'
|
lifecycle: 'production'
|
||||||
|
|||||||
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: '',
|
||||||
|
};
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
|
|
||||||
nick: Authn MFE
|
nick: Authn MFE
|
||||||
oeps: {}
|
oeps: {}
|
||||||
owner: openedx/2u-vanguards
|
owner: openedx/2u-infinity
|
||||||
openedx-release:
|
openedx-release:
|
||||||
ref: master
|
ref: master
|
||||||
|
|||||||
14846
package-lock.json
generated
14846
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -33,12 +33,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||||
"@edx/frontend-platform": "7.1.3",
|
"@edx/frontend-platform": "^8.0.0",
|
||||||
"@edx/openedx-atlas": "^0.6.0",
|
"@edx/openedx-atlas": "^0.6.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.5.2",
|
"@fortawesome/fontawesome-svg-core": "6.7.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "6.5.2",
|
"@fortawesome/free-brands-svg-icons": "6.7.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.5.2",
|
"@fortawesome/free-solid-svg-icons": "6.7.1",
|
||||||
"@fortawesome/react-fontawesome": "0.2.0",
|
"@fortawesome/react-fontawesome": "0.2.2",
|
||||||
|
"@openedx/frontend-plugin-framework": "^1.3.0",
|
||||||
"@openedx/paragon": "^22.1.1",
|
"@openedx/paragon": "^22.1.1",
|
||||||
"@optimizely/react-sdk": "^2.9.1",
|
"@optimizely/react-sdk": "^2.9.1",
|
||||||
"@redux-devtools/extension": "3.3.0",
|
"@redux-devtools/extension": "3.3.0",
|
||||||
@@ -47,38 +48,39 @@
|
|||||||
"algoliasearch": "^4.14.3",
|
"algoliasearch": "^4.14.3",
|
||||||
"algoliasearch-helper": "^3.14.0",
|
"algoliasearch-helper": "^3.14.0",
|
||||||
"classnames": "2.5.1",
|
"classnames": "2.5.1",
|
||||||
"core-js": "3.36.1",
|
"core-js": "3.39.0",
|
||||||
"fastest-levenshtein": "1.0.16",
|
"fastest-levenshtein": "1.0.16",
|
||||||
"form-urlencoded": "6.1.4",
|
"form-urlencoded": "6.1.5",
|
||||||
"prop-types": "15.8.1",
|
"prop-types": "15.8.1",
|
||||||
"query-string": "7.1.3",
|
"query-string": "7.1.3",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-error-boundary": "^4.0.13",
|
||||||
"react-helmet": "6.1.0",
|
"react-helmet": "6.1.0",
|
||||||
"react-loading-skeleton": "3.4.0",
|
"react-loading-skeleton": "3.5.0",
|
||||||
"react-redux": "7.2.9",
|
"react-redux": "7.2.9",
|
||||||
"react-responsive": "8.2.0",
|
"react-responsive": "8.2.0",
|
||||||
"react-router": "6.22.3",
|
"react-router": "6.28.0",
|
||||||
"react-router-dom": "6.22.3",
|
"react-router-dom": "6.28.0",
|
||||||
"react-zendesk": "^0.1.13",
|
"react-zendesk": "^0.1.13",
|
||||||
"redux": "4.2.0",
|
"redux": "4.2.1",
|
||||||
"redux-logger": "3.0.6",
|
"redux-logger": "3.0.6",
|
||||||
"redux-mock-store": "1.5.4",
|
"redux-mock-store": "1.5.5",
|
||||||
"redux-saga": "1.3.0",
|
"redux-saga": "1.3.0",
|
||||||
"redux-thunk": "2.4.2",
|
"redux-thunk": "2.4.2",
|
||||||
"regenerator-runtime": "0.14.1",
|
"regenerator-runtime": "0.14.1",
|
||||||
"reselect": "4.1.8",
|
"reselect": "5.1.1",
|
||||||
"universal-cookie": "4.0.4"
|
"universal-cookie": "7.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/browserslist-config": "^1.1.1",
|
"@edx/browserslist-config": "^1.1.1",
|
||||||
"@edx/reactifex": "1.1.0",
|
"@edx/reactifex": "1.1.0",
|
||||||
"@openedx/frontend-build": "13.1.4",
|
"@openedx/frontend-build": "^14.0.3",
|
||||||
"babel-plugin-formatjs": "10.5.14",
|
"babel-plugin-formatjs": "10.5.26",
|
||||||
"eslint-plugin-import": "2.29.1",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"glob": "7.2.3",
|
"glob": "7.2.3",
|
||||||
"history": "5.3.0",
|
"history": "5.3.0",
|
||||||
"husky": "7.0.4",
|
"husky": "9.1.7",
|
||||||
"jest": "29.7.0",
|
"jest": "29.7.0",
|
||||||
"react-test-renderer": "^17.0.2"
|
"react-test-renderer": "^17.0.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,30 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en-us">
|
<html lang="en-us">
|
||||||
<head>
|
<head>
|
||||||
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></title>
|
<title>
|
||||||
<meta charset="utf-8">
|
<%= (process.env.SITE_NAME && process.env.SITE_NAME !='null' ) ?
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
'Authentication | ' + process.env.SITE_NAME : 'Authentication' %>
|
||||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon"/>
|
</title>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.9/iframeResizer.contentWindow.min.js"
|
<meta charset="utf-8" />
|
||||||
integrity="sha512-mdT/HQRzoRP4laVz49Mndx6rcCGA3IhuyhP3gaY0E9sZPkwbtDk9ttQIq9o8qGCf5VvJv1Xsy3k2yTjfUoczqw=="
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
crossorigin="anonymous"
|
<link
|
||||||
referrerpolicy="no-referrer">
|
rel="shortcut icon"
|
||||||
</script>
|
href="<%=htmlWebpackPlugin.options.FAVICON_URL%>"
|
||||||
<% if (process.env.OPTIMIZELY_URL) { %>
|
type="image/x-icon"
|
||||||
<script
|
/>
|
||||||
src="<%= process.env.OPTIMIZELY_URL %>"
|
<script
|
||||||
></script>
|
src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
|
||||||
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
|
||||||
<script
|
crossorigin="anonymous"
|
||||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
referrerpolicy="no-referrer"
|
||||||
></script>
|
></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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Helmet } from 'react-helmet';
|
|||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
|
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute,
|
||||||
} from './common-components';
|
} from './common-components';
|
||||||
import configureStore from './data/configureStore';
|
import configureStore from './data/configureStore';
|
||||||
import {
|
import {
|
||||||
@@ -22,21 +22,90 @@ import {
|
|||||||
import { updatePathWithQueryParams } from './data/utils';
|
import { updatePathWithQueryParams } from './data/utils';
|
||||||
import { ForgotPasswordPage } from './forgot-password';
|
import { ForgotPasswordPage } from './forgot-password';
|
||||||
import Logistration from './logistration/Logistration';
|
import Logistration from './logistration/Logistration';
|
||||||
|
import MainAppSlot from './plugin-slots/MainAppSlot';
|
||||||
import { ProgressiveProfiling } from './progressive-profiling';
|
import { ProgressiveProfiling } from './progressive-profiling';
|
||||||
import { RecommendationsPage } from './recommendations';
|
import { RecommendationsPage } from './recommendations';
|
||||||
import { RegistrationPage } from './register';
|
import { RegistrationPage } from './register';
|
||||||
import { ResetPasswordPage } from './reset-password';
|
import { ResetPasswordPage } from './reset-password';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
|
import './cohesion/cohesion.scss';
|
||||||
|
|
||||||
registerIcons();
|
registerIcons();
|
||||||
|
|
||||||
const MainApp = () => (
|
const MainApp = () => (
|
||||||
<AppProvider store={configureStore()}>
|
<AppProvider store={configureStore()}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
{/* Cohesion Snippet */}
|
||||||
|
<script>
|
||||||
|
{`!function (co, h, e, s, i, o, n) {
|
||||||
|
var d = 'documentElement'; var a = 'className'; h[d][a] += ' preampjs fusejs';
|
||||||
|
n.k = e; co._Cohesion = n; co._Preamp = { k: s, start: new Date }; co._Fuse = { k: i }; co._Tagular = { k: o };
|
||||||
|
[e, s, i, o].map(function (x) { co[x] = co[x] || function () { (co[x].q = co[x].q || []).push([].slice.call(arguments)) } });
|
||||||
|
var b = function () { var u = h[d][a]; h[d][a] = u.replace(/ ?preampjs| ?fusejs/g, '') };
|
||||||
|
h.addEventListener('DOMContentLoaded', function () {
|
||||||
|
co.setTimeout(b, 3e3);
|
||||||
|
co._Preamp.docReady = co._Fuse.docReady = !0
|
||||||
|
}); var z = h.createElement('script');
|
||||||
|
z.async = 1; z.src = 'https://beam.edx.org/cohesion/cohesion-latest.min.js';
|
||||||
|
z.onerror = function () { var ce = 'error',f = 'function'; for (var o of co[e].q || []) o[0] === ce && typeof o[1] == f && o[1](); co[e] = function (n, cb) { n === ce && typeof cb == f && cb() }; b() };
|
||||||
|
h.head.appendChild(z);
|
||||||
|
}
|
||||||
|
(window, document, 'cohesion', 'preamp', 'fuse', 'tagular', {
|
||||||
|
tagular: {
|
||||||
|
apiHost: 'https://beam.edx.org/v2/t',
|
||||||
|
writeKey: "${process.env.COHESION_WRITE_KEY}",
|
||||||
|
sourceKey: "${process.env.COHESION_SOURCE_KEY}",
|
||||||
|
cookieDomain: 'edx.org',
|
||||||
|
domainWhitelist: ["edx.org"],
|
||||||
|
apiVersion: 'v2/t',
|
||||||
|
multiparty: true,
|
||||||
|
useBeacon: true,
|
||||||
|
},
|
||||||
|
consent: {
|
||||||
|
onetrust: {
|
||||||
|
enabled: true,
|
||||||
|
optIn: true
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
domain: '.edx.org',
|
||||||
|
},
|
||||||
|
});`}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{/* Stitch Cohesion and Segment IDs */}
|
||||||
|
<script>
|
||||||
|
{`cohesion("tagular:ready", function () {
|
||||||
|
window.analytics.ready(function () {
|
||||||
|
const cohesionAnonymId = window.tagular("getAliasSet")["anonymousId"];
|
||||||
|
const segmentAnonymId = window.analytics.user().anonymousId();
|
||||||
|
const segmentUserId = window.analytics.user().id();
|
||||||
|
window.analytics.identify(segmentUserId, {
|
||||||
|
cohesion_anonymous_id: cohesionAnonymId,
|
||||||
|
});
|
||||||
|
window.tagular("beam", {
|
||||||
|
"@type": "core.Identify.v1",
|
||||||
|
traits: {},
|
||||||
|
externalIds: [
|
||||||
|
{
|
||||||
|
id: segmentAnonymId,
|
||||||
|
type: "segment_anonymous_id",
|
||||||
|
collection: "users",
|
||||||
|
encoding: "none",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: cohesionAnonymId,
|
||||||
|
type: "cohesion_anonymous_id",
|
||||||
|
collection: "users",
|
||||||
|
encoding: "none",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});`}
|
||||||
|
</script>
|
||||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||||
</Helmet>
|
</Helmet>
|
||||||
{getConfig().ZENDESK_KEY && <Zendesk />}
|
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||||
<Route
|
<Route
|
||||||
@@ -57,6 +126,7 @@ const MainApp = () => (
|
|||||||
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
|
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
|
||||||
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
|
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
<MainAppSlot />
|
||||||
</AppProvider>
|
</AppProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
4
src/cohesion/cohesion.scss
Normal file
4
src/cohesion/cohesion.scss
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.preampjs [data-preamp],
|
||||||
|
.fusejs [data-fuse] {
|
||||||
|
opacity: 0 !important;
|
||||||
|
}
|
||||||
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,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Form, TransitionReplace,
|
Form, TransitionReplace,
|
||||||
@@ -7,6 +7,38 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
const FormGroup = (props) => {
|
const FormGroup = (props) => {
|
||||||
const [hasFocus, setHasFocus] = useState(false);
|
const [hasFocus, setHasFocus] = useState(false);
|
||||||
|
const [isAutoFill, setIsAutoFill] = useState(false);
|
||||||
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkAutoFill = () => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
// Check both standard and vendor-prefixed autofill
|
||||||
|
const isAutoFillField = inputRef.current.matches(
|
||||||
|
':autofill, :-webkit-autofill',
|
||||||
|
);
|
||||||
|
setIsAutoFill(isAutoFillField);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const observer = new MutationObserver(checkAutoFill);
|
||||||
|
if (inputRef.current) {
|
||||||
|
// Check immediately on mount
|
||||||
|
checkAutoFill();
|
||||||
|
|
||||||
|
// Configure observer to watch style changes only
|
||||||
|
observer.observe(inputRef.current, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['style'], // Autofill often changes styles
|
||||||
|
});
|
||||||
|
// Fallback check for browser timing issues
|
||||||
|
const timeoutId = setTimeout(checkAutoFill, 100);
|
||||||
|
return () => {
|
||||||
|
observer.disconnect();
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleFocus = (e) => {
|
const handleFocus = (e) => {
|
||||||
setHasFocus(true);
|
setHasFocus(true);
|
||||||
@@ -23,6 +55,7 @@ const FormGroup = (props) => {
|
|||||||
return (
|
return (
|
||||||
<Form.Group controlId={props.name} className={props.className} isInvalid={props.errorMessage !== ''}>
|
<Form.Group controlId={props.name} className={props.className} isInvalid={props.errorMessage !== ''}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
|
ref={inputRef}
|
||||||
as={props.as}
|
as={props.as}
|
||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
type={props.type}
|
type={props.type}
|
||||||
@@ -39,6 +72,7 @@ const FormGroup = (props) => {
|
|||||||
controlClassName={props.borderClass}
|
controlClassName={props.borderClass}
|
||||||
trailingElement={props.trailingElement}
|
trailingElement={props.trailingElement}
|
||||||
floatingLabel={props.floatingLabel}
|
floatingLabel={props.floatingLabel}
|
||||||
|
isAutoFill={isAutoFill}
|
||||||
>
|
>
|
||||||
{props.options ? props.options() : null}
|
{props.options ? props.options() : null}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const InstitutionLogistration = props => {
|
|||||||
className="btn nav-item p-0 mb-1 institutions--provider-link"
|
className="btn nav-item p-0 mb-1 institutions--provider-link"
|
||||||
destination={lmsBaseUrl + provider.loginUrl}
|
destination={lmsBaseUrl + provider.loginUrl}
|
||||||
>
|
>
|
||||||
{provider.name}
|
{provider?.name}
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import trackCohesionEvent from '../cohesion/trackers';
|
||||||
import {
|
import {
|
||||||
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
|
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
|
||||||
} from '../data/constants';
|
} from '../data/constants';
|
||||||
import { setCookie } from '../data/utils';
|
import setCookie from '../data/utils/cookies';
|
||||||
|
import { redirectWithDelay } from '../data/utils/dataUtils';
|
||||||
|
|
||||||
const RedirectLogistration = (props) => {
|
const RedirectLogistration = (props) => {
|
||||||
const {
|
const {
|
||||||
@@ -20,10 +24,16 @@ const RedirectLogistration = (props) => {
|
|||||||
userId,
|
userId,
|
||||||
registrationEmbedded,
|
registrationEmbedded,
|
||||||
host,
|
host,
|
||||||
|
currectProvider,
|
||||||
} = props;
|
} = props;
|
||||||
|
const cohesionEventData = useSelector(state => state.cohesion.eventData);
|
||||||
let finalRedirectUrl = '';
|
let finalRedirectUrl = '';
|
||||||
|
|
||||||
if (success) {
|
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
|
// 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.
|
// 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
|
// Note: For multiple enterprise use case, we need to make sure that user first visits the
|
||||||
@@ -75,8 +85,7 @@ const RedirectLogistration = (props) => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
redirectWithDelay(finalRedirectUrl);
|
||||||
window.location.href = finalRedirectUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -94,6 +103,7 @@ RedirectLogistration.defaultProps = {
|
|||||||
userId: null,
|
userId: null,
|
||||||
registrationEmbedded: false,
|
registrationEmbedded: false,
|
||||||
host: '',
|
host: '',
|
||||||
|
currectProvider: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
RedirectLogistration.propTypes = {
|
RedirectLogistration.propTypes = {
|
||||||
@@ -108,6 +118,7 @@ RedirectLogistration.propTypes = {
|
|||||||
userId: PropTypes.number,
|
userId: PropTypes.number,
|
||||||
registrationEmbedded: PropTypes.bool,
|
registrationEmbedded: PropTypes.bool,
|
||||||
host: PropTypes.string,
|
host: PropTypes.string,
|
||||||
|
currectProvider: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RedirectLogistration;
|
export default RedirectLogistration;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
@@ -8,17 +9,35 @@ import { Login } from '@openedx/paragon/icons';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { LOGIN_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 SocialAuthProviders = (props) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { referrer, socialAuthProviders } = props;
|
const { referrer, socialAuthProviders } = props;
|
||||||
|
const registrationFields = useSelector(state => state.register.registrationFormData);
|
||||||
|
|
||||||
function handleSubmit(e) {
|
function handleSubmit(e, providerName) {
|
||||||
e.preventDefault();
|
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;
|
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) => (
|
const socialAuth = socialAuthProviders.map((provider, index) => (
|
||||||
@@ -28,7 +47,7 @@ const SocialAuthProviders = (props) => {
|
|||||||
type="button"
|
type="button"
|
||||||
className={`btn-social btn-${provider.id} ${index % 2 === 0 ? 'mr-3' : ''}`}
|
className={`btn-social btn-${provider.id} ${index % 2 === 0 ? 'mr-3' : ''}`}
|
||||||
data-provider-url={referrer === LOGIN_PAGE ? provider.loginUrl : provider.registerUrl}
|
data-provider-url={referrer === LOGIN_PAGE ? provider.loginUrl : provider.registerUrl}
|
||||||
onClick={handleSubmit}
|
onClick={(event) => handleSubmit(event, provider?.name)}
|
||||||
>
|
>
|
||||||
{provider.iconImage ? (
|
{provider.iconImage ? (
|
||||||
<div aria-hidden="true">
|
<div aria-hidden="true">
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const ThirdPartyAuth = (props) => {
|
|||||||
const isSocialAuthActive = !!providers.length && !currentProvider;
|
const isSocialAuthActive = !!providers.length && !currentProvider;
|
||||||
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
|
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
|
||||||
const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL;
|
const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL;
|
||||||
|
const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -61,7 +62,7 @@ const ThirdPartyAuth = (props) => {
|
|||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{thirdPartyAuthApiStatus === PENDING_STATE ? (
|
{thirdPartyAuthApiStatus === PENDING_STATE && isThirdPartyAuthActive ? (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<Skeleton className="tpa-skeleton" height={36} count={2} />
|
<Skeleton className="tpa-skeleton" height={36} count={2} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import PropTypes from 'prop-types';
|
|||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||||
|
import setCookie from '../data/utils/cookies';
|
||||||
|
|
||||||
const ThirdPartyAuthAlert = (props) => {
|
const ThirdPartyAuthAlert = (props) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@@ -20,7 +21,10 @@ const ThirdPartyAuthAlert = (props) => {
|
|||||||
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!currentProvider) {
|
if (currentProvider) {
|
||||||
|
// Setting this cookie to capture marketingEmailsOptIn for SSO flow on the onboarding component
|
||||||
|
setCookie('ssoPipelineRedirectionDone', true);
|
||||||
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ import { getConfig } from '@edx/frontend-platform';
|
|||||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import {
|
import { RESET_PAGE } from '../data/constants';
|
||||||
DEFAULT_REDIRECT_URL,
|
import { updatePathWithQueryParams } from '../data/utils';
|
||||||
} from '../data/constants';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This wrapper redirects the requester to our default redirect url if they are
|
* This wrapper redirects the requester to our default redirect url if they are
|
||||||
@@ -25,7 +24,12 @@ const UnAuthOnlyRoute = ({ children }) => {
|
|||||||
|
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
if (authUser && authUser.username) {
|
if (authUser && authUser.username) {
|
||||||
global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
const updatedPath = updatePathWithQueryParams(window.location.pathname);
|
||||||
|
if (updatedPath.startsWith(RESET_PAGE)) {
|
||||||
|
global.location.href = getConfig().LMS_BASE_URL;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
global.location.href = getConfig().LMS_BASE_URL.concat(updatedPath);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
79
src/common-components/data/constants.js
Normal file
79
src/common-components/data/constants.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
export const registerFields = {
|
||||||
|
fields: {
|
||||||
|
country: {
|
||||||
|
name: 'country',
|
||||||
|
error_message: 'Select your country or region of residence',
|
||||||
|
},
|
||||||
|
honor_code: {
|
||||||
|
name: 'honor_code',
|
||||||
|
type: 'tos_and_honor_code',
|
||||||
|
error_message: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const progressiveProfilingFields = {
|
||||||
|
extended_profile: [],
|
||||||
|
fields: {
|
||||||
|
level_of_education: {
|
||||||
|
name: 'level_of_education',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Highest level of education completed',
|
||||||
|
error_message: '',
|
||||||
|
options: [
|
||||||
|
[
|
||||||
|
'p',
|
||||||
|
'Doctorate',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'm',
|
||||||
|
"Master's or professional degree",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'b',
|
||||||
|
"Bachelor's degree",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'a',
|
||||||
|
'Associate degree',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'hs',
|
||||||
|
'Secondary/high school',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'jhs',
|
||||||
|
'Junior secondary/junior high/middle school',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'none',
|
||||||
|
'No formal education',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'other',
|
||||||
|
'Other education',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
gender: {
|
||||||
|
name: 'gender',
|
||||||
|
type: 'select',
|
||||||
|
label: 'Gender',
|
||||||
|
error_message: '',
|
||||||
|
options: [
|
||||||
|
[
|
||||||
|
'm',
|
||||||
|
'Male',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'f',
|
||||||
|
'Female',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'o',
|
||||||
|
'Other/Prefer Not to Say',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { logError } from '@edx/frontend-platform/logging';
|
import { logError } from '@edx/frontend-platform/logging';
|
||||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||||
|
|
||||||
@@ -7,6 +8,7 @@ import {
|
|||||||
getThirdPartyAuthContextSuccess,
|
getThirdPartyAuthContextSuccess,
|
||||||
THIRD_PARTY_AUTH_CONTEXT,
|
THIRD_PARTY_AUTH_CONTEXT,
|
||||||
} from './actions';
|
} from './actions';
|
||||||
|
import { progressiveProfilingFields, registerFields } from './constants';
|
||||||
import {
|
import {
|
||||||
getThirdPartyAuthContext,
|
getThirdPartyAuthContext,
|
||||||
} from './service';
|
} from './service';
|
||||||
@@ -20,7 +22,16 @@ export function* fetchThirdPartyAuthContext(action) {
|
|||||||
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
||||||
|
|
||||||
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
|
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
|
||||||
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
|
// hard code country field, level of education and gender fields
|
||||||
|
if (getConfig().ENABLE_HARD_CODE_OPTIONAL_FIELDS) {
|
||||||
|
yield put(getThirdPartyAuthContextSuccess(
|
||||||
|
registerFields,
|
||||||
|
progressiveProfilingFields,
|
||||||
|
thirdPartyAuthContext,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
yield put(getThirdPartyAuthContextFailure());
|
yield put(getThirdPartyAuthContextFailure());
|
||||||
logError(e);
|
logError(e);
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ import React from 'react';
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import { PAGE_NOT_FOUND, REGISTER_EMBEDDED_PAGE } from '../../data/constants';
|
|
||||||
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MemoryRouter, Route, BrowserRouter as Router, Routes,
|
MemoryRouter, Route, BrowserRouter as Router, Routes,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import { PAGE_NOT_FOUND, REGISTER_EMBEDDED_PAGE } from '../../data/constants';
|
||||||
|
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
|
||||||
|
|
||||||
const RRD = require('react-router-dom');
|
const RRD = require('react-router-dom');
|
||||||
// Just render plain div with its children
|
// Just render plain div with its children
|
||||||
// eslint-disable-next-line react/prop-types
|
// eslint-disable-next-line react/prop-types
|
||||||
|
|||||||
@@ -1,16 +1,35 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
|
|
||||||
import registerIcons from '../RegisterFaIcons';
|
import registerIcons from '../RegisterFaIcons';
|
||||||
import SocialAuthProviders from '../SocialAuthProviders';
|
import SocialAuthProviders from '../SocialAuthProviders';
|
||||||
|
|
||||||
registerIcons();
|
registerIcons();
|
||||||
|
const mockStore = configureStore();
|
||||||
|
|
||||||
describe('SocialAuthProviders', () => {
|
describe('SocialAuthProviders', () => {
|
||||||
let props = {};
|
let props = {};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
register: {
|
||||||
|
registrationFormData: {
|
||||||
|
configurableFormFields: {
|
||||||
|
marketingEmailsOptIn: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const store = mockStore(initialState);
|
||||||
|
const reduxWrapper = children => (
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<Provider store={store}>{children}</Provider>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
|
||||||
const appleProvider = {
|
const appleProvider = {
|
||||||
id: 'oa2-apple-id',
|
id: 'oa2-apple-id',
|
||||||
name: 'Apple',
|
name: 'Apple',
|
||||||
@@ -30,11 +49,11 @@ describe('SocialAuthProviders', () => {
|
|||||||
it('should match social auth provider with iconImage snapshot', () => {
|
it('should match social auth provider with iconImage snapshot', () => {
|
||||||
props = { socialAuthProviders: [appleProvider, facebookProvider] };
|
props = { socialAuthProviders: [appleProvider, facebookProvider] };
|
||||||
|
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(reduxWrapper(
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<SocialAuthProviders {...props} />
|
<SocialAuthProviders {...props} />
|
||||||
</IntlProvider>,
|
</IntlProvider>,
|
||||||
).toJSON();
|
)).toJSON();
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@@ -48,11 +67,11 @@ describe('SocialAuthProviders', () => {
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(reduxWrapper(
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<SocialAuthProviders {...props} />
|
<SocialAuthProviders {...props} />
|
||||||
</IntlProvider>,
|
</IntlProvider>,
|
||||||
).toJSON();
|
)).toJSON();
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
@@ -66,11 +85,11 @@ describe('SocialAuthProviders', () => {
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(reduxWrapper(
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<SocialAuthProviders {...props} />
|
<SocialAuthProviders {...props} />
|
||||||
</IntlProvider>,
|
</IntlProvider>,
|
||||||
).toJSON();
|
)).toJSON();
|
||||||
|
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,14 +5,13 @@ import React from 'react';
|
|||||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import { UnAuthOnlyRoute } from '..';
|
|
||||||
import { REGISTER_PAGE } from '../../data/constants';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MemoryRouter, Route, BrowserRouter as Router, Routes,
|
MemoryRouter, Route, BrowserRouter as Router, Routes,
|
||||||
} from 'react-router-dom';
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import { UnAuthOnlyRoute } from '..';
|
||||||
|
import { REGISTER_PAGE } from '../../data/constants';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||||
getAuthenticatedUser: jest.fn(),
|
getAuthenticatedUser: jest.fn(),
|
||||||
fetchAuthenticatedUser: jest.fn(),
|
fetchAuthenticatedUser: jest.fn(),
|
||||||
|
|||||||
@@ -66,14 +66,14 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
|
|||||||
data-prefix="fab"
|
data-prefix="fab"
|
||||||
focusable="false"
|
focusable="false"
|
||||||
role="img"
|
role="img"
|
||||||
style={Object {}}
|
style={{}}
|
||||||
viewBox="0 0 488 512"
|
viewBox="0 0 488 512"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
|
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
style={Object {}}
|
style={{}}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,7 +93,7 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`SocialAuthProviders should match social auth provider with iconImage snapshot 1`] = `
|
exports[`SocialAuthProviders should match social auth provider with iconImage snapshot 1`] = `
|
||||||
Array [
|
[
|
||||||
<button
|
<button
|
||||||
className="btn-social btn-oa2-apple-id mr-3"
|
className="btn-social btn-oa2-apple-id mr-3"
|
||||||
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"
|
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
|
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
|
||||||
Array [
|
[
|
||||||
<div
|
<div
|
||||||
className="fade alert-content alert-success mt-n2 mb-5 alert show"
|
className="fade alert-content alert-success mt-n2 mb-5 alert show"
|
||||||
id="tpa-alert"
|
id="tpa-alert"
|
||||||
|
|||||||
@@ -5,23 +5,23 @@ exports[`Zendesk Help should match login page third party auth alert message sna
|
|||||||
cookies={true}
|
cookies={true}
|
||||||
defer={true}
|
defer={true}
|
||||||
webWidget={
|
webWidget={
|
||||||
Object {
|
{
|
||||||
"answerBot": Object {
|
"answerBot": {
|
||||||
"avatar": Object {
|
"avatar": {
|
||||||
"name": Object {
|
"name": {
|
||||||
"*": "edX Support",
|
"*": "edX Support",
|
||||||
},
|
},
|
||||||
"url": undefined,
|
"url": undefined,
|
||||||
},
|
},
|
||||||
"contactOnlyAfterQuery": true,
|
"contactOnlyAfterQuery": true,
|
||||||
"suppress": false,
|
"suppress": false,
|
||||||
"title": Object {
|
"title": {
|
||||||
"*": "edX Support",
|
"*": "edX Support",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"chat": Object {
|
"chat": {
|
||||||
"departments": Object {
|
"departments": {
|
||||||
"enabled": Array [
|
"enabled": [
|
||||||
"account settings",
|
"account settings",
|
||||||
"billing and payments",
|
"billing and payments",
|
||||||
"certificates",
|
"certificates",
|
||||||
@@ -33,17 +33,17 @@ exports[`Zendesk Help should match login page third party auth alert message sna
|
|||||||
},
|
},
|
||||||
"suppress": false,
|
"suppress": false,
|
||||||
},
|
},
|
||||||
"contactForm": Object {
|
"contactForm": {
|
||||||
"attachments": true,
|
"attachments": true,
|
||||||
"selectTicketForm": Object {
|
"selectTicketForm": {
|
||||||
"*": "Please choose your request type:",
|
"*": "Please choose your request type:",
|
||||||
},
|
},
|
||||||
"ticketForms": Array [
|
"ticketForms": [
|
||||||
Object {
|
{
|
||||||
"fields": Array [
|
"fields": [
|
||||||
Object {
|
{
|
||||||
"id": "description",
|
"id": "description",
|
||||||
"prefill": Object {
|
"prefill": {
|
||||||
"*": "",
|
"*": "",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -53,10 +53,10 @@ exports[`Zendesk Help should match login page third party auth alert message sna
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"contactOptions": Object {
|
"contactOptions": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
},
|
},
|
||||||
"helpCenter": Object {
|
"helpCenter": {
|
||||||
"originalArticleButton": true,
|
"originalArticleButton": true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ const configuration = {
|
|||||||
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
|
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
|
||||||
// Features
|
// Features
|
||||||
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
|
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
|
||||||
|
ENABLE_AUTO_GENERATED_USERNAME: process.env.ENABLE_AUTO_GENERATED_USERNAME || false,
|
||||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
|
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
|
||||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
|
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
|
||||||
ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,
|
ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,
|
||||||
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
|
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
|
||||||
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
|
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
|
||||||
SHOW_REGISTRATION_LINKS: process.env.SHOW_REGISTRATION_LINKS !== 'false',
|
SHOW_REGISTRATION_LINKS: process.env.SHOW_REGISTRATION_LINKS !== 'false',
|
||||||
|
ENABLE_HARD_CODE_OPTIONAL_FIELDS: process.env.ENABLE_HARD_CODE_OPTIONAL_FIELDS || false,
|
||||||
ENABLE_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false,
|
ENABLE_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false,
|
||||||
// Links
|
// Links
|
||||||
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
|
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
|
||||||
@@ -34,6 +36,7 @@ const configuration = {
|
|||||||
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
|
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
|
||||||
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
|
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
|
||||||
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
|
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
|
||||||
|
AUTO_GENERATED_USERNAME_EXPERIMENT_ID: process.env.AUTO_GENERATED_USERNAME_EXPERIMENT_ID || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default configuration;
|
export default configuration;
|
||||||
|
|||||||
@@ -37,3 +37,4 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
|
|||||||
// things like auto-enrollment upon login and registration.
|
// things like auto-enrollment upon login and registration.
|
||||||
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta'];
|
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta'];
|
||||||
export const REDIRECT = 'redirect';
|
export const REDIRECT = 'redirect';
|
||||||
|
export const APP_NAME = 'authn_mfe';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { combineReducers } from 'redux';
|
import { combineReducers } from 'redux';
|
||||||
|
|
||||||
|
import { reducer as cohesionReducer, storeName as cohesionStoreName } from '../cohesion/data/reducers';
|
||||||
import {
|
import {
|
||||||
reducer as commonComponentsReducer,
|
reducer as commonComponentsReducer,
|
||||||
storeName as commonComponentsStoreName,
|
storeName as commonComponentsStoreName,
|
||||||
@@ -31,6 +32,7 @@ const createRootReducer = () => combineReducers({
|
|||||||
[commonComponentsStoreName]: commonComponentsReducer,
|
[commonComponentsStoreName]: commonComponentsReducer,
|
||||||
[forgotPasswordStoreName]: forgotPasswordReducer,
|
[forgotPasswordStoreName]: forgotPasswordReducer,
|
||||||
[resetPasswordStoreName]: resetPasswordReducer,
|
[resetPasswordStoreName]: resetPasswordReducer,
|
||||||
|
[cohesionStoreName]: cohesionReducer,
|
||||||
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
|
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
|
||||||
});
|
});
|
||||||
export default createRootReducer;
|
export default createRootReducer;
|
||||||
|
|||||||
37
src/data/segment/utils.js
Normal file
37
src/data/segment/utils.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/* eslint-disable import/prefer-default-export */
|
||||||
|
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||||
|
|
||||||
|
import { APP_NAME } from '../constants';
|
||||||
|
|
||||||
|
export const LINK_TIMEOUT = 300;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an event tracker function that sends a tracking event with the given name and options.
|
||||||
|
*
|
||||||
|
* @param {string} name - The name of the event to be tracked.
|
||||||
|
* @param {object} [options={}] - Additional options to be included with the event.
|
||||||
|
* @returns {function} - A function that, when called, sends the tracking event.
|
||||||
|
*/
|
||||||
|
export const createEventTracker = (name, options = {}) => () => sendTrackEvent(
|
||||||
|
name,
|
||||||
|
{ ...options, app_name: APP_NAME },
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an event tracker function that sends a tracking event with the given name and options.
|
||||||
|
*
|
||||||
|
* @param {string} name - The name of the event to be tracked.
|
||||||
|
* @param {object} [options={}] - Additional options to be included with the event.
|
||||||
|
* @returns {function} - A function that, when called, sends the tracking event.
|
||||||
|
*/
|
||||||
|
export const createPageEventTracker = (name, options = null) => () => sendPageEvent(
|
||||||
|
name,
|
||||||
|
options,
|
||||||
|
{ app_name: APP_NAME },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const createLinkTracker = (tracker, href) => (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
tracker();
|
||||||
|
return setTimeout(() => { window.location.href = href; }, LINK_TIMEOUT);
|
||||||
|
};
|
||||||
@@ -11,3 +11,11 @@ export default function setCookie(cookieName, cookieValue, cookieExpiry) {
|
|||||||
cookies.set(cookieName, cookieValue, options);
|
cookies.set(cookieName, cookieValue, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeCookie(cookieName) {
|
||||||
|
if (cookieName) {
|
||||||
|
const cookies = new Cookies();
|
||||||
|
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
|
||||||
|
cookies.remove(cookieName, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -81,3 +81,9 @@ export const isHostAvailableInQueryParams = () => {
|
|||||||
const queryParams = getAllPossibleQueryParams();
|
const queryParams = getAllPossibleQueryParams();
|
||||||
return 'host' in queryParams;
|
return 'host' in queryParams;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const redirectWithDelay = (redirectUrl) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = redirectUrl;
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,4 +8,4 @@ export {
|
|||||||
windowScrollTo,
|
windowScrollTo,
|
||||||
} from './dataUtils';
|
} from './dataUtils';
|
||||||
export { default as AsyncActionType } from './reduxUtils';
|
export { default as AsyncActionType } from './reduxUtils';
|
||||||
export { default as setCookie } from './cookies';
|
export { default as setCookie, removeCookie } from './cookies';
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
@@ -25,6 +24,10 @@ import BaseContainer from '../base-container';
|
|||||||
import { FormGroup } from '../common-components';
|
import { FormGroup } from '../common-components';
|
||||||
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
||||||
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||||
|
import {
|
||||||
|
trackForgotPasswordPageEvent,
|
||||||
|
trackForgotPasswordPageViewed,
|
||||||
|
} from '../tracking/trackers/forgotpassword';
|
||||||
|
|
||||||
const ForgotPasswordPage = (props) => {
|
const ForgotPasswordPage = (props) => {
|
||||||
const platformName = getConfig().SITE_NAME;
|
const platformName = getConfig().SITE_NAME;
|
||||||
@@ -41,8 +44,8 @@ const ForgotPasswordPage = (props) => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sendPageEvent('login_and_registration', 'reset');
|
trackForgotPasswordPageEvent();
|
||||||
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
|
trackForgotPasswordPageViewed();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ErrorPage } from '@edx/frontend-platform/react';
|
|||||||
import configuration from './config';
|
import configuration from './config';
|
||||||
import messages from './i18n';
|
import messages from './i18n';
|
||||||
import MainApp from './MainApp';
|
import MainApp from './MainApp';
|
||||||
|
import './cohesion/cohesion.scss';
|
||||||
|
|
||||||
subscribe(APP_READY, () => {
|
subscribe(APP_READY, () => {
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
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 { CheckCircle, Error } from '@openedx/paragon/icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
@@ -36,19 +36,17 @@ const AccountActivationMessage = ({ messageType }) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
|
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
|
||||||
const supportLink = (
|
const supportEmail = (
|
||||||
<Alert.Link href={getConfig().ACTIVATION_EMAIL_SUPPORT_LINK}>
|
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
|
||||||
{formatMessage(messages['account.activation.support.link'])}
|
|
||||||
</Alert.Link>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
heading = formatMessage(messages[`account.${activationOrConfirmation}.error.message.title`]);
|
heading = formatMessage(messages[`account.${activationOrConfirmation}.error.message.title`]);
|
||||||
activationMessage = (
|
activationMessage = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="account.activation.error.message"
|
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"
|
description="Account activation error message"
|
||||||
values={{ supportLink }}
|
values={{ supportEmail }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
@@ -10,19 +11,23 @@ import PropTypes from 'prop-types';
|
|||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import trackCohesionEvent from '../cohesion/trackers';
|
||||||
import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants';
|
import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants';
|
||||||
import { updatePathWithQueryParams } from '../data/utils';
|
import { updatePathWithQueryParams } from '../data/utils';
|
||||||
|
import { redirectWithDelay } from '../data/utils/dataUtils';
|
||||||
import useMobileResponsive from '../data/utils/useMobileResponsive';
|
import useMobileResponsive from '../data/utils/useMobileResponsive';
|
||||||
|
|
||||||
const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
|
const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
|
||||||
const isMobileView = useMobileResponsive();
|
const isMobileView = useMobileResponsive();
|
||||||
const [redirectToResetPasswordPage, setRedirectToResetPasswordPage] = useState(false);
|
const [redirectToResetPasswordPage, setRedirectToResetPasswordPage] = useState(false);
|
||||||
|
const cohesionEventData = useSelector(state => state.cohesion.eventData);
|
||||||
const handlers = {
|
const handlers = {
|
||||||
handleToggleOff: () => {
|
handleToggleOff: () => {
|
||||||
if (variant === 'block') {
|
if (variant === 'block') {
|
||||||
setRedirectToResetPasswordPage(true);
|
setRedirectToResetPasswordPage(true);
|
||||||
} else {
|
} 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,8 +1,7 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
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 { getConfig } from '@edx/frontend-platform';
|
||||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
|
||||||
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
Form, StatefulButton,
|
Form, StatefulButton,
|
||||||
@@ -21,6 +20,10 @@ import {
|
|||||||
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
|
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
|
||||||
import LoginFailureMessage from './LoginFailure';
|
import LoginFailureMessage from './LoginFailure';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
|
import {
|
||||||
|
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
|
||||||
|
} from '../cohesion/constants';
|
||||||
|
import { setCohesionEventStates } from '../cohesion/data/actions';
|
||||||
import {
|
import {
|
||||||
FormGroup,
|
FormGroup,
|
||||||
InstitutionLogistration,
|
InstitutionLogistration,
|
||||||
@@ -32,9 +35,7 @@ import { getThirdPartyAuthContext } from '../common-components/data/actions';
|
|||||||
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
|
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
|
||||||
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
||||||
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
|
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
|
||||||
import {
|
import { DEFAULT_STATE, PENDING_STATE, RESET_PAGE } from '../data/constants';
|
||||||
DEFAULT_STATE, PENDING_STATE, RESET_PAGE,
|
|
||||||
} from '../data/constants';
|
|
||||||
import {
|
import {
|
||||||
getActivationStatus,
|
getActivationStatus,
|
||||||
getAllPossibleQueryParams,
|
getAllPossibleQueryParams,
|
||||||
@@ -42,7 +43,11 @@ import {
|
|||||||
getTpaProvider,
|
getTpaProvider,
|
||||||
updatePathWithQueryParams,
|
updatePathWithQueryParams,
|
||||||
} from '../data/utils';
|
} from '../data/utils';
|
||||||
|
import { removeCookie } from '../data/utils/cookies';
|
||||||
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
|
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
|
||||||
|
import {
|
||||||
|
trackForgotPasswordLinkClick, trackLoginPageViewed, trackLoginSuccess,
|
||||||
|
} from '../tracking/trackers/login';
|
||||||
|
|
||||||
const LoginPage = (props) => {
|
const LoginPage = (props) => {
|
||||||
const {
|
const {
|
||||||
@@ -69,6 +74,7 @@ const LoginPage = (props) => {
|
|||||||
getTPADataFromBackend,
|
getTPADataFromBackend,
|
||||||
} = props;
|
} = props;
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
const dispatch = useDispatch();
|
||||||
const activationMsgType = getActivationStatus();
|
const activationMsgType = getActivationStatus();
|
||||||
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||||
|
|
||||||
@@ -78,9 +84,17 @@ const LoginPage = (props) => {
|
|||||||
const tpaHint = getTpaHint();
|
const tpaHint = getTpaHint();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sendPageEvent('login_and_registration', 'login');
|
trackLoginPageViewed();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loginResult.success) {
|
||||||
|
trackLoginSuccess();
|
||||||
|
// Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
|
||||||
|
removeCookie('ssoPipelineRedirectionDone');
|
||||||
|
}
|
||||||
|
}, [loginResult]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const payload = { ...queryParams };
|
const payload = { ...queryParams };
|
||||||
if (tpaHint) {
|
if (tpaHint) {
|
||||||
@@ -140,6 +154,15 @@ const LoginPage = (props) => {
|
|||||||
|
|
||||||
const handleSubmit = (event) => {
|
const handleSubmit = (event) => {
|
||||||
event.preventDefault();
|
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) {
|
if (showResetPasswordSuccessBanner) {
|
||||||
props.dismissPasswordResetBanner();
|
props.dismissPasswordResetBanner();
|
||||||
}
|
}
|
||||||
@@ -170,9 +193,6 @@ const LoginPage = (props) => {
|
|||||||
const { name } = event.target;
|
const { name } = event.target;
|
||||||
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||||
};
|
};
|
||||||
const trackForgotPasswordLinkClick = () => {
|
|
||||||
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
|
|
||||||
};
|
|
||||||
|
|
||||||
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
|
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
|
||||||
|
|
||||||
@@ -208,6 +228,7 @@ const LoginPage = (props) => {
|
|||||||
success={loginResult.success}
|
success={loginResult.success}
|
||||||
redirectUrl={loginResult.redirectUrl}
|
redirectUrl={loginResult.redirectUrl}
|
||||||
finishAuthUrl={finishAuthUrl}
|
finishAuthUrl={finishAuthUrl}
|
||||||
|
currentProvider={currentProvider}
|
||||||
/>
|
/>
|
||||||
<div className="mw-xs mt-3 mb-2">
|
<div className="mw-xs mt-3 mb-2">
|
||||||
<LoginFailureMessage
|
<LoginFailureMessage
|
||||||
|
|||||||
@@ -95,11 +95,6 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Your account could not be activated',
|
defaultMessage: 'Your account could not be activated',
|
||||||
description: 'Account Activation error message title',
|
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
|
// Email Confirmation Strings
|
||||||
'account.confirmation.success.message.title': {
|
'account.confirmation.success.message.title': {
|
||||||
id: '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'
|
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(
|
expect(screen.getByText(
|
||||||
'',
|
'',
|
||||||
@@ -121,7 +121,7 @@ describe('EmailConfirmationMessage', () => {
|
|||||||
</IntlProvider>,
|
</IntlProvider>,
|
||||||
);
|
);
|
||||||
const expectedMessage = 'Your email could not be confirmed'
|
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(
|
expect(screen.getByText(
|
||||||
'',
|
'',
|
||||||
{ selector: '#account-activation-message' },
|
{ selector: '#account-activation-message' },
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
fireEvent, render, screen,
|
fireEvent, render, screen, waitFor,
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
|
|
||||||
|
import mockTagular from '../../cohesion/utils';
|
||||||
import { RESET_PAGE } from '../../data/constants';
|
import { RESET_PAGE } from '../../data/constants';
|
||||||
import ChangePasswordPrompt from '../ChangePasswordPrompt';
|
import ChangePasswordPrompt from '../ChangePasswordPrompt';
|
||||||
|
|
||||||
const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt);
|
const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt);
|
||||||
const mockedNavigator = jest.fn();
|
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.mock('react-router-dom', () => ({
|
||||||
...(jest.requireActual('react-router-dom')),
|
...(jest.requireActual('react-router-dom')),
|
||||||
@@ -21,8 +33,14 @@ jest.mock('react-router-dom', () => ({
|
|||||||
|
|
||||||
describe('ChangePasswordPromptTests', () => {
|
describe('ChangePasswordPromptTests', () => {
|
||||||
let props = {};
|
let props = {};
|
||||||
|
let store = {};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
cohesion: { eventData: {} },
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
store = mockStore(initialState);
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
writable: true,
|
writable: true,
|
||||||
value: jest.fn().mockImplementation(query => ({
|
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');
|
const dashboardUrl = getConfig().BASE_URL.concat('/dashboard');
|
||||||
props = {
|
props = {
|
||||||
variant: 'nudge',
|
variant: 'nudge',
|
||||||
redirectUrl: dashboardUrl,
|
redirectUrl: dashboardUrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
cohesion: {
|
||||||
|
eventData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
delete window.location;
|
delete window.location;
|
||||||
window.location = { href: getConfig().BASE_URL };
|
window.location = { href: getConfig().BASE_URL };
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<MemoryRouter>
|
<Provider store={store}>
|
||||||
<IntlChangePasswordPrompt {...props} />
|
<MemoryRouter>
|
||||||
</MemoryRouter>
|
<IntlChangePasswordPrompt {...props} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</Provider>
|
||||||
</IntlProvider>,
|
</IntlProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('Close'));
|
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 () => {
|
it('[block modal] should redirect to reset password page when user clicks outside modal', async () => {
|
||||||
props = {
|
props = {
|
||||||
variant: 'block',
|
variant: 'block',
|
||||||
};
|
};
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
cohesion: {
|
||||||
|
eventData,
|
||||||
|
},
|
||||||
|
});
|
||||||
render(
|
render(
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<MemoryRouter>
|
<Provider store={store}>
|
||||||
<IntlChangePasswordPrompt {...props} />
|
<MemoryRouter>
|
||||||
</MemoryRouter>
|
<IntlChangePasswordPrompt {...props} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</Provider>
|
||||||
</IntlProvider>,
|
</IntlProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
|
||||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
render, screen,
|
render, screen,
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import configureStore from 'redux-mock-store';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ACCOUNT_LOCKED_OUT,
|
ACCOUNT_LOCKED_OUT,
|
||||||
@@ -25,13 +27,27 @@ import LoginFailureMessage from '../LoginFailure';
|
|||||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||||
getAuthService: jest.fn(),
|
getAuthService: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
const mockStore = configureStore();
|
||||||
|
|
||||||
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
|
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
|
||||||
|
|
||||||
|
const eventData = {
|
||||||
|
pageType: 'test-page',
|
||||||
|
elementType: 'test-element-type',
|
||||||
|
webElementText: 'test-element-text',
|
||||||
|
webElementName: 'test-element-name',
|
||||||
|
};
|
||||||
|
|
||||||
describe('LoginFailureMessage', () => {
|
describe('LoginFailureMessage', () => {
|
||||||
let props = {};
|
let props = {};
|
||||||
|
let store = {};
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
cohesion: { eventData: {} },
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
|
store = mockStore(initialState);
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
writable: true,
|
writable: true,
|
||||||
value: jest.fn().mockImplementation(query => ({
|
value: jest.fn().mockImplementation(query => ({
|
||||||
@@ -298,11 +314,19 @@ describe('LoginFailureMessage', () => {
|
|||||||
errorCount: 0,
|
errorCount: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
cohesion: {
|
||||||
|
eventData,
|
||||||
|
},
|
||||||
|
});
|
||||||
render(
|
render(
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<MemoryRouter>
|
<Provider store={store}>
|
||||||
<IntlLoginFailureMessage {...props} />
|
<MemoryRouter>
|
||||||
</MemoryRouter>
|
<IntlLoginFailureMessage {...props} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</Provider>
|
||||||
</IntlProvider>,
|
</IntlProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -323,12 +347,20 @@ describe('LoginFailureMessage', () => {
|
|||||||
errorCode: REQUIRE_PASSWORD_CHANGE,
|
errorCode: REQUIRE_PASSWORD_CHANGE,
|
||||||
errorCount: 0,
|
errorCount: 0,
|
||||||
};
|
};
|
||||||
|
store = mockStore({
|
||||||
|
...initialState,
|
||||||
|
cohesion: {
|
||||||
|
eventData,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
render(
|
render(
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<MemoryRouter>
|
<Provider store={store}>
|
||||||
<IntlLoginFailureMessage {...props} />
|
<MemoryRouter>
|
||||||
</MemoryRouter>
|
<IntlLoginFailureMessage {...props} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</Provider>
|
||||||
</IntlProvider>,
|
</IntlProvider>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import { act } from 'react-dom/test-utils';
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
|
|
||||||
import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE } from '../../data/constants';
|
import mockTagular from '../../cohesion/utils';
|
||||||
|
import {
|
||||||
|
APP_NAME, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE,
|
||||||
|
} from '../../data/constants';
|
||||||
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions';
|
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions';
|
||||||
import { INTERNAL_SERVER_ERROR } from '../data/constants';
|
import { INTERNAL_SERVER_ERROR } from '../data/constants';
|
||||||
import LoginPage from '../LoginPage';
|
import LoginPage from '../LoginPage';
|
||||||
@@ -23,6 +26,7 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
|
|||||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||||
getAuthService: jest.fn(),
|
getAuthService: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
mockTagular();
|
||||||
|
|
||||||
const IntlLoginPage = injectIntl(LoginPage);
|
const IntlLoginPage = injectIntl(LoginPage);
|
||||||
const mockStore = configureStore();
|
const mockStore = configureStore();
|
||||||
@@ -56,6 +60,7 @@ describe('LoginPage', () => {
|
|||||||
register: {
|
register: {
|
||||||
validationApiRateLimited: false,
|
validationApiRateLimited: false,
|
||||||
},
|
},
|
||||||
|
cohesion: { eventData: {} },
|
||||||
};
|
};
|
||||||
|
|
||||||
const secondaryProviders = {
|
const secondaryProviders = {
|
||||||
@@ -510,7 +515,7 @@ describe('LoginPage', () => {
|
|||||||
|
|
||||||
// ******** test redirection ********
|
// ******** 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/';
|
const dashboardURL = 'https://test.com/testing-dashboard/';
|
||||||
store = mockStore({
|
store = mockStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
@@ -526,10 +531,12 @@ describe('LoginPage', () => {
|
|||||||
delete window.location;
|
delete window.location;
|
||||||
window.location = { href: getConfig().BASE_URL };
|
window.location = { href: getConfig().BASE_URL };
|
||||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
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/';
|
const authCompleteUrl = '/auth/complete/google-oauth2/';
|
||||||
store = mockStore({
|
store = mockStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
@@ -553,10 +560,12 @@ describe('LoginPage', () => {
|
|||||||
window.location = { href: getConfig().BASE_URL };
|
window.location = { href: getConfig().BASE_URL };
|
||||||
|
|
||||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
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({
|
store = mockStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
commonComponents: {
|
commonComponents: {
|
||||||
@@ -577,10 +586,12 @@ describe('LoginPage', () => {
|
|||||||
'',
|
'',
|
||||||
{ selector: '#oa2-apple-id' },
|
{ 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/';
|
const finishAuthUrl = '/auth/complete/google-oauth2/';
|
||||||
store = mockStore({
|
store = mockStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
@@ -601,7 +612,9 @@ describe('LoginPage', () => {
|
|||||||
window.location = { href: getConfig().BASE_URL };
|
window.location = { href: getConfig().BASE_URL };
|
||||||
|
|
||||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
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 ********
|
// ******** test hinted third party auth ********
|
||||||
@@ -751,7 +764,7 @@ describe('LoginPage', () => {
|
|||||||
|
|
||||||
it('should send page event when login page is rendered', () => {
|
it('should send page event when login page is rendered', () => {
|
||||||
render(reduxWrapper(<IntlLoginPage {...props} />));
|
render(reduxWrapper(<IntlLoginPage {...props} />));
|
||||||
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login');
|
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login', { app_name: APP_NAME });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('tests that form is in invalid state when it is submitted', () => {
|
it('tests that form is in invalid state when it is submitted', () => {
|
||||||
@@ -784,7 +797,7 @@ describe('LoginPage', () => {
|
|||||||
{ selector: '#forgot-password' },
|
{ selector: '#forgot-password' },
|
||||||
));
|
));
|
||||||
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
|
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should backup the login form state when shouldBackupState is true', () => {
|
it('should backup the login form state when shouldBackupState is true', () => {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
tpaProvidersSelector,
|
tpaProvidersSelector,
|
||||||
} from '../common-components/data/selectors';
|
} from '../common-components/data/selectors';
|
||||||
import messages from '../common-components/messages';
|
import messages from '../common-components/messages';
|
||||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
import { APP_NAME, LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||||
import {
|
import {
|
||||||
getTpaHint, getTpaProvider, updatePathWithQueryParams,
|
getTpaHint, getTpaProvider, updatePathWithQueryParams,
|
||||||
} from '../data/utils';
|
} from '../data/utils';
|
||||||
@@ -56,11 +56,11 @@ const Logistration = (props) => {
|
|||||||
}, [navigate, disablePublicAccountCreation]);
|
}, [navigate, disablePublicAccountCreation]);
|
||||||
|
|
||||||
const handleInstitutionLogin = (e) => {
|
const handleInstitutionLogin = (e) => {
|
||||||
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
|
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
|
||||||
if (typeof e === 'string') {
|
if (typeof e === 'string') {
|
||||||
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register');
|
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register', { app_name: APP_NAME });
|
||||||
} else {
|
} else {
|
||||||
sendPageEvent('login_and_registration', e.target.dataset.eventName);
|
sendPageEvent('login_and_registration', e.target.dataset.eventName, { app_name: APP_NAME });
|
||||||
}
|
}
|
||||||
|
|
||||||
setInstitutionLogin(!institutionLogin);
|
setInstitutionLogin(!institutionLogin);
|
||||||
@@ -70,7 +70,7 @@ const Logistration = (props) => {
|
|||||||
if (tabKey === currentTab) {
|
if (tabKey === currentTab) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
|
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement', app_name: APP_NAME });
|
||||||
props.clearThirdPartyAuthContextErrorMessage();
|
props.clearThirdPartyAuthContextErrorMessage();
|
||||||
if (tabKey === LOGIN_PAGE) {
|
if (tabKey === LOGIN_PAGE) {
|
||||||
props.backupRegistrationForm();
|
props.backupRegistrationForm();
|
||||||
|
|||||||
@@ -11,16 +11,21 @@ import configureStore from 'redux-mock-store';
|
|||||||
import Logistration from './Logistration';
|
import Logistration from './Logistration';
|
||||||
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
|
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
|
||||||
import {
|
import {
|
||||||
|
APP_NAME,
|
||||||
COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE,
|
COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE,
|
||||||
} from '../data/constants';
|
} from '../data/constants';
|
||||||
import { backupLoginForm } from '../login/data/actions';
|
import { backupLoginForm } from '../login/data/actions';
|
||||||
import { backupRegistrationForm } from '../register/data/actions';
|
import { backupRegistrationForm } from '../register/data/actions';
|
||||||
|
import { NOT_INITIALIZED } from '../register/data/optimizelyExperiment/helper';
|
||||||
|
import useAutoGeneratedUsernameExperimentVariation
|
||||||
|
from '../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||||
sendPageEvent: jest.fn(),
|
sendPageEvent: jest.fn(),
|
||||||
sendTrackEvent: jest.fn(),
|
sendTrackEvent: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('@edx/frontend-platform/auth');
|
jest.mock('@edx/frontend-platform/auth');
|
||||||
|
jest.mock('../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||||
|
|
||||||
const mockStore = configureStore();
|
const mockStore = configureStore();
|
||||||
const IntlLogistration = injectIntl(Logistration);
|
const IntlLogistration = injectIntl(Logistration);
|
||||||
@@ -64,6 +69,7 @@ describe('Logistration', () => {
|
|||||||
usernameSuggestions: [],
|
usernameSuggestions: [],
|
||||||
validationApiRateLimited: false,
|
validationApiRateLimited: false,
|
||||||
},
|
},
|
||||||
|
cohesion: { eventData: {} },
|
||||||
commonComponents: {
|
commonComponents: {
|
||||||
thirdPartyAuthContext: {
|
thirdPartyAuthContext: {
|
||||||
providers: [],
|
providers: [],
|
||||||
@@ -84,6 +90,7 @@ describe('Logistration', () => {
|
|||||||
})),
|
})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
|
||||||
configure({
|
configure({
|
||||||
loggingService: { logError: jest.fn() },
|
loggingService: { logError: jest.fn() },
|
||||||
config: {
|
config: {
|
||||||
@@ -224,8 +231,8 @@ describe('Logistration', () => {
|
|||||||
render(reduxWrapper(<IntlLogistration {...props} />));
|
render(reduxWrapper(<IntlLogistration {...props} />));
|
||||||
fireEvent.click(screen.getByText('Institution/campus credentials'));
|
fireEvent.click(screen.getByText('Institution/campus credentials'));
|
||||||
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
|
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
|
||||||
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
|
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login', { app_name: APP_NAME });
|
||||||
|
|
||||||
mergeConfig({
|
mergeConfig({
|
||||||
DISABLE_ENTERPRISE_LOGIN: '',
|
DISABLE_ENTERPRISE_LOGIN: '',
|
||||||
|
|||||||
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/)
|
||||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||||
import { identifyAuthenticatedUser, sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
import { identifyAuthenticatedUser } from '@edx/frontend-platform/analytics';
|
||||||
import {
|
import {
|
||||||
AxiosJwtAuthService,
|
AxiosJwtAuthService,
|
||||||
configure as configureAuth,
|
configure as configureAuth,
|
||||||
@@ -39,6 +39,13 @@ import {
|
|||||||
import isOneTrustFunctionalCookieEnabled from '../data/oneTrust';
|
import isOneTrustFunctionalCookieEnabled from '../data/oneTrust';
|
||||||
import { getAllPossibleQueryParams, isHostAvailableInQueryParams } from '../data/utils';
|
import { getAllPossibleQueryParams, isHostAvailableInQueryParams } from '../data/utils';
|
||||||
import { FormFieldRenderer } from '../field-renderer';
|
import { FormFieldRenderer } from '../field-renderer';
|
||||||
|
import {
|
||||||
|
trackDisablePostRegistrationRecommendations,
|
||||||
|
trackProgressiveProfilingPageViewed,
|
||||||
|
trackProgressiveProfilingSkipLinkClick,
|
||||||
|
trackProgressiveProfilingSubmitClick,
|
||||||
|
trackProgressiveProfilingSupportLinkCLick,
|
||||||
|
} from '../tracking/trackers/progressive-profiling';
|
||||||
|
|
||||||
const ProgressiveProfiling = (props) => {
|
const ProgressiveProfiling = (props) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@@ -98,14 +105,13 @@ const ProgressiveProfiling = (props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authenticatedUser?.userId) {
|
if (authenticatedUser?.userId) {
|
||||||
identifyAuthenticatedUser(authenticatedUser.userId);
|
identifyAuthenticatedUser(authenticatedUser.userId);
|
||||||
sendPageEvent('login_and_registration', 'welcome');
|
trackProgressiveProfilingPageViewed();
|
||||||
}
|
}
|
||||||
}, [authenticatedUser]);
|
}, [authenticatedUser]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enablePostRegistrationRecommendations) {
|
if (!enablePostRegistrationRecommendations) {
|
||||||
sendTrackEvent(
|
trackDisablePostRegistrationRecommendations(
|
||||||
'edx.bi.user.recommendations.not.enabled',
|
|
||||||
{ functionalCookiesConsent, page: 'authn_recommendations' },
|
{ functionalCookiesConsent, page: 'authn_recommendations' },
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -149,29 +155,23 @@ const ProgressiveProfiling = (props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
|
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
|
||||||
|
const eventProperties = {
|
||||||
sendTrackEvent(
|
isGenderSelected: !!values.gender,
|
||||||
'edx.bi.welcome.page.submit.clicked',
|
isYearOfBirthSelected: !!values.year_of_birth,
|
||||||
{
|
isLevelOfEducationSelected: !!values.level_of_education,
|
||||||
isGenderSelected: !!values.gender,
|
isWorkExperienceSelected: !!values.work_experience,
|
||||||
isYearOfBirthSelected: !!values.year_of_birth,
|
host: queryParams?.host || '',
|
||||||
isLevelOfEducationSelected: !!values.level_of_education,
|
};
|
||||||
isWorkExperienceSelected: !!values.work_experience,
|
trackProgressiveProfilingSubmitClick(eventProperties);
|
||||||
host: queryParams?.host || '',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSkip = (e) => {
|
const handleSkip = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.history.replaceState(location.state, null, '');
|
window.history.replaceState(location.state, null, '');
|
||||||
setShowModal(true);
|
setShowModal(true);
|
||||||
sendTrackEvent(
|
trackProgressiveProfilingSkipLinkClick({
|
||||||
'edx.bi.welcome.page.skip.link.clicked',
|
host: queryParams?.host || '',
|
||||||
{
|
});
|
||||||
host: queryParams?.host || '',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeHandler = (e) => {
|
const onChangeHandler = (e) => {
|
||||||
@@ -242,7 +242,7 @@ const ProgressiveProfiling = (props) => {
|
|||||||
destination={getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK}
|
destination={getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
showLaunchIcon={false}
|
showLaunchIcon={false}
|
||||||
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
|
onClick={() => (trackProgressiveProfilingSupportLinkCLick())}
|
||||||
>
|
>
|
||||||
{formatMessage(messages['optional.fields.information.link'])}
|
{formatMessage(messages['optional.fields.information.link'])}
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platfor
|
|||||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||||
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||||
import {
|
import {
|
||||||
fireEvent, render, screen,
|
fireEvent, render, screen, waitFor,
|
||||||
} from '@testing-library/react';
|
} from '@testing-library/react';
|
||||||
import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
|
import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
|
|
||||||
|
import mockTagular from '../../cohesion/utils';
|
||||||
import {
|
import {
|
||||||
|
APP_NAME,
|
||||||
AUTHN_PROGRESSIVE_PROFILING,
|
AUTHN_PROGRESSIVE_PROFILING,
|
||||||
COMPLETE_STATE, DEFAULT_REDIRECT_URL,
|
COMPLETE_STATE, DEFAULT_REDIRECT_URL,
|
||||||
EMBEDDED,
|
EMBEDDED,
|
||||||
@@ -24,6 +26,7 @@ import ProgressiveProfiling from '../ProgressiveProfiling';
|
|||||||
|
|
||||||
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
|
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
|
||||||
const mockStore = configureStore();
|
const mockStore = configureStore();
|
||||||
|
mockTagular();
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||||
sendPageEvent: jest.fn(),
|
sendPageEvent: jest.fn(),
|
||||||
@@ -54,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', () => {
|
describe('ProgressiveProfilingTests', () => {
|
||||||
let store = {};
|
let store = {};
|
||||||
|
|
||||||
@@ -143,8 +153,9 @@ describe('ProgressiveProfilingTests', () => {
|
|||||||
const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container');
|
const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container');
|
||||||
|
|
||||||
expect(modalContentContainer).toBeTruthy();
|
expect(modalContentContainer).toBeTruthy();
|
||||||
|
const payload = { host: '', app_name: APP_NAME };
|
||||||
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host: '' });
|
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ******** test event functionality ********
|
// ******** test event functionality ********
|
||||||
@@ -165,7 +176,7 @@ describe('ProgressiveProfilingTests', () => {
|
|||||||
const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i });
|
const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i });
|
||||||
fireEvent.click(supportLink);
|
fireEvent.click(supportLink);
|
||||||
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked');
|
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked', { app_name: APP_NAME });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set empty host property value for non-embedded experience', () => {
|
it('should set empty host property value for non-embedded experience', () => {
|
||||||
@@ -175,6 +186,7 @@ describe('ProgressiveProfilingTests', () => {
|
|||||||
isLevelOfEducationSelected: false,
|
isLevelOfEducationSelected: false,
|
||||||
isWorkExperienceSelected: false,
|
isWorkExperienceSelected: false,
|
||||||
host: '',
|
host: '',
|
||||||
|
app_name: APP_NAME,
|
||||||
};
|
};
|
||||||
delete window.location;
|
delete window.location;
|
||||||
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
|
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
|
||||||
@@ -249,6 +261,9 @@ describe('ProgressiveProfilingTests', () => {
|
|||||||
...initialState.welcomePage,
|
...initialState.welcomePage,
|
||||||
success: true,
|
success: true,
|
||||||
},
|
},
|
||||||
|
cohesion: {
|
||||||
|
eventData,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||||
const nextButton = container.querySelector('button.btn-brand');
|
const nextButton = container.querySelector('button.btn-brand');
|
||||||
@@ -275,13 +290,18 @@ describe('ProgressiveProfilingTests', () => {
|
|||||||
...initialState.welcomePage,
|
...initialState.welcomePage,
|
||||||
success: true,
|
success: true,
|
||||||
},
|
},
|
||||||
|
cohesion: {
|
||||||
|
eventData,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||||
const nextButton = container.querySelector('button.btn-brand');
|
const nextButton = container.querySelector('button.btn-brand');
|
||||||
expect(nextButton.textContent).toEqual('Submit');
|
expect(nextButton.textContent).toEqual('Submit');
|
||||||
|
|
||||||
expect(window.location.href).toEqual(redirectUrl);
|
await waitFor(() => {
|
||||||
|
expect(window.location.href).toEqual(redirectUrl);
|
||||||
|
}, { timeout: 1100 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,7 +336,7 @@ describe('ProgressiveProfilingTests', () => {
|
|||||||
const skipLinkButton = screen.getByText('Skip for now');
|
const skipLinkButton = screen.getByText('Skip for now');
|
||||||
fireEvent.click(skipLinkButton);
|
fireEvent.click(skipLinkButton);
|
||||||
|
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host });
|
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host, app_name: APP_NAME });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show spinner while fetching the optional fields', () => {
|
it('should show spinner while fetching the optional fields', () => {
|
||||||
@@ -349,6 +369,7 @@ describe('ProgressiveProfilingTests', () => {
|
|||||||
isLevelOfEducationSelected: false,
|
isLevelOfEducationSelected: false,
|
||||||
isWorkExperienceSelected: false,
|
isWorkExperienceSelected: false,
|
||||||
host: 'http://example.com',
|
host: 'http://example.com',
|
||||||
|
app_name: APP_NAME,
|
||||||
};
|
};
|
||||||
delete window.location;
|
delete window.location;
|
||||||
window.location = {
|
window.location = {
|
||||||
@@ -395,7 +416,7 @@ describe('ProgressiveProfilingTests', () => {
|
|||||||
expect(window.location.href).toBe(DASHBOARD_URL);
|
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';
|
const redirectUrl = 'https://redirect-test.com';
|
||||||
delete window.location;
|
delete window.location;
|
||||||
window.location = {
|
window.location = {
|
||||||
@@ -417,12 +438,17 @@ describe('ProgressiveProfilingTests', () => {
|
|||||||
...initialState.welcomePage,
|
...initialState.welcomePage,
|
||||||
success: true,
|
success: true,
|
||||||
},
|
},
|
||||||
|
cohesion: {
|
||||||
|
eventData,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
render(reduxWrapper(<IntlProgressiveProfilingPage />));
|
||||||
const submitButton = screen.getByText('Submit');
|
const submitButton = screen.getByText('Submit');
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
expect(window.location.href).toBe(redirectUrl);
|
await waitFor(() => {
|
||||||
|
expect(window.location.href).toBe(redirectUrl);
|
||||||
|
}, { timeout: 1100 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const generateProductKey = (product) => (
|
|||||||
export const getProductMapping = (recommendedProducts) => recommendedProducts.map((product) => ({
|
export const getProductMapping = (recommendedProducts) => recommendedProducts.map((product) => ({
|
||||||
product_key: generateProductKey(product),
|
product_key: generateProductKey(product),
|
||||||
product_line: product.cardType,
|
product_line: product.cardType,
|
||||||
product_source: product.productSource.name,
|
product_source: product?.productSource?.name,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const trackRecommendationClick = (product, position, userId) => {
|
export const trackRecommendationClick = (product, position, userId) => {
|
||||||
@@ -25,7 +25,7 @@ export const trackRecommendationClick = (product, position, userId) => {
|
|||||||
recommendation_type: product.recommendationType,
|
recommendation_type: product.recommendationType,
|
||||||
product_key: generateProductKey(product),
|
product_key: generateProductKey(product),
|
||||||
product_line: product.cardType,
|
product_line: product.cardType,
|
||||||
product_source: product.productSource.name,
|
product_source: product?.productSource?.name,
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ const CountryField = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getCountryList = () => countryList.map((country) => (
|
const getCountryList = () => countryList.map((country) => (
|
||||||
<FormAutosuggestOption key={country[COUNTRY_CODE_KEY]} id={country[COUNTRY_CODE_KEY]}>
|
<FormAutosuggestOption key={country[COUNTRY_DISPLAY_KEY]} id={country[COUNTRY_CODE_KEY]}>
|
||||||
{country[COUNTRY_DISPLAY_KEY]}
|
{country[COUNTRY_DISPLAY_KEY]}
|
||||||
</FormAutosuggestOption>
|
</FormAutosuggestOption>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const INVALID_NAME_REGEX = /https?:\/\/(?:[-\w.]|(?:%[\da-fA-F]{2}))*/g;
|
|||||||
|
|
||||||
const validateName = (value, formatMessage) => {
|
const validateName = (value, formatMessage) => {
|
||||||
let fieldError = '';
|
let fieldError = '';
|
||||||
if (!value.trim()) {
|
if (!value || (value && !value.trim())) {
|
||||||
fieldError = formatMessage(messages['empty.name.field.error']);
|
fieldError = formatMessage(messages['empty.name.field.error']);
|
||||||
} else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) {
|
} else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) {
|
||||||
fieldError = formatMessage(messages['name.validation.message']);
|
fieldError = formatMessage(messages['name.validation.message']);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import React, {
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import { Form, Spinner, StatefulButton } from '@openedx/paragon';
|
import { Form, Spinner, StatefulButton } from '@openedx/paragon';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
@@ -12,12 +11,19 @@ import PropTypes from 'prop-types';
|
|||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import Skeleton from 'react-loading-skeleton';
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
|
||||||
|
import {
|
||||||
|
InstitutionLogistration,
|
||||||
|
PasswordField,
|
||||||
|
RedirectLogistration,
|
||||||
|
ThirdPartyAuthAlert,
|
||||||
|
} from '../common-components';
|
||||||
import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm';
|
import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm';
|
||||||
import RegistrationFailure from './components/RegistrationFailure';
|
import RegistrationFailure from './components/RegistrationFailure';
|
||||||
import {
|
import {
|
||||||
backupRegistrationFormBegin,
|
backupRegistrationFormBegin,
|
||||||
clearRegistrationBackendError,
|
clearRegistrationBackendError,
|
||||||
registerNewUser,
|
registerNewUser,
|
||||||
|
setAutoGeneratedUsernameExperimentData,
|
||||||
setEmailSuggestionInStore,
|
setEmailSuggestionInStore,
|
||||||
setUserPipelineDataLoaded,
|
setUserPipelineDataLoaded,
|
||||||
} from './data/actions';
|
} from './data/actions';
|
||||||
@@ -25,6 +31,8 @@ import {
|
|||||||
FORM_SUBMISSION_ERROR,
|
FORM_SUBMISSION_ERROR,
|
||||||
TPA_AUTHENTICATION_FAILURE,
|
TPA_AUTHENTICATION_FAILURE,
|
||||||
} from './data/constants';
|
} from './data/constants';
|
||||||
|
import { AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION, NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
|
||||||
|
import useAutoGeneratedUsernameExperimentVariation from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||||
import getBackendValidations from './data/selectors';
|
import getBackendValidations from './data/selectors';
|
||||||
import {
|
import {
|
||||||
isFormValid, prepareRegistrationPayload,
|
isFormValid, prepareRegistrationPayload,
|
||||||
@@ -32,21 +40,20 @@ import {
|
|||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import { EmailField, NameField, UsernameField } from './RegistrationFields';
|
import { EmailField, NameField, UsernameField } from './RegistrationFields';
|
||||||
import {
|
import {
|
||||||
InstitutionLogistration,
|
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
|
||||||
PasswordField,
|
} from '../cohesion/constants';
|
||||||
RedirectLogistration,
|
import { setCohesionEventStates } from '../cohesion/data/actions';
|
||||||
ThirdPartyAuthAlert,
|
|
||||||
} from '../common-components';
|
|
||||||
import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../common-components/data/actions';
|
import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../common-components/data/actions';
|
||||||
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
||||||
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
|
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
|
||||||
import {
|
import {
|
||||||
COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
|
APP_NAME, COMPLETE_STATE, PENDING_STATE,
|
||||||
|
REGISTER_PAGE,
|
||||||
} from '../data/constants';
|
} from '../data/constants';
|
||||||
import {
|
import {
|
||||||
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie,
|
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, removeCookie, setCookie,
|
||||||
} from '../data/utils';
|
} from '../data/utils';
|
||||||
|
import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
|
||||||
/**
|
/**
|
||||||
* Main Registration Page component
|
* Main Registration Page component
|
||||||
*/
|
*/
|
||||||
@@ -60,6 +67,7 @@ const RegistrationPage = (props) => {
|
|||||||
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
|
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
|
||||||
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
|
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
|
||||||
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
|
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
|
||||||
|
autoGeneratedUsernameEnabled: getConfig().ENABLE_AUTO_GENERATED_USERNAME,
|
||||||
};
|
};
|
||||||
const {
|
const {
|
||||||
handleInstitutionLogin,
|
handleInstitutionLogin,
|
||||||
@@ -67,6 +75,7 @@ const RegistrationPage = (props) => {
|
|||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const backedUpFormData = useSelector(state => state.register.registrationFormData);
|
const backedUpFormData = useSelector(state => state.register.registrationFormData);
|
||||||
|
const initExpVariation = useSelector(state => state.register.autoGeneratedUsernameExperimentVariation);
|
||||||
const registrationError = useSelector(state => state.register.registrationError);
|
const registrationError = useSelector(state => state.register.registrationError);
|
||||||
const registrationErrorCode = registrationError?.errorCode;
|
const registrationErrorCode = registrationError?.errorCode;
|
||||||
const registrationResult = useSelector(state => state.register.registrationResult);
|
const registrationResult = useSelector(state => state.register.registrationResult);
|
||||||
@@ -102,6 +111,12 @@ const RegistrationPage = (props) => {
|
|||||||
? formatMessage(messages['create.account.cta.button'], { label: cta })
|
? formatMessage(messages['create.account.cta.button'], { label: cta })
|
||||||
: formatMessage(messages['create.account.for.free.button']);
|
: formatMessage(messages['create.account.for.free.button']);
|
||||||
|
|
||||||
|
const autoGeneratedUsernameExpVariation = useAutoGeneratedUsernameExperimentVariation(
|
||||||
|
initExpVariation, registrationEmbedded, tpaHint, currentProvider, thirdPartyAuthApiStatus,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hideUsernameField = flags.autoGeneratedUsernameEnabled
|
||||||
|
|| autoGeneratedUsernameExpVariation === AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION;
|
||||||
/**
|
/**
|
||||||
* Set the userPipelineDetails data in formFields for only first time
|
* Set the userPipelineDetails data in formFields for only first time
|
||||||
*/
|
*/
|
||||||
@@ -127,7 +142,7 @@ const RegistrationPage = (props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!formStartTime) {
|
if (!formStartTime) {
|
||||||
sendPageEvent('login_and_registration', 'register');
|
trackRegistrationPageViewed();
|
||||||
const payload = { ...queryParams, is_register_page: true };
|
const payload = { ...queryParams, is_register_page: true };
|
||||||
if (tpaHint) {
|
if (tpaHint) {
|
||||||
payload.tpa_hint = tpaHint;
|
payload.tpa_hint = tpaHint;
|
||||||
@@ -148,8 +163,10 @@ const RegistrationPage = (props) => {
|
|||||||
formFields: { ...formFields },
|
formFields: { ...formFields },
|
||||||
errors: { ...errors },
|
errors: { ...errors },
|
||||||
}));
|
}));
|
||||||
|
dispatch(setAutoGeneratedUsernameExperimentData(autoGeneratedUsernameExpVariation));
|
||||||
}
|
}
|
||||||
}, [shouldBackupState, configurableFormFields, formFields, errors, dispatch, backedUpFormData]);
|
}, [shouldBackupState, configurableFormFields, // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
formFields, errors, dispatch, backedUpFormData]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (backendValidations) {
|
if (backendValidations) {
|
||||||
@@ -170,10 +187,15 @@ const RegistrationPage = (props) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (registrationResult.success) {
|
if (registrationResult.success) {
|
||||||
// This event is used by GTM
|
// This event is used by GTM
|
||||||
sendTrackEvent('edx.bi.user.account.registered.client', {});
|
trackRegistrationSuccess();
|
||||||
|
|
||||||
// This is used by the "User Retention Rate Event" on GTM
|
// This is used by the "User Retention Rate Event" on GTM
|
||||||
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
|
setCookie(getConfig().USER_RETENTION_COOKIE_NAME, true);
|
||||||
|
|
||||||
|
// Remove marketingEmailsOptIn cookie that was set on SSO registration flow
|
||||||
|
removeCookie('marketingEmailsOptIn');
|
||||||
|
// Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
|
||||||
|
removeCookie('ssoPipelineRedirectionDone');
|
||||||
}
|
}
|
||||||
}, [registrationResult]);
|
}, [registrationResult]);
|
||||||
|
|
||||||
@@ -209,12 +231,15 @@ const RegistrationPage = (props) => {
|
|||||||
|
|
||||||
const registerUser = () => {
|
const registerUser = () => {
|
||||||
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
|
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
|
||||||
let payload = { ...formFields };
|
let payload = { ...formFields, app_name: APP_NAME };
|
||||||
|
|
||||||
if (currentProvider) {
|
if (currentProvider) {
|
||||||
delete payload.password;
|
delete payload.password;
|
||||||
payload.social_auth_provider = currentProvider;
|
payload.social_auth_provider = currentProvider;
|
||||||
}
|
}
|
||||||
|
if (hideUsernameField) {
|
||||||
|
delete payload.username;
|
||||||
|
}
|
||||||
|
|
||||||
// Validating form data before submitting
|
// Validating form data before submitting
|
||||||
const { isValid, fieldErrors, emailSuggestion } = isFormValid(
|
const { isValid, fieldErrors, emailSuggestion } = isFormValid(
|
||||||
@@ -247,6 +272,14 @@ const RegistrationPage = (props) => {
|
|||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
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();
|
registerUser();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -281,105 +314,111 @@ const RegistrationPage = (props) => {
|
|||||||
redirectToProgressiveProfilingPage={
|
redirectToProgressiveProfilingPage={
|
||||||
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
|
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
|
||||||
}
|
}
|
||||||
|
currentProvider={currentProvider}
|
||||||
/>
|
/>
|
||||||
{autoSubmitRegForm && !errorCode.type ? (
|
{(autoSubmitRegForm && !errorCode.type)
|
||||||
<div className="mw-xs mt-5 text-center">
|
|| (!autoGeneratedUsernameExpVariation && !(
|
||||||
<Spinner animation="border" variant="primary" id="tpa-spinner" />
|
autoGeneratedUsernameExpVariation === NOT_INITIALIZED
|
||||||
</div>
|
|| registrationEmbedded || !!tpaHint || !!currentProvider))
|
||||||
) : (
|
? (
|
||||||
<div
|
<div className="mw-xs mt-5 text-center">
|
||||||
className={classNames(
|
<Spinner animation="border" variant="primary" id="tpa-spinner" />
|
||||||
'mw-xs mt-3',
|
</div>
|
||||||
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded },
|
) : (
|
||||||
)}
|
<div
|
||||||
>
|
className={classNames(
|
||||||
<ThirdPartyAuthAlert
|
'mw-xs mt-3',
|
||||||
currentProvider={currentProvider}
|
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded },
|
||||||
platformName={platformName}
|
)}
|
||||||
referrer={REGISTER_PAGE}
|
>
|
||||||
/>
|
<ThirdPartyAuthAlert
|
||||||
<RegistrationFailure
|
currentProvider={currentProvider}
|
||||||
errorCode={errorCode.type}
|
platformName={platformName}
|
||||||
failureCount={errorCode.count}
|
referrer={REGISTER_PAGE}
|
||||||
context={{ provider: currentProvider, errorMessage: thirdPartyAuthErrorMessage }}
|
|
||||||
/>
|
|
||||||
<Form id="registration-form" name="registration-form">
|
|
||||||
<NameField
|
|
||||||
name="name"
|
|
||||||
value={formFields.name}
|
|
||||||
shouldFetchUsernameSuggestions={!formFields.username.trim()}
|
|
||||||
handleChange={handleOnChange}
|
|
||||||
handleErrorChange={handleErrorChange}
|
|
||||||
errorMessage={errors.name}
|
|
||||||
helpText={[formatMessage(messages['help.text.name'])]}
|
|
||||||
floatingLabel={formatMessage(messages['registration.fullname.label'])}
|
|
||||||
/>
|
/>
|
||||||
<EmailField
|
<RegistrationFailure
|
||||||
name="email"
|
errorCode={errorCode.type}
|
||||||
value={formFields.email}
|
failureCount={errorCode.count}
|
||||||
confirmEmailValue={configurableFormFields?.confirm_email}
|
context={{ provider: currentProvider, errorMessage: thirdPartyAuthErrorMessage }}
|
||||||
handleErrorChange={handleErrorChange}
|
|
||||||
handleChange={handleOnChange}
|
|
||||||
errorMessage={errors.email}
|
|
||||||
helpText={[formatMessage(messages['help.text.email'])]}
|
|
||||||
floatingLabel={formatMessage(messages['registration.email.label'])}
|
|
||||||
/>
|
/>
|
||||||
<UsernameField
|
<Form id="registration-form" name="registration-form">
|
||||||
name="username"
|
<NameField
|
||||||
spellCheck="false"
|
name="name"
|
||||||
value={formFields.username}
|
value={formFields.name}
|
||||||
handleChange={handleOnChange}
|
shouldFetchUsernameSuggestions={!formFields.username.trim()}
|
||||||
handleErrorChange={handleErrorChange}
|
|
||||||
errorMessage={errors.username}
|
|
||||||
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
|
|
||||||
floatingLabel={formatMessage(messages['registration.username.label'])}
|
|
||||||
/>
|
|
||||||
{!currentProvider && (
|
|
||||||
<PasswordField
|
|
||||||
name="password"
|
|
||||||
value={formFields.password}
|
|
||||||
handleChange={handleOnChange}
|
handleChange={handleOnChange}
|
||||||
handleErrorChange={handleErrorChange}
|
handleErrorChange={handleErrorChange}
|
||||||
errorMessage={errors.password}
|
errorMessage={errors.name}
|
||||||
floatingLabel={formatMessage(messages['registration.password.label'])}
|
helpText={[formatMessage(messages['help.text.name'])]}
|
||||||
|
floatingLabel={formatMessage(messages['registration.fullname.label'])}
|
||||||
/>
|
/>
|
||||||
)}
|
<EmailField
|
||||||
<ConfigurableRegistrationForm
|
name="email"
|
||||||
email={formFields.email}
|
value={formFields.email}
|
||||||
fieldErrors={errors}
|
confirmEmailValue={configurableFormFields?.confirm_email}
|
||||||
formFields={configurableFormFields}
|
handleErrorChange={handleErrorChange}
|
||||||
setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors}
|
handleChange={handleOnChange}
|
||||||
setFormFields={setConfigurableFormFields}
|
errorMessage={errors.email}
|
||||||
autoSubmitRegisterForm={autoSubmitRegForm}
|
helpText={[formatMessage(messages['help.text.email'])]}
|
||||||
fieldDescriptions={fieldDescriptions}
|
floatingLabel={formatMessage(messages['registration.email.label'])}
|
||||||
/>
|
|
||||||
<StatefulButton
|
|
||||||
id="register-user"
|
|
||||||
name="register-user"
|
|
||||||
type="submit"
|
|
||||||
variant="brand"
|
|
||||||
className="register-button mt-4 mb-4"
|
|
||||||
state={submitState}
|
|
||||||
labels={{
|
|
||||||
default: buttonLabel,
|
|
||||||
pending: '',
|
|
||||||
}}
|
|
||||||
onClick={handleSubmit}
|
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
|
||||||
/>
|
|
||||||
{!registrationEmbedded && (
|
|
||||||
<ThirdPartyAuth
|
|
||||||
currentProvider={currentProvider}
|
|
||||||
providers={providers}
|
|
||||||
secondaryProviders={secondaryProviders}
|
|
||||||
handleInstitutionLogin={handleInstitutionLogin}
|
|
||||||
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
|
||||||
/>
|
/>
|
||||||
)}
|
{!hideUsernameField && (
|
||||||
</Form>
|
<UsernameField
|
||||||
</div>
|
name="username"
|
||||||
)}
|
spellCheck="false"
|
||||||
|
value={formFields.username}
|
||||||
|
handleChange={handleOnChange}
|
||||||
|
handleErrorChange={handleErrorChange}
|
||||||
|
errorMessage={errors.username}
|
||||||
|
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
|
||||||
|
floatingLabel={formatMessage(messages['registration.username.label'])}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!currentProvider && (
|
||||||
|
<PasswordField
|
||||||
|
name="password"
|
||||||
|
value={formFields.password}
|
||||||
|
handleChange={handleOnChange}
|
||||||
|
handleErrorChange={handleErrorChange}
|
||||||
|
errorMessage={errors.password}
|
||||||
|
floatingLabel={formatMessage(messages['registration.password.label'])}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ConfigurableRegistrationForm
|
||||||
|
email={formFields.email}
|
||||||
|
fieldErrors={errors}
|
||||||
|
formFields={configurableFormFields}
|
||||||
|
setFieldErrors={registrationEmbedded ? setTemporaryErrors : setErrors}
|
||||||
|
setFormFields={setConfigurableFormFields}
|
||||||
|
autoSubmitRegisterForm={autoSubmitRegForm}
|
||||||
|
fieldDescriptions={fieldDescriptions}
|
||||||
|
/>
|
||||||
|
<StatefulButton
|
||||||
|
id="register-user"
|
||||||
|
name="register-user"
|
||||||
|
type="submit"
|
||||||
|
variant="brand"
|
||||||
|
className="register-button mt-4 mb-4"
|
||||||
|
state={submitState}
|
||||||
|
labels={{
|
||||||
|
default: buttonLabel,
|
||||||
|
pending: '',
|
||||||
|
}}
|
||||||
|
onClick={handleSubmit}
|
||||||
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
|
/>
|
||||||
|
{!registrationEmbedded && (
|
||||||
|
<ThirdPartyAuth
|
||||||
|
currentProvider={currentProvider}
|
||||||
|
providers={providers}
|
||||||
|
secondaryProviders={secondaryProviders}
|
||||||
|
handleInstitutionLogin={handleInstitutionLogin}
|
||||||
|
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'
|
|||||||
import {
|
import {
|
||||||
configure, getLocale, injectIntl, IntlProvider,
|
configure, getLocale, injectIntl, IntlProvider,
|
||||||
} from '@edx/frontend-platform/i18n';
|
} 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 { mockNavigate, BrowserRouter as Router } from 'react-router-dom';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
|
|
||||||
@@ -17,9 +17,13 @@ import {
|
|||||||
setUserPipelineDataLoaded,
|
setUserPipelineDataLoaded,
|
||||||
} from './data/actions';
|
} from './data/actions';
|
||||||
import { INTERNAL_SERVER_ERROR } from './data/constants';
|
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';
|
||||||
import RegistrationPage from './RegistrationPage';
|
import RegistrationPage from './RegistrationPage';
|
||||||
import {
|
import {
|
||||||
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
|
APP_NAME, AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
|
||||||
} from '../data/constants';
|
} from '../data/constants';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||||
@@ -30,9 +34,11 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
|||||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||||
getLocale: jest.fn(),
|
getLocale: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('./data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||||
|
|
||||||
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
||||||
const mockStore = configureStore();
|
const mockStore = configureStore();
|
||||||
|
mockTagular();
|
||||||
|
|
||||||
jest.mock('react-router-dom', () => {
|
jest.mock('react-router-dom', () => {
|
||||||
const mockNavigation = jest.fn();
|
const mockNavigation = jest.fn();
|
||||||
@@ -102,6 +108,7 @@ describe('RegistrationPage', () => {
|
|||||||
usernameSuggestions: [],
|
usernameSuggestions: [],
|
||||||
|
|
||||||
},
|
},
|
||||||
|
cohesion: { eventData: {} },
|
||||||
commonComponents: {
|
commonComponents: {
|
||||||
thirdPartyAuthApiStatus: null,
|
thirdPartyAuthApiStatus: null,
|
||||||
thirdPartyAuthContext,
|
thirdPartyAuthContext,
|
||||||
@@ -128,15 +135,23 @@ describe('RegistrationPage', () => {
|
|||||||
institutionLogin: false,
|
institutionLogin: false,
|
||||||
};
|
};
|
||||||
window.location = { search: '' };
|
window.location = { search: '' };
|
||||||
|
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
const populateRequiredFields = (getByLabelText, payload, isThirdPartyAuth = false) => {
|
const populateRequiredFields = (
|
||||||
|
getByLabelText,
|
||||||
|
payload,
|
||||||
|
isThirdPartyAuth = false,
|
||||||
|
autoGeneratedUsernameEnabled = false,
|
||||||
|
) => {
|
||||||
fireEvent.change(getByLabelText('Full name'), { target: { value: payload.name, name: 'name' } });
|
fireEvent.change(getByLabelText('Full name'), { target: { value: payload.name, name: 'name' } });
|
||||||
fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } });
|
if (!autoGeneratedUsernameEnabled) {
|
||||||
|
fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } });
|
||||||
|
}
|
||||||
fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } });
|
fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } });
|
||||||
|
|
||||||
fireEvent.change(getByLabelText('Country/Region'), { target: { value: payload.country, name: 'country' } });
|
fireEvent.change(getByLabelText('Country/Region'), { target: { value: payload.country, name: 'country' } });
|
||||||
@@ -176,8 +191,9 @@ describe('RegistrationPage', () => {
|
|||||||
password: 'password1',
|
password: 'password1',
|
||||||
country: 'Pakistan',
|
country: 'Pakistan',
|
||||||
honor_code: true,
|
honor_code: true,
|
||||||
totalRegistrationTime: 0,
|
total_registration_time: 0,
|
||||||
next: '/course/demo-course-url',
|
next: '/course/demo-course-url',
|
||||||
|
app_name: APP_NAME,
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch = jest.fn(store.dispatch);
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
@@ -199,7 +215,8 @@ describe('RegistrationPage', () => {
|
|||||||
country: 'Pakistan',
|
country: 'Pakistan',
|
||||||
honor_code: true,
|
honor_code: true,
|
||||||
social_auth_provider: 'Apple',
|
social_auth_provider: 'Apple',
|
||||||
totalRegistrationTime: 0,
|
total_registration_time: 0,
|
||||||
|
app_name: APP_NAME,
|
||||||
};
|
};
|
||||||
|
|
||||||
store = mockStore({
|
store = mockStore({
|
||||||
@@ -232,7 +249,7 @@ describe('RegistrationPage', () => {
|
|||||||
password: 'password1',
|
password: 'password1',
|
||||||
country: 'Ukraine',
|
country: 'Ukraine',
|
||||||
honor_code: true,
|
honor_code: true,
|
||||||
totalRegistrationTime: 0,
|
total_registration_time: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch = jest.fn(store.dispatch);
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
@@ -257,7 +274,7 @@ describe('RegistrationPage', () => {
|
|||||||
password: 'password1',
|
password: 'password1',
|
||||||
country: 'Ukraine',
|
country: 'Ukraine',
|
||||||
honor_code: true,
|
honor_code: true,
|
||||||
totalRegistrationTime: 0,
|
total_registration_time: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch = jest.fn(store.dispatch);
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
@@ -283,8 +300,9 @@ describe('RegistrationPage', () => {
|
|||||||
password: 'password1',
|
password: 'password1',
|
||||||
country: 'Pakistan',
|
country: 'Pakistan',
|
||||||
honor_code: true,
|
honor_code: true,
|
||||||
totalRegistrationTime: 0,
|
total_registration_time: 0,
|
||||||
marketing_emails_opt_in: true,
|
marketing_emails_opt_in: true,
|
||||||
|
app_name: APP_NAME,
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch = jest.fn(store.dispatch);
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
@@ -299,6 +317,45 @@ describe('RegistrationPage', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should submit form without UsernameField when autoGeneratedUsernameEnabled is true', () => {
|
||||||
|
mergeConfig({
|
||||||
|
ENABLE_AUTO_GENERATED_USERNAME: true,
|
||||||
|
});
|
||||||
|
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||||
|
const payload = {
|
||||||
|
name: 'John Doe',
|
||||||
|
email: 'john.doe@gmail.com',
|
||||||
|
password: 'password1',
|
||||||
|
country: 'Pakistan',
|
||||||
|
honor_code: true,
|
||||||
|
total_registration_time: 0,
|
||||||
|
app_name: APP_NAME,
|
||||||
|
};
|
||||||
|
|
||||||
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
|
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||||
|
populateRequiredFields(getByLabelText, payload, false, true);
|
||||||
|
const button = container.querySelector('button.btn-brand');
|
||||||
|
fireEvent.click(button);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
||||||
|
mergeConfig({
|
||||||
|
ENABLE_AUTO_GENERATED_USERNAME: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display UsernameField when ENABLE_AUTO_GENERATED_USERNAME is true', () => {
|
||||||
|
mergeConfig({
|
||||||
|
ENABLE_AUTO_GENERATED_USERNAME: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { queryByLabelText } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||||
|
expect(queryByLabelText('Username')).toBeNull();
|
||||||
|
|
||||||
|
mergeConfig({
|
||||||
|
ENABLE_AUTO_GENERATED_USERNAME: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should not dispatch registerNewUser on empty form Submission', () => {
|
it('should not dispatch registerNewUser on empty form Submission', () => {
|
||||||
store.dispatch = jest.fn(store.dispatch);
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
|
|
||||||
@@ -451,7 +508,7 @@ describe('RegistrationPage', () => {
|
|||||||
expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`);
|
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/';
|
const dashboardURL = 'https://test.com/testing-dashboard/';
|
||||||
store = mockStore({
|
store = mockStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
@@ -466,10 +523,12 @@ describe('RegistrationPage', () => {
|
|||||||
delete window.location;
|
delete window.location;
|
||||||
window.location = { href: getConfig().BASE_URL };
|
window.location = { href: getConfig().BASE_URL };
|
||||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
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({
|
mergeConfig({
|
||||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true,
|
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true,
|
||||||
});
|
});
|
||||||
@@ -493,7 +552,9 @@ describe('RegistrationPage', () => {
|
|||||||
delete window.location;
|
delete window.location;
|
||||||
window.location = { href: getConfig().BASE_URL };
|
window.location = { href: getConfig().BASE_URL };
|
||||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
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', () => {
|
it('should redirect to progressive profiling page if optional fields are configured', () => {
|
||||||
@@ -547,7 +608,7 @@ describe('RegistrationPage', () => {
|
|||||||
|
|
||||||
it('should send page event when register page is rendered', () => {
|
it('should send page event when register page is rendered', () => {
|
||||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||||
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
|
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register', { app_name: APP_NAME });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send track event when user has successfully registered', () => {
|
it('should send track event when user has successfully registered', () => {
|
||||||
@@ -565,7 +626,7 @@ describe('RegistrationPage', () => {
|
|||||||
delete window.location;
|
delete window.location;
|
||||||
window.location = { href: getConfig().BASE_URL };
|
window.location = { href: getConfig().BASE_URL };
|
||||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
||||||
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', {});
|
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', { app_name: APP_NAME });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should populate form with pipeline user details', () => {
|
it('should populate form with pipeline user details', () => {
|
||||||
@@ -837,7 +898,8 @@ describe('RegistrationPage', () => {
|
|||||||
email: 'john.doe@example.com',
|
email: 'john.doe@example.com',
|
||||||
country: 'PK',
|
country: 'PK',
|
||||||
social_auth_provider: 'Apple',
|
social_auth_provider: 'Apple',
|
||||||
totalRegistrationTime: 0,
|
total_registration_time: 0,
|
||||||
|
app_name: APP_NAME,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import React, { useEffect, useMemo } from 'react';
|
import React, { useEffect, useMemo } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
|
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
|
||||||
import PropTypes from 'prop-types';
|
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 { FormFieldRenderer } from '../../field-renderer';
|
||||||
|
import { backupRegistrationFormBegin } from '../data/actions';
|
||||||
import { FIELDS } from '../data/constants';
|
import { FIELDS } from '../data/constants';
|
||||||
import messages from '../messages';
|
import messages from '../messages';
|
||||||
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
|
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
|
||||||
@@ -32,8 +38,16 @@ const ConfigurableRegistrationForm = (props) => {
|
|||||||
setFormFields,
|
setFormFields,
|
||||||
autoSubmitRegistrationForm,
|
autoSubmitRegistrationForm,
|
||||||
} = props;
|
} = props;
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const countryList = useMemo(() => getCountryList(getLocale()), []);
|
/** The reason for adding the entry 'United States' is that Chrome browser aut-fill the form with the 'Unites
|
||||||
|
States' instead of 'United States of America' which does not exist in country dropdown list and gets the user
|
||||||
|
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 showTermsOfServiceAndHonorCode = false;
|
||||||
let showCountryField = false;
|
let showCountryField = false;
|
||||||
@@ -46,6 +60,8 @@ const ConfigurableRegistrationForm = (props) => {
|
|||||||
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
|
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const backedUpFormData = useSelector(state => state.register.registrationFormData);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
|
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
|
||||||
*/
|
*/
|
||||||
@@ -86,6 +102,25 @@ const ConfigurableRegistrationForm = (props) => {
|
|||||||
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 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: {
|
||||||
|
...backedUpFormData.configurableFormFields,
|
||||||
|
[name]: value,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import { fireEvent, render } from '@testing-library/react';
|
|||||||
import { BrowserRouter as Router } from 'react-router-dom';
|
import { BrowserRouter as Router } from 'react-router-dom';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
|
|
||||||
|
import { APP_NAME } from '../../../data/constants';
|
||||||
import { registerNewUser } from '../../data/actions';
|
import { registerNewUser } from '../../data/actions';
|
||||||
import { FIELDS } from '../../data/constants';
|
import { FIELDS } from '../../data/constants';
|
||||||
|
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
|
||||||
|
import useAutoGeneratedUsernameExperimentVariation
|
||||||
|
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||||
import RegistrationPage from '../../RegistrationPage';
|
import RegistrationPage from '../../RegistrationPage';
|
||||||
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
|
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
|
||||||
|
|
||||||
@@ -22,6 +26,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
|||||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||||
getLocale: jest.fn(),
|
getLocale: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||||
|
|
||||||
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
|
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
|
||||||
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
||||||
@@ -94,6 +99,7 @@ describe('ConfigurableRegistrationForm', () => {
|
|||||||
registrationFormData,
|
registrationFormData,
|
||||||
usernameSuggestions: [],
|
usernameSuggestions: [],
|
||||||
},
|
},
|
||||||
|
cohesion: { eventData: {} },
|
||||||
commonComponents: {
|
commonComponents: {
|
||||||
thirdPartyAuthApiStatus: null,
|
thirdPartyAuthApiStatus: null,
|
||||||
thirdPartyAuthContext,
|
thirdPartyAuthContext,
|
||||||
@@ -121,6 +127,7 @@ describe('ConfigurableRegistrationForm', () => {
|
|||||||
};
|
};
|
||||||
window.location = { search: '' };
|
window.location = { search: '' };
|
||||||
getLocale.mockImplementationOnce(() => ('en-us'));
|
getLocale.mockImplementationOnce(() => ('en-us'));
|
||||||
|
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -245,7 +252,7 @@ describe('ConfigurableRegistrationForm', () => {
|
|||||||
country: 'Pakistan',
|
country: 'Pakistan',
|
||||||
honor_code: true,
|
honor_code: true,
|
||||||
profession: 'Engineer',
|
profession: 'Engineer',
|
||||||
totalRegistrationTime: 0,
|
total_registration_time: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
store.dispatch = jest.fn(store.dispatch);
|
store.dispatch = jest.fn(store.dispatch);
|
||||||
@@ -260,7 +267,7 @@ describe('ConfigurableRegistrationForm', () => {
|
|||||||
|
|
||||||
fireEvent.click(submitButton);
|
fireEvent.click(submitButton);
|
||||||
|
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
|
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK', app_name: APP_NAME }));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show error messages for required fields on empty form submission', () => {
|
it('should show error messages for required fields on empty form submission', () => {
|
||||||
@@ -356,7 +363,7 @@ describe('ConfigurableRegistrationForm', () => {
|
|||||||
password: 'password1',
|
password: 'password1',
|
||||||
country: 'Ukraine',
|
country: 'Ukraine',
|
||||||
honor_code: true,
|
honor_code: true,
|
||||||
totalRegistrationTime: 0,
|
total_registration_time: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
store = mockStore({
|
store = mockStore({
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import configureStore from 'redux-mock-store';
|
|||||||
import {
|
import {
|
||||||
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
|
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
|
||||||
} from '../../data/constants';
|
} from '../../data/constants';
|
||||||
|
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
|
||||||
|
import useAutoGeneratedUsernameExperimentVariation
|
||||||
|
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||||
import RegistrationPage from '../../RegistrationPage';
|
import RegistrationPage from '../../RegistrationPage';
|
||||||
import RegistrationFailureMessage from '../RegistrationFailure';
|
import RegistrationFailureMessage from '../RegistrationFailure';
|
||||||
|
|
||||||
@@ -23,6 +26,7 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
|||||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||||
getLocale: jest.fn(),
|
getLocale: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||||
|
|
||||||
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
||||||
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
|
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
|
||||||
@@ -95,6 +99,7 @@ describe('RegistrationFailure', () => {
|
|||||||
registrationFormData,
|
registrationFormData,
|
||||||
usernameSuggestions: [],
|
usernameSuggestions: [],
|
||||||
},
|
},
|
||||||
|
cohesion: { eventData: {} },
|
||||||
commonComponents: {
|
commonComponents: {
|
||||||
thirdPartyAuthApiStatus: null,
|
thirdPartyAuthApiStatus: null,
|
||||||
thirdPartyAuthContext,
|
thirdPartyAuthContext,
|
||||||
@@ -121,6 +126,7 @@ describe('RegistrationFailure', () => {
|
|||||||
institutionLogin: false,
|
institutionLogin: false,
|
||||||
};
|
};
|
||||||
window.location = { search: '' };
|
window.location = { search: '' };
|
||||||
|
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@@ -5,13 +5,17 @@ import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
|||||||
import {
|
import {
|
||||||
configure, getLocale, injectIntl, IntlProvider,
|
configure, getLocale, injectIntl, IntlProvider,
|
||||||
} from '@edx/frontend-platform/i18n';
|
} 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 { BrowserRouter as Router } from 'react-router-dom';
|
||||||
import configureStore from 'redux-mock-store';
|
import configureStore from 'redux-mock-store';
|
||||||
|
|
||||||
|
import mockTagular from '../../../cohesion/utils';
|
||||||
import {
|
import {
|
||||||
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
|
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
|
||||||
} from '../../../data/constants';
|
} from '../../../data/constants';
|
||||||
|
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
|
||||||
|
import useAutoGeneratedUsernameExperimentVariation
|
||||||
|
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
|
||||||
import RegistrationPage from '../../RegistrationPage';
|
import RegistrationPage from '../../RegistrationPage';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||||
@@ -22,6 +26,8 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
|
|||||||
...jest.requireActual('@edx/frontend-platform/i18n'),
|
...jest.requireActual('@edx/frontend-platform/i18n'),
|
||||||
getLocale: jest.fn(),
|
getLocale: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
|
||||||
|
mockTagular();
|
||||||
|
|
||||||
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
const IntlRegistrationPage = injectIntl(RegistrationPage);
|
||||||
const mockStore = configureStore();
|
const mockStore = configureStore();
|
||||||
@@ -94,6 +100,7 @@ describe('ThirdPartyAuth', () => {
|
|||||||
registrationFormData,
|
registrationFormData,
|
||||||
usernameSuggestions: [],
|
usernameSuggestions: [],
|
||||||
},
|
},
|
||||||
|
cohesion: { eventData: {} },
|
||||||
commonComponents: {
|
commonComponents: {
|
||||||
thirdPartyAuthApiStatus: null,
|
thirdPartyAuthApiStatus: null,
|
||||||
thirdPartyAuthContext,
|
thirdPartyAuthContext,
|
||||||
@@ -120,6 +127,7 @@ describe('ThirdPartyAuth', () => {
|
|||||||
institutionLogin: false,
|
institutionLogin: false,
|
||||||
};
|
};
|
||||||
window.location = { search: '' };
|
window.location = { search: '' };
|
||||||
|
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -334,7 +342,7 @@ describe('ThirdPartyAuth', () => {
|
|||||||
expect(headingElement).toBeTruthy();
|
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';
|
const registerUrl = '/auth/login/apple-id/?auth_entry=register&next=/dashboard';
|
||||||
store = mockStore({
|
store = mockStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
@@ -360,10 +368,12 @@ describe('ThirdPartyAuth', () => {
|
|||||||
const ssoButton = container.querySelector('button#oa2-apple-id');
|
const ssoButton = container.querySelector('button#oa2-apple-id');
|
||||||
fireEvent.click(ssoButton);
|
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/';
|
const authCompleteUrl = '/auth/complete/google-oauth2/';
|
||||||
store = mockStore({
|
store = mockStore({
|
||||||
...initialState,
|
...initialState,
|
||||||
@@ -386,7 +396,9 @@ describe('ThirdPartyAuth', () => {
|
|||||||
window.location = { href: getConfig().BASE_URL };
|
window.location = { href: getConfig().BASE_URL };
|
||||||
|
|
||||||
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
|
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 ********
|
// ******** test alert messages ********
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERRO
|
|||||||
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
|
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
|
||||||
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
|
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
|
||||||
export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
|
export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
|
||||||
|
export const REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA = 'REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA';
|
||||||
// Backup registration form
|
// Backup registration form
|
||||||
export const backupRegistrationForm = () => ({
|
export const backupRegistrationForm = () => ({
|
||||||
type: BACKUP_REGISTRATION_DATA.BASE,
|
type: BACKUP_REGISTRATION_DATA.BASE,
|
||||||
@@ -83,3 +83,9 @@ export const setUserPipelineDataLoaded = (value) => ({
|
|||||||
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
|
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
|
||||||
payload: { value },
|
payload: { value },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Auto Generated Username Registration Experiment Actions
|
||||||
|
export const setAutoGeneratedUsernameExperimentData = (autoGeneratedRegExpVariation) => ({
|
||||||
|
type: REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
|
||||||
|
payload: { autoGeneratedRegExpVariation },
|
||||||
|
});
|
||||||
|
|||||||
30
src/register/data/optimizelyExperiment/helper.js
Normal file
30
src/register/data/optimizelyExperiment/helper.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* This file contains data for auto generated username Optimizely experiment
|
||||||
|
*/
|
||||||
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
|
|
||||||
|
export const NOT_INITIALIZED = 'experiment-not-initialized';
|
||||||
|
export const CONTROL = 'control-registration-page';
|
||||||
|
export const AUTO_GENERATED_USERNAME_REGISTRATION_EXP_VARIATION = 'auto-generated-username-register-page';
|
||||||
|
const AUTO_GENERATED_USERNAME_EXP_PAGE = 'targeting_for_auto_generated_username_page';
|
||||||
|
|
||||||
|
export function getAutoGeneratedUsernameExperimentVariation() {
|
||||||
|
try {
|
||||||
|
if (window.optimizely
|
||||||
|
&& window.optimizely.get('data').experiments[getConfig().AUTO_GENERATED_USERNAME_EXPERIMENT_ID]) {
|
||||||
|
const selectedVariant = window.optimizely.get('state').getVariationMap()[
|
||||||
|
getConfig().AUTO_GENERATED_USERNAME_EXPERIMENT_ID
|
||||||
|
];
|
||||||
|
return selectedVariant?.name;
|
||||||
|
}
|
||||||
|
} catch (e) { /* empty */ }
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activateAutoGeneratedUsernameExperiment() {
|
||||||
|
window.optimizely = window.optimizely || [];
|
||||||
|
window.optimizely.push({
|
||||||
|
type: 'page',
|
||||||
|
pageName: AUTO_GENERATED_USERNAME_EXP_PAGE,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
activateAutoGeneratedUsernameExperiment,
|
||||||
|
getAutoGeneratedUsernameExperimentVariation,
|
||||||
|
NOT_INITIALIZED,
|
||||||
|
} from './helper';
|
||||||
|
import { COMPLETE_STATE } from '../../../data/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This hook returns activates multi step registration experiment and returns the experiment
|
||||||
|
* variation for the user.
|
||||||
|
*/
|
||||||
|
const useAutoGeneratedUsernameExperimentVariation = (
|
||||||
|
initExpVariation,
|
||||||
|
registrationEmbedded,
|
||||||
|
tpaHint,
|
||||||
|
currentProvider,
|
||||||
|
thirdPartyAuthApiStatus,
|
||||||
|
) => {
|
||||||
|
const [variation, setVariation] = useState(initExpVariation);
|
||||||
|
useEffect(() => {
|
||||||
|
if (initExpVariation || registrationEmbedded || !!tpaHint || !!currentProvider
|
||||||
|
|| thirdPartyAuthApiStatus !== COMPLETE_STATE) {
|
||||||
|
return variation;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVariation = () => {
|
||||||
|
const expVariation = getAutoGeneratedUsernameExperimentVariation();
|
||||||
|
if (expVariation) {
|
||||||
|
setVariation(expVariation);
|
||||||
|
} else {
|
||||||
|
// This is to handle the case when user dont get variation for some reason, the register page
|
||||||
|
// shows unlimited spinner.
|
||||||
|
setVariation(NOT_INITIALIZED);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
activateAutoGeneratedUsernameExperiment();
|
||||||
|
|
||||||
|
const timer = setTimeout(getVariation, 300);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
};
|
||||||
|
}, [ // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
initExpVariation, currentProvider, registrationEmbedded, thirdPartyAuthApiStatus, tpaHint,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return variation;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useAutoGeneratedUsernameExperimentVariation;
|
||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
|
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
|
||||||
REGISTER_FORM_VALIDATIONS,
|
REGISTER_FORM_VALIDATIONS,
|
||||||
REGISTER_NEW_USER,
|
REGISTER_NEW_USER,
|
||||||
|
REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
|
||||||
REGISTER_SET_COUNTRY_CODE,
|
REGISTER_SET_COUNTRY_CODE,
|
||||||
REGISTER_SET_EMAIL_SUGGESTIONS,
|
REGISTER_SET_EMAIL_SUGGESTIONS,
|
||||||
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
|
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
|
||||||
@@ -39,6 +40,7 @@ export const defaultState = {
|
|||||||
usernameSuggestions: [],
|
usernameSuggestions: [],
|
||||||
validationApiRateLimited: false,
|
validationApiRateLimited: false,
|
||||||
shouldBackupState: false,
|
shouldBackupState: false,
|
||||||
|
autoGeneratedUsernameExperimentVariation: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const reducer = (state = defaultState, action = {}) => {
|
const reducer = (state = defaultState, action = {}) => {
|
||||||
@@ -55,6 +57,12 @@ const reducer = (state = defaultState, action = {}) => {
|
|||||||
registrationFormData: { ...action.payload },
|
registrationFormData: { ...action.payload },
|
||||||
userPipelineDataLoaded: state.userPipelineDataLoaded,
|
userPipelineDataLoaded: state.userPipelineDataLoaded,
|
||||||
};
|
};
|
||||||
|
case REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA: {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
autoGeneratedUsernameExperimentVariation: action.payload.autoGeneratedRegExpVariation,
|
||||||
|
};
|
||||||
|
}
|
||||||
case REGISTER_NEW_USER.BEGIN:
|
case REGISTER_NEW_USER.BEGIN:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ describe('Registration Reducer Tests', () => {
|
|||||||
usernameSuggestions: [],
|
usernameSuggestions: [],
|
||||||
validationApiRateLimited: false,
|
validationApiRateLimited: false,
|
||||||
shouldBackupState: false,
|
shouldBackupState: false,
|
||||||
|
autoGeneratedUsernameExperimentVariation: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should return the initial state', () => {
|
it('should return the initial state', () => {
|
||||||
|
|||||||
77
src/register/data/tests/utils.test.js
Normal file
77
src/register/data/tests/utils.test.js
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { isFormValid } from '../utils';
|
||||||
|
|
||||||
|
describe('Payload validation', () => {
|
||||||
|
let formatMessage;
|
||||||
|
let configurableFormFields;
|
||||||
|
let fieldDescriptions;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
formatMessage = jest.fn(msg => msg);
|
||||||
|
configurableFormFields = {
|
||||||
|
confirm_email: true,
|
||||||
|
};
|
||||||
|
fieldDescriptions = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates name field correctly', () => {
|
||||||
|
const payload = { name: ' ' };
|
||||||
|
const errors = {};
|
||||||
|
const { isValid, fieldErrors } = isFormValid(
|
||||||
|
payload,
|
||||||
|
errors,
|
||||||
|
configurableFormFields,
|
||||||
|
fieldDescriptions,
|
||||||
|
formatMessage);
|
||||||
|
|
||||||
|
expect(fieldErrors.name).toBeDefined();
|
||||||
|
expect(isValid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates email field correctly', () => {
|
||||||
|
const payload = { email: 'invalid-email' };
|
||||||
|
const errors = {};
|
||||||
|
const { isValid, fieldErrors } = isFormValid(
|
||||||
|
payload, errors, configurableFormFields, fieldDescriptions, formatMessage);
|
||||||
|
|
||||||
|
expect(fieldErrors.email).toBeDefined();
|
||||||
|
expect(isValid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates username field correctly', () => {
|
||||||
|
const payload = { username: 'invalid username' };
|
||||||
|
const errors = {};
|
||||||
|
const { isValid, fieldErrors } = isFormValid(
|
||||||
|
payload, errors, configurableFormFields, fieldDescriptions, formatMessage);
|
||||||
|
|
||||||
|
expect(fieldErrors.username).toBeDefined();
|
||||||
|
expect(isValid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates password field correctly', () => {
|
||||||
|
const payload = { password: 'short' };
|
||||||
|
const errors = {};
|
||||||
|
const { isValid, fieldErrors } = isFormValid(
|
||||||
|
payload, errors, configurableFormFields, fieldDescriptions, formatMessage);
|
||||||
|
|
||||||
|
expect(fieldErrors.password).toBeDefined();
|
||||||
|
expect(isValid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('validates multiple fields correctly', () => {
|
||||||
|
const payload = {
|
||||||
|
name: 'InvalidName!',
|
||||||
|
email: 'invalid-email',
|
||||||
|
username: 'invalid username',
|
||||||
|
password: 'short',
|
||||||
|
};
|
||||||
|
const errors = {};
|
||||||
|
const { isValid, fieldErrors } = isFormValid(
|
||||||
|
payload, errors, configurableFormFields, fieldDescriptions, formatMessage);
|
||||||
|
|
||||||
|
expect(fieldErrors.name).toBeDefined();
|
||||||
|
expect(fieldErrors.email).toBeDefined();
|
||||||
|
expect(fieldErrors.username).toBeDefined();
|
||||||
|
expect(fieldErrors.password).toBeDefined();
|
||||||
|
expect(isValid).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -59,6 +59,7 @@ export const isFormValid = (
|
|||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
emailSuggestion = suggestion;
|
emailSuggestion = suggestion;
|
||||||
|
if (fieldErrors.email) { isValid = false; }
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'username':
|
case 'username':
|
||||||
@@ -125,8 +126,8 @@ export const prepareRegistrationPayload = (
|
|||||||
delete payload.marketingEmailsOptIn;
|
delete payload.marketingEmailsOptIn;
|
||||||
}
|
}
|
||||||
|
|
||||||
payload = snakeCaseObject(payload);
|
|
||||||
payload.totalRegistrationTime = totalRegistrationTime;
|
payload.totalRegistrationTime = totalRegistrationTime;
|
||||||
|
payload = snakeCaseObject(payload);
|
||||||
|
|
||||||
// add query params to the payload
|
// add query params to the payload
|
||||||
payload = { ...payload, ...queryParams };
|
payload = { ...payload, ...queryParams };
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||||||
|
|
||||||
import { resetPassword, validateToken } from './data/actions';
|
import { resetPassword, validateToken } from './data/actions';
|
||||||
import {
|
import {
|
||||||
FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, TOKEN_STATE,
|
FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, SUCCESS, TOKEN_STATE,
|
||||||
} from './data/constants';
|
} from './data/constants';
|
||||||
import { resetPasswordResultSelector } from './data/selectors';
|
import { resetPasswordResultSelector } from './data/selectors';
|
||||||
import { validatePassword } from './data/service';
|
import { validatePassword } from './data/service';
|
||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
LETTER_REGEX, LOGIN_PAGE, NUMBER_REGEX, RESET_PAGE,
|
LETTER_REGEX, LOGIN_PAGE, NUMBER_REGEX, RESET_PAGE,
|
||||||
} from '../data/constants';
|
} from '../data/constants';
|
||||||
import { getAllPossibleQueryParams, updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
import { getAllPossibleQueryParams, updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||||
|
import { trackPasswordResetSuccess, trackResetPasswordPageViewed } from '../tracking/trackers/reset-password';
|
||||||
|
|
||||||
const ResetPasswordPage = (props) => {
|
const ResetPasswordPage = (props) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@@ -42,6 +43,15 @@ const ResetPasswordPage = (props) => {
|
|||||||
const { token } = useParams();
|
const { token } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.status === TOKEN_STATE.VALID) {
|
||||||
|
trackResetPasswordPageViewed();
|
||||||
|
}
|
||||||
|
if (props.status === SUCCESS) {
|
||||||
|
trackPasswordResetSuccess();
|
||||||
|
}
|
||||||
|
}, [props.status]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) {
|
if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) {
|
||||||
setErrorCode(props.status);
|
setErrorCode(props.status);
|
||||||
@@ -139,7 +149,7 @@ const ResetPasswordPage = (props) => {
|
|||||||
}
|
}
|
||||||
} else if (props.status === PASSWORD_RESET_ERROR) {
|
} else if (props.status === PASSWORD_RESET_ERROR) {
|
||||||
navigate(updatePathWithQueryParams(RESET_PAGE));
|
navigate(updatePathWithQueryParams(RESET_PAGE));
|
||||||
} else if (props.status === 'success') {
|
} else if (props.status === SUCCESS) {
|
||||||
navigate(updatePathWithQueryParams(LOGIN_PAGE));
|
navigate(updatePathWithQueryParams(LOGIN_PAGE));
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ import ResetPasswordPage from '../ResetPasswordPage';
|
|||||||
const mockedNavigator = jest.fn();
|
const mockedNavigator = jest.fn();
|
||||||
const token = '1c-bmjdkc-5e60e084cf8113048ca7';
|
const token = '1c-bmjdkc-5e60e084cf8113048ca7';
|
||||||
|
|
||||||
|
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||||
|
sendPageEvent: jest.fn(),
|
||||||
|
sendTrackEvent: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/auth');
|
jest.mock('@edx/frontend-platform/auth');
|
||||||
jest.mock('react-router-dom', () => ({
|
jest.mock('react-router-dom', () => ({
|
||||||
...(jest.requireActual('react-router-dom')),
|
...(jest.requireActual('react-router-dom')),
|
||||||
|
|||||||
22
src/tracking/trackers/forgotpassword.js
Normal file
22
src/tracking/trackers/forgotpassword.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
|
||||||
|
|
||||||
|
export const eventNames = {
|
||||||
|
loginAndRegistration: 'login_and_registration',
|
||||||
|
forgotPasswordPageViewed: 'edx.bi.password_reset_form.viewed',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const categories = {
|
||||||
|
userEngagement: 'user-engagement',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event tracker for forgot password page viewed
|
||||||
|
export const trackForgotPasswordPageViewed = () => createEventTracker(
|
||||||
|
eventNames.forgotPasswordPageViewed,
|
||||||
|
{
|
||||||
|
category: categories.userEngagement,
|
||||||
|
},
|
||||||
|
)();
|
||||||
|
|
||||||
|
export const trackForgotPasswordPageEvent = () => {
|
||||||
|
createPageEventTracker(eventNames.loginAndRegistration, 'forgot-password')();
|
||||||
|
};
|
||||||
29
src/tracking/trackers/login.js
Normal file
29
src/tracking/trackers/login.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
|
||||||
|
|
||||||
|
export const eventNames = {
|
||||||
|
forgotPasswordLinkClicked: 'edx.bi.password-reset_form.toggled',
|
||||||
|
loginAndRegistration: 'login_and_registration',
|
||||||
|
registerFormToggled: 'edx.bi.register_form.toggled',
|
||||||
|
loginSuccess: 'edx.bi.user.account.authenticated.client',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const categories = {
|
||||||
|
userEngagement: 'user-engagement',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event tracker for Forgot Password link click
|
||||||
|
export const trackForgotPasswordLinkClick = () => createEventTracker(
|
||||||
|
eventNames.forgotPasswordLinkClicked,
|
||||||
|
{ category: categories.userEngagement },
|
||||||
|
)();
|
||||||
|
|
||||||
|
// Tracks the login page event.
|
||||||
|
export const trackLoginPageViewed = () => {
|
||||||
|
createPageEventTracker(eventNames.loginAndRegistration, 'login')();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tracks the login sucess event.
|
||||||
|
export const trackLoginSuccess = () => createEventTracker(
|
||||||
|
eventNames.loginSuccess,
|
||||||
|
{},
|
||||||
|
)();
|
||||||
37
src/tracking/trackers/progressive-profiling.js
Normal file
37
src/tracking/trackers/progressive-profiling.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
|
||||||
|
|
||||||
|
export const eventNames = {
|
||||||
|
progressiveProfilingSubmitClick: 'edx.bi.welcome.page.submit.clicked',
|
||||||
|
progressiveProfilingSkipLinkClick: 'edx.bi.welcome.page.skip.link.clicked',
|
||||||
|
disablePostRegistrationRecommendations: 'edx.bi.user.recommendations.not.enabled',
|
||||||
|
progressiveProfilingSupportLinkCLick: 'edx.bi.welcome.page.support.link.clicked',
|
||||||
|
loginAndRegistration: 'login_and_registration',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event link tracker for Progressive profiling skip button click
|
||||||
|
export const trackProgressiveProfilingSkipLinkClick = evenProperties => createEventTracker(
|
||||||
|
eventNames.progressiveProfilingSkipLinkClick, { ...evenProperties },
|
||||||
|
)();
|
||||||
|
|
||||||
|
// Event tracker for progressive profiling submit button click
|
||||||
|
export const trackProgressiveProfilingSubmitClick = (evenProperties) => createEventTracker(
|
||||||
|
eventNames.progressiveProfilingSubmitClick,
|
||||||
|
{ ...evenProperties },
|
||||||
|
)();
|
||||||
|
|
||||||
|
// Event tracker for progressive profiling submit button click
|
||||||
|
export const trackDisablePostRegistrationRecommendations = (evenProperties) => createEventTracker(
|
||||||
|
eventNames.disablePostRegistrationRecommendations,
|
||||||
|
{ ...evenProperties },
|
||||||
|
)();
|
||||||
|
|
||||||
|
// Tracks the progressive profiling page event.
|
||||||
|
export const trackProgressiveProfilingPageViewed = () => {
|
||||||
|
createPageEventTracker(eventNames.loginAndRegistration, 'welcome')();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tracks the progressive profiling spport link click.
|
||||||
|
export const trackProgressiveProfilingSupportLinkCLick = () => createEventTracker(
|
||||||
|
eventNames.progressiveProfilingSupportLinkCLick,
|
||||||
|
{},
|
||||||
|
)();
|
||||||
22
src/tracking/trackers/register.js
Normal file
22
src/tracking/trackers/register.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
|
||||||
|
|
||||||
|
export const eventNames = {
|
||||||
|
loginAndRegistration: 'login_and_registration',
|
||||||
|
registrationSuccess: 'edx.bi.user.account.registered.client',
|
||||||
|
loginFormToggled: 'edx.bi.login_form.toggled',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const categories = {
|
||||||
|
userEngagement: 'user-engagement',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event tracker for successful registration
|
||||||
|
export const trackRegistrationSuccess = () => createEventTracker(
|
||||||
|
eventNames.registrationSuccess,
|
||||||
|
{},
|
||||||
|
)();
|
||||||
|
|
||||||
|
// Tracks the progressive profiling page event.
|
||||||
|
export const trackRegistrationPageViewed = () => {
|
||||||
|
createPageEventTracker(eventNames.loginAndRegistration, 'register')();
|
||||||
|
};
|
||||||
14
src/tracking/trackers/reset-password.js
Normal file
14
src/tracking/trackers/reset-password.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createEventTracker, createPageEventTracker } from '../../data/segment/utils';
|
||||||
|
|
||||||
|
export const eventNames = {
|
||||||
|
loginAndRegistration: 'login_and_registration',
|
||||||
|
resetPasswordSuccess: 'edx.bi.user.password.reset.success',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trackResetPasswordPageViewed = () => {
|
||||||
|
createPageEventTracker(eventNames.loginAndRegistration, 'reset-password')();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trackPasswordResetSuccess = () => {
|
||||||
|
createEventTracker(eventNames.resetPasswordSuccess, {})();
|
||||||
|
};
|
||||||
37
src/tracking/trackers/tests/forgot-password.test.jsx
Normal file
37
src/tracking/trackers/tests/forgot-password.test.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
|
||||||
|
import {
|
||||||
|
categories,
|
||||||
|
eventNames,
|
||||||
|
trackForgotPasswordPageEvent,
|
||||||
|
trackForgotPasswordPageViewed,
|
||||||
|
} from '../forgotpassword';
|
||||||
|
|
||||||
|
// Mock createEventTracker function
|
||||||
|
jest.mock('../../../data/segment/utils', () => ({
|
||||||
|
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
|
||||||
|
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Tracking Functions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fire trackForgotPasswordPageEvent', () => {
|
||||||
|
trackForgotPasswordPageEvent();
|
||||||
|
|
||||||
|
expect(createPageEventTracker).toHaveBeenCalledWith(
|
||||||
|
eventNames.loginAndRegistration,
|
||||||
|
'forgot-password',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fire forgotPasswordPageViewedEvent', () => {
|
||||||
|
trackForgotPasswordPageViewed();
|
||||||
|
|
||||||
|
expect(createEventTracker).toHaveBeenCalledWith(
|
||||||
|
eventNames.forgotPasswordPageViewed,
|
||||||
|
{ category: categories.userEngagement },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
37
src/tracking/trackers/tests/login.test.jsx
Normal file
37
src/tracking/trackers/tests/login.test.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
|
||||||
|
import {
|
||||||
|
categories,
|
||||||
|
eventNames,
|
||||||
|
trackForgotPasswordLinkClick,
|
||||||
|
trackLoginPageViewed,
|
||||||
|
} from '../login';
|
||||||
|
|
||||||
|
// Mock createEventTracker function
|
||||||
|
jest.mock('../../../data/segment/utils', () => ({
|
||||||
|
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
|
||||||
|
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Tracking Functions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trackForgotPasswordLinkClick function', () => {
|
||||||
|
trackForgotPasswordLinkClick();
|
||||||
|
|
||||||
|
expect(createEventTracker).toHaveBeenCalledWith(
|
||||||
|
eventNames.forgotPasswordLinkClicked,
|
||||||
|
{ category: categories.userEngagement },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('trackLoginPageEvent function', () => {
|
||||||
|
trackLoginPageViewed();
|
||||||
|
|
||||||
|
expect(createPageEventTracker).toHaveBeenCalledWith(
|
||||||
|
eventNames.loginAndRegistration,
|
||||||
|
'login',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
37
src/tracking/trackers/tests/progressive-profiling.test.jsx
Normal file
37
src/tracking/trackers/tests/progressive-profiling.test.jsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
|
||||||
|
import {
|
||||||
|
eventNames,
|
||||||
|
trackProgressiveProfilingPageViewed,
|
||||||
|
trackProgressiveProfilingSkipLinkClick,
|
||||||
|
} from '../progressive-profiling';
|
||||||
|
|
||||||
|
// Mock createEventTracker function
|
||||||
|
jest.mock('../../../data/segment/utils', () => ({
|
||||||
|
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
|
||||||
|
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
|
||||||
|
createLinkTracker: jest.fn().mockImplementation(() => jest.fn()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Tracking Functions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fire trackProgressiveProfilingSkipLinkClickEvent', () => {
|
||||||
|
trackProgressiveProfilingSkipLinkClick();
|
||||||
|
|
||||||
|
expect(createEventTracker).toHaveBeenCalledWith(
|
||||||
|
eventNames.progressiveProfilingSkipLinkClick,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fire trackProgressiveProfilingPageEvent', () => {
|
||||||
|
trackProgressiveProfilingPageViewed();
|
||||||
|
|
||||||
|
expect(createPageEventTracker).toHaveBeenCalledWith(
|
||||||
|
eventNames.loginAndRegistration,
|
||||||
|
'welcome',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
36
src/tracking/trackers/tests/register.test.jsx
Normal file
36
src/tracking/trackers/tests/register.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { createEventTracker, createPageEventTracker } from '../../../data/segment/utils';
|
||||||
|
import {
|
||||||
|
eventNames,
|
||||||
|
trackRegistrationPageViewed,
|
||||||
|
trackRegistrationSuccess,
|
||||||
|
} from '../register';
|
||||||
|
|
||||||
|
// Mock createEventTracker function
|
||||||
|
jest.mock('../../../data/segment/utils', () => ({
|
||||||
|
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
|
||||||
|
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Tracking Functions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fire registrationSuccessEvent', () => {
|
||||||
|
trackRegistrationSuccess();
|
||||||
|
|
||||||
|
expect(createEventTracker).toHaveBeenCalledWith(
|
||||||
|
eventNames.registrationSuccess,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fire trackRegistrationPageEvent', () => {
|
||||||
|
trackRegistrationPageViewed();
|
||||||
|
|
||||||
|
expect(createPageEventTracker).toHaveBeenCalledWith(
|
||||||
|
eventNames.loginAndRegistration,
|
||||||
|
'register',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
26
src/tracking/trackers/tests/reset-password.test.jsx
Normal file
26
src/tracking/trackers/tests/reset-password.test.jsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createPageEventTracker } from '../../../data/segment/utils';
|
||||||
|
import {
|
||||||
|
eventNames,
|
||||||
|
trackResetPasswordPageViewed,
|
||||||
|
} from '../reset-password';
|
||||||
|
|
||||||
|
// Mock createEventTracker function
|
||||||
|
jest.mock('../../../data/segment/utils', () => ({
|
||||||
|
createEventTracker: jest.fn().mockImplementation(() => jest.fn()),
|
||||||
|
createPageEventTracker: jest.fn().mockImplementation(() => jest.fn()),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Tracking Functions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fire trackResettPasswordPageEvent', () => {
|
||||||
|
trackResetPasswordPageViewed();
|
||||||
|
|
||||||
|
expect(createPageEventTracker).toHaveBeenCalledWith(
|
||||||
|
eventNames.loginAndRegistration,
|
||||||
|
'reset-password',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
14
webpack.dev.config.js
Normal file
14
webpack.dev.config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const { createConfig } = require('@openedx/frontend-build');
|
||||||
|
|
||||||
|
const config = createConfig('webpack-dev');
|
||||||
|
|
||||||
|
config.resolve.modules = [
|
||||||
|
path.resolve(__dirname, './src'),
|
||||||
|
'node_modules',
|
||||||
|
];
|
||||||
|
|
||||||
|
config.module.rules[0].exclude = /node_modules\/(?!(fastest-levenshtein|@edx))/;
|
||||||
|
|
||||||
|
module.exports = config;
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
const { createConfig } = require('@openedx/frontend-build');
|
const { createConfig } = require('@openedx/frontend-build');
|
||||||
|
|
||||||
const config = createConfig('webpack-prod');
|
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.module.rules[0].exclude = /node_modules\/(?!(fastest-levenshtein|@edx))/;
|
||||||
|
|
||||||
module.exports = config;
|
module.exports = config;
|
||||||
|
|||||||
Reference in New Issue
Block a user