Compare commits
1 Commits
2u-main
...
sundas/INF
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3355a014e6 |
6
.env
6
.env
@@ -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
|
||||
@@ -44,6 +41,3 @@ BANNER_IMAGE_EXTRA_SMALL=''
|
||||
# ***** Miscellaneous *****
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
RECAPTCHA_SITE_KEY_WEB=''
|
||||
|
||||
@@ -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'
|
||||
@@ -44,6 +41,3 @@ APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
RECAPTCHA_SITE_KEY_WEB=''
|
||||
|
||||
@@ -18,7 +18,3 @@ SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
COHESION_WRITE_KEY=''
|
||||
COHESION_SOURCE_KEY=''
|
||||
PARAGON_THEME_URLS={}
|
||||
RECAPTCHA_SITE_KEY_WEB=''
|
||||
|
||||
13
.eslintrc.js
13
.eslintrc.js
@@ -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;
|
||||
|
||||
2
CODEOWNERS
Normal file
2
CODEOWNERS
Normal file
@@ -0,0 +1,2 @@
|
||||
# The following users are the owners of all frontend-app-authn files
|
||||
* @openedx/2u-infinity
|
||||
14
README.rst
14
README.rst
@@ -29,13 +29,7 @@ Getting Started
|
||||
Installation
|
||||
============
|
||||
|
||||
`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
|
||||
==================================
|
||||
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.
|
||||
|
||||
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
|
||||
|
||||
@@ -57,7 +51,7 @@ Devstack (Deprecated) instructions
|
||||
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://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
|
||||
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>`__.
|
||||
|
||||
The authentication micro-frontend also requires the following additional variable:
|
||||
|
||||
@@ -148,13 +142,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://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
|
||||
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
|
||||
How To Contribute
|
||||
=================
|
||||
|
||||
Contributions are very welcome, and strongly encouraged! We've
|
||||
put together `some documentation that describes our contribution process <https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html>`_.
|
||||
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
|
||||
|
||||
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.
|
||||
|
||||
|
||||
@@ -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://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/tpa/index.html>`_ for backend configuration.
|
||||
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.
|
||||
|
||||
2. Authn has a component for rendering Social Auth providers at frontend which goes through each provider.
|
||||
|
||||
|
||||
@@ -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: '',
|
||||
};
|
||||
23832
package-lock.json
generated
23832
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
46
package.json
46
package.json
@@ -13,12 +13,15 @@
|
||||
"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",
|
||||
@@ -30,35 +33,33 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/frontend-platform": "^8.0.0",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@openedx/frontend-plugin-framework": "^1.3.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.6",
|
||||
"@openedx/paragon": "^23.4.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@openedx/paragon": "^22.1.1",
|
||||
"@optimizely/react-sdk": "^2.9.1",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"algoliasearch": "^4.14.3",
|
||||
"algoliasearch-helper": "^3.26.0",
|
||||
"algoliasearch-helper": "^3.14.0",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.43.0",
|
||||
"core-js": "3.40.0",
|
||||
"fastest-levenshtein": "1.0.16",
|
||||
"form-urlencoded": "6.1.6",
|
||||
"form-urlencoded": "6.1.5",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"react-error-boundary": "^4.0.13",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-google-recaptcha-v3": "^1.11.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-loading-skeleton": "3.5.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-router": "6.30.1",
|
||||
"react-router-dom": "6.30.1",
|
||||
"react-router": "6.29.0",
|
||||
"react-router-dom": "6.29.0",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.2.1",
|
||||
"redux-logger": "3.0.6",
|
||||
@@ -72,14 +73,13 @@
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"babel-plugin-formatjs": "10.5.39",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"@openedx/frontend-build": "^14.0.3",
|
||||
"babel-plugin-formatjs": "10.5.35",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"glob": "7.2.3",
|
||||
"history": "5.3.0",
|
||||
"jest": "30.1.3",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"ts-jest": "^29.4.0"
|
||||
"husky": "9.1.7",
|
||||
"jest": "29.7.0",
|
||||
"react-test-renderer": "^17.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +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" />
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href="<%=htmlWebpackPlugin.options.FAVICON_URL%>"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
<script defer src="https://www.edx.org/beam-wrapper.js" ></script>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
|
||||
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
></script>
|
||||
<% if (process.env.OPTIMIZELY_URL) { %>
|
||||
<script src="<%= process.env.OPTIMIZELY_URL %>"></script>
|
||||
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"></script>
|
||||
<% } %>
|
||||
<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>
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
@@ -2,12 +2,11 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { GoogleReCaptchaProvider } from 'react-google-recaptcha-v3';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, RouteTracker, UnAuthOnlyRoute,
|
||||
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
|
||||
} from './common-components';
|
||||
import configureStore from './data/configureStore';
|
||||
import {
|
||||
@@ -23,7 +22,6 @@ 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';
|
||||
@@ -33,43 +31,33 @@ import './index.scss';
|
||||
|
||||
registerIcons();
|
||||
|
||||
const MainApp = () => {
|
||||
const recaptchaKey = getConfig().RECAPTCHA_SITE_KEY_WEB;
|
||||
|
||||
return (
|
||||
<GoogleReCaptchaProvider
|
||||
reCaptchaKey={recaptchaKey}
|
||||
useEnterprise
|
||||
>
|
||||
<AppProvider store={configureStore()}>
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||
<Route
|
||||
path={REGISTER_EMBEDDED_PAGE}
|
||||
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
|
||||
/>
|
||||
<Route
|
||||
path={LOGIN_PAGE}
|
||||
element={
|
||||
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
|
||||
}
|
||||
/>
|
||||
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
|
||||
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
|
||||
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
|
||||
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
|
||||
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
|
||||
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
|
||||
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
|
||||
</Routes>
|
||||
<RouteTracker />
|
||||
<MainAppSlot />
|
||||
</AppProvider>
|
||||
</GoogleReCaptchaProvider>
|
||||
);
|
||||
};
|
||||
const MainApp = () => (
|
||||
<AppProvider store={configureStore()}>
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
{getConfig().ZENDESK_KEY && <Zendesk />}
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
|
||||
<Route
|
||||
path={REGISTER_EMBEDDED_PAGE}
|
||||
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
|
||||
/>
|
||||
<Route
|
||||
path={LOGIN_PAGE}
|
||||
element={
|
||||
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
|
||||
}
|
||||
/>
|
||||
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
|
||||
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
|
||||
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
|
||||
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
|
||||
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
|
||||
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
|
||||
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
|
||||
</Routes>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
export default MainApp;
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -1,6 +0,0 @@
|
||||
const mockTagular = () => {
|
||||
const getTagular = jest.fn();
|
||||
window.tagular = getTagular;
|
||||
};
|
||||
|
||||
export default mockTagular;
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
const RouteTracker = () => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
window.tagular?.('pageView');
|
||||
}, [location]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default RouteTracker;
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,10 +17,10 @@ export const getThirdPartyAuthContextSuccess = (
|
||||
fieldDescriptions,
|
||||
optionalFields,
|
||||
thirdPartyAuthContext,
|
||||
countriesCodesList) => ({
|
||||
countries) => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
|
||||
payload: {
|
||||
fieldDescriptions, optionalFields, thirdPartyAuthContext, countriesCodesList,
|
||||
fieldDescriptions, optionalFields, thirdPartyAuthContext, countries,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,83 +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',
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FIELD_LABELS = {
|
||||
COUNTRY: 'country',
|
||||
};
|
||||
@@ -35,7 +35,7 @@ const reducer = (state = defaultState, action = {}) => {
|
||||
optionalFields: action.payload.optionalFields,
|
||||
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
countriesCodesList: action.payload.countriesCodesList,
|
||||
countries: action.payload.countries,
|
||||
};
|
||||
}
|
||||
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
|
||||
|
||||
@@ -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 {
|
||||
getCountryList,
|
||||
getThirdPartyAuthContext,
|
||||
@@ -21,25 +19,10 @@ export function* fetchThirdPartyAuthContext(action) {
|
||||
const {
|
||||
fieldDescriptions, optionalFields, thirdPartyAuthContext,
|
||||
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
||||
const countriesCodesList = (yield call(getCountryList)) || [];
|
||||
const countries = (yield call(getCountryList)) || [];
|
||||
|
||||
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,
|
||||
countriesCodesList,
|
||||
));
|
||||
} else {
|
||||
yield put(getThirdPartyAuthContextSuccess(
|
||||
fieldDescriptions,
|
||||
optionalFields,
|
||||
thirdPartyAuthContext,
|
||||
countriesCodesList,
|
||||
));
|
||||
}
|
||||
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext, countries));
|
||||
} catch (e) {
|
||||
yield put(getThirdPartyAuthContextFailure());
|
||||
logError(e);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { FIELD_LABELS } from './constants';
|
||||
import { FIELD_LABELS } from '../../data/constants';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function getThirdPartyAuthContext(urlParams) {
|
||||
@@ -30,7 +30,7 @@ export async function getThirdPartyAuthContext(urlParams) {
|
||||
function extractCountryList(data) {
|
||||
return data?.fields
|
||||
.find(({ name }) => name === FIELD_LABELS.COUNTRY)
|
||||
?.options?.map(({ value }) => (value)) || [];
|
||||
?.options?.map(({ value, name }) => ({ code: value, name })) || [];
|
||||
}
|
||||
|
||||
export async function getCountryList() {
|
||||
@@ -42,8 +42,8 @@ export async function getCountryList() {
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(
|
||||
`${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`,
|
||||
requestConfig,
|
||||
`${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`,
|
||||
requestConfig,
|
||||
);
|
||||
return extractCountryList(data);
|
||||
} catch (e) {
|
||||
|
||||
@@ -36,7 +36,7 @@ describe('fetchThirdPartyAuthContext', () => {
|
||||
thirdPartyAuthContext: data,
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
countriesCodesList: [],
|
||||
countries: [],
|
||||
}));
|
||||
|
||||
const dispatched = [];
|
||||
|
||||
@@ -2,7 +2,6 @@ export { default as RedirectLogistration } from './RedirectLogistration';
|
||||
export { default as registerIcons } from './RegisterFaIcons';
|
||||
export { default as EmbeddedRegistrationRoute } from './EmbeddedRegistrationRoute';
|
||||
export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute';
|
||||
export { default as RouteTracker } from './RouteTracker';
|
||||
export { default as NotFoundPage } from './NotFoundPage';
|
||||
export { default as SocialAuthProviders } from './SocialAuthProviders';
|
||||
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
@@ -36,6 +37,7 @@ describe('FormGroup', () => {
|
||||
|
||||
describe('PasswordField', () => {
|
||||
const mockStore = configureStore();
|
||||
const IntlPasswordField = injectIntl(PasswordField);
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
@@ -64,7 +66,7 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should show/hide password on icon click', () => {
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
|
||||
const showPasswordButton = getByLabelText('Show password');
|
||||
@@ -77,7 +79,7 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should show password requirement tooltip on focus', async () => {
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
jest.useFakeTimers();
|
||||
await act(async () => {
|
||||
@@ -94,7 +96,7 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
value: '',
|
||||
};
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
jest.useFakeTimers();
|
||||
await act(async () => {
|
||||
@@ -117,7 +119,7 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should update password requirement checks', async () => {
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
jest.useFakeTimers();
|
||||
await act(async () => {
|
||||
@@ -140,7 +142,7 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should not run validations when blur is fired on password icon click', () => {
|
||||
const { container, getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const { container, getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
@@ -161,7 +163,7 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
handleBlur: jest.fn(),
|
||||
};
|
||||
const { container } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
fireEvent.blur(passwordInput, {
|
||||
@@ -179,7 +181,7 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
const { container } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
fireEvent.blur(passwordInput, {
|
||||
@@ -202,7 +204,7 @@ describe('PasswordField', () => {
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
@@ -222,7 +224,7 @@ describe('PasswordField', () => {
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
@@ -246,7 +248,7 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordField = getByLabelText('Password');
|
||||
fireEvent.blur(passwordField, {
|
||||
target: {
|
||||
@@ -266,7 +268,7 @@ describe('PasswordField', () => {
|
||||
handleErrorChange: jest.fn(),
|
||||
handleBlur: jest.fn(),
|
||||
};
|
||||
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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,8 +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 || '',
|
||||
RECAPTCHA_SITE_KEY_WEB: process.env.RECAPTCHA_SITE_KEY_WEB || '',
|
||||
};
|
||||
|
||||
export default configuration;
|
||||
|
||||
@@ -37,4 +37,6 @@ 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';
|
||||
export const FIELD_LABELS = {
|
||||
COUNTRY: 'country',
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,9 +81,3 @@ export const isHostAvailableInQueryParams = () => {
|
||||
const queryParams = getAllPossibleQueryParams();
|
||||
return 'host' in queryParams;
|
||||
};
|
||||
|
||||
export const redirectWithDelay = (redirectUrl) => {
|
||||
setTimeout(() => {
|
||||
window.location.href = redirectUrl;
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
fireEvent, render, screen,
|
||||
} from '@testing-library/react';
|
||||
@@ -25,6 +26,7 @@ jest.mock('react-router-dom', () => ({
|
||||
useNavigate: () => mockedNavigator,
|
||||
}));
|
||||
|
||||
const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
const initialState = {
|
||||
@@ -76,7 +78,7 @@ describe('ForgotPasswordPage', () => {
|
||||
);
|
||||
|
||||
it('not should display need other help signing in button', () => {
|
||||
const { queryByTestId } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const { queryByTestId } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const forgotPasswordButton = queryByTestId('forgot-password');
|
||||
expect(forgotPasswordButton).toBeNull();
|
||||
});
|
||||
@@ -85,14 +87,14 @@ describe('ForgotPasswordPage', () => {
|
||||
mergeConfig({
|
||||
LOGIN_ISSUE_SUPPORT_LINK: '/support',
|
||||
});
|
||||
render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const forgotPasswordButton = screen.findByText('Need help signing in?');
|
||||
expect(forgotPasswordButton).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display email validation error message', async () => {
|
||||
const validationMessage = 'We were unable to contact you.Enter a valid email address below.';
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
|
||||
@@ -113,7 +115,7 @@ describe('ForgotPasswordPage', () => {
|
||||
const expectedMessage = 'We were unable to contact you.'
|
||||
+ 'An error has occurred. Try refreshing the page, or check your internet connection.';
|
||||
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
const alertElements = container.querySelectorAll('.alert-danger');
|
||||
const validationErrors = alertElements[0].textContent;
|
||||
@@ -122,7 +124,7 @@ describe('ForgotPasswordPage', () => {
|
||||
|
||||
it('should display empty email validation message', async () => {
|
||||
const validationMessage = 'We were unable to contact you.Enter your email below.';
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
const submitButton = screen.getByText('Submit');
|
||||
fireEvent.click(submitButton);
|
||||
@@ -139,7 +141,7 @@ describe('ForgotPasswordPage', () => {
|
||||
forgotPassword: { status: 'forbidden' },
|
||||
});
|
||||
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
const alertElements = container.querySelectorAll('.alert-danger');
|
||||
const validationErrors = alertElements[0].textContent;
|
||||
@@ -147,7 +149,7 @@ describe('ForgotPasswordPage', () => {
|
||||
});
|
||||
|
||||
it('should not display any error message on change event', () => {
|
||||
render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
|
||||
@@ -170,7 +172,7 @@ describe('ForgotPasswordPage', () => {
|
||||
};
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
|
||||
fireEvent.blur(emailInput);
|
||||
@@ -185,7 +187,7 @@ describe('ForgotPasswordPage', () => {
|
||||
emailValidationError: validationMessage,
|
||||
email: '',
|
||||
};
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const validationElement = container.querySelector('.pgn__form-text-invalid');
|
||||
expect(validationElement.textContent).toEqual(validationMessage);
|
||||
});
|
||||
@@ -203,7 +205,7 @@ describe('ForgotPasswordPage', () => {
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const emailInput = screen.getByLabelText('Email');
|
||||
|
||||
fireEvent.focus(emailInput);
|
||||
@@ -217,7 +219,7 @@ describe('ForgotPasswordPage', () => {
|
||||
emailValidationError: '',
|
||||
email: '',
|
||||
};
|
||||
render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const errorElement = screen.queryByTestId('email-invalid-feedback');
|
||||
expect(errorElement).toBeNull();
|
||||
});
|
||||
@@ -234,7 +236,7 @@ describe('ForgotPasswordPage', () => {
|
||||
+ 'receive a password reset message after 1 minute, verify that you entered the correct email address,'
|
||||
+ ' or check your spam folder. If you need further assistance, contact technical support.';
|
||||
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const successElement = findByTextContent(container, successMessage);
|
||||
|
||||
expect(successElement).toBeDefined();
|
||||
@@ -252,7 +254,7 @@ describe('ForgotPasswordPage', () => {
|
||||
+ 'This password reset link is invalid. It may have been used already. '
|
||||
+ 'Enter your email below to receive a new link.';
|
||||
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
const successElement = findByTextContent(container, successMessage);
|
||||
|
||||
expect(successElement).toBeDefined();
|
||||
@@ -260,7 +262,7 @@ describe('ForgotPasswordPage', () => {
|
||||
});
|
||||
|
||||
it('should redirect onto login page', async () => {
|
||||
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
|
||||
const { container } = render(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
|
||||
const navElement = container.querySelector('nav');
|
||||
const anchorElement = navElement.querySelector('a');
|
||||
|
||||
@@ -1,36 +1,27 @@
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import React, { StrictMode } from 'react';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {
|
||||
APP_INIT_ERROR, APP_READY, initialize, mergeConfig, subscribe,
|
||||
} from '@edx/frontend-platform';
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import configuration from './config';
|
||||
import messages from './i18n';
|
||||
import MainApp from './MainApp';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<MainApp />
|
||||
</StrictMode>,
|
||||
ReactDOM.render(
|
||||
<MainApp />,
|
||||
document.getElementById('root'),
|
||||
);
|
||||
});
|
||||
|
||||
subscribe(APP_INIT_ERROR, (error) => {
|
||||
const root = createRoot(document.getElementById('root'));
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ErrorPage message={error.message} />
|
||||
</StrictMode>,
|
||||
);
|
||||
ReactDOM.render(<ErrorPage message={error.message} />, document.getElementById('root'));
|
||||
});
|
||||
|
||||
initialize({
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@openedx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
@import "sass/style";
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, 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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, StatefulButton,
|
||||
} from '@openedx/paragon';
|
||||
@@ -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
|
||||
@@ -386,4 +365,4 @@ export default connect(
|
||||
loginRequest,
|
||||
getTPADataFromBackend: getThirdPartyAuthContext,
|
||||
},
|
||||
)(LoginPage);
|
||||
)(injectIntl(LoginPage));
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
render, screen,
|
||||
} from '@testing-library/react';
|
||||
@@ -7,6 +9,8 @@ import {
|
||||
import AccountActivationMessage from '../AccountActivationMessage';
|
||||
import { ACCOUNT_ACTIVATION_MESSAGE } from '../data/constants';
|
||||
|
||||
const IntlAccountActivationMessage = injectIntl(AccountActivationMessage);
|
||||
|
||||
describe('AccountActivationMessage', () => {
|
||||
beforeEach(() => {
|
||||
mergeConfig({
|
||||
@@ -17,7 +21,7 @@ describe('AccountActivationMessage', () => {
|
||||
it('should match account already activated message', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -32,7 +36,7 @@ describe('AccountActivationMessage', () => {
|
||||
it('should match account activated success message', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -49,12 +53,12 @@ describe('AccountActivationMessage', () => {
|
||||
it('should match account activation error message', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
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(
|
||||
'',
|
||||
@@ -65,7 +69,7 @@ describe('AccountActivationMessage', () => {
|
||||
it('should not display anything for invalid message type', () => {
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<AccountActivationMessage messageType="invalid-message" />
|
||||
<IntlAccountActivationMessage messageType="invalid-message" />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -84,7 +88,7 @@ describe('EmailConfirmationMessage', () => {
|
||||
it('should match email already confirmed message', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.INFO} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
@@ -99,7 +103,7 @@ describe('EmailConfirmationMessage', () => {
|
||||
it('should match email confirmation success message', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.SUCCESS} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const expectedMessage = 'Success! You have confirmed your email.Sign in to continue.';
|
||||
@@ -113,11 +117,11 @@ describe('EmailConfirmationMessage', () => {
|
||||
it('should match email confirmation error message', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<AccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
|
||||
<IntlAccountActivationMessage messageType={ACCOUNT_ACTIVATION_MESSAGE.ERROR} />
|
||||
</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' },
|
||||
|
||||
@@ -1,29 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
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')),
|
||||
@@ -32,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 => ({
|
||||
@@ -48,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>
|
||||
<ChangePasswordPrompt {...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>
|
||||
<ChangePasswordPrompt {...props} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
<MemoryRouter>
|
||||
<IntlChangePasswordPrompt {...props} />
|
||||
</MemoryRouter>
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user