Compare commits
56 Commits
dependabot
...
frontend-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0cf4623a4 | ||
|
|
12670240b3 | ||
|
|
444e825fde | ||
|
|
c2a3c70c9d | ||
|
|
56d8a10694 | ||
|
|
86b4ea79de | ||
|
|
8dcf55254e | ||
|
|
e52d3d89fd | ||
|
|
bb3ab6cba4 | ||
|
|
fc40952da3 | ||
|
|
1733f6ec01 | ||
|
|
5d8743fb29 | ||
|
|
70ffc552b5 | ||
|
|
3dbd6a76df | ||
|
|
cb3ad5c53a | ||
|
|
65462e7d80 | ||
|
|
7bfb5d16d0 | ||
|
|
c31c397c61 | ||
|
|
4fc41b0fe7 | ||
|
|
df9454bbe6 | ||
|
|
5e41382f24 | ||
|
|
fe44896856 | ||
|
|
ed58e02eae | ||
|
|
2337a7105f | ||
|
|
b9f2c5da43 | ||
|
|
1a0a6e3179 | ||
|
|
92208a0a7c | ||
|
|
45535ee055 | ||
|
|
e53c4997bb | ||
|
|
f311539e12 | ||
|
|
c1070930bf | ||
|
|
dad2887eed | ||
|
|
4f79099eca | ||
|
|
49f42a8857 | ||
|
|
2d6b401312 | ||
|
|
c602136798 | ||
|
|
9a48fe323b | ||
|
|
967c52bde9 | ||
|
|
3252b593fd | ||
|
|
c98b9a1408 | ||
|
|
b6c659ccb8 | ||
|
|
7efa279a29 | ||
|
|
f83f24af89 | ||
|
|
b8116485b0 | ||
|
|
762ff75fc4 | ||
|
|
095b156b95 | ||
|
|
033e0fd7c5 | ||
|
|
1ecdb0b6af | ||
|
|
18951cc4d0 | ||
|
|
4dd5ddcc8b | ||
|
|
6acbf64a71 | ||
|
|
aea12a6a37 | ||
|
|
3d778807f1 | ||
|
|
014a990c22 | ||
|
|
7c8051a440 | ||
|
|
8f8531a242 |
43
.env
43
.env
@@ -1,43 +0,0 @@
|
|||||||
NODE_ENV='production'
|
|
||||||
ACCESS_TOKEN_COOKIE_NAME=null
|
|
||||||
BASE_URL=null
|
|
||||||
CREDENTIALS_BASE_URL=null
|
|
||||||
CSRF_TOKEN_API_PATH=null
|
|
||||||
ECOMMERCE_BASE_URL=null
|
|
||||||
LANGUAGE_PREFERENCE_COOKIE_NAME=null
|
|
||||||
LMS_BASE_URL=null
|
|
||||||
LOGIN_URL=null
|
|
||||||
LOGOUT_URL=null
|
|
||||||
MARKETING_SITE_BASE_URL=null
|
|
||||||
ORDER_HISTORY_URL=null
|
|
||||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
|
||||||
SEGMENT_KEY=''
|
|
||||||
SITE_NAME=null
|
|
||||||
INFO_EMAIL=''
|
|
||||||
# ***** Cookies *****
|
|
||||||
USER_RETENTION_COOKIE_NAME=null
|
|
||||||
# ***** Links *****
|
|
||||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
|
||||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
|
|
||||||
POST_REGISTRATION_REDIRECT_URL=''
|
|
||||||
SEARCH_CATALOG_URL=''
|
|
||||||
# ***** Features flags *****
|
|
||||||
DISABLE_ENTERPRISE_LOGIN=''
|
|
||||||
ENABLE_AUTO_GENERATED_USERNAME=''
|
|
||||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
|
|
||||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
|
|
||||||
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
|
|
||||||
MARKETING_EMAILS_OPT_IN=''
|
|
||||||
SHOW_CONFIGURABLE_EDX_FIELDS=''
|
|
||||||
ENABLE_IMAGE_LAYOUT=''
|
|
||||||
# ***** Zendesk related keys *****
|
|
||||||
ZENDESK_KEY=''
|
|
||||||
ZENDESK_LOGO_URL=''
|
|
||||||
# ***** Base Container Images *****
|
|
||||||
BANNER_IMAGE_LARGE=''
|
|
||||||
BANNER_IMAGE_MEDIUM=''
|
|
||||||
BANNER_IMAGE_SMALL=''
|
|
||||||
BANNER_IMAGE_EXTRA_SMALL=''
|
|
||||||
# ***** Miscellaneous *****
|
|
||||||
APP_ID=''
|
|
||||||
MFE_CONFIG_API_URL=''
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# Copy these to the .env.private to enable edX specific functionality on local system
|
|
||||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
|
|
||||||
MARKETING_EMAILS_OPT_IN='true'
|
|
||||||
SHOW_CONFIGURABLE_EDX_FIELDS='true'
|
|
||||||
20
.env.test
20
.env.test
@@ -1,20 +0,0 @@
|
|||||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
|
||||||
BASE_URL='http://localhost:1995'
|
|
||||||
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:18000/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'
|
|
||||||
APP_ID=''
|
|
||||||
MFE_CONFIG_API_URL=''
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
coverage/*
|
|
||||||
dist/
|
|
||||||
docs
|
|
||||||
node_modules/
|
|
||||||
__mocks__/
|
|
||||||
__snapshots__/
|
|
||||||
52
.eslintrc.js
52
.eslintrc.js
@@ -1,52 +0,0 @@
|
|||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
||||||
const { createConfig } = require('@openedx/frontend-build');
|
|
||||||
|
|
||||||
module.exports = createConfig('eslint', {
|
|
||||||
rules: {
|
|
||||||
// Temporarily update the 'indent', 'template-curly-spacing' and
|
|
||||||
// 'no-multiple-empty-lines' rules since they are causing eslint
|
|
||||||
// to fail for no apparent reason since upgrading
|
|
||||||
// @openedx/frontend-build from v3 to v5:
|
|
||||||
// - TypeError: Cannot read property 'range' of null
|
|
||||||
indent: [
|
|
||||||
'error',
|
|
||||||
2,
|
|
||||||
{ ignoredNodes: ['TemplateLiteral', 'SwitchCase'] },
|
|
||||||
],
|
|
||||||
'template-curly-spacing': 'off',
|
|
||||||
'jsx-a11y/label-has-associated-control': ['error', {
|
|
||||||
labelComponents: [],
|
|
||||||
labelAttributes: [],
|
|
||||||
controlComponents: [],
|
|
||||||
assert: 'htmlFor',
|
|
||||||
depth: 25,
|
|
||||||
}],
|
|
||||||
'sort-imports': ['error', { ignoreCase: true, ignoreDeclarationSort: true }],
|
|
||||||
'import/order': [
|
|
||||||
'error',
|
|
||||||
{
|
|
||||||
groups: [
|
|
||||||
'builtin',
|
|
||||||
'external',
|
|
||||||
'internal',
|
|
||||||
['sibling', 'parent'],
|
|
||||||
'index',
|
|
||||||
],
|
|
||||||
pathGroups: [
|
|
||||||
{
|
|
||||||
pattern: '@(react|react-dom|react-redux)',
|
|
||||||
group: 'external',
|
|
||||||
position: 'before',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
pathGroupsExcludedImportTypes: ['react'],
|
|
||||||
'newlines-between': 'always',
|
|
||||||
alphabetize: {
|
|
||||||
order: 'asc',
|
|
||||||
caseInsensitive: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'function-paren-newline': 'off',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,21 +1,15 @@
|
|||||||
.DS_Store
|
|
||||||
.eslintcache
|
|
||||||
.idea
|
|
||||||
node_modules
|
node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
coverage
|
coverage
|
||||||
module.config.js
|
module.config.js
|
||||||
.env.private
|
|
||||||
|
|
||||||
dist/
|
dist/
|
||||||
|
/*.tgz
|
||||||
|
|
||||||
|
### i18n ###
|
||||||
src/i18n/transifex_input.json
|
src/i18n/transifex_input.json
|
||||||
temp/babel-plugin-react-intl
|
|
||||||
|
|
||||||
### pyenv ###
|
### Editors ###
|
||||||
.python-version
|
.DS_Store
|
||||||
|
|
||||||
### Emacs ###
|
|
||||||
*~
|
*~
|
||||||
/temp
|
/temp
|
||||||
/.vscode
|
/.vscode
|
||||||
src/i18n/messages
|
|
||||||
15
.npmignore
15
.npmignore
@@ -1,11 +1,6 @@
|
|||||||
.eslintignore
|
__mocks__
|
||||||
.eslintrc.json
|
|
||||||
.gitignore
|
|
||||||
docker-compose.yml
|
|
||||||
Dockerfile
|
|
||||||
Makefile
|
|
||||||
npm-debug.log
|
|
||||||
|
|
||||||
coverage
|
|
||||||
node_modules
|
node_modules
|
||||||
public
|
*.test.js
|
||||||
|
*.test.jsx
|
||||||
|
*.test.ts
|
||||||
|
*.test.tsx
|
||||||
|
|||||||
13
Makefile
13
Makefile
@@ -13,6 +13,19 @@ precommit:
|
|||||||
requirements:
|
requirements:
|
||||||
npm ci
|
npm ci
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf dist
|
||||||
|
|
||||||
|
build: clean
|
||||||
|
tsc --project tsconfig.build.json
|
||||||
|
tsc-alias -p tsconfig.build.json
|
||||||
|
find src -type f -name '*.scss' -exec sh -c '\
|
||||||
|
for f in "$$@"; do \
|
||||||
|
d="dist/$${f#src/}"; \
|
||||||
|
mkdir -p "$$(dirname "$$d")"; \
|
||||||
|
cp "$$f" "$$d"; \
|
||||||
|
done' sh {} +
|
||||||
|
|
||||||
i18n.extract:
|
i18n.extract:
|
||||||
# Pulling display strings from .jsx files into .json files...
|
# Pulling display strings from .jsx files into .json files...
|
||||||
rm -rf $(transifex_temp)
|
rm -rf $(transifex_temp)
|
||||||
|
|||||||
26
README.rst
26
README.rst
@@ -34,26 +34,6 @@ Installation
|
|||||||
.. _Tutor: https://github.com/overhangio/tutor
|
.. _Tutor: https://github.com/overhangio/tutor
|
||||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
|
.. _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.
|
|
||||||
|
|
||||||
2. Start up LMS, if it's not already started.
|
|
||||||
|
|
||||||
4. Within this project (frontend-app-authn), install requirements and start the development server:
|
|
||||||
|
|
||||||
.. code-block::
|
|
||||||
|
|
||||||
npm install
|
|
||||||
npm start # The server will run on port 1999
|
|
||||||
|
|
||||||
5. Once the dev server is up, visit http://localhost:1999 to access the MFE
|
|
||||||
|
|
||||||
.. image:: ./docs/images/frontend-app-authn-localhost-preview.png
|
|
||||||
|
|
||||||
**Note:** Follow `Enable social auth locally <docs/how_tos/enable_social_auth.rst>`_ for enabling Social Sign-on Buttons (SSO) locally
|
|
||||||
|
|
||||||
Environment Variables/Setup Notes
|
Environment Variables/Setup Notes
|
||||||
=================================
|
=================================
|
||||||
|
|
||||||
@@ -143,10 +123,6 @@ Furthermore, there are several edX-specific environment variables that enable in
|
|||||||
- Enables support for opting in marketing emails that helps us getting user consent for sending marketing emails.
|
- Enables support for opting in marketing emails that helps us getting user consent for sending marketing emails.
|
||||||
- ``true`` | ``''`` (empty strings are falsy)
|
- ``true`` | ``''`` (empty strings are falsy)
|
||||||
|
|
||||||
* - ``SHOW_CONFIGURABLE_EDX_FIELDS``
|
|
||||||
- For edX, country and honor code fields are required by default. This flag enables edX specific required fields.
|
|
||||||
- ``true`` | ``''`` (empty strings are falsy)
|
|
||||||
|
|
||||||
For more information see the document: `Micro-frontend applications in Open
|
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://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
|
||||||
|
|
||||||
@@ -225,4 +201,4 @@ Please see `LICENSE <https://github.com/openedx/frontend-app-authn/blob/master/L
|
|||||||
:target: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml
|
:target: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml
|
||||||
:alt: Continuous Integration
|
:alt: Continuous Integration
|
||||||
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
|
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
|
||||||
:target: https://github.com/semantic-release/semantic-release
|
:target: https://github.com/semantic-release/semantic-release
|
||||||
|
|||||||
5
app.d.ts
vendored
Normal file
5
app.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="@openedx/frontend-base" />
|
||||||
|
|
||||||
|
declare module 'site.config' {
|
||||||
|
export default SiteConfig;
|
||||||
|
}
|
||||||
3
babel.config.js
Normal file
3
babel.config.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
const { createConfig } = require('@openedx/frontend-base/tools');
|
||||||
|
|
||||||
|
module.exports = createConfig('babel');
|
||||||
22
eslint.config.js
Normal file
22
eslint.config.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// @ts-check
|
||||||
|
|
||||||
|
const { createLintConfig } = require('@openedx/frontend-base/tools');
|
||||||
|
|
||||||
|
module.exports = createLintConfig(
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'src/**/*',
|
||||||
|
'site.config.*',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
'coverage/*',
|
||||||
|
'dist/*',
|
||||||
|
'docs/*',
|
||||||
|
'node_modules/*',
|
||||||
|
'**/__mocks__/*',
|
||||||
|
'**/__snapshots__/*',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
const { createConfig } = require('@openedx/frontend-build');
|
const { createConfig } = require('@openedx/frontend-base/tools');
|
||||||
|
|
||||||
module.exports = createConfig('jest', {
|
module.exports = createConfig('test', {
|
||||||
setupFiles: [
|
setupFilesAfterEnv: [
|
||||||
'<rootDir>/src/setupTest.js',
|
'<rootDir>/src/setupTest.js',
|
||||||
],
|
],
|
||||||
coveragePathIgnorePatterns: [
|
coveragePathIgnorePatterns: [
|
||||||
'src/setupTest.js',
|
'src/setupTest.js',
|
||||||
'src/i18n',
|
'src/i18n',
|
||||||
'src/index.jsx',
|
|
||||||
'MainApp.jsx',
|
|
||||||
],
|
],
|
||||||
testEnvironment: 'jsdom',
|
moduleNameMapper: {
|
||||||
|
'\\.svg$': '<rootDir>/src/__mocks__/svg.js',
|
||||||
|
'\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/src/__mocks__/file.js',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
14357
package-lock.json
generated
14357
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
114
package.json
114
package.json
@@ -1,25 +1,40 @@
|
|||||||
{
|
{
|
||||||
"name": "@edx/frontend-app-authn",
|
"name": "@openedx/frontend-app-authn",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0-alpha.6",
|
||||||
"description": "Frontend application template",
|
"description": "Frontend authentication",
|
||||||
|
"engines": {
|
||||||
|
"node": "^24.12"
|
||||||
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/openedx/frontend-app-authn.git"
|
"url": "git+https://github.com/openedx/frontend-app-authn.git"
|
||||||
},
|
},
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/index.js",
|
||||||
|
"./app.scss": "./dist/app.scss"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"/dist"
|
||||||
|
],
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"extends @edx/browserslist-config"
|
"extends @edx/browserslist-config"
|
||||||
],
|
],
|
||||||
|
"sideEffects": [
|
||||||
|
"*.css",
|
||||||
|
"*.scss"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "fedx-scripts webpack",
|
"build": "make build",
|
||||||
"i18n_extract": "fedx-scripts formatjs extract",
|
"clean": "make clean",
|
||||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
"dev": "PORT=1999 PUBLIC_PATH=/authn openedx dev",
|
||||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
|
"i18n_extract": "openedx formatjs extract",
|
||||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
"lint": "openedx lint .",
|
||||||
"start": "fedx-scripts webpack-dev-server --progress",
|
"lint:fix": "openedx lint --fix .",
|
||||||
"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",
|
"prepack": "npm run build",
|
||||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
"snapshot": "openedx test --updateSnapshot",
|
||||||
|
"test": "openedx test --coverage --passWithNoTests"
|
||||||
},
|
},
|
||||||
"author": "edX",
|
"author": "Open edX",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
|
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
@@ -29,52 +44,41 @@
|
|||||||
"url": "https://github.com/openedx/frontend-app-authn/issues"
|
"url": "https://github.com/openedx/frontend-app-authn/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
|
||||||
"@edx/frontend-platform": "^8.3.1",
|
"@edx/openedx-atlas": "^0.7.0",
|
||||||
"@edx/openedx-atlas": "^0.6.0",
|
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||||
"@fortawesome/react-fontawesome": "0.2.2",
|
"classnames": "^2.5.1",
|
||||||
"@openedx/paragon": "^22.16.0",
|
"fastest-levenshtein": "^1.0.16",
|
||||||
"@optimizely/react-sdk": "^2.9.1",
|
"form-urlencoded": "^6.1.5",
|
||||||
"@redux-devtools/extension": "3.3.0",
|
"i18n-iso-countries": "^7.13.0",
|
||||||
"@testing-library/react": "^16.2.0",
|
"prop-types": "^15.8.1",
|
||||||
"algoliasearch": "^4.14.3",
|
"query-string": "^7.1.3",
|
||||||
"algoliasearch-helper": "^3.14.0",
|
"react-helmet": "^6.1.0",
|
||||||
"classnames": "2.5.1",
|
"react-loading-skeleton": "^3.5.0",
|
||||||
"core-js": "3.43.0",
|
"react-responsive": "^8.2.0",
|
||||||
"fastest-levenshtein": "1.0.16",
|
"universal-cookie": "^8.0.1"
|
||||||
"form-urlencoded": "6.1.5",
|
|
||||||
"prop-types": "15.8.1",
|
|
||||||
"query-string": "7.1.3",
|
|
||||||
"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.30.1",
|
|
||||||
"react-router-dom": "6.30.1",
|
|
||||||
"react-zendesk": "^0.1.13",
|
|
||||||
"redux": "4.2.1",
|
|
||||||
"redux-logger": "3.0.6",
|
|
||||||
"redux-mock-store": "1.5.5",
|
|
||||||
"redux-saga": "1.3.0",
|
|
||||||
"redux-thunk": "2.4.2",
|
|
||||||
"regenerator-runtime": "0.14.1",
|
|
||||||
"reselect": "5.1.1",
|
|
||||||
"universal-cookie": "7.2.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@edx/browserslist-config": "^1.1.1",
|
"@edx/browserslist-config": "^1.5.0",
|
||||||
"@edx/reactifex": "1.1.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@openedx/frontend-build": "^14.4.2",
|
"@types/jest": "^29.5.14",
|
||||||
"babel-plugin-formatjs": "10.5.38",
|
"babel-plugin-formatjs": "10.5.38",
|
||||||
"eslint-plugin-import": "2.31.0",
|
"eslint-plugin-import": "2.31.0",
|
||||||
"glob": "7.2.3",
|
"jest": "^29.7.0",
|
||||||
"history": "5.3.0",
|
"react-test-renderer": "^18.3.1",
|
||||||
"jest": "30.0.0",
|
"ts-jest": "^29.4.0",
|
||||||
"react-test-renderer": "^18.3.1"
|
"tsc-alias": "^1.8.16"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@openedx/frontend-base": "^1.0.0-alpha.14",
|
||||||
|
"@openedx/paragon": "^23",
|
||||||
|
"@tanstack/react-query": "^5",
|
||||||
|
"react": "^18",
|
||||||
|
"react-dom": "^18",
|
||||||
|
"react-router": "^6",
|
||||||
|
"react-router-dom": "^6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,9 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en-us">
|
<html lang="en-us">
|
||||||
<head>
|
<head>
|
||||||
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></title>
|
<title>Authentication Development Site></title>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
19
site.config.dev.tsx
Normal file
19
site.config.dev.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { EnvironmentTypes, SiteConfig } from '@openedx/frontend-base';
|
||||||
|
|
||||||
|
import { authnApp } from './src';
|
||||||
|
|
||||||
|
import './src/app.scss';
|
||||||
|
|
||||||
|
const siteConfig: SiteConfig = {
|
||||||
|
siteId: 'authn-dev',
|
||||||
|
siteName: 'Authn Dev',
|
||||||
|
baseUrl: 'http://apps.local.openedx.io:8080',
|
||||||
|
lmsBaseUrl: 'http://local.openedx.io:8000',
|
||||||
|
loginUrl: 'http://local.openedx.io:8000/login',
|
||||||
|
logoutUrl: 'http://local.openedx.io:8000/logout',
|
||||||
|
|
||||||
|
environment: EnvironmentTypes.DEVELOPMENT,
|
||||||
|
apps: [authnApp],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default siteConfig;
|
||||||
52
site.config.test.tsx
Normal file
52
site.config.test.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import type { SiteConfig } from '@openedx/frontend-base';
|
||||||
|
|
||||||
|
import { appId } from './src/constants';
|
||||||
|
|
||||||
|
const siteConfig: SiteConfig = {
|
||||||
|
siteId: 'test-site',
|
||||||
|
siteName: 'Test Site',
|
||||||
|
baseUrl: 'http://localhost:1996',
|
||||||
|
lmsBaseUrl: 'http://localhost:8000',
|
||||||
|
loginUrl: 'http://localhost:8000/login',
|
||||||
|
logoutUrl: 'http://localhost:8000/logout',
|
||||||
|
|
||||||
|
// Use 'test' instead of EnvironmentTypes.TEST to break a circular dependency
|
||||||
|
// when mocking `@openedx/frontend-base` itself.
|
||||||
|
environment: 'test' as SiteConfig['environment'],
|
||||||
|
apps: [{
|
||||||
|
appId,
|
||||||
|
config: {
|
||||||
|
ACTIVATION_EMAIL_SUPPORT_LINK: null,
|
||||||
|
ALLOW_PUBLIC_ACCOUNT_CREATION: false,
|
||||||
|
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: null,
|
||||||
|
BANNER_IMAGE_EXTRA_SMALL: '',
|
||||||
|
BANNER_IMAGE_LARGE: '',
|
||||||
|
BANNER_IMAGE_MEDIUM: '',
|
||||||
|
BANNER_IMAGE_SMALL: '',
|
||||||
|
DISABLE_ENTERPRISE_LOGIN: true,
|
||||||
|
ENABLE_AUTO_GENERATED_USERNAME: false,
|
||||||
|
ENABLE_DYNAMIC_REGISTRATION_FIELDS: false,
|
||||||
|
ENABLE_IMAGE_LAYOUT: false,
|
||||||
|
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: false,
|
||||||
|
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
|
||||||
|
INFO_EMAIL: '',
|
||||||
|
LOGIN_ISSUE_SUPPORT_LINK: null,
|
||||||
|
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
|
||||||
|
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
|
||||||
|
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
|
||||||
|
MARKETING_EMAILS_OPT_IN: '',
|
||||||
|
MARKETING_SITE_BASE_URL: 'http://localhost:18000',
|
||||||
|
PASSWORD_RESET_SUPPORT_LINK: null,
|
||||||
|
POST_REGISTRATION_REDIRECT_URL: '',
|
||||||
|
PRIVACY_POLICY: null,
|
||||||
|
SEARCH_CATALOG_URL: null,
|
||||||
|
SESSION_COOKIE_DOMAIN: 'local.openedx.io',
|
||||||
|
SHOW_REGISTRATION_LINKS: false,
|
||||||
|
TOS_AND_HONOR_CODE: null,
|
||||||
|
TOS_LINK: null,
|
||||||
|
USER_RETENTION_COOKIE_NAME: '',
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default siteConfig;
|
||||||
19
src/Main.tsx
Executable file
19
src/Main.tsx
Executable file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
import { CurrentAppProvider } from '@openedx/frontend-base';
|
||||||
|
|
||||||
|
import { appId } from './constants';
|
||||||
|
import {
|
||||||
|
registerIcons,
|
||||||
|
} from './common-components';
|
||||||
|
|
||||||
|
import './sass/_style.scss';
|
||||||
|
|
||||||
|
registerIcons();
|
||||||
|
|
||||||
|
const Main = () => (
|
||||||
|
<CurrentAppProvider appId={appId}>
|
||||||
|
<Outlet />
|
||||||
|
</CurrentAppProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Main;
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { AppProvider } from '@edx/frontend-platform/react';
|
|
||||||
import { Helmet } from 'react-helmet';
|
|
||||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
|
||||||
|
|
||||||
import {
|
|
||||||
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
|
|
||||||
} from './common-components';
|
|
||||||
import configureStore from './data/configureStore';
|
|
||||||
import {
|
|
||||||
AUTHN_PROGRESSIVE_PROFILING,
|
|
||||||
LOGIN_PAGE,
|
|
||||||
PAGE_NOT_FOUND,
|
|
||||||
PASSWORD_RESET_CONFIRM,
|
|
||||||
RECOMMENDATIONS,
|
|
||||||
REGISTER_EMBEDDED_PAGE,
|
|
||||||
REGISTER_PAGE,
|
|
||||||
RESET_PAGE,
|
|
||||||
} from './data/constants';
|
|
||||||
import { updatePathWithQueryParams } from './data/utils';
|
|
||||||
import { ForgotPasswordPage } from './forgot-password';
|
|
||||||
import Logistration from './logistration/Logistration';
|
|
||||||
import { ProgressiveProfiling } from './progressive-profiling';
|
|
||||||
import { RecommendationsPage } from './recommendations';
|
|
||||||
import { RegistrationPage } from './register';
|
|
||||||
import { ResetPasswordPage } from './reset-password';
|
|
||||||
|
|
||||||
import './index.scss';
|
|
||||||
|
|
||||||
registerIcons();
|
|
||||||
|
|
||||||
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
src/__mocks__/file.js
Normal file
1
src/__mocks__/file.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = 'FileMock';
|
||||||
1
src/__mocks__/svg.js
Normal file
1
src/__mocks__/svg.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
module.exports = 'SvgURL';
|
||||||
2
src/app.scss
Executable file
2
src/app.scss
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
@use "@openedx/frontend-base/shell/app.scss";
|
||||||
|
@use "./sass/style";
|
||||||
43
src/app.ts
Normal file
43
src/app.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { App } from '@openedx/frontend-base';
|
||||||
|
import { appId } from './constants';
|
||||||
|
import routes from './routes';
|
||||||
|
import messages from './i18n';
|
||||||
|
|
||||||
|
const app: App = {
|
||||||
|
appId,
|
||||||
|
routes,
|
||||||
|
messages,
|
||||||
|
config: {
|
||||||
|
ACTIVATION_EMAIL_SUPPORT_LINK: null,
|
||||||
|
ALLOW_PUBLIC_ACCOUNT_CREATION: true,
|
||||||
|
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: null,
|
||||||
|
BANNER_IMAGE_EXTRA_SMALL: '',
|
||||||
|
BANNER_IMAGE_LARGE: '',
|
||||||
|
BANNER_IMAGE_MEDIUM: '',
|
||||||
|
BANNER_IMAGE_SMALL: '',
|
||||||
|
DISABLE_ENTERPRISE_LOGIN: true,
|
||||||
|
ENABLE_AUTO_GENERATED_USERNAME: false,
|
||||||
|
ENABLE_DYNAMIC_REGISTRATION_FIELDS: false,
|
||||||
|
ENABLE_IMAGE_LAYOUT: false,
|
||||||
|
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: false,
|
||||||
|
FAVICON_URL: 'https://edx-cdn.org/v3/default/favicon.ico',
|
||||||
|
INFO_EMAIL: '',
|
||||||
|
LOGIN_ISSUE_SUPPORT_LINK: null,
|
||||||
|
LOGO_TRADEMARK_URL: 'https://edx-cdn.org/v3/default/logo-trademark.svg',
|
||||||
|
LOGO_URL: 'https://edx-cdn.org/v3/default/logo.svg',
|
||||||
|
LOGO_WHITE_URL: 'https://edx-cdn.org/v3/default/logo-white.svg',
|
||||||
|
MARKETING_EMAILS_OPT_IN: '',
|
||||||
|
MARKETING_SITE_BASE_URL: 'http://local.openedx.io',
|
||||||
|
PASSWORD_RESET_SUPPORT_LINK: null,
|
||||||
|
POST_REGISTRATION_REDIRECT_URL: '',
|
||||||
|
PRIVACY_POLICY: null,
|
||||||
|
SEARCH_CATALOG_URL: null,
|
||||||
|
SESSION_COOKIE_DOMAIN: 'local.openedx.io',
|
||||||
|
SHOW_REGISTRATION_LINKS: true,
|
||||||
|
TOS_AND_HONOR_CODE: null,
|
||||||
|
TOS_LINK: null,
|
||||||
|
USER_RETENTION_COOKIE_NAME: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default app;
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import React from 'react';
|
import { IntlProvider } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index';
|
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index';
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React from 'react';
|
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@openedx/paragon';
|
import { Hyperlink, Image } from '@openedx/paragon';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
@@ -13,20 +10,20 @@ const LargeLayout = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-50 d-flex">
|
<div className="w-50 d-flex">
|
||||||
<div className="col-md-9 bg-primary-400">
|
<div className="col-md-9 bg-primary-400">
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<Image className="logo position-absolute" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
<Image className="logo position-absolute" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
<div className="min-vh-100 d-flex align-items-center">
|
<div className="min-vh-100 d-flex align-items-center">
|
||||||
<div className={classNames({ 'large-yellow-line mr-n4.5': getConfig().SITE_NAME === 'edX' })} />
|
<div className={classNames({ 'large-yellow-line mr-n4.5': getSiteConfig().siteName === 'edX' })} />
|
||||||
<h1
|
<h1
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'display-2 text-white mw-xs',
|
'display-2 text-white mw-xs',
|
||||||
{ 'ml-6': getConfig().SITE_NAME !== 'edX' },
|
{ 'ml-6': getSiteConfig().siteName !== 'edX' },
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{formatMessage(messages['start.learning'])}
|
{formatMessage(messages['start.learning'])}
|
||||||
<div className="text-accent-a">
|
<div className="text-accent-a">
|
||||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
{formatMessage(messages['with.site.name'], { siteName: getSiteConfig().siteName })}
|
||||||
</div>
|
</div>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React from 'react';
|
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@openedx/paragon';
|
import { Hyperlink, Image } from '@openedx/paragon';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
@@ -15,22 +12,22 @@ const MediumLayout = () => {
|
|||||||
<div className="w-100 medium-screen-top-stripe" />
|
<div className="w-100 medium-screen-top-stripe" />
|
||||||
<div className="w-100 p-0 mb-3 d-flex">
|
<div className="w-100 p-0 mb-3 d-flex">
|
||||||
<div className="col-md-10 bg-primary-400">
|
<div className="col-md-10 bg-primary-400">
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
|
<Image alt={getSiteConfig().siteName} className="logo" src={useAppConfig().LOGO_WHITE_URL} />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
<div className="d-flex align-items-center justify-content-center mb-4 ">
|
<div className="d-flex align-items-center justify-content-center mb-4 ">
|
||||||
<div className={classNames({ 'mt-1 medium-yellow-line': getConfig().SITE_NAME === 'edX' })} />
|
<div className={classNames({ 'mt-1 medium-yellow-line': getSiteConfig().siteName === 'edX' })} />
|
||||||
<div>
|
<div>
|
||||||
<h1
|
<h1
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'display-1 text-white mt-5 mb-5 mr-2 main-heading',
|
'display-1 text-white mt-5 mb-5 mr-2 main-heading',
|
||||||
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
{ 'ml-4.5': getSiteConfig().siteName !== 'edX' },
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{formatMessage(messages['start.learning'])}{' '}
|
{formatMessage(messages['start.learning'])}{' '}
|
||||||
<span className="text-accent-a d-inline-block">
|
<span className="text-accent-a d-inline-block">
|
||||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
{formatMessage(messages['with.site.name'], { siteName: getSiteConfig().siteName })}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React from 'react';
|
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@openedx/paragon';
|
import { Hyperlink, Image } from '@openedx/paragon';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
@@ -14,11 +11,11 @@ const SmallLayout = () => {
|
|||||||
<span className="bg-primary-400 w-100">
|
<span className="bg-primary-400 w-100">
|
||||||
<div className="col-md-12 small-screen-top-stripe" />
|
<div className="col-md-12 small-screen-top-stripe" />
|
||||||
<div>
|
<div>
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
<Image className="logo-small" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
<div className="d-flex align-items-center m-3.5">
|
<div className="d-flex align-items-center m-3.5">
|
||||||
<div className={classNames({ 'small-yellow-line mr-n2.5': getConfig().SITE_NAME === 'edX' })} />
|
<div className={classNames({ 'small-yellow-line mr-n2.5': getSiteConfig().siteName === 'edX' })} />
|
||||||
<h1
|
<h1
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'text-white mt-3.5 mb-3.5',
|
'text-white mt-3.5 mb-3.5',
|
||||||
@@ -27,7 +24,7 @@ const SmallLayout = () => {
|
|||||||
<span>
|
<span>
|
||||||
{formatMessage(messages['start.learning'])}{' '}
|
{formatMessage(messages['start.learning'])}{' '}
|
||||||
<span className="text-accent-a d-inline-block">
|
<span className="text-accent-a d-inline-block">
|
||||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
{formatMessage(messages['with.site.name'], { siteName: getSiteConfig().siteName })}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
import { defineMessages } from '@openedx/frontend-base';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
'start.learning': {
|
'start.learning': {
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React from 'react';
|
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@openedx/paragon';
|
import { Hyperlink, Image } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
@@ -12,10 +9,10 @@ const ExtraSmallLayout = () => {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="w-100 bg-primary-500 banner__image extra-small-layout"
|
className="w-100 bg-primary-500 banner__image extra-small-layout"
|
||||||
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_EXTRA_SMALL})` }}
|
style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_EXTRA_SMALL})` }}
|
||||||
>
|
>
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<Image className="company-logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
<Image className="company-logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
<div className="ml-4.5 mr-1 pb-3.5 pt-3.5">
|
<div className="ml-4.5 mr-1 pb-3.5 pt-3.5">
|
||||||
<h1 className="banner__heading">
|
<h1 className="banner__heading">
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React from 'react';
|
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@openedx/paragon';
|
import { Hyperlink, Image } from '@openedx/paragon';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
@@ -13,10 +10,10 @@ const LargeLayout = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-50 bg-primary-500 banner__image large-layout"
|
className="w-50 bg-primary-500 banner__image large-layout"
|
||||||
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_LARGE})` }}
|
style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_LARGE})` }}
|
||||||
>
|
>
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<Image className="company-logo position-absolute" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
<Image className="company-logo position-absolute" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
<div className="min-vh-100 p-5 d-flex align-items-end">
|
<div className="min-vh-100 p-5 d-flex align-items-end">
|
||||||
<h1 className="display-2 mw-sm mb-3 d-flex flex-column flex-shrink-0 justify-content-center">
|
<h1 className="display-2 mw-sm mb-3 d-flex flex-column flex-shrink-0 justify-content-center">
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React from 'react';
|
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@openedx/paragon';
|
import { Hyperlink, Image } from '@openedx/paragon';
|
||||||
|
|
||||||
import './index.scss';
|
import './index.scss';
|
||||||
@@ -13,10 +10,10 @@ const MediumLayout = () => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-100 mb-3 bg-primary-500 banner__image medium-layout"
|
className="w-100 mb-3 bg-primary-500 banner__image medium-layout"
|
||||||
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_MEDIUM})` }}
|
style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_MEDIUM})` }}
|
||||||
>
|
>
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<Image className="company-logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
<Image className="company-logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
<div className="ml-5 pb-4 pt-4">
|
<div className="ml-5 pb-4 pt-4">
|
||||||
<h1 className="display-2 banner__heading">
|
<h1 className="display-2 banner__heading">
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React from 'react';
|
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@openedx/paragon';
|
import { Hyperlink, Image } from '@openedx/paragon';
|
||||||
|
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
@@ -12,10 +9,10 @@ const SmallLayout = () => {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className="w-100 bg-primary-500 banner__image small-layout"
|
className="w-100 bg-primary-500 banner__image small-layout"
|
||||||
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_SMALL})` }}
|
style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_SMALL})` }}
|
||||||
>
|
>
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<Image className="company-logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
<Image className="company-logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
<div className="ml-5 mr-1 pb-3.5 pt-3.5">
|
<div className="ml-5 mr-1 pb-3.5 pt-3.5">
|
||||||
<h1 className="display-2">
|
<h1 className="display-2">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
import { defineMessages } from '@openedx/frontend-base';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
'your.career.turning.point': {
|
'your.career.turning.point': {
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React from 'react';
|
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@openedx/paragon';
|
import { Hyperlink, Image } from '@openedx/paragon';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
@@ -13,14 +10,14 @@ const LargeLayout = ({ fullName }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-50 d-flex">
|
<div className="w-50 d-flex">
|
||||||
<div className="col-md-10 bg-light-200 p-0">
|
<div className="col-md-10 bg-light-200 p-0">
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<Image className="logo position-absolute" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
<Image className="logo position-absolute" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_URL} />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
<div className="min-vh-100 d-flex align-items-center">
|
<div className="min-vh-100 d-flex align-items-center">
|
||||||
<div className="large-screen-left-container mr-n4.5 large-yellow-line mt-5" />
|
<div className="large-screen-left-container mr-n4.5 large-yellow-line mt-5" />
|
||||||
<div>
|
<div>
|
||||||
<h1 className="welcome-to-platform data-hj-suppress">
|
<h1 className="welcome-to-platform data-hj-suppress">
|
||||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, fullName })}
|
{formatMessage(messages['welcome.to.platform'], { siteName: getSiteConfig().siteName, fullName })}
|
||||||
</h1>
|
</h1>
|
||||||
<h2 className="complete-your-profile">
|
<h2 className="complete-your-profile">
|
||||||
{formatMessage(messages['complete.your.profile.1'])}
|
{formatMessage(messages['complete.your.profile.1'])}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React from 'react';
|
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@openedx/paragon';
|
import { Hyperlink, Image } from '@openedx/paragon';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
@@ -15,14 +12,14 @@ const MediumLayout = ({ fullName }) => {
|
|||||||
<div className="w-100 medium-screen-top-stripe" />
|
<div className="w-100 medium-screen-top-stripe" />
|
||||||
<div className="w-100 p-0 mb-3 d-flex">
|
<div className="w-100 p-0 mb-3 d-flex">
|
||||||
<div className="col-md-10 bg-light-200">
|
<div className="col-md-10 bg-light-200">
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<Image className="logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
<Image className="logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_URL} />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
<div className="d-flex align-items-center justify-content-center mb-4 ml-5">
|
<div className="d-flex align-items-center justify-content-center mb-4 ml-5">
|
||||||
<div className="medium-yellow-line mt-5 mr-n2" />
|
<div className="medium-yellow-line mt-5 mr-n2" />
|
||||||
<div>
|
<div>
|
||||||
<h1 className="h3 data-hj-suppress mw-320">
|
<h1 className="h3 data-hj-suppress mw-320">
|
||||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, fullName })}
|
{formatMessage(messages['welcome.to.platform'], { siteName: getSiteConfig().siteName, fullName })}
|
||||||
</h1>
|
</h1>
|
||||||
<h2 className="display-1">
|
<h2 className="display-1">
|
||||||
{formatMessage(messages['complete.your.profile.1'])}
|
{formatMessage(messages['complete.your.profile.1'])}
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React from 'react';
|
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Hyperlink, Image } from '@openedx/paragon';
|
import { Hyperlink, Image } from '@openedx/paragon';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
@@ -13,14 +10,14 @@ const SmallLayout = ({ fullName }) => {
|
|||||||
return (
|
return (
|
||||||
<div className="min-vw-100 bg-light-200">
|
<div className="min-vw-100 bg-light-200">
|
||||||
<div className="col-md-12 small-screen-top-stripe" />
|
<div className="col-md-12 small-screen-top-stripe" />
|
||||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||||
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
<Image className="logo-small" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_URL} />
|
||||||
</Hyperlink>
|
</Hyperlink>
|
||||||
<div className="d-flex align-items-center m-3.5">
|
<div className="d-flex align-items-center m-3.5">
|
||||||
<div className="small-yellow-line mt-4.5" />
|
<div className="small-yellow-line mt-4.5" />
|
||||||
<div>
|
<div>
|
||||||
<h1 className="h5 data-hj-suppress">
|
<h1 className="h5 data-hj-suppress">
|
||||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, fullName })}
|
{formatMessage(messages['welcome.to.platform'], { siteName: getSiteConfig().siteName, fullName })}
|
||||||
</h1>
|
</h1>
|
||||||
<h2 className="h1">
|
<h2 className="h1">
|
||||||
{formatMessage(messages['complete.your.profile.1'])}
|
{formatMessage(messages['complete.your.profile.1'])}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
import { defineMessages } from '@openedx/frontend-base';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
'welcome.to.platform': {
|
'welcome.to.platform': {
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import React from 'react';
|
import { useAppConfig } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { breakpoints } from '@openedx/paragon';
|
import { breakpoints } from '@openedx/paragon';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@@ -13,7 +11,7 @@ import {
|
|||||||
import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout';
|
import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout';
|
||||||
|
|
||||||
const BaseContainer = ({ children, showWelcomeBanner, fullName }) => {
|
const BaseContainer = ({ children, showWelcomeBanner, fullName }) => {
|
||||||
const enableImageLayout = getConfig().ENABLE_IMAGE_LAYOUT;
|
const enableImageLayout = useAppConfig().ENABLE_IMAGE_LAYOUT;
|
||||||
|
|
||||||
if (enableImageLayout) {
|
if (enableImageLayout) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import React from 'react';
|
import { IntlProvider, mergeAppConfig } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { mergeConfig } from '@edx/frontend-platform';
|
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { Context as ResponsiveContext } from 'react-responsive';
|
import { Context as ResponsiveContext } from 'react-responsive';
|
||||||
|
|
||||||
import BaseContainer from '../index';
|
import BaseContainer from '../index';
|
||||||
|
import { appId } from '../../constants';
|
||||||
|
|
||||||
const LargeScreen = {
|
const LargeScreen = {
|
||||||
wrappingComponent: ResponsiveContext.Provider,
|
wrappingComponent: ResponsiveContext.Provider,
|
||||||
@@ -28,7 +26,7 @@ describe('Base component tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders Image layout when ENABLE_IMAGE_LAYOUT configuration is enabled', () => {
|
it('renders Image layout when ENABLE_IMAGE_LAYOUT configuration is enabled', () => {
|
||||||
mergeConfig({
|
mergeAppConfig(appId, {
|
||||||
ENABLE_IMAGE_LAYOUT: true,
|
ENABLE_IMAGE_LAYOUT: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
import {
|
import {
|
||||||
Button, Form,
|
Button, Form,
|
||||||
Icon,
|
Icon,
|
||||||
@@ -10,8 +7,8 @@ import {
|
|||||||
import { Login } from '@openedx/paragon/icons';
|
import { Login } from '@openedx/paragon/icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the Single sign-on (SSO) button only for the tpa provider passed
|
* This component renders the Single sign-on (SSO) button only for the tpa provider passed
|
||||||
@@ -19,12 +16,12 @@ import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
|||||||
const EnterpriseSSO = (props) => {
|
const EnterpriseSSO = (props) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const tpaProvider = props.provider;
|
const tpaProvider = props.provider;
|
||||||
const hideRegistrationLink = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false
|
const hideRegistrationLink = useAppConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false
|
||||||
|| getConfig().SHOW_REGISTRATION_LINKS === false;
|
|| useAppConfig().SHOW_REGISTRATION_LINKS === false;
|
||||||
|
|
||||||
const handleSubmit = (e, url) => {
|
const handleSubmit = (e, url) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
window.location.href = getConfig().LMS_BASE_URL + url;
|
window.location.href = getSiteConfig().lmsBaseUrl + url;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = (e) => {
|
const handleClick = (e) => {
|
||||||
@@ -50,7 +47,7 @@ const EnterpriseSSO = (props) => {
|
|||||||
{tpaProvider.iconImage ? (
|
{tpaProvider.iconImage ? (
|
||||||
<div aria-hidden="true">
|
<div aria-hidden="true">
|
||||||
<img className="btn-tpa__image-icon" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
|
<img className="btn-tpa__image-icon" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
|
||||||
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
<span className="pl-2" aria-hidden="true">{tpaProvider.name}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
: (
|
: (
|
||||||
@@ -62,7 +59,7 @@ const EnterpriseSSO = (props) => {
|
|||||||
<Icon className="h-75" src={Login} />
|
<Icon className="h-75" src={Login} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
<span className="pl-2" aria-hidden="true">{tpaProvider.name}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Form, TransitionReplace,
|
Form, TransitionReplace,
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React from 'react';
|
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Button, Hyperlink, Icon } from '@openedx/paragon';
|
import { Button, Hyperlink, Icon } from '@openedx/paragon';
|
||||||
import { Institution } from '@openedx/paragon/icons';
|
import { Institution } from '@openedx/paragon/icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
@@ -31,7 +28,7 @@ export const RenderInstitutionButton = props => {
|
|||||||
* This component renders the page list of available institutions for login
|
* This component renders the page list of available institutions for login
|
||||||
* */
|
* */
|
||||||
const InstitutionLogistration = props => {
|
const InstitutionLogistration = props => {
|
||||||
const lmsBaseUrl = getConfig().LMS_BASE_URL;
|
const lmsBaseUrl = getSiteConfig().lmsBaseUrl;
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const {
|
const {
|
||||||
secondaryProviders,
|
secondaryProviders,
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import React from 'react';
|
import { FormattedMessage } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
|
||||||
|
|
||||||
const NotFoundPage = () => (
|
const NotFoundPage = () => (
|
||||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
import { useIntl } from '@openedx/frontend-base';
|
||||||
import {
|
import {
|
||||||
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
|
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
@@ -10,19 +9,23 @@ import {
|
|||||||
} from '@openedx/paragon/icons';
|
} from '@openedx/paragon/icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
|
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
|
||||||
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../register/data/actions';
|
|
||||||
import { validatePasswordField } from '../register/data/utils';
|
import { validatePasswordField } from '../register/data/utils';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const noopFn = () => {};
|
||||||
|
|
||||||
const PasswordField = (props) => {
|
const PasswordField = (props) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
|
|
||||||
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
|
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
|
||||||
const [showTooltip, setShowTooltip] = useState(false);
|
const [showTooltip, setShowTooltip] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
validationApiRateLimited = false,
|
||||||
|
clearRegistrationBackendError = noopFn,
|
||||||
|
validateField = noopFn,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const handleBlur = (e) => {
|
const handleBlur = (e) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
|
if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
|
||||||
@@ -50,7 +53,7 @@ const PasswordField = (props) => {
|
|||||||
if (fieldError) {
|
if (fieldError) {
|
||||||
props.handleErrorChange('password', fieldError);
|
props.handleErrorChange('password', fieldError);
|
||||||
} else if (!validationApiRateLimited) {
|
} else if (!validationApiRateLimited) {
|
||||||
dispatch(fetchRealtimeValidations({ password: passwordValue }));
|
validateField({ password: passwordValue });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -65,7 +68,7 @@ const PasswordField = (props) => {
|
|||||||
}
|
}
|
||||||
if (props.handleErrorChange) {
|
if (props.handleErrorChange) {
|
||||||
props.handleErrorChange('password', '');
|
props.handleErrorChange('password', '');
|
||||||
dispatch(clearRegistrationBackendError('password'));
|
clearRegistrationBackendError('password');
|
||||||
}
|
}
|
||||||
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
|
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
|
||||||
};
|
};
|
||||||
@@ -155,6 +158,9 @@ PasswordField.defaultProps = {
|
|||||||
showRequirements: true,
|
showRequirements: true,
|
||||||
showScreenReaderText: true,
|
showScreenReaderText: true,
|
||||||
autoComplete: null,
|
autoComplete: null,
|
||||||
|
clearRegistrationBackendError: noopFn,
|
||||||
|
validateField: noopFn,
|
||||||
|
validationApiRateLimited: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
PasswordField.propTypes = {
|
PasswordField.propTypes = {
|
||||||
@@ -170,6 +176,9 @@ PasswordField.propTypes = {
|
|||||||
value: PropTypes.string.isRequired,
|
value: PropTypes.string.isRequired,
|
||||||
autoComplete: PropTypes.string,
|
autoComplete: PropTypes.string,
|
||||||
showScreenReaderText: PropTypes.bool,
|
showScreenReaderText: PropTypes.bool,
|
||||||
|
clearRegistrationBackendError: PropTypes.func,
|
||||||
|
validateField: PropTypes.func,
|
||||||
|
validationApiRateLimited: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PasswordField;
|
export default PasswordField;
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { useAppConfig, getSiteConfig } from '@openedx/frontend-base';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
|
AUTHN_PROGRESSIVE_PROFILING, REDIRECT,
|
||||||
} from '../data/constants';
|
} from '../data/constants';
|
||||||
import { setCookie } from '../data/utils';
|
import { setCookie } from '../data/utils';
|
||||||
|
|
||||||
@@ -15,7 +15,6 @@ const RedirectLogistration = (props) => {
|
|||||||
redirectToProgressiveProfilingPage,
|
redirectToProgressiveProfilingPage,
|
||||||
success,
|
success,
|
||||||
optionalFields,
|
optionalFields,
|
||||||
redirectToRecommendationsPage,
|
|
||||||
educationLevel,
|
educationLevel,
|
||||||
userId,
|
userId,
|
||||||
registrationEmbedded,
|
registrationEmbedded,
|
||||||
@@ -29,7 +28,7 @@ const RedirectLogistration = (props) => {
|
|||||||
// Note: For multiple enterprise use case, we need to make sure that user first visits the
|
// Note: For multiple enterprise use case, we need to make sure that user first visits the
|
||||||
// enterprise selection page and then complete the auth workflow
|
// enterprise selection page and then complete the auth workflow
|
||||||
if (finishAuthUrl && !redirectUrl.includes(finishAuthUrl)) {
|
if (finishAuthUrl && !redirectUrl.includes(finishAuthUrl)) {
|
||||||
finalRedirectUrl = getConfig().LMS_BASE_URL + finishAuthUrl;
|
finalRedirectUrl = getSiteConfig().lmsBaseUrl + finishAuthUrl;
|
||||||
} else {
|
} else {
|
||||||
finalRedirectUrl = redirectUrl;
|
finalRedirectUrl = redirectUrl;
|
||||||
}
|
}
|
||||||
@@ -37,12 +36,12 @@ const RedirectLogistration = (props) => {
|
|||||||
// Redirect to Progressive Profiling after successful registration
|
// Redirect to Progressive Profiling after successful registration
|
||||||
if (redirectToProgressiveProfilingPage) {
|
if (redirectToProgressiveProfilingPage) {
|
||||||
// TODO: Do we still need this cookie?
|
// TODO: Do we still need this cookie?
|
||||||
setCookie('van-504-returning-user', true);
|
setCookie('van-504-returning-user', true, useAppConfig().SESSION_COOKIE_DOMAIN);
|
||||||
|
|
||||||
if (registrationEmbedded) {
|
if (registrationEmbedded) {
|
||||||
window.parent.postMessage({
|
window.parent.postMessage({
|
||||||
action: REDIRECT,
|
action: REDIRECT,
|
||||||
redirectUrl: getConfig().POST_REGISTRATION_REDIRECT_URL,
|
redirectUrl: useAppConfig().POST_REGISTRATION_REDIRECT_URL,
|
||||||
}, host);
|
}, host);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -60,22 +59,6 @@ const RedirectLogistration = (props) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to Recommendation page
|
|
||||||
if (redirectToRecommendationsPage) {
|
|
||||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
|
||||||
return (
|
|
||||||
<Navigate
|
|
||||||
to={RECOMMENDATIONS}
|
|
||||||
state={{
|
|
||||||
registrationResult,
|
|
||||||
educationLevel,
|
|
||||||
userId,
|
|
||||||
}}
|
|
||||||
replace
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
window.location.href = finalRedirectUrl;
|
window.location.href = finalRedirectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +73,6 @@ RedirectLogistration.defaultProps = {
|
|||||||
redirectUrl: '',
|
redirectUrl: '',
|
||||||
redirectToProgressiveProfilingPage: false,
|
redirectToProgressiveProfilingPage: false,
|
||||||
optionalFields: {},
|
optionalFields: {},
|
||||||
redirectToRecommendationsPage: false,
|
|
||||||
userId: null,
|
userId: null,
|
||||||
registrationEmbedded: false,
|
registrationEmbedded: false,
|
||||||
host: '',
|
host: '',
|
||||||
@@ -104,7 +86,6 @@ RedirectLogistration.propTypes = {
|
|||||||
redirectUrl: PropTypes.string,
|
redirectUrl: PropTypes.string,
|
||||||
redirectToProgressiveProfilingPage: PropTypes.bool,
|
redirectToProgressiveProfilingPage: PropTypes.bool,
|
||||||
optionalFields: PropTypes.shape({}),
|
optionalFields: PropTypes.shape({}),
|
||||||
redirectToRecommendationsPage: PropTypes.bool,
|
|
||||||
userId: PropTypes.number,
|
userId: PropTypes.number,
|
||||||
registrationEmbedded: PropTypes.bool,
|
registrationEmbedded: PropTypes.bool,
|
||||||
host: PropTypes.string,
|
host: PropTypes.string,
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
import { Icon } from '@openedx/paragon';
|
import { Icon } from '@openedx/paragon';
|
||||||
import { Login } from '@openedx/paragon/icons';
|
import { Login } from '@openedx/paragon/icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
const SocialAuthProviders = (props) => {
|
const SocialAuthProviders = (props) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@@ -18,7 +15,7 @@ const SocialAuthProviders = (props) => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const url = e.currentTarget.dataset.providerUrl;
|
const url = e.currentTarget.dataset.providerUrl;
|
||||||
window.location.href = getConfig().LMS_BASE_URL + url;
|
window.location.href = getSiteConfig().lmsBaseUrl + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
const socialAuth = socialAuthProviders.map((provider, index) => (
|
const socialAuth = socialAuthProviders.map((provider, index) => (
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import React from 'react';
|
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import {
|
import {
|
||||||
Hyperlink, Icon,
|
Hyperlink, Icon,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
@@ -10,10 +7,10 @@ import classNames from 'classnames';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import Skeleton from 'react-loading-skeleton';
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
import {
|
import {
|
||||||
ENTERPRISE_LOGIN_URL, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
|
ENTERPRISE_LOGIN_URL, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
|
||||||
} from '../data/constants';
|
} from '../data/constants';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
RenderInstitutionButton,
|
RenderInstitutionButton,
|
||||||
@@ -35,8 +32,8 @@ const ThirdPartyAuth = (props) => {
|
|||||||
} = props;
|
} = props;
|
||||||
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
|
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
|
||||||
const isSocialAuthActive = !!providers.length && !currentProvider;
|
const isSocialAuthActive = !!providers.length && !currentProvider;
|
||||||
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
|
const isEnterpriseLoginDisabled = useAppConfig().DISABLE_ENTERPRISE_LOGIN;
|
||||||
const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL;
|
const enterpriseLoginURL = getSiteConfig().lmsBaseUrl + ENTERPRISE_LOGIN_URL;
|
||||||
const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive);
|
const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import React from 'react';
|
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Alert } from '@openedx/paragon';
|
import { Alert } from '@openedx/paragon';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
const ThirdPartyAuthAlert = (props) => {
|
const ThirdPartyAuthAlert = (props) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { currentProvider, referrer } = props;
|
const { currentProvider, referrer } = props;
|
||||||
const platformName = getConfig().SITE_NAME;
|
const platformName = getSiteConfig().siteName;
|
||||||
let message;
|
let message;
|
||||||
|
|
||||||
if (referrer === LOGIN_PAGE) {
|
if (referrer === LOGIN_PAGE) {
|
||||||
@@ -30,7 +27,7 @@ const ThirdPartyAuthAlert = (props) => {
|
|||||||
{referrer === REGISTER_PAGE ? (
|
{referrer === REGISTER_PAGE ? (
|
||||||
<Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
<Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||||
) : null}
|
) : null}
|
||||||
<p>{ message }</p>
|
<p>{message}</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
{referrer === REGISTER_PAGE ? (
|
{referrer === REGISTER_PAGE ? (
|
||||||
<h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
<h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { fetchAuthenticatedUser, getAuthenticatedUser, getSiteConfig } from '@openedx/frontend-base';
|
||||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -25,7 +24,7 @@ const UnAuthOnlyRoute = ({ children }) => {
|
|||||||
|
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
if (authUser && authUser.username) {
|
if (authUser && authUser.username) {
|
||||||
global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
global.location.href = getSiteConfig().lmsBaseUrl.concat(DEFAULT_REDIRECT_URL);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import Zendesk from 'react-zendesk';
|
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
import { REGISTER_EMBEDDED_PAGE } from '../data/constants';
|
|
||||||
|
|
||||||
const ZendeskHelp = () => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const setting = {
|
|
||||||
cookies: true,
|
|
||||||
webWidget: {
|
|
||||||
contactOptions: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
chat: {
|
|
||||||
suppress: false,
|
|
||||||
departments: {
|
|
||||||
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
contactForm: {
|
|
||||||
ticketForms: [
|
|
||||||
{
|
|
||||||
id: 360003368814,
|
|
||||||
subject: false,
|
|
||||||
fields: [{ id: 'description', prefill: { '*': '' } }],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
selectTicketForm: {
|
|
||||||
'*': formatMessage(messages.selectTicketForm),
|
|
||||||
},
|
|
||||||
attachments: true,
|
|
||||||
},
|
|
||||||
helpCenter: {
|
|
||||||
originalArticleButton: true,
|
|
||||||
},
|
|
||||||
answerBot: {
|
|
||||||
suppress: false,
|
|
||||||
contactOnlyAfterQuery: true,
|
|
||||||
title: { '*': formatMessage(messages.supportTitle) },
|
|
||||||
avatar: {
|
|
||||||
url: getConfig().ZENDESK_LOGO_URL,
|
|
||||||
name: { '*': formatMessage(messages.supportTitle) },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (window.location.pathname === REGISTER_EMBEDDED_PAGE) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} />
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ZendeskHelp;
|
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { ThirdPartyAuthProvider, useThirdPartyAuthContext } from './ThirdPartyAuthContext';
|
||||||
|
|
||||||
|
const TestComponent = () => {
|
||||||
|
const {
|
||||||
|
fieldDescriptions,
|
||||||
|
optionalFields,
|
||||||
|
thirdPartyAuthApiStatus,
|
||||||
|
thirdPartyAuthContext,
|
||||||
|
} = useThirdPartyAuthContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>{fieldDescriptions ? 'FieldDescriptions Available' : 'FieldDescriptions Not Available'}</div>
|
||||||
|
<div>{optionalFields ? 'OptionalFields Available' : 'OptionalFields Not Available'}</div>
|
||||||
|
<div>{thirdPartyAuthApiStatus !== null ? 'AuthApiStatus Available' : 'AuthApiStatus Not Available'}</div>
|
||||||
|
<div>{thirdPartyAuthContext ? 'AuthContext Available' : 'AuthContext Not Available'}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ThirdPartyAuthContext', () => {
|
||||||
|
it('should render children', () => {
|
||||||
|
render(
|
||||||
|
<ThirdPartyAuthProvider>
|
||||||
|
<div>Test Child</div>
|
||||||
|
</ThirdPartyAuthProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('Test Child')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should provide all context values to children', () => {
|
||||||
|
render(
|
||||||
|
<ThirdPartyAuthProvider>
|
||||||
|
<TestComponent />
|
||||||
|
</ThirdPartyAuthProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('FieldDescriptions Available')).toBeTruthy();
|
||||||
|
expect(screen.getByText('OptionalFields Available')).toBeTruthy();
|
||||||
|
expect(screen.getByText('AuthApiStatus Not Available')).toBeTruthy(); // Initially null
|
||||||
|
expect(screen.getByText('AuthContext Available')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render multiple children', () => {
|
||||||
|
render(
|
||||||
|
<ThirdPartyAuthProvider>
|
||||||
|
<div>First Child</div>
|
||||||
|
<div>Second Child</div>
|
||||||
|
<div>Third Child</div>
|
||||||
|
</ThirdPartyAuthProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText('First Child')).toBeTruthy();
|
||||||
|
expect(screen.getByText('Second Child')).toBeTruthy();
|
||||||
|
expect(screen.getByText('Third Child')).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
133
src/common-components/components/ThirdPartyAuthContext.tsx
Normal file
133
src/common-components/components/ThirdPartyAuthContext.tsx
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import {
|
||||||
|
createContext, FC, ReactNode, useCallback, useContext, useMemo, useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
|
||||||
|
|
||||||
|
interface ThirdPartyAuthContextType {
|
||||||
|
fieldDescriptions: any,
|
||||||
|
optionalFields: {
|
||||||
|
fields: any,
|
||||||
|
extended_profile: any[],
|
||||||
|
},
|
||||||
|
thirdPartyAuthApiStatus: string | null,
|
||||||
|
thirdPartyAuthContext: {
|
||||||
|
platformName: string | null,
|
||||||
|
autoSubmitRegForm: boolean,
|
||||||
|
currentProvider: string | null,
|
||||||
|
finishAuthUrl: string | null,
|
||||||
|
countryCode: string | null,
|
||||||
|
providers: any[],
|
||||||
|
secondaryProviders: any[],
|
||||||
|
pipelineUserDetails: any | null,
|
||||||
|
errorMessage: string | null,
|
||||||
|
welcomePageRedirectUrl: string | null,
|
||||||
|
},
|
||||||
|
setThirdPartyAuthContextBegin: () => void,
|
||||||
|
setThirdPartyAuthContextSuccess: (fieldDescData: any, optionalFieldsData: any, contextData: any) => void,
|
||||||
|
setThirdPartyAuthContextFailure: () => void,
|
||||||
|
clearThirdPartyAuthErrorMessage: () => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThirdPartyAuthContext = createContext<ThirdPartyAuthContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
interface ThirdPartyAuthProviderProps {
|
||||||
|
children: ReactNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ThirdPartyAuthProvider: FC<ThirdPartyAuthProviderProps> = ({ children }) => {
|
||||||
|
const [fieldDescriptions, setFieldDescriptions] = useState({});
|
||||||
|
const [optionalFields, setOptionalFields] = useState({
|
||||||
|
fields: {},
|
||||||
|
extended_profile: [],
|
||||||
|
});
|
||||||
|
const [thirdPartyAuthApiStatus, setThirdPartyAuthApiStatus] = useState<string | null>(null);
|
||||||
|
const [thirdPartyAuthContext, setThirdPartyAuthContext] = useState({
|
||||||
|
platformName: null,
|
||||||
|
autoSubmitRegForm: false,
|
||||||
|
currentProvider: null,
|
||||||
|
finishAuthUrl: null,
|
||||||
|
countryCode: null,
|
||||||
|
providers: [],
|
||||||
|
secondaryProviders: [],
|
||||||
|
pipelineUserDetails: null,
|
||||||
|
errorMessage: null,
|
||||||
|
welcomePageRedirectUrl: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Function to handle begin state - mirrors THIRD_PARTY_AUTH_CONTEXT.BEGIN
|
||||||
|
const setThirdPartyAuthContextBegin = useCallback(() => {
|
||||||
|
setThirdPartyAuthApiStatus(PENDING_STATE);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Function to handle success - mirrors THIRD_PARTY_AUTH_CONTEXT.SUCCESS
|
||||||
|
const setThirdPartyAuthContextSuccess = useCallback((fieldDescData, optionalFieldsData, contextData) => {
|
||||||
|
setFieldDescriptions(fieldDescData?.fields || {});
|
||||||
|
setOptionalFields(optionalFieldsData || { fields: {}, extended_profile: [] });
|
||||||
|
setThirdPartyAuthContext(contextData || {
|
||||||
|
platformName: null,
|
||||||
|
autoSubmitRegForm: false,
|
||||||
|
currentProvider: null,
|
||||||
|
finishAuthUrl: null,
|
||||||
|
countryCode: null,
|
||||||
|
providers: [],
|
||||||
|
secondaryProviders: [],
|
||||||
|
pipelineUserDetails: null,
|
||||||
|
errorMessage: null,
|
||||||
|
welcomePageRedirectUrl: null,
|
||||||
|
});
|
||||||
|
setThirdPartyAuthApiStatus(COMPLETE_STATE);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Function to handle failure - mirrors THIRD_PARTY_AUTH_CONTEXT.FAILURE
|
||||||
|
const setThirdPartyAuthContextFailure = useCallback(() => {
|
||||||
|
setThirdPartyAuthApiStatus(FAILURE_STATE);
|
||||||
|
setThirdPartyAuthContext(prev => ({
|
||||||
|
...prev,
|
||||||
|
errorMessage: null,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Function to clear error message - mirrors THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG
|
||||||
|
const clearThirdPartyAuthErrorMessage = useCallback(() => {
|
||||||
|
setThirdPartyAuthApiStatus(PENDING_STATE);
|
||||||
|
setThirdPartyAuthContext(prev => ({
|
||||||
|
...prev,
|
||||||
|
errorMessage: null,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const value = useMemo(() => ({
|
||||||
|
fieldDescriptions,
|
||||||
|
optionalFields,
|
||||||
|
thirdPartyAuthApiStatus,
|
||||||
|
thirdPartyAuthContext,
|
||||||
|
setThirdPartyAuthContextBegin,
|
||||||
|
setThirdPartyAuthContextSuccess,
|
||||||
|
setThirdPartyAuthContextFailure,
|
||||||
|
clearThirdPartyAuthErrorMessage,
|
||||||
|
}), [
|
||||||
|
fieldDescriptions,
|
||||||
|
optionalFields,
|
||||||
|
thirdPartyAuthApiStatus,
|
||||||
|
thirdPartyAuthContext,
|
||||||
|
setThirdPartyAuthContextBegin,
|
||||||
|
setThirdPartyAuthContextSuccess,
|
||||||
|
setThirdPartyAuthContextFailure,
|
||||||
|
clearThirdPartyAuthErrorMessage,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThirdPartyAuthContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
</ThirdPartyAuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useThirdPartyAuthContext = (): ThirdPartyAuthContextType => {
|
||||||
|
const context = useContext(ThirdPartyAuthContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useThirdPartyAuthContext must be used within a ThirdPartyAuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { AsyncActionType } from '../../data/utils';
|
|
||||||
|
|
||||||
export const THIRD_PARTY_AUTH_CONTEXT = new AsyncActionType('THIRD_PARTY_AUTH', 'GET_THIRD_PARTY_AUTH_CONTEXT');
|
|
||||||
export const THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG = 'THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG';
|
|
||||||
|
|
||||||
// Third party auth context
|
|
||||||
export const getThirdPartyAuthContext = (urlParams) => ({
|
|
||||||
type: THIRD_PARTY_AUTH_CONTEXT.BASE,
|
|
||||||
payload: { urlParams },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getThirdPartyAuthContextBegin = () => ({
|
|
||||||
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({
|
|
||||||
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
|
|
||||||
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getThirdPartyAuthContextFailure = () => ({
|
|
||||||
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const clearThirdPartyAuthContextErrorMessage = () => ({
|
|
||||||
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
|
|
||||||
});
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getAuthenticatedHttpClient, getSiteConfig } from '@openedx/frontend-base';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
const getThirdPartyAuthContext = async (urlParams: string) => {
|
||||||
export async function getThirdPartyAuthContext(urlParams) {
|
|
||||||
const requestConfig = {
|
const requestConfig = {
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
params: urlParams,
|
params: urlParams,
|
||||||
@@ -11,15 +9,16 @@ export async function getThirdPartyAuthContext(urlParams) {
|
|||||||
|
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
.get(
|
.get(
|
||||||
`${getConfig().LMS_BASE_URL}/api/mfe_context`,
|
`${getSiteConfig().lmsBaseUrl}/api/mfe_context`,
|
||||||
requestConfig,
|
requestConfig,
|
||||||
)
|
);
|
||||||
.catch((e) => {
|
|
||||||
throw (e);
|
|
||||||
});
|
|
||||||
return {
|
return {
|
||||||
fieldDescriptions: data.registrationFields || {},
|
fieldDescriptions: data.registrationFields || {},
|
||||||
optionalFields: data.optionalFields || {},
|
optionalFields: data.optionalFields || {},
|
||||||
thirdPartyAuthContext: data.contextData || {},
|
thirdPartyAuthContext: data.contextData || {},
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
getThirdPartyAuthContext,
|
||||||
|
};
|
||||||
19
src/common-components/data/apiHook.ts
Normal file
19
src/common-components/data/apiHook.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { getThirdPartyAuthContext } from './api';
|
||||||
|
import { ThirdPartyAuthQueryKeys } from './queryKeys';
|
||||||
|
|
||||||
|
// Error constants
|
||||||
|
export const THIRD_PARTY_AUTH_ERROR = 'third-party-auth-error';
|
||||||
|
|
||||||
|
const useThirdPartyAuthHook = (pageId, payload, { enabled = true } = {}) => useQuery({
|
||||||
|
queryKey: ThirdPartyAuthQueryKeys.byPage(pageId, payload),
|
||||||
|
queryFn: () => getThirdPartyAuthContext(payload),
|
||||||
|
retry: false,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5 minutes — TPA context is effectively static per session
|
||||||
|
enabled,
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
useThirdPartyAuthHook,
|
||||||
|
};
|
||||||
6
src/common-components/data/queryKeys.ts
Normal file
6
src/common-components/data/queryKeys.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { appId } from '../../constants';
|
||||||
|
|
||||||
|
export const ThirdPartyAuthQueryKeys = {
|
||||||
|
all: [appId, 'ThirdPartyAuth'] as const,
|
||||||
|
byPage: (pageId: string, payload?: unknown) => [appId, 'ThirdPartyAuth', pageId, payload] as const,
|
||||||
|
};
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions';
|
|
||||||
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
|
|
||||||
|
|
||||||
export const defaultState = {
|
|
||||||
fieldDescriptions: {},
|
|
||||||
optionalFields: {
|
|
||||||
fields: {},
|
|
||||||
extended_profile: [],
|
|
||||||
},
|
|
||||||
thirdPartyAuthApiStatus: null,
|
|
||||||
thirdPartyAuthContext: {
|
|
||||||
autoSubmitRegForm: false,
|
|
||||||
currentProvider: null,
|
|
||||||
finishAuthUrl: null,
|
|
||||||
countryCode: null,
|
|
||||||
providers: [],
|
|
||||||
secondaryProviders: [],
|
|
||||||
pipelineUserDetails: null,
|
|
||||||
errorMessage: null,
|
|
||||||
welcomePageRedirectUrl: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const reducer = (state = defaultState, action = {}) => {
|
|
||||||
switch (action.type) {
|
|
||||||
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
|
||||||
};
|
|
||||||
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS: {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
fieldDescriptions: action.payload.fieldDescriptions?.fields,
|
|
||||||
optionalFields: action.payload.optionalFields,
|
|
||||||
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
|
|
||||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
thirdPartyAuthApiStatus: FAILURE_STATE,
|
|
||||||
thirdPartyAuthContext: {
|
|
||||||
...state.thirdPartyAuthContext,
|
|
||||||
errorMessage: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
case THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG:
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
|
||||||
thirdPartyAuthContext: {
|
|
||||||
...state.thirdPartyAuthContext,
|
|
||||||
errorMessage: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
default:
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default reducer;
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { logError } from '@edx/frontend-platform/logging';
|
|
||||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getThirdPartyAuthContextBegin,
|
|
||||||
getThirdPartyAuthContextFailure,
|
|
||||||
getThirdPartyAuthContextSuccess,
|
|
||||||
THIRD_PARTY_AUTH_CONTEXT,
|
|
||||||
} from './actions';
|
|
||||||
import {
|
|
||||||
getThirdPartyAuthContext,
|
|
||||||
} from './service';
|
|
||||||
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
|
|
||||||
|
|
||||||
export function* fetchThirdPartyAuthContext(action) {
|
|
||||||
try {
|
|
||||||
yield put(getThirdPartyAuthContextBegin());
|
|
||||||
const {
|
|
||||||
fieldDescriptions, optionalFields, thirdPartyAuthContext,
|
|
||||||
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
|
|
||||||
|
|
||||||
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
|
|
||||||
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
|
|
||||||
} catch (e) {
|
|
||||||
yield put(getThirdPartyAuthContextFailure());
|
|
||||||
logError(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function* saga() {
|
|
||||||
yield takeEvery(THIRD_PARTY_AUTH_CONTEXT.BASE, fetchThirdPartyAuthContext);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
export const storeName = 'commonComponents';
|
|
||||||
|
|
||||||
export const commonComponentsSelector = state => ({ ...state[storeName] });
|
|
||||||
|
|
||||||
export const thirdPartyAuthContextSelector = createSelector(
|
|
||||||
commonComponentsSelector,
|
|
||||||
commonComponents => commonComponents.thirdPartyAuthContext,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const fieldDescriptionSelector = createSelector(
|
|
||||||
commonComponentsSelector,
|
|
||||||
commonComponents => commonComponents.fieldDescriptions,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const optionalFieldsSelector = createSelector(
|
|
||||||
commonComponentsSelector,
|
|
||||||
commonComponents => commonComponents.optionalFields,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const tpaProvidersSelector = createSelector(
|
|
||||||
commonComponentsSelector,
|
|
||||||
commonComponents => ({
|
|
||||||
providers: commonComponents.thirdPartyAuthContext.providers,
|
|
||||||
secondaryProviders: commonComponents.thirdPartyAuthContext.secondaryProviders,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { PENDING_STATE } from '../../../data/constants';
|
|
||||||
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from '../actions';
|
|
||||||
import reducer from '../reducers';
|
|
||||||
|
|
||||||
describe('common components reducer', () => {
|
|
||||||
it('test mfe context response', () => {
|
|
||||||
const state = {
|
|
||||||
fieldDescriptions: {},
|
|
||||||
optionalFields: {},
|
|
||||||
thirdPartyAuthApiStatus: null,
|
|
||||||
thirdPartyAuthContext: {
|
|
||||||
currentProvider: null,
|
|
||||||
finishAuthUrl: null,
|
|
||||||
countryCode: null,
|
|
||||||
providers: [],
|
|
||||||
secondaryProviders: [],
|
|
||||||
pipelineUserDetails: null,
|
|
||||||
errorMessage: null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const fieldDescriptions = {
|
|
||||||
fields: [],
|
|
||||||
};
|
|
||||||
const optionalFields = {
|
|
||||||
fields: [],
|
|
||||||
extended_profile: {},
|
|
||||||
};
|
|
||||||
const thirdPartyAuthContext = { ...state.thirdPartyAuthContext };
|
|
||||||
const action = {
|
|
||||||
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
|
|
||||||
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(
|
|
||||||
reducer(state, action),
|
|
||||||
).toEqual(
|
|
||||||
{
|
|
||||||
...state,
|
|
||||||
fieldDescriptions: [],
|
|
||||||
optionalFields: {
|
|
||||||
fields: [],
|
|
||||||
extended_profile: {},
|
|
||||||
},
|
|
||||||
thirdPartyAuthApiStatus: 'complete',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should clear tpa context error message', () => {
|
|
||||||
const state = {
|
|
||||||
fieldDescriptions: {},
|
|
||||||
optionalFields: {},
|
|
||||||
thirdPartyAuthApiStatus: null,
|
|
||||||
thirdPartyAuthContext: {
|
|
||||||
currentProvider: null,
|
|
||||||
finishAuthUrl: null,
|
|
||||||
countryCode: null,
|
|
||||||
providers: [],
|
|
||||||
secondaryProviders: [],
|
|
||||||
pipelineUserDetails: null,
|
|
||||||
errorMessage: 'An error occurred',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const action = {
|
|
||||||
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(
|
|
||||||
reducer(state, action),
|
|
||||||
).toEqual(
|
|
||||||
{
|
|
||||||
...state,
|
|
||||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
|
||||||
thirdPartyAuthContext: {
|
|
||||||
...state.thirdPartyAuthContext,
|
|
||||||
errorMessage: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { runSaga } from 'redux-saga';
|
|
||||||
|
|
||||||
import { setCountryFromThirdPartyAuthContext } from '../../../register/data/actions';
|
|
||||||
import initializeMockLogging from '../../../setupTest';
|
|
||||||
import * as actions from '../actions';
|
|
||||||
import { fetchThirdPartyAuthContext } from '../sagas';
|
|
||||||
import * as api from '../service';
|
|
||||||
|
|
||||||
const { loggingService } = initializeMockLogging();
|
|
||||||
|
|
||||||
describe('fetchThirdPartyAuthContext', () => {
|
|
||||||
const params = {
|
|
||||||
payload: { urlParams: {} },
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
currentProvider: null,
|
|
||||||
providers: [],
|
|
||||||
secondaryProviders: [],
|
|
||||||
finishAuthUrl: null,
|
|
||||||
pipelineUserDetails: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
loggingService.logError.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call service and dispatch success action', async () => {
|
|
||||||
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
|
|
||||||
.mockImplementation(() => Promise.resolve({
|
|
||||||
thirdPartyAuthContext: data,
|
|
||||||
fieldDescriptions: {},
|
|
||||||
optionalFields: {},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const dispatched = [];
|
|
||||||
await runSaga(
|
|
||||||
{ dispatch: (action) => dispatched.push(action) },
|
|
||||||
fetchThirdPartyAuthContext,
|
|
||||||
params,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
|
|
||||||
expect(dispatched).toEqual([
|
|
||||||
actions.getThirdPartyAuthContextBegin(),
|
|
||||||
setCountryFromThirdPartyAuthContext(),
|
|
||||||
actions.getThirdPartyAuthContextSuccess({}, {}, data),
|
|
||||||
]);
|
|
||||||
getThirdPartyAuthContext.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call service and dispatch error action', async () => {
|
|
||||||
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
|
|
||||||
.mockImplementation(() => Promise.reject(new Error('something went wrong')));
|
|
||||||
|
|
||||||
const dispatched = [];
|
|
||||||
await runSaga(
|
|
||||||
{ dispatch: (action) => dispatched.push(action) },
|
|
||||||
fetchThirdPartyAuthContext,
|
|
||||||
params,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
|
|
||||||
expect(loggingService.logError).toHaveBeenCalled();
|
|
||||||
expect(dispatched).toEqual([
|
|
||||||
actions.getThirdPartyAuthContextBegin(),
|
|
||||||
actions.getThirdPartyAuthContextFailure(),
|
|
||||||
]);
|
|
||||||
getThirdPartyAuthContext.mockClear();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -7,9 +7,5 @@ export { default as SocialAuthProviders } from './SocialAuthProviders';
|
|||||||
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';
|
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';
|
||||||
export { default as InstitutionLogistration } from './InstitutionLogistration';
|
export { default as InstitutionLogistration } from './InstitutionLogistration';
|
||||||
export { RenderInstitutionButton } from './InstitutionLogistration';
|
export { RenderInstitutionButton } from './InstitutionLogistration';
|
||||||
export { default as reducer } from './data/reducers';
|
|
||||||
export { default as saga } from './data/sagas';
|
|
||||||
export { storeName } from './data/selectors';
|
|
||||||
export { default as FormGroup } from './FormGroup';
|
export { default as FormGroup } from './FormGroup';
|
||||||
export { default as PasswordField } from './PasswordField';
|
export { default as PasswordField } from './PasswordField';
|
||||||
export { default as Zendesk } from './Zendesk';
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
import { defineMessages } from '@openedx/frontend-base';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
// institution login strings
|
// institution login strings
|
||||||
@@ -85,33 +85,23 @@ const messages = defineMessages({
|
|||||||
'login.third.party.auth.account.not.linked': {
|
'login.third.party.auth.account.not.linked': {
|
||||||
id: 'login.third.party.auth.account.not.linked',
|
id: 'login.third.party.auth.account.not.linked',
|
||||||
defaultMessage: 'You have successfully signed into {currentProvider}, but your {currentProvider} '
|
defaultMessage: 'You have successfully signed into {currentProvider}, but your {currentProvider} '
|
||||||
+ 'account does not have a linked {platformName} account. To link your accounts, '
|
+ 'account does not have a linked {platformName} account. To link your accounts, '
|
||||||
+ 'sign in now using your {platformName} password.',
|
+ 'sign in now using your {platformName} password.',
|
||||||
description: 'Message that appears on login page if user has successfully authenticated with social '
|
description: 'Message that appears on login page if user has successfully authenticated with social '
|
||||||
+ 'auth but no associated platform account exists',
|
+ 'auth but no associated platform account exists',
|
||||||
},
|
},
|
||||||
'register.third.party.auth.account.not.linked': {
|
'register.third.party.auth.account.not.linked': {
|
||||||
id: 'register.third.party.auth.account.not.linked',
|
id: 'register.third.party.auth.account.not.linked',
|
||||||
defaultMessage: 'You\'ve successfully signed into {currentProvider}! We just need a little more information '
|
defaultMessage: 'You\'ve successfully signed into {currentProvider}! We just need a little more information '
|
||||||
+ 'before you start learning with {platformName}.',
|
+ 'before you start learning with {platformName}.',
|
||||||
description: 'Message that appears on register page if user has successfully authenticated with TPA '
|
description: 'Message that appears on register page if user has successfully authenticated with TPA '
|
||||||
+ 'but no associated platform account exists',
|
+ 'but no associated platform account exists',
|
||||||
},
|
},
|
||||||
'registration.using.tpa.form.heading': {
|
'registration.using.tpa.form.heading': {
|
||||||
id: 'registration.using.tpa.form.heading',
|
id: 'registration.using.tpa.form.heading',
|
||||||
defaultMessage: 'Finish creating your account',
|
defaultMessage: 'Finish creating your account',
|
||||||
description: 'Heading that appears above form when user is trying to create account using social auth',
|
description: 'Heading that appears above form when user is trying to create account using social auth',
|
||||||
},
|
},
|
||||||
supportTitle: {
|
|
||||||
id: 'zendesk.supportTitle',
|
|
||||||
description: 'Title for the support button',
|
|
||||||
defaultMessage: 'edX Support',
|
|
||||||
},
|
|
||||||
selectTicketForm: {
|
|
||||||
id: 'zendesk.selectTicketForm',
|
|
||||||
description: 'Select ticket form',
|
|
||||||
defaultMessage: 'Please choose your request type:',
|
|
||||||
},
|
|
||||||
'registration.other.options.heading': {
|
'registration.other.options.heading': {
|
||||||
id: 'registration.other.options.heading',
|
id: 'registration.other.options.heading',
|
||||||
defaultMessage: 'Or register with:',
|
defaultMessage: 'Or register with:',
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/* eslint-disable import/no-import-module-exports */
|
/* eslint-disable import/no-import-module-exports */
|
||||||
/* eslint-disable react/function-component-definition */
|
/* eslint-disable react/function-component-definition */
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getSiteConfig } from '@openedx/frontend-base';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import {
|
import {
|
||||||
@@ -15,7 +14,7 @@ import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
|
|||||||
const RRD = require('react-router-dom');
|
const RRD = require('react-router-dom');
|
||||||
// Just render plain div with its children
|
// Just render plain div with its children
|
||||||
// eslint-disable-next-line react/prop-types
|
// eslint-disable-next-line react/prop-types
|
||||||
RRD.BrowserRouter = ({ children }) => <div>{ children }</div>;
|
RRD.BrowserRouter = ({ children }) => <div>{children}</div>;
|
||||||
module.exports = RRD;
|
module.exports = RRD;
|
||||||
|
|
||||||
const TestApp = () => (
|
const TestApp = () => (
|
||||||
@@ -60,7 +59,7 @@ describe('EmbeddedRegistrationRoute', () => {
|
|||||||
it('should render embedded register page if host query param is available in the url (embedded)', async () => {
|
it('should render embedded register page if host query param is available in the url (embedded)', async () => {
|
||||||
delete window.location;
|
delete window.location;
|
||||||
window.location = {
|
window.location = {
|
||||||
href: getConfig().BASE_URL.concat(REGISTER_EMBEDDED_PAGE),
|
href: getSiteConfig().baseUrl.concat(REGISTER_EMBEDDED_PAGE),
|
||||||
search: '?host=http://localhost/host-websit',
|
search: '?host=http://localhost/host-websit',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
import React from 'react';
|
import { IntlProvider } from '@openedx/frontend-base';
|
||||||
import { Provider } from 'react-redux';
|
|
||||||
|
|
||||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
|
||||||
import { fireEvent, render } from '@testing-library/react';
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import { MemoryRouter } from 'react-router-dom';
|
|
||||||
import configureStore from 'redux-mock-store';
|
|
||||||
|
|
||||||
import { fetchRealtimeValidations } from '../../register/data/actions';
|
|
||||||
import FormGroup from '../FormGroup';
|
import FormGroup from '../FormGroup';
|
||||||
import PasswordField from '../PasswordField';
|
import PasswordField from '../PasswordField';
|
||||||
|
|
||||||
|
|
||||||
describe('FormGroup', () => {
|
describe('FormGroup', () => {
|
||||||
const props = {
|
const props = {
|
||||||
floatingLabel: 'Email',
|
floatingLabel: 'Email',
|
||||||
@@ -36,27 +31,15 @@ describe('FormGroup', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PasswordField', () => {
|
describe('PasswordField', () => {
|
||||||
const mockStore = configureStore();
|
|
||||||
const IntlPasswordField = injectIntl(PasswordField);
|
|
||||||
let props = {};
|
let props = {};
|
||||||
let store = {};
|
|
||||||
|
|
||||||
const reduxWrapper = children => (
|
const wrapper = children => (
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<MemoryRouter>
|
{children}
|
||||||
<Provider store={store}>{children}</Provider>
|
|
||||||
</MemoryRouter>
|
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialState = {
|
|
||||||
register: {
|
|
||||||
validationApiRateLimited: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store = mockStore(initialState);
|
|
||||||
props = {
|
props = {
|
||||||
floatingLabel: 'Password',
|
floatingLabel: 'Password',
|
||||||
name: 'password',
|
name: 'password',
|
||||||
@@ -66,7 +49,7 @@ describe('PasswordField', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should show/hide password on icon click', () => {
|
it('should show/hide password on icon click', () => {
|
||||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||||
const passwordInput = getByLabelText('Password');
|
const passwordInput = getByLabelText('Password');
|
||||||
|
|
||||||
const showPasswordButton = getByLabelText('Show password');
|
const showPasswordButton = getByLabelText('Show password');
|
||||||
@@ -79,7 +62,7 @@ describe('PasswordField', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should show password requirement tooltip on focus', async () => {
|
it('should show password requirement tooltip on focus', async () => {
|
||||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||||
const passwordInput = getByLabelText('Password');
|
const passwordInput = getByLabelText('Password');
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -96,7 +79,7 @@ describe('PasswordField', () => {
|
|||||||
...props,
|
...props,
|
||||||
value: '',
|
value: '',
|
||||||
};
|
};
|
||||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||||
const passwordInput = getByLabelText('Password');
|
const passwordInput = getByLabelText('Password');
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -119,7 +102,7 @@ describe('PasswordField', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should update password requirement checks', async () => {
|
it('should update password requirement checks', async () => {
|
||||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||||
const passwordInput = getByLabelText('Password');
|
const passwordInput = getByLabelText('Password');
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
@@ -142,7 +125,7 @@ describe('PasswordField', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not run validations when blur is fired on password icon click', () => {
|
it('should not run validations when blur is fired on password icon click', () => {
|
||||||
const { container, getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
const { container, getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||||
const passwordInput = container.querySelector('input[name="password"]');
|
const passwordInput = container.querySelector('input[name="password"]');
|
||||||
|
|
||||||
const passwordIcon = getByLabelText('Show password');
|
const passwordIcon = getByLabelText('Show password');
|
||||||
@@ -163,7 +146,7 @@ describe('PasswordField', () => {
|
|||||||
...props,
|
...props,
|
||||||
handleBlur: jest.fn(),
|
handleBlur: jest.fn(),
|
||||||
};
|
};
|
||||||
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
const { container } = render(wrapper(<PasswordField {...props} />));
|
||||||
const passwordInput = container.querySelector('input[name="password"]');
|
const passwordInput = container.querySelector('input[name="password"]');
|
||||||
|
|
||||||
fireEvent.blur(passwordInput, {
|
fireEvent.blur(passwordInput, {
|
||||||
@@ -181,7 +164,7 @@ describe('PasswordField', () => {
|
|||||||
...props,
|
...props,
|
||||||
handleErrorChange: jest.fn(),
|
handleErrorChange: jest.fn(),
|
||||||
};
|
};
|
||||||
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
const { container } = render(wrapper(<PasswordField {...props} />));
|
||||||
const passwordInput = container.querySelector('input[name="password"]');
|
const passwordInput = container.querySelector('input[name="password"]');
|
||||||
|
|
||||||
fireEvent.blur(passwordInput, {
|
fireEvent.blur(passwordInput, {
|
||||||
@@ -204,7 +187,7 @@ describe('PasswordField', () => {
|
|||||||
handleErrorChange: jest.fn(),
|
handleErrorChange: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||||
|
|
||||||
const passwordIcon = getByLabelText('Show password');
|
const passwordIcon = getByLabelText('Show password');
|
||||||
|
|
||||||
@@ -224,7 +207,7 @@ describe('PasswordField', () => {
|
|||||||
handleErrorChange: jest.fn(),
|
handleErrorChange: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||||
|
|
||||||
const passwordIcon = getByLabelText('Show password');
|
const passwordIcon = getByLabelText('Show password');
|
||||||
|
|
||||||
@@ -243,12 +226,13 @@ describe('PasswordField', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should run backend validations when frontend validations pass on blur when rendered from register page', () => {
|
it('should run backend validations when frontend validations pass on blur when rendered from register page', () => {
|
||||||
store.dispatch = jest.fn(store.dispatch);
|
const mockValidateField = jest.fn();
|
||||||
props = {
|
props = {
|
||||||
...props,
|
...props,
|
||||||
handleErrorChange: jest.fn(),
|
handleErrorChange: jest.fn(),
|
||||||
|
validateField: mockValidateField,
|
||||||
};
|
};
|
||||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||||
const passwordField = getByLabelText('Password');
|
const passwordField = getByLabelText('Password');
|
||||||
fireEvent.blur(passwordField, {
|
fireEvent.blur(passwordField, {
|
||||||
target: {
|
target: {
|
||||||
@@ -257,18 +241,17 @@ describe('PasswordField', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ password: 'password123' }));
|
expect(mockValidateField).toHaveBeenCalledWith({ password: 'password123' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use password value from prop when password icon is focused out (blur due to icon)', () => {
|
it('should use password value from prop when password icon is focused out (blur due to icon)', () => {
|
||||||
store.dispatch = jest.fn(store.dispatch);
|
|
||||||
props = {
|
props = {
|
||||||
...props,
|
...props,
|
||||||
value: 'testPassword',
|
value: 'testPassword',
|
||||||
handleErrorChange: jest.fn(),
|
handleErrorChange: jest.fn(),
|
||||||
handleBlur: jest.fn(),
|
handleBlur: jest.fn(),
|
||||||
};
|
};
|
||||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||||
|
|
||||||
const passwordIcon = getByLabelText('Show password');
|
const passwordIcon = getByLabelText('Show password');
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import React from 'react';
|
import { IntlProvider } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
import registerIcons from '../RegisterFaIcons';
|
import registerIcons from '../RegisterFaIcons';
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import React from 'react';
|
import { IntlProvider } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
|
||||||
import renderer from 'react-test-renderer';
|
import renderer from 'react-test-renderer';
|
||||||
|
|
||||||
import { REGISTER_PAGE } from '../../data/constants';
|
import { PENDING_STATE, REGISTER_PAGE } from '../../data/constants';
|
||||||
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
|
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
|
||||||
|
|
||||||
describe('ThirdPartyAuthAlert', () => {
|
describe('ThirdPartyAuthAlert', () => {
|
||||||
@@ -38,4 +36,19 @@ describe('ThirdPartyAuthAlert', () => {
|
|||||||
).toJSON();
|
).toJSON();
|
||||||
expect(tree).toMatchSnapshot();
|
expect(tree).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders skeleton for pending third-party auth', () => {
|
||||||
|
props = {
|
||||||
|
...props,
|
||||||
|
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||||
|
isThirdPartyAuthActive: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const tree = renderer.create(
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<ThirdPartyAuthAlert {...props} />
|
||||||
|
</IntlProvider>,
|
||||||
|
).toJSON();
|
||||||
|
expect(tree).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
/* eslint-disable import/no-import-module-exports */
|
/* eslint-disable import/no-import-module-exports */
|
||||||
/* eslint-disable react/function-component-definition */
|
/* eslint-disable react/function-component-definition */
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@openedx/frontend-base';
|
||||||
import { render } from '@testing-library/react';
|
import { render } from '@testing-library/react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import {
|
import {
|
||||||
@@ -12,7 +11,8 @@ import {
|
|||||||
import { UnAuthOnlyRoute } from '..';
|
import { UnAuthOnlyRoute } from '..';
|
||||||
import { REGISTER_PAGE } from '../../data/constants';
|
import { REGISTER_PAGE } from '../../data/constants';
|
||||||
|
|
||||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
jest.mock('@openedx/frontend-base', () => ({
|
||||||
|
...jest.requireActual('@openedx/frontend-base'),
|
||||||
getAuthenticatedUser: jest.fn(),
|
getAuthenticatedUser: jest.fn(),
|
||||||
fetchAuthenticatedUser: jest.fn(),
|
fetchAuthenticatedUser: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
|
||||||
import renderer from 'react-test-renderer';
|
|
||||||
|
|
||||||
import Zendesk from '../Zendesk';
|
|
||||||
|
|
||||||
jest.mock('react-zendesk', () => 'Zendesk');
|
|
||||||
|
|
||||||
describe('Zendesk Help', () => {
|
|
||||||
it('should match login page third party auth alert message snapshot', () => {
|
|
||||||
const tree = renderer.create(
|
|
||||||
<IntlProvider locale="en">
|
|
||||||
<Zendesk />
|
|
||||||
</IntlProvider>,
|
|
||||||
).toJSON();
|
|
||||||
expect(tree).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,5 +1,25 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`ThirdPartyAuthAlert renders skeleton for pending third-party auth 1`] = `
|
||||||
|
<div
|
||||||
|
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
|
||||||
|
id="tpa-alert"
|
||||||
|
role="alert"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="pgn__alert-message-wrapper"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="alert-message-content"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
You have successfully signed into Google, but your Google account does not have a linked Test Site account. To link your accounts, sign in now using your Test Site password.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
|
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
|
||||||
<div
|
<div
|
||||||
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
|
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
|
||||||
@@ -13,7 +33,7 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
|
|||||||
className="alert-message-content"
|
className="alert-message-content"
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
You have successfully signed into Google, but your Google account does not have a linked Your Platform Name Here account. To link your accounts, sign in now using your Your Platform Name Here password.
|
You have successfully signed into Google, but your Google account does not have a linked Test Site account. To link your accounts, sign in now using your Test Site password.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -39,7 +59,7 @@ exports[`ThirdPartyAuthAlert should match register page third party auth alert m
|
|||||||
Almost done!
|
Almost done!
|
||||||
</div>
|
</div>
|
||||||
<p>
|
<p>
|
||||||
You've successfully signed into Google! We just need a little more information before you start learning with Your Platform Name Here.
|
You've successfully signed into Google! We just need a little more information before you start learning with Test Site.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`Zendesk Help should match login page third party auth alert message snapshot 1`] = `
|
|
||||||
<Zendesk
|
|
||||||
cookies={true}
|
|
||||||
defer={true}
|
|
||||||
webWidget={
|
|
||||||
{
|
|
||||||
"answerBot": {
|
|
||||||
"avatar": {
|
|
||||||
"name": {
|
|
||||||
"*": "edX Support",
|
|
||||||
},
|
|
||||||
"url": undefined,
|
|
||||||
},
|
|
||||||
"contactOnlyAfterQuery": true,
|
|
||||||
"suppress": false,
|
|
||||||
"title": {
|
|
||||||
"*": "edX Support",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"chat": {
|
|
||||||
"departments": {
|
|
||||||
"enabled": [
|
|
||||||
"account settings",
|
|
||||||
"billing and payments",
|
|
||||||
"certificates",
|
|
||||||
"deadlines",
|
|
||||||
"errors and technical issues",
|
|
||||||
"other",
|
|
||||||
"proctoring",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"suppress": false,
|
|
||||||
},
|
|
||||||
"contactForm": {
|
|
||||||
"attachments": true,
|
|
||||||
"selectTicketForm": {
|
|
||||||
"*": "Please choose your request type:",
|
|
||||||
},
|
|
||||||
"ticketForms": [
|
|
||||||
{
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"id": "description",
|
|
||||||
"prefill": {
|
|
||||||
"*": "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"id": 360003368814,
|
|
||||||
"subject": false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
"contactOptions": {
|
|
||||||
"enabled": false,
|
|
||||||
},
|
|
||||||
"helpCenter": {
|
|
||||||
"originalArticleButton": true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
`;
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
const configuration = {
|
|
||||||
// Cookies related configs
|
|
||||||
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN,
|
|
||||||
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
|
|
||||||
// Features
|
|
||||||
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
|
|
||||||
ENABLE_AUTO_GENERATED_USERNAME: process.env.ENABLE_AUTO_GENERATED_USERNAME || false,
|
|
||||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
|
|
||||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
|
|
||||||
ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,
|
|
||||||
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_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false,
|
|
||||||
// Links
|
|
||||||
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
|
|
||||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: process.env.AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK || null,
|
|
||||||
LOGIN_ISSUE_SUPPORT_LINK: process.env.LOGIN_ISSUE_SUPPORT_LINK || null,
|
|
||||||
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || null,
|
|
||||||
POST_REGISTRATION_REDIRECT_URL: process.env.POST_REGISTRATION_REDIRECT_URL || '',
|
|
||||||
PRIVACY_POLICY: process.env.PRIVACY_POLICY || null,
|
|
||||||
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
|
|
||||||
TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null,
|
|
||||||
TOS_LINK: process.env.TOS_LINK || null,
|
|
||||||
// Base container images
|
|
||||||
BANNER_IMAGE_LARGE: process.env.BANNER_IMAGE_LARGE || '',
|
|
||||||
BANNER_IMAGE_MEDIUM: process.env.BANNER_IMAGE_MEDIUM || '',
|
|
||||||
BANNER_IMAGE_SMALL: process.env.BANNER_IMAGE_SMALL || '',
|
|
||||||
BANNER_IMAGE_EXTRA_SMALL: process.env.BANNER_IMAGE_EXTRA_SMALL || '',
|
|
||||||
// Recommendation constants
|
|
||||||
GENERAL_RECOMMENDATIONS: process.env.GENERAL_RECOMMENDATIONS || '[]',
|
|
||||||
// Miscellaneous
|
|
||||||
INFO_EMAIL: process.env.INFO_EMAIL || '',
|
|
||||||
ZENDESK_KEY: process.env.ZENDESK_KEY,
|
|
||||||
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 || '',
|
|
||||||
};
|
|
||||||
|
|
||||||
export default configuration;
|
|
||||||
1
src/constants.ts
Normal file
1
src/constants.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const appId = 'org.openedx.frontend.app.authn';
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import algoliasearch from 'algoliasearch';
|
|
||||||
|
|
||||||
// initialize Algolia workers
|
|
||||||
const initializeSearchClient = () => algoliasearch(
|
|
||||||
getConfig().ALGOLIA_APP_ID,
|
|
||||||
getConfig().ALGOLIA_SEARCH_API_KEY,
|
|
||||||
);
|
|
||||||
|
|
||||||
const getLocationRestrictionFilter = (userCountry) => {
|
|
||||||
if (userCountry) {
|
|
||||||
return `NOT blocked_in:"${userCountry}" AND (allowed_in:"null" OR allowed_in:"${userCountry}")`;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
initializeSearchClient,
|
|
||||||
getLocationRestrictionFilter,
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { composeWithDevTools } from '@redux-devtools/extension';
|
|
||||||
import { applyMiddleware, compose, createStore } from 'redux';
|
|
||||||
import { createLogger } from 'redux-logger';
|
|
||||||
import createSagaMiddleware from 'redux-saga';
|
|
||||||
import thunkMiddleware from 'redux-thunk';
|
|
||||||
|
|
||||||
import createRootReducer from './reducers';
|
|
||||||
import rootSaga from './sagas';
|
|
||||||
|
|
||||||
const sagaMiddleware = createSagaMiddleware();
|
|
||||||
|
|
||||||
function composeMiddleware() {
|
|
||||||
if (getConfig().ENVIRONMENT === 'development') {
|
|
||||||
const loggerMiddleware = createLogger({
|
|
||||||
collapsed: true,
|
|
||||||
});
|
|
||||||
return composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware));
|
|
||||||
}
|
|
||||||
|
|
||||||
return compose(applyMiddleware(thunkMiddleware, sagaMiddleware));
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function configureStore(initialState = {}) {
|
|
||||||
const store = createStore(
|
|
||||||
createRootReducer(),
|
|
||||||
initialState,
|
|
||||||
composeMiddleware(),
|
|
||||||
);
|
|
||||||
sagaMiddleware.run(rootSaga);
|
|
||||||
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ export const REGISTER_EMBEDDED_PAGE = '/register-embedded';
|
|||||||
export const RESET_PAGE = '/reset';
|
export const RESET_PAGE = '/reset';
|
||||||
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
|
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
|
||||||
export const DEFAULT_REDIRECT_URL = '/dashboard';
|
export const DEFAULT_REDIRECT_URL = '/dashboard';
|
||||||
export const RECOMMENDATIONS = '/recommendations';
|
|
||||||
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
|
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
|
||||||
export const PAGE_NOT_FOUND = '/notfound';
|
export const PAGE_NOT_FOUND = '/notfound';
|
||||||
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
||||||
@@ -29,9 +28,9 @@ export const EMBEDDED = 'embedded';
|
|||||||
export const LETTER_REGEX = /[a-zA-Z]/;
|
export const LETTER_REGEX = /[a-zA-Z]/;
|
||||||
export const NUMBER_REGEX = /\d/;
|
export const NUMBER_REGEX = /\d/;
|
||||||
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
|
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
|
||||||
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
|
+ '|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"'
|
||||||
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
|
+ ')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+)(?:[A-Z0-9-]{2,63})'
|
||||||
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
||||||
|
|
||||||
// Query string parameters that can be passed to LMS to manage
|
// Query string parameters that can be passed to LMS to manage
|
||||||
// things like auto-enrollment upon login and registration.
|
// things like auto-enrollment upon login and registration.
|
||||||
|
|||||||
59
src/data/countries.ts
Normal file
59
src/data/countries.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { getPrimaryLanguageSubtag } from '@openedx/frontend-base';
|
||||||
|
import COUNTRIES, { langs as countryLangs } from 'i18n-iso-countries';
|
||||||
|
import arLocale from 'i18n-iso-countries/langs/ar.json';
|
||||||
|
import caLocale from 'i18n-iso-countries/langs/ca.json';
|
||||||
|
import enLocale from 'i18n-iso-countries/langs/en.json';
|
||||||
|
import esLocale from 'i18n-iso-countries/langs/es.json';
|
||||||
|
import frLocale from 'i18n-iso-countries/langs/fr.json';
|
||||||
|
import heLocale from 'i18n-iso-countries/langs/he.json';
|
||||||
|
import idLocale from 'i18n-iso-countries/langs/id.json';
|
||||||
|
import koLocale from 'i18n-iso-countries/langs/ko.json';
|
||||||
|
import plLocale from 'i18n-iso-countries/langs/pl.json';
|
||||||
|
import ptLocale from 'i18n-iso-countries/langs/pt.json';
|
||||||
|
import ruLocale from 'i18n-iso-countries/langs/ru.json';
|
||||||
|
import ukLocale from 'i18n-iso-countries/langs/uk.json';
|
||||||
|
import zhLocale from 'i18n-iso-countries/langs/zh.json';
|
||||||
|
|
||||||
|
COUNTRIES.registerLocale(arLocale);
|
||||||
|
COUNTRIES.registerLocale(enLocale);
|
||||||
|
COUNTRIES.registerLocale(esLocale);
|
||||||
|
COUNTRIES.registerLocale(frLocale);
|
||||||
|
COUNTRIES.registerLocale(zhLocale);
|
||||||
|
COUNTRIES.registerLocale(caLocale);
|
||||||
|
COUNTRIES.registerLocale(heLocale);
|
||||||
|
COUNTRIES.registerLocale(idLocale);
|
||||||
|
COUNTRIES.registerLocale(koLocale);
|
||||||
|
COUNTRIES.registerLocale(plLocale);
|
||||||
|
COUNTRIES.registerLocale(ptLocale);
|
||||||
|
COUNTRIES.registerLocale(ruLocale);
|
||||||
|
COUNTRIES.registerLocale(ukLocale);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a lookup table of country IDs to country names for the current locale.
|
||||||
|
*
|
||||||
|
* @memberof module:I18n
|
||||||
|
*/
|
||||||
|
export function getCountryMessages(locale) {
|
||||||
|
const primaryLanguageSubtag = getPrimaryLanguageSubtag(locale);
|
||||||
|
const languageCode = countryLangs().includes(primaryLanguageSubtag) ? primaryLanguageSubtag : 'en';
|
||||||
|
|
||||||
|
return COUNTRIES.getNames(languageCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a list of countries represented as objects of the following shape:
|
||||||
|
*
|
||||||
|
* {
|
||||||
|
* key, // The ID of the country
|
||||||
|
* name // The localized name of the country
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* TODO: ARCH-878: The list should be sorted alphabetically in the current locale.
|
||||||
|
* This is useful for populating dropdowns.
|
||||||
|
*
|
||||||
|
* @memberof module:I18n
|
||||||
|
*/
|
||||||
|
export function getCountryList(locale) {
|
||||||
|
const countryMessages = getCountryMessages(locale);
|
||||||
|
return Object.entries(countryMessages).map(([code, name]) => ({ code, name }));
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import {
|
|
||||||
createInstance,
|
|
||||||
} from '@optimizely/react-sdk';
|
|
||||||
|
|
||||||
const OPTIMIZELY_SDK_KEY = process.env.OPTIMIZELY_FULL_STACK_SDK_KEY;
|
|
||||||
|
|
||||||
const getOptimizelyInstance = () => {
|
|
||||||
if (OPTIMIZELY_SDK_KEY) {
|
|
||||||
return createInstance({
|
|
||||||
sdkKey: OPTIMIZELY_SDK_KEY,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default getOptimizelyInstance();
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { combineReducers } from 'redux';
|
|
||||||
|
|
||||||
import {
|
|
||||||
reducer as commonComponentsReducer,
|
|
||||||
storeName as commonComponentsStoreName,
|
|
||||||
} from '../common-components';
|
|
||||||
import {
|
|
||||||
reducer as forgotPasswordReducer,
|
|
||||||
storeName as forgotPasswordStoreName,
|
|
||||||
} from '../forgot-password';
|
|
||||||
import {
|
|
||||||
reducer as loginReducer,
|
|
||||||
storeName as loginStoreName,
|
|
||||||
} from '../login';
|
|
||||||
import {
|
|
||||||
reducer as authnProgressiveProfilingReducers,
|
|
||||||
storeName as authnProgressiveProfilingStoreName,
|
|
||||||
} from '../progressive-profiling';
|
|
||||||
import {
|
|
||||||
reducer as registerReducer,
|
|
||||||
storeName as registerStoreName,
|
|
||||||
} from '../register';
|
|
||||||
import {
|
|
||||||
reducer as resetPasswordReducer,
|
|
||||||
storeName as resetPasswordStoreName,
|
|
||||||
} from '../reset-password';
|
|
||||||
|
|
||||||
const createRootReducer = () => combineReducers({
|
|
||||||
[loginStoreName]: loginReducer,
|
|
||||||
[registerStoreName]: registerReducer,
|
|
||||||
[commonComponentsStoreName]: commonComponentsReducer,
|
|
||||||
[forgotPasswordStoreName]: forgotPasswordReducer,
|
|
||||||
[resetPasswordStoreName]: resetPasswordReducer,
|
|
||||||
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
|
|
||||||
});
|
|
||||||
export default createRootReducer;
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { all } from 'redux-saga/effects';
|
|
||||||
|
|
||||||
import { saga as commonComponentsSaga } from '../common-components';
|
|
||||||
import { saga as forgotPasswordSaga } from '../forgot-password';
|
|
||||||
import { saga as loginSaga } from '../login';
|
|
||||||
import { saga as authnProgressiveProfilingSaga } from '../progressive-profiling';
|
|
||||||
import { saga as registrationSaga } from '../register';
|
|
||||||
import { saga as resetPasswordSaga } from '../reset-password';
|
|
||||||
|
|
||||||
export default function* rootSaga() {
|
|
||||||
yield all([
|
|
||||||
loginSaga(),
|
|
||||||
registrationSaga(),
|
|
||||||
commonComponentsSaga(),
|
|
||||||
forgotPasswordSaga(),
|
|
||||||
resetPasswordSaga(),
|
|
||||||
authnProgressiveProfilingSaga(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { getLocationRestrictionFilter } from '../algolia';
|
|
||||||
|
|
||||||
describe('algoliaUtilsTests', () => {
|
|
||||||
it('test getLocationRestrictionFilter returns filter if country is passed', () => {
|
|
||||||
const countryCode = 'PK';
|
|
||||||
const filter = getLocationRestrictionFilter(countryCode);
|
|
||||||
const expectedFilter = `NOT blocked_in:"${countryCode}" AND (allowed_in:"null" OR allowed_in:"${countryCode}")`;
|
|
||||||
expect(filter).toEqual(expectedFilter);
|
|
||||||
});
|
|
||||||
it('test getLocationRestrictionFilter returns empty string if country is not passed', () => {
|
|
||||||
const countryCode = '';
|
|
||||||
const filter = getLocationRestrictionFilter(countryCode);
|
|
||||||
const expectedFilter = '';
|
|
||||||
expect(filter).toEqual(expectedFilter);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import Cookies from 'universal-cookie';
|
import Cookies from 'universal-cookie';
|
||||||
|
|
||||||
import { setCookie } from '../utils';
|
import { setCookie } from '../utils';
|
||||||
|
|
||||||
// Mock getConfig function
|
|
||||||
jest.mock('@edx/frontend-platform', () => ({
|
|
||||||
getConfig: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock Cookies class
|
// Mock Cookies class
|
||||||
jest.mock('universal-cookie');
|
jest.mock('universal-cookie');
|
||||||
|
|
||||||
@@ -17,9 +11,7 @@ describe('setCookie function', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set a cookie with default options', () => {
|
it('should set a cookie with default options', () => {
|
||||||
getConfig.mockReturnValue({ SESSION_COOKIE_DOMAIN: 'example.com' });
|
setCookie('testCookie', 'testValue', 'example.com');
|
||||||
|
|
||||||
setCookie('testCookie', 'testValue');
|
|
||||||
|
|
||||||
expect(Cookies).toHaveBeenCalled();
|
expect(Cookies).toHaveBeenCalled();
|
||||||
expect(Cookies).toHaveBeenCalledWith();
|
expect(Cookies).toHaveBeenCalledWith();
|
||||||
@@ -30,10 +22,8 @@ describe('setCookie function', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set a cookie with specified expiry', () => {
|
it('should set a cookie with specified expiry', () => {
|
||||||
getConfig.mockReturnValue({ SESSION_COOKIE_DOMAIN: 'example.com' });
|
|
||||||
|
|
||||||
const expiry = new Date('2023-12-31');
|
const expiry = new Date('2023-12-31');
|
||||||
setCookie('testCookie', 'testValue', expiry);
|
setCookie('testCookie', 'testValue', 'example.com', expiry);
|
||||||
|
|
||||||
expect(Cookies).toHaveBeenCalled();
|
expect(Cookies).toHaveBeenCalled();
|
||||||
expect(Cookies).toHaveBeenCalledWith();
|
expect(Cookies).toHaveBeenCalledWith();
|
||||||
@@ -45,7 +35,7 @@ describe('setCookie function', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not set a cookie if cookieName is undefined', () => {
|
it('should not set a cookie if cookieName is undefined', () => {
|
||||||
setCookie(undefined, 'testValue');
|
setCookie(undefined, 'testValue', 'example.com');
|
||||||
|
|
||||||
expect(Cookies).not.toHaveBeenCalled();
|
expect(Cookies).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
import AsyncActionType from '../utils/reduxUtils';
|
|
||||||
|
|
||||||
describe('AsyncActionType', () => {
|
|
||||||
it('should return well formatted action strings', () => {
|
|
||||||
const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
|
|
||||||
|
|
||||||
expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
|
|
||||||
expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
|
|
||||||
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
|
|
||||||
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
|
|
||||||
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
|
|
||||||
expect(actionType.FORBIDDEN).toBe('HOUSE_CATS__START_THE_RACE__FORBIDDEN');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import Cookies from 'universal-cookie';
|
import Cookies from 'universal-cookie';
|
||||||
|
|
||||||
export default function setCookie(cookieName, cookieValue, cookieExpiry) {
|
export default function setCookie(cookieName, cookieValue, cookieDomain, cookieExpiry) {
|
||||||
if (cookieName) { // To avoid setting getting exception when setting cookie with undefined names.
|
if (cookieName) { // To avoid setting getting exception when setting cookie with undefined names.
|
||||||
const cookies = new Cookies();
|
const cookies = new Cookies();
|
||||||
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
|
const options = { domain: cookieDomain, path: '/' };
|
||||||
if (cookieExpiry) {
|
if (cookieExpiry) {
|
||||||
options.expires = cookieExpiry;
|
options.expires = cookieExpiry;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,10 +40,8 @@ export const updatePathWithQueryParams = (path) => {
|
|||||||
return path;
|
return path;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queryParams.indexOf('track=pwreset') > -1) {
|
if (queryParams.includes('track=pwreset')) {
|
||||||
queryParams = queryParams.replace(
|
queryParams = queryParams.replace('?track=pwreset&', '?',).replace('?track=pwreset', '').replace('&track=pwreset', '').replace('?&', '?');
|
||||||
'?track=pwreset&', '?',
|
|
||||||
).replace('?track=pwreset', '').replace('&track=pwreset', '').replace('?&', '?');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${path}${queryParams}`;
|
return `${path}${queryParams}`;
|
||||||
@@ -53,7 +51,7 @@ export const getAllPossibleQueryParams = (locationURl = null) => {
|
|||||||
const urlParams = locationURl ? QueryString.parseUrl(locationURl).query : QueryString.parse(window.location.search);
|
const urlParams = locationURl ? QueryString.parseUrl(locationURl).query : QueryString.parse(window.location.search);
|
||||||
const params = {};
|
const params = {};
|
||||||
Object.entries(urlParams).forEach(([key, value]) => {
|
Object.entries(urlParams).forEach(([key, value]) => {
|
||||||
if (AUTH_PARAMS.indexOf(key) > -1) {
|
if (AUTH_PARAMS.includes(key)) {
|
||||||
params[key] = value;
|
params[key] = value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,5 +7,4 @@ export {
|
|||||||
updatePathWithQueryParams,
|
updatePathWithQueryParams,
|
||||||
windowScrollTo,
|
windowScrollTo,
|
||||||
} from './dataUtils';
|
} from './dataUtils';
|
||||||
export { default as AsyncActionType } from './reduxUtils';
|
|
||||||
export { default as setCookie } from './cookies';
|
export { default as setCookie } from './cookies';
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Helper class to save time when writing out action types for asynchronous methods. Also helps
|
|
||||||
* ensure that actions are namespaced.
|
|
||||||
*/
|
|
||||||
export default class AsyncActionType {
|
|
||||||
constructor(topic, name) {
|
|
||||||
this.topic = topic;
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
get BASE() {
|
|
||||||
return `${this.topic}__${this.name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get BEGIN() {
|
|
||||||
return `${this.topic}__${this.name}__BEGIN`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get SUCCESS() {
|
|
||||||
return `${this.topic}__${this.name}__SUCCESS`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get FAILURE() {
|
|
||||||
return `${this.topic}__${this.name}__FAILURE`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get RESET() {
|
|
||||||
return `${this.topic}__${this.name}__RESET`;
|
|
||||||
}
|
|
||||||
|
|
||||||
get FORBIDDEN() {
|
|
||||||
return `${this.topic}__${this.name}__FORBIDDEN`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,7 @@ import { breakpoints } from '@openedx/paragon';
|
|||||||
const useMobileResponsive = (breakpoint) => {
|
const useMobileResponsive = (breakpoint) => {
|
||||||
const [isMobileWindow, setIsMobileWindow] = useState();
|
const [isMobileWindow, setIsMobileWindow] = useState();
|
||||||
const checkForMobile = () => {
|
const checkForMobile = () => {
|
||||||
setIsMobileWindow(window.matchMedia(`(max-width: ${breakpoint || breakpoints.small.maxWidth}px)`).matches);
|
setIsMobileWindow(window.matchMedia(`(max-width: ${breakpoint ?? breakpoints.small.maxWidth}px)`).matches);
|
||||||
};
|
};
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkForMobile();
|
checkForMobile();
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Form, Icon } from '@openedx/paragon';
|
import { Form, Icon } from '@openedx/paragon';
|
||||||
import { ExpandMore } from '@openedx/paragon/icons';
|
import { ExpandMore } from '@openedx/paragon/icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import React from 'react';
|
import { getSiteConfig } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { fireEvent, render } from '@testing-library/react';
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
|
|
||||||
import FieldRenderer from '../FieldRenderer';
|
import FieldRenderer from '../FieldRenderer';
|
||||||
@@ -45,7 +43,7 @@ describe('FieldRendererTests', () => {
|
|||||||
name: 'yob-field',
|
name: 'yob-field',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
|
const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => { }} />);
|
||||||
expect(container.innerHTML).toEqual('');
|
expect(container.innerHTML).toEqual('');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -86,7 +84,7 @@ describe('FieldRendererTests', () => {
|
|||||||
it('should render checkbox field', () => {
|
it('should render checkbox field', () => {
|
||||||
const fieldData = {
|
const fieldData = {
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
label: `I agree that ${getConfig().SITE_NAME} may send me marketing messages.`,
|
label: `I agree that ${getSiteConfig().siteName} may send me marketing messages.`,
|
||||||
name: 'marketing-emails-opt-in-field',
|
name: 'marketing-emails-opt-in-field',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -105,7 +103,7 @@ describe('FieldRendererTests', () => {
|
|||||||
type: 'unknown',
|
type: 'unknown',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
|
const { container } = render(<FieldRenderer fieldData={fieldData} onChangeHandler={() => { }} />);
|
||||||
expect(container.innerHTML).toContain('');
|
expect(container.innerHTML).toContain('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import React from 'react';
|
import { FormattedMessage, useAppConfig, useIntl } from '@openedx/frontend-base';
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
|
||||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
|
||||||
import { Alert } from '@openedx/paragon';
|
import { Alert } from '@openedx/paragon';
|
||||||
import { CheckCircle, Error } from '@openedx/paragon/icons';
|
import { CheckCircle, Error } from '@openedx/paragon/icons';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
import messages from './messages';
|
|
||||||
import {
|
import {
|
||||||
COMPLETE_STATE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR,
|
COMPLETE_STATE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR,
|
||||||
} from '../data/constants';
|
} from '../data/constants';
|
||||||
import { PASSWORD_RESET } from '../reset-password/data/constants';
|
import { PASSWORD_RESET } from '../reset-password/data/constants';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
const ForgotPasswordAlert = (props) => {
|
const ForgotPasswordAlert = (props) => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@@ -36,14 +33,14 @@ const ForgotPasswordAlert = (props) => {
|
|||||||
values={{
|
values={{
|
||||||
email: <span className="data-hj-suppress">{email}</span>,
|
email: <span className="data-hj-suppress">{email}</span>,
|
||||||
supportLink: (
|
supportLink: (
|
||||||
<Alert.Link href={getConfig().PASSWORD_RESET_SUPPORT_LINK} target="_blank">
|
<Alert.Link href={useAppConfig().PASSWORD_RESET_SUPPORT_LINK} target="_blank">
|
||||||
{formatMessage(messages['confirmation.support.link'])}
|
{formatMessage(messages['confirmation.support.link'])}
|
||||||
</Alert.Link>
|
</Alert.Link>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case INTERNAL_SERVER_ERROR:
|
case INTERNAL_SERVER_ERROR:
|
||||||
message = formatMessage(messages['internal.server.error']);
|
message = formatMessage(messages['internal.server.error']);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
|
|
||||||
import { getConfig } from '@edx/frontend-platform';
|
import {
|
||||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
getSiteConfig, sendPageEvent, sendTrackEvent, useAppConfig, useIntl,
|
||||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
} from '@openedx/frontend-base';
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
Hyperlink,
|
Hyperlink,
|
||||||
@@ -13,42 +12,40 @@ import {
|
|||||||
Tabs,
|
Tabs,
|
||||||
} from '@openedx/paragon';
|
} from '@openedx/paragon';
|
||||||
import { ChevronLeft } from '@openedx/paragon/icons';
|
import { ChevronLeft } from '@openedx/paragon/icons';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useLocation, useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
import { forgotPassword, setForgotPasswordFormData } from './data/actions';
|
import { useForgotPassword } from './data/apiHook';
|
||||||
import { forgotPasswordResultSelector } from './data/selectors';
|
|
||||||
import ForgotPasswordAlert from './ForgotPasswordAlert';
|
import ForgotPasswordAlert from './ForgotPasswordAlert';
|
||||||
import messages from './messages';
|
import messages from './messages';
|
||||||
import BaseContainer from '../base-container';
|
import BaseContainer from '../base-container';
|
||||||
import { FormGroup } from '../common-components';
|
import { FormGroup } from '../common-components';
|
||||||
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
import { LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
||||||
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||||
|
|
||||||
const ForgotPasswordPage = (props) => {
|
const ForgotPasswordPage = () => {
|
||||||
const platformName = getConfig().SITE_NAME;
|
const platformName = getSiteConfig().siteName;
|
||||||
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||||
const {
|
|
||||||
status, submitState, emailValidationError,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const [email, setEmail] = useState(props.email);
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
const appConfig = useAppConfig();
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
const [bannerEmail, setBannerEmail] = useState('');
|
const [bannerEmail, setBannerEmail] = useState('');
|
||||||
const [formErrors, setFormErrors] = useState('');
|
const [formErrors, setFormErrors] = useState('');
|
||||||
const [validationError, setValidationError] = useState(emailValidationError);
|
const [validationError, setValidationError] = useState('');
|
||||||
const navigate = useNavigate();
|
const [status, setStatus] = useState(location.state?.status || null);
|
||||||
|
|
||||||
|
// React Query hook for forgot password
|
||||||
|
const { mutate: sendForgotPassword, isPending: isSending } = useForgotPassword();
|
||||||
|
|
||||||
|
const submitState = isSending ? 'pending' : 'default';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sendPageEvent('login_and_registration', 'reset');
|
sendPageEvent('login_and_registration', 'reset');
|
||||||
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
|
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValidationError(emailValidationError);
|
|
||||||
}, [emailValidationError]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === 'complete') {
|
if (status === 'complete') {
|
||||||
setEmail('');
|
setEmail('');
|
||||||
@@ -68,22 +65,38 @@ const ForgotPasswordPage = (props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleBlur = () => {
|
const handleBlur = () => {
|
||||||
props.setForgotPasswordFormData({ email, emailValidationError: getValidationMessage(email) });
|
setValidationError(getValidationMessage(email));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFocus = () => props.setForgotPasswordFormData({ emailValidationError: '' });
|
const handleFocus = () => {
|
||||||
|
setValidationError('');
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = (e) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setBannerEmail(email);
|
setBannerEmail(email);
|
||||||
|
|
||||||
const error = getValidationMessage(email);
|
const validateError = getValidationMessage(email);
|
||||||
if (error) {
|
if (validateError) {
|
||||||
setFormErrors(error);
|
setFormErrors(validateError);
|
||||||
props.setForgotPasswordFormData({ email, emailValidationError: error });
|
setValidationError(validateError);
|
||||||
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
||||||
} else {
|
} else {
|
||||||
props.forgotPassword(email);
|
setFormErrors('');
|
||||||
|
sendForgotPassword(email, {
|
||||||
|
onSuccess: (data, emailUsed) => {
|
||||||
|
setStatus('complete');
|
||||||
|
setBannerEmail(emailUsed);
|
||||||
|
setFormErrors('');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
if (error.response && error.response.status === 403) {
|
||||||
|
setStatus('forbidden');
|
||||||
|
} else {
|
||||||
|
setStatus('server-error');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,7 +111,7 @@ const ForgotPasswordPage = (props) => {
|
|||||||
<BaseContainer>
|
<BaseContainer>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{formatMessage(messages['forgot.password.page.title'],
|
<title>{formatMessage(messages['forgot.password.page.title'],
|
||||||
{ siteName: getConfig().SITE_NAME })}
|
{ siteName: getSiteConfig().siteName })}
|
||||||
</title>
|
</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div>
|
<div>
|
||||||
@@ -139,12 +152,12 @@ const ForgotPasswordPage = (props) => {
|
|||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
onMouseDown={(e) => e.preventDefault()}
|
onMouseDown={(e) => e.preventDefault()}
|
||||||
/>
|
/>
|
||||||
{(getConfig().LOGIN_ISSUE_SUPPORT_LINK) && (
|
{(appConfig.LOGIN_ISSUE_SUPPORT_LINK) && (
|
||||||
<Hyperlink
|
<Hyperlink
|
||||||
id="forgot-password"
|
id="forgot-password"
|
||||||
name="forgot-password"
|
name="forgot-password"
|
||||||
className="ml-4 font-weight-500 text-body"
|
className="ml-4 font-weight-500 text-body"
|
||||||
destination={getConfig().LOGIN_ISSUE_SUPPORT_LINK}
|
destination={appConfig.LOGIN_ISSUE_SUPPORT_LINK}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
showLaunchIcon={false}
|
showLaunchIcon={false}
|
||||||
>
|
>
|
||||||
@@ -153,8 +166,8 @@ const ForgotPasswordPage = (props) => {
|
|||||||
)}
|
)}
|
||||||
<p className="mt-5.5 small text-gray-700">
|
<p className="mt-5.5 small text-gray-700">
|
||||||
{formatMessage(messages['additional.help.text'], { platformName })}
|
{formatMessage(messages['additional.help.text'], { platformName })}
|
||||||
<span>
|
<span className="mx-1">
|
||||||
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
|
<Hyperlink isInline destination={`mailto:${appConfig.INFO_EMAIL}`}>{appConfig.INFO_EMAIL}</Hyperlink>
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -164,26 +177,4 @@ const ForgotPasswordPage = (props) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ForgotPasswordPage.propTypes = {
|
export default ForgotPasswordPage;
|
||||||
email: PropTypes.string,
|
|
||||||
emailValidationError: PropTypes.string,
|
|
||||||
forgotPassword: PropTypes.func.isRequired,
|
|
||||||
setForgotPasswordFormData: PropTypes.func.isRequired,
|
|
||||||
status: PropTypes.string,
|
|
||||||
submitState: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
ForgotPasswordPage.defaultProps = {
|
|
||||||
email: '',
|
|
||||||
emailValidationError: '',
|
|
||||||
status: null,
|
|
||||||
submitState: DEFAULT_STATE,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default connect(
|
|
||||||
forgotPasswordResultSelector,
|
|
||||||
{
|
|
||||||
forgotPassword,
|
|
||||||
setForgotPasswordFormData,
|
|
||||||
},
|
|
||||||
)(ForgotPasswordPage);
|
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import { AsyncActionType } from '../../data/utils';
|
|
||||||
|
|
||||||
export const FORGOT_PASSWORD = new AsyncActionType('FORGOT', 'PASSWORD');
|
|
||||||
export const FORGOT_PASSWORD_PERSIST_FORM_DATA = 'FORGOT_PASSWORD_PERSIST_FORM_DATA';
|
|
||||||
|
|
||||||
// Forgot Password
|
|
||||||
export const forgotPassword = email => ({
|
|
||||||
type: FORGOT_PASSWORD.BASE,
|
|
||||||
payload: { email },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const forgotPasswordBegin = () => ({
|
|
||||||
type: FORGOT_PASSWORD.BEGIN,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const forgotPasswordSuccess = email => ({
|
|
||||||
type: FORGOT_PASSWORD.SUCCESS,
|
|
||||||
payload: { email },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const forgotPasswordForbidden = () => ({
|
|
||||||
type: FORGOT_PASSWORD.FORBIDDEN,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const forgotPasswordServerError = () => ({
|
|
||||||
type: FORGOT_PASSWORD.FAILURE,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setForgotPasswordFormData = (forgotPasswordFormData) => ({
|
|
||||||
type: FORGOT_PASSWORD_PERSIST_FORM_DATA,
|
|
||||||
payload: { forgotPasswordFormData },
|
|
||||||
});
|
|
||||||
139
src/forgot-password/data/api.test.ts
Normal file
139
src/forgot-password/data/api.test.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { getAuthenticatedHttpClient, getSiteConfig } from '@openedx/frontend-base';
|
||||||
|
import formurlencoded from 'form-urlencoded';
|
||||||
|
|
||||||
|
import { forgotPassword } from './api';
|
||||||
|
|
||||||
|
// Mock the platform dependencies
|
||||||
|
jest.mock('@openedx/frontend-base', () => ({
|
||||||
|
getSiteConfig: jest.fn(),
|
||||||
|
getAuthenticatedHttpClient: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('form-urlencoded', () => jest.fn());
|
||||||
|
|
||||||
|
const mockGetSiteConfig = getSiteConfig as jest.MockedFunction<typeof getSiteConfig>;
|
||||||
|
const mockGetAuthenticatedHttpClient = getAuthenticatedHttpClient as
|
||||||
|
jest.MockedFunction<typeof getAuthenticatedHttpClient>;
|
||||||
|
const mockFormurlencoded = formurlencoded as jest.MockedFunction<typeof formurlencoded>;
|
||||||
|
|
||||||
|
describe('forgot-password api', () => {
|
||||||
|
const mockHttpClient = {
|
||||||
|
post: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockConfig = {
|
||||||
|
lmsBaseUrl: 'http://localhost:18000',
|
||||||
|
} as ReturnType<typeof getSiteConfig>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mockGetSiteConfig.mockReturnValue(mockConfig);
|
||||||
|
mockGetAuthenticatedHttpClient.mockReturnValue(mockHttpClient as any);
|
||||||
|
mockFormurlencoded.mockImplementation((data) => `encoded=${JSON.stringify(data)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('forgotPassword', () => {
|
||||||
|
const testEmail = 'test@example.com';
|
||||||
|
const expectedUrl = `${mockConfig.lmsBaseUrl}/account/password`;
|
||||||
|
const expectedConfig = {
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
isPublic: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should send forgot password request successfully', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
message: 'Password reset email sent successfully',
|
||||||
|
success: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await forgotPassword(testEmail);
|
||||||
|
|
||||||
|
expect(mockGetAuthenticatedHttpClient).toHaveBeenCalled();
|
||||||
|
expect(mockFormurlencoded).toHaveBeenCalledWith({ email: testEmail });
|
||||||
|
expect(mockHttpClient.post).toHaveBeenCalledWith(
|
||||||
|
expectedUrl,
|
||||||
|
`encoded=${JSON.stringify({ email: testEmail })}`,
|
||||||
|
expectedConfig,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(mockResponse.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty email address', async () => {
|
||||||
|
const emptyEmail = '';
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
message: 'Email is required',
|
||||||
|
success: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await forgotPassword(emptyEmail);
|
||||||
|
|
||||||
|
expect(mockFormurlencoded).toHaveBeenCalledWith({ email: emptyEmail });
|
||||||
|
expect(mockHttpClient.post).toHaveBeenCalledWith(
|
||||||
|
expectedUrl,
|
||||||
|
`encoded=${JSON.stringify({ email: emptyEmail })}`,
|
||||||
|
expectedConfig,
|
||||||
|
);
|
||||||
|
expect(result).toEqual(mockResponse.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle network errors without response', async () => {
|
||||||
|
const networkError = new Error('Network Error');
|
||||||
|
networkError.name = 'NetworkError';
|
||||||
|
mockHttpClient.post.mockRejectedValueOnce(networkError);
|
||||||
|
|
||||||
|
await expect(forgotPassword(testEmail)).rejects.toThrow('Network Error');
|
||||||
|
|
||||||
|
expect(mockHttpClient.post).toHaveBeenCalledWith(
|
||||||
|
expectedUrl,
|
||||||
|
expect.any(String),
|
||||||
|
expectedConfig,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle timeout errors', async () => {
|
||||||
|
const timeoutError = new Error('Request timeout');
|
||||||
|
timeoutError.name = 'TimeoutError';
|
||||||
|
mockHttpClient.post.mockRejectedValueOnce(timeoutError);
|
||||||
|
|
||||||
|
await expect(forgotPassword(testEmail)).rejects.toThrow('Request timeout');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle response with no data field', async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
status: 200,
|
||||||
|
statusText: 'OK',
|
||||||
|
};
|
||||||
|
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await forgotPassword(testEmail);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return exactly the data field from response', async () => {
|
||||||
|
const expectedData = {
|
||||||
|
message: 'Password reset email sent successfully',
|
||||||
|
success: true,
|
||||||
|
timestamp: '2026-02-05T10:00:00Z',
|
||||||
|
};
|
||||||
|
const mockResponse = {
|
||||||
|
data: expectedData,
|
||||||
|
status: 200,
|
||||||
|
headers: {},
|
||||||
|
};
|
||||||
|
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const result = await forgotPassword(testEmail);
|
||||||
|
|
||||||
|
expect(result).toEqual(expectedData);
|
||||||
|
expect(result).not.toHaveProperty('status');
|
||||||
|
expect(result).not.toHaveProperty('headers');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getAuthenticatedHttpClient, getSiteConfig } from '@openedx/frontend-base';
|
||||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
|
||||||
import formurlencoded from 'form-urlencoded';
|
import formurlencoded from 'form-urlencoded';
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
const forgotPassword = async (email: string) => {
|
||||||
export async function forgotPassword(email) {
|
|
||||||
const requestConfig = {
|
const requestConfig = {
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
isPublic: true,
|
isPublic: true,
|
||||||
@@ -11,7 +9,7 @@ export async function forgotPassword(email) {
|
|||||||
|
|
||||||
const { data } = await getAuthenticatedHttpClient()
|
const { data } = await getAuthenticatedHttpClient()
|
||||||
.post(
|
.post(
|
||||||
`${getConfig().LMS_BASE_URL}/account/password`,
|
`${getSiteConfig().lmsBaseUrl}/account/password`,
|
||||||
formurlencoded({ email }),
|
formurlencoded({ email }),
|
||||||
requestConfig,
|
requestConfig,
|
||||||
)
|
)
|
||||||
@@ -20,4 +18,8 @@ export async function forgotPassword(email) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
forgotPassword,
|
||||||
|
};
|
||||||
175
src/forgot-password/data/apiHook.test.ts
Normal file
175
src/forgot-password/data/apiHook.test.ts
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { logError, logInfo } from '@openedx/frontend-base';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { renderHook, waitFor } from '@testing-library/react';
|
||||||
|
|
||||||
|
import * as api from './api';
|
||||||
|
import { useForgotPassword } from './apiHook';
|
||||||
|
|
||||||
|
// Mock the logging functions
|
||||||
|
jest.mock('@openedx/frontend-base', () => ({
|
||||||
|
logError: jest.fn(),
|
||||||
|
logInfo: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock the API function
|
||||||
|
jest.mock('./api', () => ({
|
||||||
|
forgotPassword: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockForgotPassword = api.forgotPassword as jest.MockedFunction<typeof api.forgotPassword>;
|
||||||
|
const mockLogError = logError as jest.MockedFunction<typeof logError>;
|
||||||
|
const mockLogInfo = logInfo as jest.MockedFunction<typeof logInfo>;
|
||||||
|
|
||||||
|
// Test wrapper component
|
||||||
|
const createWrapper = () => {
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: { retry: false },
|
||||||
|
mutations: { retry: false },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return function TestWrapper({ children }: { children: React.ReactNode }) {
|
||||||
|
return React.createElement(QueryClientProvider, { client: queryClient }, children);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('useForgotPassword', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize with default state', () => {
|
||||||
|
const { result } = renderHook(() => useForgotPassword(), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current.isPending).toBe(false);
|
||||||
|
expect(result.current.isError).toBe(false);
|
||||||
|
expect(result.current.isSuccess).toBe(false);
|
||||||
|
expect(result.current.error).toBe(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send forgot password email successfully and log success', async () => {
|
||||||
|
const testEmail = 'test@example.com';
|
||||||
|
const mockResponse = {
|
||||||
|
message: 'Password reset email sent successfully',
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockForgotPassword.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useForgotPassword(), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
result.current.mutate(testEmail);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.isSuccess).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
|
||||||
|
expect(result.current.data).toEqual(mockResponse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle 403 forbidden error and log as info', async () => {
|
||||||
|
const testEmail = 'blocked@example.com';
|
||||||
|
const mockError = {
|
||||||
|
response: {
|
||||||
|
status: 403,
|
||||||
|
data: {
|
||||||
|
detail: 'Too many password reset attempts',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
message: 'Forbidden',
|
||||||
|
};
|
||||||
|
|
||||||
|
mockForgotPassword.mockRejectedValueOnce(mockError);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useForgotPassword(), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
result.current.mutate(testEmail);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.isError).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
|
||||||
|
expect(mockLogInfo).toHaveBeenCalledWith(mockError);
|
||||||
|
expect(mockLogError).not.toHaveBeenCalled();
|
||||||
|
expect(result.current.error).toEqual(mockError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle network errors without response and log as error', async () => {
|
||||||
|
const testEmail = 'test@example.com';
|
||||||
|
const networkError = new Error('Network Error');
|
||||||
|
networkError.name = 'NetworkError';
|
||||||
|
|
||||||
|
mockForgotPassword.mockRejectedValueOnce(networkError);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useForgotPassword(), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
result.current.mutate(testEmail);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.isError).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
|
||||||
|
expect(mockLogError).toHaveBeenCalledWith(networkError);
|
||||||
|
expect(mockLogInfo).not.toHaveBeenCalled();
|
||||||
|
expect(result.current.error).toEqual(networkError);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty email address', async () => {
|
||||||
|
const testEmail = '';
|
||||||
|
const mockResponse = {
|
||||||
|
message: 'Email sent',
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockForgotPassword.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useForgotPassword(), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
result.current.mutate(testEmail);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.isSuccess).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockForgotPassword).toHaveBeenCalledWith('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle email with special characters', async () => {
|
||||||
|
const testEmail = 'user+test@example-domain.co.uk';
|
||||||
|
const mockResponse = {
|
||||||
|
message: 'Password reset email sent',
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
mockForgotPassword.mockResolvedValueOnce(mockResponse);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useForgotPassword(), {
|
||||||
|
wrapper: createWrapper(),
|
||||||
|
});
|
||||||
|
|
||||||
|
result.current.mutate(testEmail);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(result.current.isSuccess).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
|
||||||
|
expect(result.current.data).toEqual(mockResponse);
|
||||||
|
});
|
||||||
|
});
|
||||||
47
src/forgot-password/data/apiHook.ts
Normal file
47
src/forgot-password/data/apiHook.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { logError, logInfo } from '@openedx/frontend-base';
|
||||||
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { forgotPassword } from './api';
|
||||||
|
|
||||||
|
interface ForgotPasswordResult {
|
||||||
|
success: boolean,
|
||||||
|
message?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseForgotPasswordOptions {
|
||||||
|
onSuccess?: (data: ForgotPasswordResult, email: string) => void,
|
||||||
|
onError?: (error: Error) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApiError extends Error {
|
||||||
|
response?: {
|
||||||
|
status: number,
|
||||||
|
data: Record<string, unknown>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const useForgotPassword = (options: UseForgotPasswordOptions = {}) => useMutation({
|
||||||
|
mutationFn: (email: string) => (
|
||||||
|
forgotPassword(email)
|
||||||
|
),
|
||||||
|
onSuccess: (data: ForgotPasswordResult, email: string) => {
|
||||||
|
if (options.onSuccess) {
|
||||||
|
options.onSuccess(data, email);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error: ApiError) => {
|
||||||
|
// Handle different error types like the saga did
|
||||||
|
if (error.response?.status === 403) {
|
||||||
|
logInfo(error);
|
||||||
|
} else {
|
||||||
|
logError(error);
|
||||||
|
}
|
||||||
|
if (options.onError) {
|
||||||
|
options.onError(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export {
|
||||||
|
useForgotPassword,
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user