Compare commits

..

61 Commits

Author SHA1 Message Date
Adolfo R. Brandes
0e94124d74 Merge pull request #1470 from regisb/regisb/no-husky
chore: remove husky 🪓🐶
2025-04-14 14:12:48 -03:00
Régis Behmo
af7edd8a3f chore: remove husky 🪓🐶
We remove husky, which is triggering pre-push git hooks, including
running "npm lint". This is causing failures when building Docker
images, because "npm clean-install --omit=dev" automatically triggers "npm
prepare", which attemps to run "husky". But husky is not listed in the
build dependencies, only in devDependencies. As a consequence, package
installation is failing with the following error:

        14.13 > @edx/frontend-app-ora-grading@0.0.1 prepare
        14.13 > husky install
        14.13
        14.15 sh: 1: husky: not found

Similar to: https://github.com/openedx/frontend-app-learning/pull/1622
2025-04-14 18:53:56 +02:00
renovate[bot]
9323f119c8 chore(deps): update dependency @openedx/frontend-build to v14.5.0 (#1474)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-11 18:08:35 +00:00
Brian Smith
38a1924c6a feat: upgrade to react 18 (#1466) 2025-04-04 10:17:53 -04:00
renovate[bot]
2d7303009f fix(deps): update dependency @edx/frontend-platform to v8.3.4 (#1471)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-04-02 18:46:51 +00:00
Brian Smith
dfcb94a831 fix: properly set background color for floating labels (#1468) 2025-04-01 12:51:38 -04:00
renovate[bot]
520dd6ed6b chore(deps): update dependency @openedx/frontend-build to v14.4.1 (#1464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-28 14:49:39 +00:00
renovate[bot]
1b32dbfa19 fix(deps): update dependency @openedx/paragon to v22.16.2 (#1462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 12:28:50 +00:00
renovate[bot]
f7d9bdb5b5 chore(deps): update dependency babel-plugin-formatjs to v10.5.37 (#1461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-26 06:50:31 +00:00
sarina
063ec80cde docs: Add instructions on using Tutor for development 2025-03-25 14:40:55 -03:00
sarina
3cbb134c3a docs: Update migrated edx.rtd links to docs.openedx.org 2025-03-25 14:40:55 -03:00
Brian Smith
059a60d1c8 chore(deps): update @openedx dependencies to versions that support React 18 (#1458) 2025-03-25 12:22:43 -04:00
renovate[bot]
c88d701271 fix(deps): update dependency @openedx/paragon to v22.16.1 (#1460)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 22:21:52 +00:00
renovate[bot]
33b98b356b chore(deps): update dependency @openedx/frontend-build to v14.3.3 (#1459)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 20:18:14 +00:00
renovate[bot]
1f81699af4 fix(deps): update dependency algoliasearch-helper to v3.24.3 (#1457)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 13:55:06 +00:00
renovate[bot]
13aa77fc70 fix(deps): update dependency @edx/frontend-platform to v8.3.3 (#1456)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 07:13:09 +00:00
renovate[bot]
66531831b7 chore(deps): update dependency babel-plugin-formatjs to v10.5.36 (#1455)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-24 03:42:16 +00:00
renovate[bot]
846d3f0662 fix(deps): update dependency @edx/frontend-platform to v8.3.2 (#1454)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-21 19:54:09 +00:00
renovate[bot]
f78e84ee0a fix(deps): update dependency @edx/frontend-platform to v8.3.1 (#1449)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-07 23:16:01 +00:00
renovate[bot]
2d27da2391 fix(deps): update dependency @openedx/paragon to v22.16.0 (#1446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 18:03:14 +00:00
renovate[bot]
78413be34a chore(deps): update dependency @openedx/frontend-build to v14.3.2 (#1444)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-05 01:30:17 +00:00
renovate[bot]
88866a39c1 fix(deps): update dependency algoliasearch-helper to v3.24.2 (#1442)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 16:02:35 +00:00
renovate[bot]
dc9699c033 fix(deps): update dependency @edx/frontend-platform to v8.3.0 (#1440)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-04 02:44:20 +00:00
renovate[bot]
00a0e27062 fix(deps): update dependency core-js to v3.41.0 (#1439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-03-03 03:32:41 +00:00
renovate[bot]
6839afcf3c fix(deps): update react-router monorepo to v6.30.0 (#1435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-27 18:41:40 +00:00
renovate[bot]
1cd9c58c1a fix(deps): update dependency @openedx/paragon to v22.15.3 (#1432)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-25 22:14:39 +00:00
renovate[bot]
5d481a93c7 fix(deps): update dependency @edx/frontend-platform to v8.2.1 (#1429)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 22:56:35 +00:00
renovate[bot]
438d1fcfa7 fix(deps): update dependency @edx/frontend-platform to v8.2.0 (#1428)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-21 03:16:30 +00:00
renovate[bot]
bc912ce139 chore(deps): update dependency @openedx/frontend-build to v14.3.1 (#1427)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 23:02:47 +00:00
renovate[bot]
ab1c2d5379 fix(deps): update dependency @openedx/paragon to v22.15.2 (#1426)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-20 15:27:23 +00:00
renovate[bot]
c109f6e771 chore(deps): update dependency babel-plugin-formatjs to v10.5.35 (#1421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-10 01:03:12 +00:00
renovate[bot]
8976647190 fix(deps): update dependency @openedx/paragon to v22.15.1 (#1420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 21:51:26 +00:00
renovate[bot]
cb051a83ad fix(deps): update dependency @openedx/paragon to v22.15.0 (#1419)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-31 17:40:38 +00:00
renovate[bot]
1b8aec5709 fix(deps): update react-router monorepo to v6.29.0 (#1417)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-30 17:54:09 +00:00
renovate[bot]
9a68e95fcc fix(deps): update dependency algoliasearch-helper to v3.24.1 (#1414)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-28 12:31:35 +00:00
renovate[bot]
c90980afb0 fix(deps): update dependency @openedx/paragon to v22.14.0 (#1413)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 22:06:09 +00:00
renovate[bot]
abb8ae5085 fix(deps): update dependency algoliasearch-helper to v3.23.1 (#1412)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 18:52:31 +00:00
renovate[bot]
8bb7462098 chore(deps): update dependency babel-plugin-formatjs to v10.5.34 (#1411)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 17:48:45 +00:00
renovate[bot]
b4a5397ba1 chore(deps): update dependency babel-plugin-formatjs to v10.5.33 (#1410)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-20 05:32:02 +00:00
Feanil Patel
a43c620dc4 Merge pull request #1406 from salman2013/salman/update-catalog-info-file
Update catalog-info file for release data
2025-01-17 10:42:49 -05:00
salman2013
93d11b8485 chore: Update catalog-info file for release data and remove openedx.yaml 2025-01-17 10:39:31 -05:00
renovate[bot]
68e13d4daf chore(deps): update dependency babel-plugin-formatjs to v10.5.31 (#1409)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 09:05:56 +00:00
renovate[bot]
f6617935e3 fix(deps): update react-router monorepo to v6.28.2 (#1408)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-16 18:07:00 +00:00
renovate[bot]
5f4591c046 chore(deps): update dependency @edx/browserslist-config to v1.5.0 (#1407)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-15 16:22:56 +00:00
renovate[bot]
ae52a8cb65 fix(deps): update dependency algoliasearch-helper to v3.23.0 (#1405)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 18:38:26 +00:00
renovate[bot]
b8df66ad23 fix(deps): update dependency core-js to v3.40.0 (#1404)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-07 20:46:54 +00:00
renovate[bot]
923776ab96 fix(deps): update dependency @edx/frontend-platform to v8.1.5 (#1403)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-06 12:10:02 +00:00
renovate[bot]
f170f5e3f0 chore(deps): update dependency babel-plugin-formatjs to v10.5.30 (#1402)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-02 18:14:00 +00:00
renovate[bot]
730875ceb2 fix(deps): update dependency @edx/frontend-platform to v8.1.4 (#1401)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-30 09:15:59 +00:00
renovate[bot]
812350d24a fix(deps): update react-router monorepo to v6.28.1 (#1399)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-20 21:25:00 +00:00
renovate[bot]
6879bacb89 fix(deps): update dependency @openedx/paragon to v22.13.0 (#1398)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 17:20:36 +00:00
renovate[bot]
9b2b0f2019 fix(deps): update font awesome to v6.7.2 (#1397)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 22:17:55 +00:00
renovate[bot]
87884f2d91 fix(deps): update dependency @openedx/paragon to v22.12.0 (#1395)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-16 01:22:07 +00:00
renovate[bot]
3e20fcae57 fix(deps): update dependency @openedx/paragon to v22.11.2 (#1391)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-10 01:57:22 +00:00
renovate[bot]
173896811d chore(deps): update dependency @edx/browserslist-config to v1.4.0 (#1394)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 23:47:31 +00:00
renovate[bot]
7af4a08bd9 fix(deps): update dependency algoliasearch-helper to v3.22.6 (#1393)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 18:57:08 +00:00
renovate[bot]
6c12b3b034 fix(deps): update dependency @edx/frontend-platform to v8.1.3 (#1392)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 15:21:14 +00:00
renovate[bot]
5304085cd8 chore(deps): update dependency babel-plugin-formatjs to v10.5.29 (#1389)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 09:02:19 +00:00
renovate[bot]
3cd9ae130c chore(deps): update dependency @openedx/frontend-build to v14.2.2 (#1390)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 05:41:46 +00:00
renovate[bot]
28ad2c2cf6 chore(deps): update dependency @openedx/frontend-build to v14.2.1 (#1388)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-09 00:46:21 +00:00
renovate[bot]
3e889df109 chore(deps): update dependency @edx/browserslist-config to v1.3.0 (#1386)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-06 02:36:06 +00:00
83 changed files with 5772 additions and 3383 deletions

3
.env
View File

@@ -16,9 +16,6 @@ SITE_NAME=null
INFO_EMAIL=''
# ***** Cookies *****
USER_RETENTION_COOKIE_NAME=null
# ***** Cohesion Keys *****
COHESION_WRITE_KEY=''
COHESION_SOURCE_KEY=''
# ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK=''
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null

View File

@@ -25,9 +25,6 @@ ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
# ***** Cookies *****
SESSION_COOKIE_DOMAIN='localhost'
USER_INFO_COOKIE_NAME='edx-user-info'
# ***** Cohesion Keys *****
COHESION_WRITE_KEY=''
COHESION_SOURCE_KEY=''
# ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
TOS_AND_HONOR_CODE='http://localhost:18000/honor'

View File

@@ -18,5 +18,3 @@ SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
APP_ID=''
MFE_CONFIG_API_URL=''
COHESION_WRITE_KEY=''
COHESION_SOURCE_KEY=''

View File

@@ -1,7 +1,7 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('eslint', {
module.exports = createConfig('eslint', {
rules: {
// Temporarily update the 'indent', 'template-curly-spacing' and
// 'no-multiple-empty-lines' rules since they are causing eslint
@@ -50,14 +50,3 @@ const config = createConfig('eslint', {
'function-paren-newline': 'off',
},
});
config.settings = {
'import/resolver': {
node: {
paths: ['src', 'node_modules'],
extensions: ['.js', '.jsx'],
},
},
};
module.exports = config;

View File

@@ -29,7 +29,13 @@ Getting Started
Installation
============
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
`Tutor`_ is currently recommended as a development environment for your new MFE. Please refer to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
Devstack (Deprecated) instructions
==================================
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
@@ -51,7 +57,7 @@ This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see
Environment Variables/Setup Notes
=================================
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
The authentication micro-frontend also requires the following additional variable:
@@ -142,13 +148,13 @@ Furthermore, there are several edX-specific environment variables that enable in
- ``true`` | ``''`` (empty strings are falsy)
For more information see the document: `Micro-frontend applications in Open
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
edX <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
How To Contribute
=================
Contributions are very welcome, and strongly encouraged! We've
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
put together `some documentation that describes our contribution process <https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html>`_.
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.

View File

@@ -12,6 +12,7 @@ metadata:
icon: 'Article'
annotations:
openedx.org/arch-interest-groups: ""
openedx.org/release: "master"
spec:
owner: group:2u-infinity
type: 'service'

View File

@@ -3,7 +3,7 @@ Enable Social Auth Locally
Please follow the steps below to enable social auth (SSO) locally.
1. Follow `Enabling Third Party Authentication <https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/tpa/index.html>`_ for backend configuration.
1. Follow `Enabling Third Party Authentication <https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/tpa/index.html>`_ for backend configuration.
2. Authn has a component for rendering Social Auth providers at frontend which goes through each provider.

View File

@@ -1,60 +0,0 @@
/*
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: '',
};

View File

@@ -1,8 +0,0 @@
# This file describes this Open edX repo, as described in OEP-2:
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
nick: Authn MFE
oeps: {}
owner: openedx/2u-infinity
openedx-release:
ref: master

7280
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,15 +13,12 @@
"build": "fedx-scripts webpack",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/authn/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
"test": "fedx-scripts jest --coverage --passWithNoTests"
},
"husky": {
"hooks": {
"pre-commit": "npm run lint"
}
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
@@ -33,35 +30,32 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-platform": "^8.0.0",
"@edx/frontend-platform": "^8.3.1",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.7.1",
"@fortawesome/free-brands-svg-icons": "6.7.1",
"@fortawesome/free-solid-svg-icons": "6.7.1",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-plugin-framework": "^1.3.0",
"@openedx/paragon": "^22.1.1",
"@openedx/paragon": "^22.16.0",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.3.0",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"@testing-library/react": "^16.2.0",
"algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.14.0",
"classnames": "2.5.1",
"core-js": "3.39.0",
"core-js": "3.41.0",
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.5",
"prop-types": "15.8.1",
"query-string": "7.1.3",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-error-boundary": "^4.0.13",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.5.0",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router": "6.28.0",
"react-router-dom": "6.28.0",
"react-router": "6.30.0",
"react-router-dom": "6.30.0",
"react-zendesk": "^0.1.13",
"redux": "4.2.1",
"redux-logger": "3.0.6",
@@ -75,13 +69,12 @@
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/reactifex": "1.1.0",
"@openedx/frontend-build": "^14.0.3",
"babel-plugin-formatjs": "10.5.26",
"@openedx/frontend-build": "^14.4.2",
"babel-plugin-formatjs": "10.5.37",
"eslint-plugin-import": "2.31.0",
"glob": "7.2.3",
"history": "5.3.0",
"husky": "9.1.7",
"jest": "29.7.0",
"react-test-renderer": "^17.0.2"
"react-test-renderer": "^18.3.1"
}
}

View File

@@ -1,30 +1,25 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en-us">
<head>
<title>
<%= (process.env.SITE_NAME && process.env.SITE_NAME !='null' ) ?
'Authentication | ' + process.env.SITE_NAME : 'Authentication' %>
</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="shortcut icon"
href="<%=htmlWebpackPlugin.options.FAVICON_URL%>"
type="image/x-icon"
/>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<% if (process.env.OPTIMIZELY_URL) { %>
<script src="<%= process.env.OPTIMIZELY_URL %>"></script>
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"></script>
<% } %>
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<% if (process.env.OPTIMIZELY_URL) { %>
<script
src="<%= process.env.OPTIMIZELY_URL %>"
></script>
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
></script>
<% } %>
</head>
<body>
<div id="root"></div>
</body>

View File

@@ -6,7 +6,7 @@ import { Helmet } from 'react-helmet';
import { Navigate, Route, Routes } from 'react-router-dom';
import {
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute,
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
} from './common-components';
import configureStore from './data/configureStore';
import {
@@ -22,90 +22,21 @@ import {
import { updatePathWithQueryParams } from './data/utils';
import { ForgotPasswordPage } from './forgot-password';
import Logistration from './logistration/Logistration';
import MainAppSlot from './plugin-slots/MainAppSlot';
import { ProgressiveProfiling } from './progressive-profiling';
import { RecommendationsPage } from './recommendations';
import { RegistrationPage } from './register';
import { ResetPasswordPage } from './reset-password';
import './index.scss';
import './cohesion/cohesion.scss';
registerIcons();
const MainApp = () => (
<AppProvider store={configureStore()}>
<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" />
</Helmet>
{getConfig().ZENDESK_KEY && <Zendesk />}
<Routes>
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
<Route
@@ -126,7 +57,6 @@ const MainApp = () => (
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
</Routes>
<MainAppSlot />
</AppProvider>
);

View File

@@ -1,4 +0,0 @@
.preampjs [data-preamp],
.fusejs [data-fuse] {
opacity: 0 !important;
}

View File

@@ -1,22 +0,0 @@
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',
};

View File

@@ -1,6 +0,0 @@
export const SET_COHESION_EVENT_ELEMENT_STATES = 'SET_COHESION_EVENT_ELEMENT_STATES';
export const setCohesionEventStates = (eventData) => ({
type: SET_COHESION_EVENT_ELEMENT_STATES,
payload: eventData,
});

View File

@@ -1,17 +0,0 @@
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;
};

View File

@@ -1,24 +0,0 @@
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;

View File

@@ -1,6 +0,0 @@
const mockTagular = () => {
const getTagular = jest.fn();
window.tagular = getTagular;
};
export default mockTagular;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useState } from 'react';
import {
Form, TransitionReplace,
@@ -7,38 +7,6 @@ import PropTypes from 'prop-types';
const FormGroup = (props) => {
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) => {
setHasFocus(true);
@@ -55,7 +23,6 @@ const FormGroup = (props) => {
return (
<Form.Group controlId={props.name} className={props.className} isInvalid={props.errorMessage !== ''}>
<Form.Control
ref={inputRef}
as={props.as}
readOnly={props.readOnly}
type={props.type}
@@ -72,7 +39,6 @@ const FormGroup = (props) => {
controlClassName={props.borderClass}
trailingElement={props.trailingElement}
floatingLabel={props.floatingLabel}
isAutoFill={isAutoFill}
>
{props.options ? props.options() : null}
</Form.Control>

View File

@@ -60,7 +60,7 @@ const InstitutionLogistration = props => {
className="btn nav-item p-0 mb-1 institutions--provider-link"
destination={lmsBaseUrl + provider.loginUrl}
>
{provider?.name}
{provider.name}
</Hyperlink>
</td>
</tr>

View File

@@ -1,15 +1,11 @@
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
import { Navigate } from 'react-router-dom';
import trackCohesionEvent from '../cohesion/trackers';
import {
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
} from '../data/constants';
import setCookie from '../data/utils/cookies';
import { redirectWithDelay } from '../data/utils/dataUtils';
import { setCookie } from '../data/utils';
const RedirectLogistration = (props) => {
const {
@@ -24,16 +20,10 @@ const RedirectLogistration = (props) => {
userId,
registrationEmbedded,
host,
currectProvider,
} = props;
const cohesionEventData = useSelector(state => state.cohesion.eventData);
let finalRedirectUrl = '';
if (success) {
// This event is used by cohesion upon successful login and registration
if (!currectProvider) {
trackCohesionEvent(cohesionEventData);
}
// If we're in a third party auth pipeline, we must complete the pipeline
// once user has successfully logged in. Otherwise, redirect to the specified redirect url.
// Note: For multiple enterprise use case, we need to make sure that user first visits the
@@ -85,7 +75,8 @@ const RedirectLogistration = (props) => {
/>
);
}
redirectWithDelay(finalRedirectUrl);
window.location.href = finalRedirectUrl;
}
return null;
@@ -103,7 +94,6 @@ RedirectLogistration.defaultProps = {
userId: null,
registrationEmbedded: false,
host: '',
currectProvider: '',
};
RedirectLogistration.propTypes = {
@@ -118,7 +108,6 @@ RedirectLogistration.propTypes = {
userId: PropTypes.number,
registrationEmbedded: PropTypes.bool,
host: PropTypes.string,
currectProvider: PropTypes.string,
};
export default RedirectLogistration;

View File

@@ -1,5 +1,4 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -9,35 +8,17 @@ import { Login } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';
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';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl();
const { referrer, socialAuthProviders } = props;
const registrationFields = useSelector(state => state.register.registrationFormData);
function handleSubmit(e, providerName) {
function handleSubmit(e) {
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;
redirectWithDelay(getConfig().LMS_BASE_URL + url);
window.location.href = getConfig().LMS_BASE_URL + url;
}
const socialAuth = socialAuthProviders.map((provider, index) => (
@@ -47,7 +28,7 @@ const SocialAuthProviders = (props) => {
type="button"
className={`btn-social btn-${provider.id} ${index % 2 === 0 ? 'mr-3' : ''}`}
data-provider-url={referrer === LOGIN_PAGE ? provider.loginUrl : provider.registerUrl}
onClick={(event) => handleSubmit(event, provider?.name)}
onClick={handleSubmit}
>
{provider.iconImage ? (
<div aria-hidden="true">

View File

@@ -7,7 +7,6 @@ import PropTypes from 'prop-types';
import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import setCookie from '../data/utils/cookies';
const ThirdPartyAuthAlert = (props) => {
const { formatMessage } = useIntl();
@@ -21,10 +20,7 @@ const ThirdPartyAuthAlert = (props) => {
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
}
if (currentProvider) {
// Setting this cookie to capture marketingEmailsOptIn for SSO flow on the onboarding component
setCookie('ssoPipelineRedirectionDone', true);
} else {
if (!currentProvider) {
return null;
}

View File

@@ -4,8 +4,9 @@ import { getConfig } from '@edx/frontend-platform';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import PropTypes from 'prop-types';
import { RESET_PAGE } from '../data/constants';
import { updatePathWithQueryParams } from '../data/utils';
import {
DEFAULT_REDIRECT_URL,
} from '../data/constants';
/**
* This wrapper redirects the requester to our default redirect url if they are
@@ -24,12 +25,7 @@ const UnAuthOnlyRoute = ({ children }) => {
if (isReady) {
if (authUser && authUser.username) {
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);
global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
return null;
}

View File

@@ -1,79 +0,0 @@
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',
],
],
},
},
};

View File

@@ -1,4 +1,3 @@
import { getConfig } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
@@ -8,7 +7,6 @@ import {
getThirdPartyAuthContextSuccess,
THIRD_PARTY_AUTH_CONTEXT,
} from './actions';
import { progressiveProfilingFields, registerFields } from './constants';
import {
getThirdPartyAuthContext,
} from './service';
@@ -22,16 +20,7 @@ export function* fetchThirdPartyAuthContext(action) {
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
// 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));
}
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);

View File

@@ -1,35 +1,16 @@
import React from 'react';
import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
import configureStore from 'redux-mock-store';
import registerIcons from '../RegisterFaIcons';
import SocialAuthProviders from '../SocialAuthProviders';
registerIcons();
const mockStore = configureStore();
describe('SocialAuthProviders', () => {
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 = {
id: 'oa2-apple-id',
name: 'Apple',
@@ -49,11 +30,11 @@ describe('SocialAuthProviders', () => {
it('should match social auth provider with iconImage snapshot', () => {
props = { socialAuthProviders: [appleProvider, facebookProvider] };
const tree = renderer.create(reduxWrapper(
const tree = renderer.create(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
)).toJSON();
).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -67,11 +48,11 @@ describe('SocialAuthProviders', () => {
}],
};
const tree = renderer.create(reduxWrapper(
const tree = renderer.create(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
)).toJSON();
).toJSON();
expect(tree).toMatchSnapshot();
});
@@ -85,11 +66,11 @@ describe('SocialAuthProviders', () => {
}],
};
const tree = renderer.create(reduxWrapper(
const tree = renderer.create(
<IntlProvider locale="en">
<SocialAuthProviders {...props} />
</IntlProvider>,
)).toJSON();
).toJSON();
expect(tree).toMatchSnapshot();
});

View File

@@ -11,7 +11,6 @@ const configuration = {
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || 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,
// Links
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
@@ -36,7 +35,6 @@ const configuration = {
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
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;

View File

@@ -37,4 +37,3 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
// 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 REDIRECT = 'redirect';
export const APP_NAME = 'authn_mfe';

View File

@@ -1,6 +1,5 @@
import { combineReducers } from 'redux';
import { reducer as cohesionReducer, storeName as cohesionStoreName } from '../cohesion/data/reducers';
import {
reducer as commonComponentsReducer,
storeName as commonComponentsStoreName,
@@ -32,7 +31,6 @@ const createRootReducer = () => combineReducers({
[commonComponentsStoreName]: commonComponentsReducer,
[forgotPasswordStoreName]: forgotPasswordReducer,
[resetPasswordStoreName]: resetPasswordReducer,
[cohesionStoreName]: cohesionReducer,
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
});
export default createRootReducer;

View File

@@ -1,37 +0,0 @@
/* 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);
};

View File

@@ -11,11 +11,3 @@ export default function setCookie(cookieName, cookieValue, cookieExpiry) {
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);
}
}

View File

@@ -81,9 +81,3 @@ export const isHostAvailableInQueryParams = () => {
const queryParams = getAllPossibleQueryParams();
return 'host' in queryParams;
};
export const redirectWithDelay = (redirectUrl) => {
setTimeout(() => {
window.location.href = redirectUrl;
}, 1000);
};

View File

@@ -8,4 +8,4 @@ export {
windowScrollTo,
} from './dataUtils';
export { default as AsyncActionType } from './reduxUtils';
export { default as setCookie, removeCookie } from './cookies';
export { default as setCookie } from './cookies';

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form,
@@ -24,10 +25,6 @@ import BaseContainer from '../base-container';
import { FormGroup } from '../common-components';
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
import {
trackForgotPasswordPageEvent,
trackForgotPasswordPageViewed,
} from '../tracking/trackers/forgotpassword';
const ForgotPasswordPage = (props) => {
const platformName = getConfig().SITE_NAME;
@@ -44,8 +41,8 @@ const ForgotPasswordPage = (props) => {
const navigate = useNavigate();
useEffect(() => {
trackForgotPasswordPageEvent();
trackForgotPasswordPageViewed();
sendPageEvent('login_and_registration', 'reset');
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
}, []);
useEffect(() => {

View File

@@ -1,28 +1,36 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import React from 'react';
import ReactDOM from 'react-dom';
import React, { StrictMode } from 'react';
import {
APP_INIT_ERROR, APP_READY, initialize, mergeConfig, subscribe,
} from '@edx/frontend-platform';
import { ErrorPage } from '@edx/frontend-platform/react';
import { createRoot } from 'react-dom/client';
import configuration from './config';
import messages from './i18n';
import MainApp from './MainApp';
import './cohesion/cohesion.scss';
subscribe(APP_READY, () => {
ReactDOM.render(
<MainApp />,
document.getElementById('root'),
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<MainApp />
</StrictMode>,
);
});
subscribe(APP_INIT_ERROR, (error) => {
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
const root = createRoot(document.getElementById('root'));
root.render(
<StrictMode>
<ErrorPage message={error.message} />
</StrictMode>,
);
});
initialize({

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert, Hyperlink } from '@openedx/paragon';
import { Alert } from '@openedx/paragon';
import { CheckCircle, Error } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
@@ -36,17 +36,19 @@ const AccountActivationMessage = ({ messageType }) => {
break;
}
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
const supportEmail = (
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
const supportLink = (
<Alert.Link href={getConfig().ACTIVATION_EMAIL_SUPPORT_LINK}>
{formatMessage(messages['account.activation.support.link'])}
</Alert.Link>
);
heading = formatMessage(messages[`account.${activationOrConfirmation}.error.message.title`]);
activationMessage = (
<FormattedMessage
id="account.activation.error.message"
defaultMessage="Something went wrong, please contact {supportEmail} to resolve this issue."
defaultMessage="Something went wrong, please {supportLink} to resolve this issue."
description="Account activation error message"
values={{ supportEmail }}
values={{ supportLink }}
/>
);
break;

View File

@@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -11,23 +10,19 @@ import PropTypes from 'prop-types';
import { Link, useNavigate } from 'react-router-dom';
import messages from './messages';
import trackCohesionEvent from '../cohesion/trackers';
import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants';
import { updatePathWithQueryParams } from '../data/utils';
import { redirectWithDelay } from '../data/utils/dataUtils';
import useMobileResponsive from '../data/utils/useMobileResponsive';
const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
const isMobileView = useMobileResponsive();
const [redirectToResetPasswordPage, setRedirectToResetPasswordPage] = useState(false);
const cohesionEventData = useSelector(state => state.cohesion.eventData);
const handlers = {
handleToggleOff: () => {
if (variant === 'block') {
setRedirectToResetPasswordPage(true);
} else {
trackCohesionEvent(cohesionEventData);
redirectWithDelay(redirectUrl || getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL));
window.location.href = redirectUrl || getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
}
},
};

View File

@@ -1,7 +1,8 @@
import React, { useEffect, useMemo, useState } from 'react';
import { connect, useDispatch } from 'react-redux';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
import {
Form, StatefulButton,
@@ -20,10 +21,6 @@ import {
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
import LoginFailureMessage from './LoginFailure';
import messages from './messages';
import {
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
} from '../cohesion/constants';
import { setCohesionEventStates } from '../cohesion/data/actions';
import {
FormGroup,
InstitutionLogistration,
@@ -35,7 +32,9 @@ import { getThirdPartyAuthContext } from '../common-components/data/actions';
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
import { DEFAULT_STATE, PENDING_STATE, RESET_PAGE } from '../data/constants';
import {
DEFAULT_STATE, PENDING_STATE, RESET_PAGE,
} from '../data/constants';
import {
getActivationStatus,
getAllPossibleQueryParams,
@@ -43,11 +42,7 @@ import {
getTpaProvider,
updatePathWithQueryParams,
} from '../data/utils';
import { removeCookie } from '../data/utils/cookies';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
import {
trackForgotPasswordLinkClick, trackLoginPageViewed, trackLoginSuccess,
} from '../tracking/trackers/login';
const LoginPage = (props) => {
const {
@@ -74,7 +69,6 @@ const LoginPage = (props) => {
getTPADataFromBackend,
} = props;
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const activationMsgType = getActivationStatus();
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
@@ -84,17 +78,9 @@ const LoginPage = (props) => {
const tpaHint = getTpaHint();
useEffect(() => {
trackLoginPageViewed();
sendPageEvent('login_and_registration', 'login');
}, []);
useEffect(() => {
if (loginResult.success) {
trackLoginSuccess();
// Remove this cookie that was set to capture marketingEmailsOptIn for the onboarding component
removeCookie('ssoPipelineRedirectionDone');
}
}, [loginResult]);
useEffect(() => {
const payload = { ...queryParams };
if (tpaHint) {
@@ -154,15 +140,6 @@ const LoginPage = (props) => {
const handleSubmit = (event) => {
event.preventDefault();
const eventData = {
pageType: PAGE_TYPES.SIGN_IN,
elementType: ELEMENT_TYPES.BUTTON,
webElementText: ELEMENT_TEXT.SIGN_IN,
webElementName: ELEMENT_NAME.SIGN_IN,
};
dispatch(setCohesionEventStates(eventData));
if (showResetPasswordSuccessBanner) {
props.dismissPasswordResetBanner();
}
@@ -193,6 +170,9 @@ const LoginPage = (props) => {
const { name } = event.target;
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
};
const trackForgotPasswordLinkClick = () => {
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
};
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
@@ -228,7 +208,6 @@ const LoginPage = (props) => {
success={loginResult.success}
redirectUrl={loginResult.redirectUrl}
finishAuthUrl={finishAuthUrl}
currentProvider={currentProvider}
/>
<div className="mw-xs mt-3 mb-2">
<LoginFailureMessage

View File

@@ -95,6 +95,11 @@ const messages = defineMessages({
defaultMessage: 'Your account could not be activated',
description: 'Account Activation error message title',
},
'account.activation.support.link': {
id: 'account.activation.support.link',
defaultMessage: 'contact support',
description: 'Link text used in account activation error message to go to learner help center',
},
// Email Confirmation Strings
'account.confirmation.success.message.title': {
id: 'account.confirmation.success.message.title',

View File

@@ -58,7 +58,7 @@ describe('AccountActivationMessage', () => {
);
const expectedMessage = 'Your account could not be activated'
+ 'Something went wrong, please contact to resolve this issue.';
+ 'Something went wrong, please contact support to resolve this issue.';
expect(screen.getByText(
'',
@@ -121,7 +121,7 @@ describe('EmailConfirmationMessage', () => {
</IntlProvider>,
);
const expectedMessage = 'Your email could not be confirmed'
+ 'Something went wrong, please contact to resolve this issue.';
+ 'Something went wrong, please contact support to resolve this issue.';
expect(screen.getByText(
'',
{ selector: '#account-activation-message' },

View File

@@ -1,30 +1,18 @@
import React from 'react';
import { Provider } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
fireEvent, render, screen, waitFor,
fireEvent, render, screen,
} from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import mockTagular from '../../cohesion/utils';
import { RESET_PAGE } from '../../data/constants';
import ChangePasswordPrompt from '../ChangePasswordPrompt';
const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt);
const mockedNavigator = jest.fn();
const mockStore = configureStore();
mockTagular();
const eventData = {
pageType: 'test-page',
elementType: 'test-element-type',
webElementText: 'test-element-text',
webElementName: 'test-element-name',
};
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')),
@@ -33,14 +21,8 @@ jest.mock('react-router-dom', () => ({
describe('ChangePasswordPromptTests', () => {
let props = {};
let store = {};
const initialState = {
cohesion: { eventData: {} },
};
beforeAll(() => {
store = mockStore(initialState);
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
@@ -49,56 +31,38 @@ describe('ChangePasswordPromptTests', () => {
});
});
it('[nudge modal] should redirect to next url when user clicks close button', async () => {
it('[nudge modal] should redirect to next url when user clicks close button', () => {
const dashboardUrl = getConfig().BASE_URL.concat('/dashboard');
props = {
variant: 'nudge',
redirectUrl: dashboardUrl,
};
store = mockStore({
...initialState,
cohesion: {
eventData,
},
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(
<IntlProvider locale="en">
<Provider store={store}>
<MemoryRouter>
<IntlChangePasswordPrompt {...props} />
</MemoryRouter>
</Provider>
<MemoryRouter>
<IntlChangePasswordPrompt {...props} />
</MemoryRouter>
</IntlProvider>,
);
fireEvent.click(screen.getByText('Close'));
await waitFor(() => {
expect(window.location.href).toBe(dashboardUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(dashboardUrl);
});
it('[block modal] should redirect to reset password page when user clicks outside modal', async () => {
props = {
variant: 'block',
};
store = mockStore({
...initialState,
cohesion: {
eventData,
},
});
render(
<IntlProvider locale="en">
<Provider store={store}>
<MemoryRouter>
<IntlChangePasswordPrompt {...props} />
</MemoryRouter>
</Provider>
<MemoryRouter>
<IntlChangePasswordPrompt {...props} />
</MemoryRouter>
</IntlProvider>,
);

View File

@@ -1,12 +1,10 @@
import React from 'react';
import { Provider } from 'react-redux';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
render, screen,
} from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
ACCOUNT_LOCKED_OUT,
@@ -27,27 +25,13 @@ import LoginFailureMessage from '../LoginFailure';
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: jest.fn(),
}));
const mockStore = configureStore();
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
const eventData = {
pageType: 'test-page',
elementType: 'test-element-type',
webElementText: 'test-element-text',
webElementName: 'test-element-name',
};
describe('LoginFailureMessage', () => {
let props = {};
let store = {};
const initialState = {
cohesion: { eventData: {} },
};
beforeAll(() => {
store = mockStore(initialState);
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(query => ({
@@ -314,19 +298,11 @@ describe('LoginFailureMessage', () => {
errorCount: 0,
};
store = mockStore({
...initialState,
cohesion: {
eventData,
},
});
render(
<IntlProvider locale="en">
<Provider store={store}>
<MemoryRouter>
<IntlLoginFailureMessage {...props} />
</MemoryRouter>
</Provider>
<MemoryRouter>
<IntlLoginFailureMessage {...props} />
</MemoryRouter>
</IntlProvider>,
);
@@ -347,20 +323,12 @@ describe('LoginFailureMessage', () => {
errorCode: REQUIRE_PASSWORD_CHANGE,
errorCount: 0,
};
store = mockStore({
...initialState,
cohesion: {
eventData,
},
});
render(
<IntlProvider locale="en">
<Provider store={store}>
<MemoryRouter>
<IntlLoginFailureMessage {...props} />
</MemoryRouter>
</Provider>
<MemoryRouter>
<IntlLoginFailureMessage {...props} />
</MemoryRouter>
</IntlProvider>,
);

View File

@@ -11,10 +11,7 @@ import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import mockTagular from '../../cohesion/utils';
import {
APP_NAME, COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE,
} from '../../data/constants';
import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE } from '../../data/constants';
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions';
import { INTERNAL_SERVER_ERROR } from '../data/constants';
import LoginPage from '../LoginPage';
@@ -26,7 +23,6 @@ jest.mock('@edx/frontend-platform/analytics', () => ({
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: jest.fn(),
}));
mockTagular();
const IntlLoginPage = injectIntl(LoginPage);
const mockStore = configureStore();
@@ -60,7 +56,6 @@ describe('LoginPage', () => {
register: {
validationApiRateLimited: false,
},
cohesion: { eventData: {} },
};
const secondaryProviders = {
@@ -515,7 +510,7 @@ describe('LoginPage', () => {
// ******** test redirection ********
it('should redirect to url returned by login endpoint after successful authentication', async () => {
it('should redirect to url returned by login endpoint after successful authentication', () => {
const dashboardURL = 'https://test.com/testing-dashboard/';
store = mockStore({
...initialState,
@@ -531,12 +526,10 @@ describe('LoginPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />));
await waitFor(() => {
expect(window.location.href).toBe(dashboardURL);
}, { timeout: 1100 });
expect(window.location.href).toBe(dashboardURL);
});
it('should redirect to finishAuthUrl upon successful login via SSO', async () => {
it('should redirect to finishAuthUrl upon successful login via SSO', () => {
const authCompleteUrl = '/auth/complete/google-oauth2/';
store = mockStore({
...initialState,
@@ -560,12 +553,10 @@ describe('LoginPage', () => {
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />));
await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
});
it('should redirect to social auth provider url on SSO button click', async () => {
it('should redirect to social auth provider url on SSO button click', () => {
store = mockStore({
...initialState,
commonComponents: {
@@ -586,12 +577,10 @@ describe('LoginPage', () => {
'',
{ selector: '#oa2-apple-id' },
));
await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl);
});
it('should redirect to finishAuthUrl upon successful authentication via SSO', async () => {
it('should redirect to finishAuthUrl upon successful authentication via SSO', () => {
const finishAuthUrl = '/auth/complete/google-oauth2/';
store = mockStore({
...initialState,
@@ -612,9 +601,7 @@ describe('LoginPage', () => {
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<IntlLoginPage {...props} />));
await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
});
// ******** test hinted third party auth ********
@@ -764,7 +751,7 @@ describe('LoginPage', () => {
it('should send page event when login page is rendered', () => {
render(reduxWrapper(<IntlLoginPage {...props} />));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login', { app_name: APP_NAME });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login');
});
it('tests that form is in invalid state when it is submitted', () => {
@@ -797,7 +784,7 @@ describe('LoginPage', () => {
{ selector: '#forgot-password' },
));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
});
it('should backup the login form state when shouldBackupState is true', () => {

View File

@@ -20,7 +20,7 @@ import {
tpaProvidersSelector,
} from '../common-components/data/selectors';
import messages from '../common-components/messages';
import { APP_NAME, LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import {
getTpaHint, getTpaProvider, updatePathWithQueryParams,
} from '../data/utils';
@@ -56,11 +56,11 @@ const Logistration = (props) => {
}, [navigate, disablePublicAccountCreation]);
const handleInstitutionLogin = (e) => {
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
if (typeof e === 'string') {
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register', { app_name: APP_NAME });
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register');
} else {
sendPageEvent('login_and_registration', e.target.dataset.eventName, { app_name: APP_NAME });
sendPageEvent('login_and_registration', e.target.dataset.eventName);
}
setInstitutionLogin(!institutionLogin);
@@ -70,7 +70,7 @@ const Logistration = (props) => {
if (tabKey === currentTab) {
return;
}
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement', app_name: APP_NAME });
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
props.clearThirdPartyAuthContextErrorMessage();
if (tabKey === LOGIN_PAGE) {
props.backupRegistrationForm();

View File

@@ -11,21 +11,16 @@ import configureStore from 'redux-mock-store';
import Logistration from './Logistration';
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
import {
APP_NAME,
COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE,
} from '../data/constants';
import { backupLoginForm } from '../login/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', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth');
jest.mock('../register/data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
@@ -69,7 +64,6 @@ describe('Logistration', () => {
usernameSuggestions: [],
validationApiRateLimited: false,
},
cohesion: { eventData: {} },
commonComponents: {
thirdPartyAuthContext: {
providers: [],
@@ -90,7 +84,6 @@ describe('Logistration', () => {
})),
}));
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
configure({
loggingService: { logError: jest.fn() },
config: {
@@ -231,8 +224,8 @@ describe('Logistration', () => {
render(reduxWrapper(<IntlLogistration {...props} />));
fireEvent.click(screen.getByText('Institution/campus credentials'));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement', app_name: APP_NAME });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login', { app_name: APP_NAME });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: '',

View File

@@ -1,29 +0,0 @@
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();
});
});

View File

@@ -1,41 +0,0 @@
# 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.
![Screenshot of Content added after the Main App Slot](./images/main_app_slot.png)
```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;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 269 KiB

View File

@@ -1,7 +0,0 @@
import { PluginSlot } from '@openedx/frontend-plugin-framework';
const MainAppSlot = () => (
<PluginSlot id="main_app_slot" />
);
export default MainAppSlot;

View File

@@ -1,3 +0,0 @@
# `frontend-app-authn` Plugin Slots
- [`main_app_slot`](./MainAppSlot/)

View File

@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { identifyAuthenticatedUser } from '@edx/frontend-platform/analytics';
import { identifyAuthenticatedUser, sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
AxiosJwtAuthService,
configure as configureAuth,
@@ -39,13 +39,6 @@ import {
import isOneTrustFunctionalCookieEnabled from '../data/oneTrust';
import { getAllPossibleQueryParams, isHostAvailableInQueryParams } from '../data/utils';
import { FormFieldRenderer } from '../field-renderer';
import {
trackDisablePostRegistrationRecommendations,
trackProgressiveProfilingPageViewed,
trackProgressiveProfilingSkipLinkClick,
trackProgressiveProfilingSubmitClick,
trackProgressiveProfilingSupportLinkCLick,
} from '../tracking/trackers/progressive-profiling';
const ProgressiveProfiling = (props) => {
const { formatMessage } = useIntl();
@@ -105,13 +98,14 @@ const ProgressiveProfiling = (props) => {
useEffect(() => {
if (authenticatedUser?.userId) {
identifyAuthenticatedUser(authenticatedUser.userId);
trackProgressiveProfilingPageViewed();
sendPageEvent('login_and_registration', 'welcome');
}
}, [authenticatedUser]);
useEffect(() => {
if (!enablePostRegistrationRecommendations) {
trackDisablePostRegistrationRecommendations(
sendTrackEvent(
'edx.bi.user.recommendations.not.enabled',
{ functionalCookiesConsent, page: 'authn_recommendations' },
);
return;
@@ -155,23 +149,29 @@ const ProgressiveProfiling = (props) => {
});
}
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
const eventProperties = {
isGenderSelected: !!values.gender,
isYearOfBirthSelected: !!values.year_of_birth,
isLevelOfEducationSelected: !!values.level_of_education,
isWorkExperienceSelected: !!values.work_experience,
host: queryParams?.host || '',
};
trackProgressiveProfilingSubmitClick(eventProperties);
sendTrackEvent(
'edx.bi.welcome.page.submit.clicked',
{
isGenderSelected: !!values.gender,
isYearOfBirthSelected: !!values.year_of_birth,
isLevelOfEducationSelected: !!values.level_of_education,
isWorkExperienceSelected: !!values.work_experience,
host: queryParams?.host || '',
},
);
};
const handleSkip = (e) => {
e.preventDefault();
window.history.replaceState(location.state, null, '');
setShowModal(true);
trackProgressiveProfilingSkipLinkClick({
host: queryParams?.host || '',
});
sendTrackEvent(
'edx.bi.welcome.page.skip.link.clicked',
{
host: queryParams?.host || '',
},
);
};
const onChangeHandler = (e) => {
@@ -242,7 +242,7 @@ const ProgressiveProfiling = (props) => {
destination={getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK}
target="_blank"
showLaunchIcon={false}
onClick={() => (trackProgressiveProfilingSupportLinkCLick())}
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
>
{formatMessage(messages['optional.fields.information.link'])}
</Hyperlink>

View File

@@ -6,14 +6,12 @@ import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platfor
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import {
fireEvent, render, screen, waitFor,
fireEvent, render, screen,
} from '@testing-library/react';
import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import mockTagular from '../../cohesion/utils';
import {
APP_NAME,
AUTHN_PROGRESSIVE_PROFILING,
COMPLETE_STATE, DEFAULT_REDIRECT_URL,
EMBEDDED,
@@ -26,7 +24,6 @@ import ProgressiveProfiling from '../ProgressiveProfiling';
const IntlProgressiveProfilingPage = injectIntl(ProgressiveProfiling);
const mockStore = configureStore();
mockTagular();
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
@@ -57,13 +54,6 @@ jest.mock('react-router-dom', () => {
};
});
const eventData = {
pageType: 'test-page',
elementType: 'test-element-type',
webElementText: 'test-element-text',
webElementName: 'test-element-name',
};
describe('ProgressiveProfilingTests', () => {
let store = {};
@@ -153,9 +143,8 @@ describe('ProgressiveProfilingTests', () => {
const modalContentContainer = document.getElementsByClassName('.pgn__modal-content-container');
expect(modalContentContainer).toBeTruthy();
const payload = { host: '', app_name: APP_NAME };
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', payload);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host: '' });
});
// ******** test event functionality ********
@@ -176,7 +165,7 @@ describe('ProgressiveProfilingTests', () => {
const supportLink = screen.getByRole('link', { name: /learn more about how we use this information/i });
fireEvent.click(supportLink);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked', { app_name: APP_NAME });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.support.link.clicked');
});
it('should set empty host property value for non-embedded experience', () => {
@@ -186,7 +175,6 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false,
isWorkExperienceSelected: false,
host: '',
app_name: APP_NAME,
};
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(AUTHN_PROGRESSIVE_PROFILING) };
@@ -261,9 +249,6 @@ describe('ProgressiveProfilingTests', () => {
...initialState.welcomePage,
success: true,
},
cohesion: {
eventData,
},
});
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const nextButton = container.querySelector('button.btn-brand');
@@ -290,18 +275,13 @@ describe('ProgressiveProfilingTests', () => {
...initialState.welcomePage,
success: true,
},
cohesion: {
eventData,
},
});
const { container } = render(reduxWrapper(<IntlProgressiveProfilingPage />));
const nextButton = container.querySelector('button.btn-brand');
expect(nextButton.textContent).toEqual('Submit');
await waitFor(() => {
expect(window.location.href).toEqual(redirectUrl);
}, { timeout: 1100 });
expect(window.location.href).toEqual(redirectUrl);
});
});
@@ -336,7 +316,7 @@ describe('ProgressiveProfilingTests', () => {
const skipLinkButton = screen.getByText('Skip for now');
fireEvent.click(skipLinkButton);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host, app_name: APP_NAME });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.welcome.page.skip.link.clicked', { host });
});
it('should show spinner while fetching the optional fields', () => {
@@ -369,7 +349,6 @@ describe('ProgressiveProfilingTests', () => {
isLevelOfEducationSelected: false,
isWorkExperienceSelected: false,
host: 'http://example.com',
app_name: APP_NAME,
};
delete window.location;
window.location = {
@@ -416,7 +395,7 @@ describe('ProgressiveProfilingTests', () => {
expect(window.location.href).toBe(DASHBOARD_URL);
});
it('should redirect to provided redirect url', async () => {
it('should redirect to provided redirect url', () => {
const redirectUrl = 'https://redirect-test.com';
delete window.location;
window.location = {
@@ -438,17 +417,12 @@ describe('ProgressiveProfilingTests', () => {
...initialState.welcomePage,
success: true,
},
cohesion: {
eventData,
},
});
render(reduxWrapper(<IntlProgressiveProfilingPage />));
const submitButton = screen.getByText('Submit');
fireEvent.click(submitButton);
await waitFor(() => {
expect(window.location.href).toBe(redirectUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(redirectUrl);
});
});
});

View File

@@ -1,4 +1,4 @@
import { renderHook } from '@testing-library/react-hooks';
import { renderHook } from '@testing-library/react';
import algoliasearchHelper from 'algoliasearch-helper';
import mockedRecommendedProducts from './mockedData';

View File

@@ -15,7 +15,7 @@ const generateProductKey = (product) => (
export const getProductMapping = (recommendedProducts) => recommendedProducts.map((product) => ({
product_key: generateProductKey(product),
product_line: product.cardType,
product_source: product?.productSource?.name,
product_source: product.productSource.name,
}));
export const trackRecommendationClick = (product, position, userId) => {
@@ -25,7 +25,7 @@ export const trackRecommendationClick = (product, position, userId) => {
recommendation_type: product.recommendationType,
product_key: generateProductKey(product),
product_line: product.cardType,
product_source: product?.productSource?.name,
product_source: product.productSource.name,
user_id: userId,
});

View File

@@ -11,7 +11,7 @@ export const INVALID_NAME_REGEX = /https?:\/\/(?:[-\w.]|(?:%[\da-fA-F]{2}))*/g;
const validateName = (value, formatMessage) => {
let fieldError = '';
if (!value || (value && !value.trim())) {
if (!value.trim()) {
fieldError = formatMessage(messages['empty.name.field.error']);
} else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) {
fieldError = formatMessage(messages['name.validation.message']);

View File

@@ -4,6 +4,7 @@ import React, {
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form, Spinner, StatefulButton } from '@openedx/paragon';
import classNames from 'classnames';
@@ -11,19 +12,12 @@ import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import Skeleton from 'react-loading-skeleton';
import {
InstitutionLogistration,
PasswordField,
RedirectLogistration,
ThirdPartyAuthAlert,
} from '../common-components';
import ConfigurableRegistrationForm from './components/ConfigurableRegistrationForm';
import RegistrationFailure from './components/RegistrationFailure';
import {
backupRegistrationFormBegin,
clearRegistrationBackendError,
registerNewUser,
setAutoGeneratedUsernameExperimentData,
setEmailSuggestionInStore,
setUserPipelineDataLoaded,
} from './data/actions';
@@ -31,8 +25,6 @@ import {
FORM_SUBMISSION_ERROR,
TPA_AUTHENTICATION_FAILURE,
} 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 {
isFormValid, prepareRegistrationPayload,
@@ -40,20 +32,21 @@ import {
import messages from './messages';
import { EmailField, NameField, UsernameField } from './RegistrationFields';
import {
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
} from '../cohesion/constants';
import { setCohesionEventStates } from '../cohesion/data/actions';
InstitutionLogistration,
PasswordField,
RedirectLogistration,
ThirdPartyAuthAlert,
} from '../common-components';
import { getThirdPartyAuthContext as getRegistrationDataFromBackend } from '../common-components/data/actions';
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
import {
APP_NAME, COMPLETE_STATE, PENDING_STATE,
REGISTER_PAGE,
COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
import {
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, removeCookie, setCookie,
getAllPossibleQueryParams, getTpaHint, getTpaProvider, isHostAvailableInQueryParams, setCookie,
} from '../data/utils';
import { trackRegistrationPageViewed, trackRegistrationSuccess } from '../tracking/trackers/register';
/**
* Main Registration Page component
*/
@@ -75,7 +68,6 @@ const RegistrationPage = (props) => {
} = props;
const backedUpFormData = useSelector(state => state.register.registrationFormData);
const initExpVariation = useSelector(state => state.register.autoGeneratedUsernameExperimentVariation);
const registrationError = useSelector(state => state.register.registrationError);
const registrationErrorCode = registrationError?.errorCode;
const registrationResult = useSelector(state => state.register.registrationResult);
@@ -111,12 +103,6 @@ const RegistrationPage = (props) => {
? formatMessage(messages['create.account.cta.button'], { label: cta })
: 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
*/
@@ -142,7 +128,7 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (!formStartTime) {
trackRegistrationPageViewed();
sendPageEvent('login_and_registration', 'register');
const payload = { ...queryParams, is_register_page: true };
if (tpaHint) {
payload.tpa_hint = tpaHint;
@@ -163,10 +149,8 @@ const RegistrationPage = (props) => {
formFields: { ...formFields },
errors: { ...errors },
}));
dispatch(setAutoGeneratedUsernameExperimentData(autoGeneratedUsernameExpVariation));
}
}, [shouldBackupState, configurableFormFields, // eslint-disable-line react-hooks/exhaustive-deps
formFields, errors, dispatch, backedUpFormData]);
}, [shouldBackupState, configurableFormFields, formFields, errors, dispatch, backedUpFormData]);
useEffect(() => {
if (backendValidations) {
@@ -187,15 +171,10 @@ const RegistrationPage = (props) => {
useEffect(() => {
if (registrationResult.success) {
// This event is used by GTM
trackRegistrationSuccess();
sendTrackEvent('edx.bi.user.account.registered.client', {});
// This is used by the "User Retention Rate Event" on GTM
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]);
@@ -231,13 +210,13 @@ const RegistrationPage = (props) => {
const registerUser = () => {
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
let payload = { ...formFields, app_name: APP_NAME };
let payload = { ...formFields };
if (currentProvider) {
delete payload.password;
payload.social_auth_provider = currentProvider;
}
if (hideUsernameField) {
if (flags.autoGeneratedUsernameEnabled) {
delete payload.username;
}
@@ -272,14 +251,6 @@ const RegistrationPage = (props) => {
const handleSubmit = (e) => {
e.preventDefault();
const eventData = {
pageType: PAGE_TYPES.ACCOUNT_CREATION,
elementType: ELEMENT_TYPES.BUTTON,
webElementText: ELEMENT_TEXT.CREATE_ACCOUNT,
webElementName: ELEMENT_NAME.CREATE_ACCOUNT,
};
dispatch(setCohesionEventStates(eventData));
registerUser();
};
@@ -314,111 +285,107 @@ const RegistrationPage = (props) => {
redirectToProgressiveProfilingPage={
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && !!Object.keys(optionalFields.fields).length
}
currentProvider={currentProvider}
/>
{(autoSubmitRegForm && !errorCode.type)
|| (!autoGeneratedUsernameExpVariation && !(
autoGeneratedUsernameExpVariation === NOT_INITIALIZED
|| registrationEmbedded || !!tpaHint || !!currentProvider))
? (
<div className="mw-xs mt-5 text-center">
<Spinner animation="border" variant="primary" id="tpa-spinner" />
</div>
) : (
<div
className={classNames(
'mw-xs mt-3',
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded },
{autoSubmitRegForm && !errorCode.type ? (
<div className="mw-xs mt-5 text-center">
<Spinner animation="border" variant="primary" id="tpa-spinner" />
</div>
) : (
<div
className={classNames(
'mw-xs mt-3',
{ 'w-100 m-auto pt-4 main-content': registrationEmbedded },
)}
>
<ThirdPartyAuthAlert
currentProvider={currentProvider}
platformName={platformName}
referrer={REGISTER_PAGE}
/>
<RegistrationFailure
errorCode={errorCode.type}
failureCount={errorCode.count}
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
name="email"
value={formFields.email}
confirmEmailValue={configurableFormFields?.confirm_email}
handleErrorChange={handleErrorChange}
handleChange={handleOnChange}
errorMessage={errors.email}
helpText={[formatMessage(messages['help.text.email'])]}
floatingLabel={formatMessage(messages['registration.email.label'])}
/>
{!flags.autoGeneratedUsernameEnabled && (
<UsernameField
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'])}
/>
)}
>
<ThirdPartyAuthAlert
currentProvider={currentProvider}
platformName={platformName}
referrer={REGISTER_PAGE}
/>
<RegistrationFailure
errorCode={errorCode.type}
failureCount={errorCode.count}
context={{ provider: currentProvider, errorMessage: thirdPartyAuthErrorMessage }}
/>
<Form id="registration-form" name="registration-form">
<NameField
name="name"
value={formFields.name}
shouldFetchUsernameSuggestions={!formFields.username.trim()}
{!currentProvider && (
<PasswordField
name="password"
value={formFields.password}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.name}
helpText={[formatMessage(messages['help.text.name'])]}
floatingLabel={formatMessage(messages['registration.fullname.label'])}
errorMessage={errors.password}
floatingLabel={formatMessage(messages['registration.password.label'])}
/>
<EmailField
name="email"
value={formFields.email}
confirmEmailValue={configurableFormFields?.confirm_email}
handleErrorChange={handleErrorChange}
handleChange={handleOnChange}
errorMessage={errors.email}
helpText={[formatMessage(messages['help.text.email'])]}
floatingLabel={formatMessage(messages['registration.email.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}
/>
{!hideUsernameField && (
<UsernameField
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>
)}
)}
</Form>
</div>
)}
</>
);
};

View File

@@ -6,7 +6,7 @@ import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'
import {
configure, getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { fireEvent, render, waitFor } from '@testing-library/react';
import { fireEvent, render } from '@testing-library/react';
import { mockNavigate, BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
@@ -17,13 +17,9 @@ import {
setUserPipelineDataLoaded,
} from './data/actions';
import { INTERNAL_SERVER_ERROR } from './data/constants';
import mockTagular from '../cohesion/utils';
import { NOT_INITIALIZED } from './data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from './data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from './RegistrationPage';
import {
APP_NAME, AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
AUTHN_PROGRESSIVE_PROFILING, COMPLETE_STATE, PENDING_STATE, REGISTER_PAGE,
} from '../data/constants';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -34,11 +30,9 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('./data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
mockTagular();
jest.mock('react-router-dom', () => {
const mockNavigation = jest.fn();
@@ -108,7 +102,6 @@ describe('RegistrationPage', () => {
usernameSuggestions: [],
},
cohesion: { eventData: {} },
commonComponents: {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext,
@@ -135,7 +128,6 @@ describe('RegistrationPage', () => {
institutionLogin: false,
};
window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
@@ -193,7 +185,6 @@ describe('RegistrationPage', () => {
honor_code: true,
total_registration_time: 0,
next: '/course/demo-course-url',
app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -216,7 +207,6 @@ describe('RegistrationPage', () => {
honor_code: true,
social_auth_provider: 'Apple',
total_registration_time: 0,
app_name: APP_NAME,
};
store = mockStore({
@@ -302,7 +292,6 @@ describe('RegistrationPage', () => {
honor_code: true,
total_registration_time: 0,
marketing_emails_opt_in: true,
app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -329,7 +318,6 @@ describe('RegistrationPage', () => {
country: 'Pakistan',
honor_code: true,
total_registration_time: 0,
app_name: APP_NAME,
};
store.dispatch = jest.fn(store.dispatch);
@@ -508,7 +496,7 @@ describe('RegistrationPage', () => {
expect(document.cookie).toMatch(`${getConfig().USER_RETENTION_COOKIE_NAME}=true`);
});
it('should redirect to url returned in registration result after successful account creation', async () => {
it('should redirect to url returned in registration result after successful account creation', () => {
const dashboardURL = 'https://test.com/testing-dashboard/';
store = mockStore({
...initialState,
@@ -523,12 +511,10 @@ describe('RegistrationPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
await waitFor(() => {
expect(window.location.href).toBe(dashboardURL);
}, { timeout: 1100 });
expect(window.location.href).toBe(dashboardURL);
});
it('should redirect to dashboard if features flags are configured but no optional fields are configured', async () => {
it('should redirect to dashboard if features flags are configured but no optional fields are configured', () => {
mergeConfig({
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: true,
});
@@ -552,9 +538,7 @@ describe('RegistrationPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
await waitFor(() => {
expect(window.location.href).toBe(dashboardUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(dashboardUrl);
});
it('should redirect to progressive profiling page if optional fields are configured', () => {
@@ -608,7 +592,7 @@ describe('RegistrationPage', () => {
it('should send page event when register page is rendered', () => {
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register', { app_name: APP_NAME });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
});
it('should send track event when user has successfully registered', () => {
@@ -626,7 +610,7 @@ describe('RegistrationPage', () => {
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', { app_name: APP_NAME });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.user.account.registered.client', {});
});
it('should populate form with pipeline user details', () => {
@@ -899,7 +883,6 @@ describe('RegistrationPage', () => {
country: 'PK',
social_auth_provider: 'Apple',
total_registration_time: 0,
app_name: APP_NAME,
}));
});
});

View File

@@ -1,16 +1,10 @@
import React, { useEffect, useMemo } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { getCountryList, getLocale, useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import {
ELEMENT_NAME, ELEMENT_TEXT, ELEMENT_TYPES, PAGE_TYPES,
} from '../../cohesion/constants';
import trackCohesionEvent from '../../cohesion/trackers';
import { FormFieldRenderer } from '../../field-renderer';
import { backupRegistrationFormBegin } from '../data/actions';
import { FIELDS } from '../data/constants';
import messages from '../messages';
import { CountryField, HonorCode, TermsOfService } from '../RegistrationFields';
@@ -38,16 +32,12 @@ const ConfigurableRegistrationForm = (props) => {
setFormFields,
autoSubmitRegistrationForm,
} = props;
const dispatch = useDispatch();
/** 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')
), []);
const countryList = useMemo(() => getCountryList(getLocale()).concat([{ code: 'US', name: 'United States' }]), []);
let showTermsOfServiceAndHonorCode = false;
let showCountryField = false;
@@ -60,8 +50,6 @@ const ConfigurableRegistrationForm = (props) => {
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.
*/
@@ -102,25 +90,6 @@ const ConfigurableRegistrationForm = (props) => {
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 }));
};

View File

@@ -9,12 +9,8 @@ import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import { APP_NAME } from '../../../data/constants';
import { registerNewUser } from '../../data/actions';
import { FIELDS } from '../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import ConfigurableRegistrationForm from '../ConfigurableRegistrationForm';
@@ -26,7 +22,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlConfigurableRegistrationForm = injectIntl(ConfigurableRegistrationForm);
const IntlRegistrationPage = injectIntl(RegistrationPage);
@@ -99,7 +94,6 @@ describe('ConfigurableRegistrationForm', () => {
registrationFormData,
usernameSuggestions: [],
},
cohesion: { eventData: {} },
commonComponents: {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext,
@@ -127,7 +121,6 @@ describe('ConfigurableRegistrationForm', () => {
};
window.location = { search: '' };
getLocale.mockImplementationOnce(() => ('en-us'));
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
@@ -267,7 +260,7 @@ describe('ConfigurableRegistrationForm', () => {
fireEvent.click(submitButton);
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK', app_name: APP_NAME }));
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
});
it('should show error messages for required fields on empty form submission', () => {

View File

@@ -12,9 +12,6 @@ import configureStore from 'redux-mock-store';
import {
FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
} from '../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
import RegistrationFailureMessage from '../RegistrationFailure';
@@ -26,7 +23,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
const IntlRegistrationPage = injectIntl(RegistrationPage);
const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage);
@@ -99,7 +95,6 @@ describe('RegistrationFailure', () => {
registrationFormData,
usernameSuggestions: [],
},
cohesion: { eventData: {} },
commonComponents: {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext,
@@ -126,7 +121,6 @@ describe('RegistrationFailure', () => {
institutionLogin: false,
};
window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {

View File

@@ -5,17 +5,13 @@ import { getConfig, mergeConfig } from '@edx/frontend-platform';
import {
configure, getLocale, injectIntl, IntlProvider,
} from '@edx/frontend-platform/i18n';
import { fireEvent, render, waitFor } from '@testing-library/react';
import { fireEvent, render } from '@testing-library/react';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import mockTagular from '../../../cohesion/utils';
import {
COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
} from '../../../data/constants';
import { NOT_INITIALIZED } from '../../data/optimizelyExperiment/helper';
import useAutoGeneratedUsernameExperimentVariation
from '../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation';
import RegistrationPage from '../../RegistrationPage';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -26,8 +22,6 @@ jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
}));
jest.mock('../../data/optimizelyExperiment/useAutoGeneratedUsernameExperimentVariation', () => jest.fn());
mockTagular();
const IntlRegistrationPage = injectIntl(RegistrationPage);
const mockStore = configureStore();
@@ -100,7 +94,6 @@ describe('ThirdPartyAuth', () => {
registrationFormData,
usernameSuggestions: [],
},
cohesion: { eventData: {} },
commonComponents: {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext,
@@ -127,7 +120,6 @@ describe('ThirdPartyAuth', () => {
institutionLogin: false,
};
window.location = { search: '' };
useAutoGeneratedUsernameExperimentVariation.mockReturnValue(NOT_INITIALIZED);
});
afterEach(() => {
@@ -342,7 +334,7 @@ describe('ThirdPartyAuth', () => {
expect(headingElement).toBeTruthy();
});
it('should redirect to social auth provider url on SSO button click', async () => {
it('should redirect to social auth provider url on SSO button click', () => {
const registerUrl = '/auth/login/apple-id/?auth_entry=register&next=/dashboard';
store = mockStore({
...initialState,
@@ -368,12 +360,10 @@ describe('ThirdPartyAuth', () => {
const ssoButton = container.querySelector('button#oa2-apple-id');
fireEvent.click(ssoButton);
await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl);
});
it('should redirect to finishAuthUrl upon successful registration via SSO', async () => {
it('should redirect to finishAuthUrl upon successful registration via SSO', () => {
const authCompleteUrl = '/auth/complete/google-oauth2/';
store = mockStore({
...initialState,
@@ -396,9 +386,7 @@ describe('ThirdPartyAuth', () => {
window.location = { href: getConfig().BASE_URL };
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
await waitFor(() => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
}, { timeout: 1100 });
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
});
// ******** test alert messages ********

View File

@@ -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_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
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
export const backupRegistrationForm = () => ({
type: BACKUP_REGISTRATION_DATA.BASE,
@@ -83,9 +83,3 @@ export const setUserPipelineDataLoaded = (value) => ({
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
payload: { value },
});
// Auto Generated Username Registration Experiment Actions
export const setAutoGeneratedUsernameExperimentData = (autoGeneratedRegExpVariation) => ({
type: REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
payload: { autoGeneratedRegExpVariation },
});

View File

@@ -1,30 +0,0 @@
/**
* 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,
});
}

View File

@@ -1,53 +0,0 @@
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;

View File

@@ -3,7 +3,6 @@ import {
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
REGISTER_FORM_VALIDATIONS,
REGISTER_NEW_USER,
REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA,
REGISTER_SET_COUNTRY_CODE,
REGISTER_SET_EMAIL_SUGGESTIONS,
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
@@ -40,7 +39,6 @@ export const defaultState = {
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
autoGeneratedUsernameExperimentVariation: '',
};
const reducer = (state = defaultState, action = {}) => {
@@ -57,12 +55,6 @@ const reducer = (state = defaultState, action = {}) => {
registrationFormData: { ...action.payload },
userPipelineDataLoaded: state.userPipelineDataLoaded,
};
case REGISTER_SET_AUTO_GENERATED_USERNAME_REGISTRATION_EXP_DATA: {
return {
...state,
autoGeneratedUsernameExperimentVariation: action.payload.autoGeneratedRegExpVariation,
};
}
case REGISTER_NEW_USER.BEGIN:
return {
...state,

View File

@@ -38,7 +38,6 @@ describe('Registration Reducer Tests', () => {
usernameSuggestions: [],
validationApiRateLimited: false,
shouldBackupState: false,
autoGeneratedUsernameExperimentVariation: '',
};
it('should return the initial state', () => {

View File

@@ -43,31 +43,39 @@ export const isFormValid = (
Object.keys(payload).forEach(key => {
switch (key) {
case 'name':
fieldErrors.name = validateName(payload.name, formatMessage);
if (!fieldErrors.name) {
fieldErrors.name = validateName(payload.name, formatMessage);
}
if (fieldErrors.name) { isValid = false; }
break;
case 'email': {
const {
fieldError, confirmEmailError, suggestion,
} = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
if (fieldError) {
fieldErrors.email = fieldError;
isValid = false;
if (!fieldErrors.email) {
const {
fieldError, confirmEmailError, suggestion,
} = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
if (fieldError) {
fieldErrors.email = fieldError;
isValid = false;
}
if (confirmEmailError) {
fieldErrors.confirm_email = confirmEmailError;
isValid = false;
}
emailSuggestion = suggestion;
}
if (confirmEmailError) {
fieldErrors.confirm_email = confirmEmailError;
isValid = false;
}
emailSuggestion = suggestion;
if (fieldErrors.email) { isValid = false; }
break;
}
case 'username':
fieldErrors.username = validateUsername(payload.username, formatMessage);
if (!fieldErrors.username) {
fieldErrors.username = validateUsername(payload.username, formatMessage);
}
if (fieldErrors.username) { isValid = false; }
break;
case 'password':
fieldErrors.password = validatePasswordField(payload.password, formatMessage);
if (!fieldErrors.password) {
fieldErrors.password = validatePasswordField(payload.password, formatMessage);
}
if (fieldErrors.password) { isValid = false; }
break;
default:

View File

@@ -18,7 +18,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { resetPassword, validateToken } from './data/actions';
import {
FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, SUCCESS, TOKEN_STATE,
FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, TOKEN_STATE,
} from './data/constants';
import { resetPasswordResultSelector } from './data/selectors';
import { validatePassword } from './data/service';
@@ -30,7 +30,6 @@ import {
LETTER_REGEX, LOGIN_PAGE, NUMBER_REGEX, RESET_PAGE,
} from '../data/constants';
import { getAllPossibleQueryParams, updatePathWithQueryParams, windowScrollTo } from '../data/utils';
import { trackPasswordResetSuccess, trackResetPasswordPageViewed } from '../tracking/trackers/reset-password';
const ResetPasswordPage = (props) => {
const { formatMessage } = useIntl();
@@ -43,15 +42,6 @@ const ResetPasswordPage = (props) => {
const { token } = useParams();
const navigate = useNavigate();
useEffect(() => {
if (props.status === TOKEN_STATE.VALID) {
trackResetPasswordPageViewed();
}
if (props.status === SUCCESS) {
trackPasswordResetSuccess();
}
}, [props.status]);
useEffect(() => {
if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) {
setErrorCode(props.status);
@@ -149,7 +139,7 @@ const ResetPasswordPage = (props) => {
}
} else if (props.status === PASSWORD_RESET_ERROR) {
navigate(updatePathWithQueryParams(RESET_PAGE));
} else if (props.status === SUCCESS) {
} else if (props.status === 'success') {
navigate(updatePathWithQueryParams(LOGIN_PAGE));
} else {
return (

View File

@@ -19,11 +19,6 @@ import ResetPasswordPage from '../ResetPasswordPage';
const mockedNavigator = jest.fn();
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('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')),

View File

@@ -1,22 +0,0 @@
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')();
};

View File

@@ -1,29 +0,0 @@
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,
{},
)();

View File

@@ -1,37 +0,0 @@
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,
{},
)();

View File

@@ -1,22 +0,0 @@
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')();
};

View File

@@ -1,14 +0,0 @@
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, {})();
};

View File

@@ -1,37 +0,0 @@
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 },
);
});
});

View File

@@ -1,37 +0,0 @@
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',
);
});
});

View File

@@ -1,37 +0,0 @@
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',
);
});
});

View File

@@ -1,36 +0,0 @@
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',
);
});
});

View File

@@ -1,26 +0,0 @@
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',
);
});
});

View File

@@ -1,14 +0,0 @@
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;

View File

@@ -1,14 +1,7 @@
const path = require('path');
const { createConfig } = require('@openedx/frontend-build');
const config = createConfig('webpack-prod');
config.resolve.modules = [
path.resolve(__dirname, './src'),
'node_modules',
];
config.module.rules[0].exclude = /node_modules\/(?!(fastest-levenshtein|@edx))/;
module.exports = config;