Compare commits
13 Commits
frontend-b
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e0be4e581 | ||
|
|
8b2160388f | ||
|
|
ab357704f8 | ||
|
|
2eab85e960 | ||
|
|
9f503eb5ef | ||
|
|
c2a55e125c | ||
|
|
abea379eb0 | ||
|
|
48122ff99d | ||
|
|
3153cff4ff | ||
|
|
919d98df79 | ||
|
|
c7adcecb8a | ||
|
|
db4eeac266 | ||
|
|
60e2119116 |
41
.env
Normal file
41
.env
Normal file
@@ -0,0 +1,41 @@
|
||||
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_DYNAMIC_REGISTRATION_FIELDS=''
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
|
||||
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS=''
|
||||
# ***** 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=''
|
||||
@@ -19,9 +19,6 @@ REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
INFO_EMAIL='info@example.com'
|
||||
# ***** Features *****
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS='true'
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
|
||||
# ***** Cookies *****
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
|
||||
4
.env.private.example
Normal file
4
.env.private.example
Normal file
@@ -0,0 +1,4 @@
|
||||
# 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
Normal file
20
.env.test
Normal file
@@ -0,0 +1,20 @@
|
||||
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=''
|
||||
6
.eslintignore
Executable file
6
.eslintignore
Executable file
@@ -0,0 +1,6 @@
|
||||
coverage/*
|
||||
dist/
|
||||
docs
|
||||
node_modules/
|
||||
__mocks__/
|
||||
__snapshots__/
|
||||
52
.eslintrc.js
Normal file
52
.eslintrc.js
Normal file
@@ -0,0 +1,52 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@edx/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
|
||||
// @edx/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',
|
||||
},
|
||||
});
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
* @openedx/2U-infinity
|
||||
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
@@ -1,7 +0,0 @@
|
||||
version: 2
|
||||
updates:
|
||||
# Adding new check for github-actions
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -25,5 +25,5 @@ Include a link to the sandbox for design changes or screenshot for before and af
|
||||
|
||||
#### Post-merge Checklist
|
||||
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-infinity** to do it.
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/vanguards** to do it.
|
||||
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
jobs:
|
||||
autoupdate:
|
||||
name: autoupdate
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: docker://chinthakagodawita/autoupdate-action:v1
|
||||
env:
|
||||
|
||||
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@@ -10,15 +10,17 @@ on:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
@@ -39,7 +41,4 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run Code Coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
|
||||
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -1,15 +1,20 @@
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
.idea
|
||||
node_modules
|
||||
npm-debug.log
|
||||
coverage
|
||||
module.config.js
|
||||
.env.private
|
||||
|
||||
dist/
|
||||
/*.tgz
|
||||
|
||||
### i18n ###
|
||||
src/i18n/transifex_input.json
|
||||
temp/babel-plugin-react-intl
|
||||
|
||||
### Editors ###
|
||||
.DS_Store
|
||||
### pyenv ###
|
||||
.python-version
|
||||
|
||||
### Emacs ###
|
||||
*~
|
||||
/temp
|
||||
/.vscode
|
||||
|
||||
15
.npmignore
15
.npmignore
@@ -1,6 +1,11 @@
|
||||
__mocks__
|
||||
.eslintignore
|
||||
.eslintrc.json
|
||||
.gitignore
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
Makefile
|
||||
npm-debug.log
|
||||
|
||||
coverage
|
||||
node_modules
|
||||
*.test.js
|
||||
*.test.jsx
|
||||
*.test.ts
|
||||
*.test.tsx
|
||||
public
|
||||
|
||||
9
.tx/config
Normal file
9
.tx/config
Normal file
@@ -0,0 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-authn]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# The following users are the owners of all frontend-app-authn files
|
||||
* @openedx/2u-infinity
|
||||
* @openedx/vanguards
|
||||
|
||||
43
Makefile
43
Makefile
@@ -1,30 +1,20 @@
|
||||
export TRANSIFEX_RESOURCE = frontend-app-authn
|
||||
transifex_langs = "ar,de,de_DE,es_419,fa_IR,fr,fr_CA,hi,it,it_IT,pt,pt_PT,ru,uk,zh_CN"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
|
||||
precommit:
|
||||
npm run lint
|
||||
npm audit
|
||||
|
||||
requirements:
|
||||
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 {} +
|
||||
npm install
|
||||
|
||||
i18n.extract:
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
@@ -42,16 +32,33 @@ detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/frontend-app-authn/src/i18n/messages:frontend-app-authn
|
||||
|
||||
$(intl_imports) paragon frontend-platform frontend-app-authn
|
||||
$(intl_imports) paragon frontend-app-authn
|
||||
endif
|
||||
|
||||
# This target is used by Travis.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
102
README.rst
102
README.rst
@@ -1,12 +1,12 @@
|
||||
##################
|
||||
frontend-app-authn
|
||||
##################
|
||||
|
||||
|Build Status| |ci-badge| |Codecov| |semantic-release|
|
||||
|
||||
********
|
||||
Purpose
|
||||
********
|
||||
frontend-app-authn
|
||||
====================
|
||||
|
||||
Please tag **@openedx/vanguards** on any PRs or issues. Thanks!
|
||||
|
||||
Introduction
|
||||
------------
|
||||
|
||||
This is a micro-frontend application responsible for the login, registration and password reset functionality.
|
||||
|
||||
@@ -22,22 +22,33 @@ This is a micro-frontend application responsible for the login, registration and
|
||||
|
||||
- Progressive profiling page
|
||||
|
||||
***************
|
||||
Getting Started
|
||||
***************
|
||||
|
||||
Installation
|
||||
============
|
||||
------------
|
||||
|
||||
`Tutor`_ is currently recommended as a development environment for your new MFE. Please refer to the `relevant tutor-mfe documentation`_ to get started using it.
|
||||
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
|
||||
|
||||
.. _Tutor: https://github.com/overhangio/tutor
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
|
||||
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
|
||||
=================================
|
||||
---------------------------------
|
||||
|
||||
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
|
||||
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
|
||||
The authentication micro-frontend also requires the following additional variable:
|
||||
|
||||
@@ -101,13 +112,9 @@ The authentication micro-frontend also requires the following additional variabl
|
||||
- Name of MFE, this will be used by the API to get runtime configurations for the specific micro frontend. For a frontend repo `frontend-app-appName`, use `appName` as APP_ID.
|
||||
- ``authn`` | ``''``
|
||||
|
||||
* - ``ENABLE_IMAGE_LAYOUT``
|
||||
- Enables the image layout feature within the authn. When set to True, this feature allows the inclusion of images in the base container layout. For more details on configuring this feature, please refer to the `Modifying base container <docs/how_tos/modifying_base_container.rst>`_.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
|
||||
edX-specific Environment Variables
|
||||
==================================
|
||||
**********************************
|
||||
|
||||
Furthermore, there are several edX-specific environment variables that enable integrations with closed-source services private to the edX organization, and might be unsupported in Open edX.
|
||||
|
||||
@@ -123,14 +130,17 @@ 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.
|
||||
- ``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
|
||||
edX <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.
|
||||
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
|
||||
How To Contribute
|
||||
=================
|
||||
|
||||
------------
|
||||
Contributions are very welcome, and strongly encouraged! We've
|
||||
put together `some documentation that describes our contribution process <https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html>`_.
|
||||
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
|
||||
|
||||
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.
|
||||
|
||||
@@ -139,58 +149,34 @@ can find it it at `PULL_REQUEST_TEMPLATE.md <https://github.com/openedx/frontend
|
||||
|
||||
This project is currently accepting all types of contributions, bug fixes and security fixes.
|
||||
|
||||
Getting Help
|
||||
============
|
||||
|
||||
If you're having trouble, we have discussion forums at
|
||||
https://discuss.openedx.org where you can connect with others in the community.
|
||||
|
||||
Our real-time conversations are on Slack. You can request a `Slack
|
||||
invitation`_, then join our `community Slack workspace`_. Because this is a
|
||||
frontend repository, the best place to discuss it would be in the `#wg-frontend
|
||||
channel`_.
|
||||
|
||||
For anything non-trivial, the best path is to open an issue in this repository
|
||||
with as many details about the issue you are facing as you can provide.
|
||||
|
||||
https://github.com/openedx/frontend-app-authn/issues
|
||||
|
||||
For more information about these options, see the `Getting Help`_ page.
|
||||
|
||||
.. _Slack invitation: https://openedx.org/slack
|
||||
.. _community Slack workspace: https://openedx.slack.com/
|
||||
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
|
||||
.. _Getting Help: https://openedx.org/community/connect
|
||||
|
||||
The Open edX Code of Conduct
|
||||
============================
|
||||
Open edX Code of Conduct
|
||||
------------------------
|
||||
All community members are expected to follow the `Open edX Code of Conduct <https://openedx.org/code-of-conduct/>`_.
|
||||
|
||||
People
|
||||
======
|
||||
------
|
||||
The assigned maintainers for this component and other project details may be
|
||||
found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-infinity>`_. Backstage pulls this data from the ``catalog-info.yaml``
|
||||
found in `Backstage <https://backstage.openedx.org/catalog/default/group/vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml``
|
||||
file in this repo.
|
||||
|
||||
Reporting Security Issues
|
||||
=========================
|
||||
-------------------------
|
||||
|
||||
Please do not report security issues in public. Please email security@openedx.org.
|
||||
Please do not report security issues in public. Please email security@edx.org.
|
||||
|
||||
Known Issues
|
||||
============
|
||||
------------
|
||||
|
||||
None
|
||||
|
||||
License
|
||||
=======
|
||||
-------
|
||||
|
||||
The code in this repository is licensed under the GNU Affero General Public License v3.0, unless
|
||||
otherwise noted.
|
||||
|
||||
Please see `LICENSE <https://github.com/openedx/frontend-app-authn/blob/master/LICENSE>`_ for details.
|
||||
|
||||
|
||||
==============================
|
||||
|
||||
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-authn.svg?branch=master
|
||||
@@ -201,4 +187,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
|
||||
: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
|
||||
:target: https://github.com/semantic-release/semantic-release
|
||||
:target: https://github.com/semantic-release/semantic-release
|
||||
5
app.d.ts
vendored
5
app.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
/// <reference types="@openedx/frontend-base" />
|
||||
|
||||
declare module 'site.config' {
|
||||
export default SiteConfig;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
const { createConfig } = require('@openedx/frontend-base/tools');
|
||||
|
||||
module.exports = createConfig('babel');
|
||||
@@ -12,8 +12,7 @@ metadata:
|
||||
icon: 'Article'
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: group:2u-infinity
|
||||
owner: group:vanguards
|
||||
type: 'service'
|
||||
lifecycle: 'production'
|
||||
|
||||
@@ -3,7 +3,7 @@ Enable Social Auth Locally
|
||||
|
||||
Please follow the steps below to enable social auth (SSO) locally.
|
||||
|
||||
1. Follow `Enabling Third Party Authentication <https://docs.openedx.org/en/latest/site_ops/install_configure_run_guide/configuration/tpa/index.html>`_ for backend configuration.
|
||||
1. Follow `Enabling Third Party Authentication <https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/tpa/index.html>`_ for backend configuration.
|
||||
|
||||
2. Authn has a component for rendering Social Auth providers at frontend which goes through each provider.
|
||||
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
========================================
|
||||
Modifying the Base Container in Authn
|
||||
========================================
|
||||
|
||||
The base container in Authn serves as the fundamental layout structure for rendering different components based on configurations. This document outlines the process for modifying the base container to accommodate changes or customize layouts as needed.
|
||||
|
||||
Understanding Base Container Versions
|
||||
--------------------------------------
|
||||
|
||||
The base container supports two main versions:
|
||||
|
||||
- **Default Layout:** The default layout is the standard layout used when specific configurations do not dictate otherwise.
|
||||
.. image:: ../images/default_layout.png
|
||||
- **Image Layout:** The image layout is an alternative layout option that can be enabled based on configurations.
|
||||
.. image:: ../images/image_layout.png
|
||||
|
||||
Enabling the Image Layout
|
||||
---------------------------
|
||||
|
||||
To activate the image layout feature, navigate to your .env file and update the configurations:
|
||||
|
||||
**Update Configuration**
|
||||
|
||||
Locate the ``ENABLE_IMAGE_LAYOUT`` parameter and set its value to ``true``. Additionally, ensure that the Image configuration settings are provided. Your overall configurations should resemble the following:
|
||||
|
||||
|
||||
.. code-block::
|
||||
|
||||
# ***** Image Layout Configuration *****
|
||||
ENABLE_IMAGE_LAYOUT = True # Set to True to enable image layout feature
|
||||
|
||||
# ***** Base Container Images *****
|
||||
BANNER_IMAGE_LARGE='' # Path to the large banner image
|
||||
BANNER_IMAGE_MEDIUM='' # Path to the medium-sized banner image
|
||||
BANNER_IMAGE_SMALL='' # Path to the small banner image
|
||||
BANNER_IMAGE_EXTRA_SMALL='' # Path to the extra-small banner image
|
||||
|
||||
|
||||
This allows for the customization and adaptation of the base container layout according to specific requirements.
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 252 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.4 MiB |
@@ -1,22 +0,0 @@
|
||||
// @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,15 +1,14 @@
|
||||
const { createConfig } = require('@openedx/frontend-base/tools');
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('test', {
|
||||
setupFilesAfterEnv: [
|
||||
module.exports = createConfig('jest', {
|
||||
setupFiles: [
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/setupTest.js',
|
||||
'src/i18n',
|
||||
'src/index.jsx',
|
||||
'MainApp.jsx',
|
||||
],
|
||||
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',
|
||||
},
|
||||
testEnvironment: 'jsdom',
|
||||
});
|
||||
|
||||
8
openedx.yaml
Normal file
8
openedx.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
|
||||
|
||||
nick: Authn MFE
|
||||
oeps: {}
|
||||
owner: openedx/vanguards
|
||||
openedx-release:
|
||||
ref: master
|
||||
31588
package-lock.json
generated
31588
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
124
package.json
124
package.json
@@ -1,40 +1,28 @@
|
||||
{
|
||||
"name": "@openedx/frontend-app-authn",
|
||||
"version": "1.0.0-alpha.6",
|
||||
"description": "Frontend authentication",
|
||||
"engines": {
|
||||
"node": "^24.12"
|
||||
},
|
||||
"name": "@edx/frontend-app-authn",
|
||||
"version": "0.1.0",
|
||||
"description": "Frontend application template",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/openedx/frontend-app-authn.git"
|
||||
},
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./app.scss": "./dist/app.scss"
|
||||
},
|
||||
"files": [
|
||||
"/dist"
|
||||
],
|
||||
"browserslist": [
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"sideEffects": [
|
||||
"*.css",
|
||||
"*.scss"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "make build",
|
||||
"clean": "make clean",
|
||||
"dev": "PORT=1999 PUBLIC_PATH=/authn openedx dev",
|
||||
"i18n_extract": "openedx formatjs extract",
|
||||
"lint": "openedx lint .",
|
||||
"lint:fix": "openedx lint --fix .",
|
||||
"prepack": "npm run build",
|
||||
"snapshot": "openedx test --updateSnapshot",
|
||||
"test": "openedx test --coverage --passWithNoTests"
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
||||
},
|
||||
"author": "Open edX",
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "npm run lint"
|
||||
}
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
|
||||
"publishConfig": {
|
||||
@@ -44,41 +32,55 @@
|
||||
"url": "https://github.com/openedx/frontend-app-authn/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.2",
|
||||
"classnames": "^2.5.1",
|
||||
"fastest-levenshtein": "^1.0.16",
|
||||
"form-urlencoded": "^6.1.5",
|
||||
"i18n-iso-countries": "^7.13.0",
|
||||
"prop-types": "^15.8.1",
|
||||
"query-string": "^7.1.3",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-loading-skeleton": "^3.5.0",
|
||||
"react-responsive": "^8.2.0",
|
||||
"universal-cookie": "^8.0.1"
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||
"@edx/frontend-platform": "^5.5.4",
|
||||
"@edx/paragon": "20.46.2",
|
||||
"@fortawesome/fontawesome-svg-core": "6.4.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.4.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.4.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@optimizely/react-sdk": "^2.9.1",
|
||||
"@redux-devtools/extension": "3.2.5",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"algoliasearch": "^4.14.3",
|
||||
"algoliasearch-helper": "^3.14.0",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.32.0",
|
||||
"fastest-levenshtein": "1.0.16",
|
||||
"form-urlencoded": "6.1.0",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-loading-skeleton": "3.3.1",
|
||||
"react-redux": "7.2.9",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-router": "6.15.0",
|
||||
"react-router-dom": "6.15.0",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.2.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"redux-saga": "1.2.3",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.14.0",
|
||||
"reselect": "4.1.8",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.5.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"babel-plugin-formatjs": "10.5.38",
|
||||
"eslint-plugin-import": "2.31.0",
|
||||
"jest": "^29.7.0",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"ts-jest": "^29.4.0",
|
||||
"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"
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "12.9.8",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
|
||||
"babel-plugin-formatjs": "10.5.3",
|
||||
"enzyme": "3.11.0",
|
||||
"eslint-plugin-import": "2.28.0",
|
||||
"glob": "7.2.3",
|
||||
"history": "5.3.0",
|
||||
"husky": "7.0.4",
|
||||
"jest": "29.6.2",
|
||||
"react-test-renderer": "^17.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>Authentication Development Site></title>
|
||||
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.6/iframeResizer.contentWindow.min.js"
|
||||
integrity="sha512-R7Piufj0/o6jG9ZKrAvS2dblFr2kkuG4XVQwStX+/4P+KwOLUXn2DXy0l1AJDxxqGhkM/FJllZHG2PKOAheYzg=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer">
|
||||
</script>
|
||||
<% if (process.env.OPTIMIZELY_URL) { %>
|
||||
<script
|
||||
src="<%= process.env.OPTIMIZELY_URL %>"
|
||||
></script>
|
||||
<% } else if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script
|
||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
||||
></script>
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
@@ -1,52 +0,0 @@
|
||||
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
19
src/Main.tsx
@@ -1,19 +0,0 @@
|
||||
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;
|
||||
63
src/MainApp.jsx
Executable file
63
src/MainApp.jsx
Executable file
@@ -0,0 +1,63 @@
|
||||
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 +0,0 @@
|
||||
module.exports = 'FileMock';
|
||||
@@ -1 +0,0 @@
|
||||
module.exports = 'SvgURL';
|
||||
@@ -1,2 +0,0 @@
|
||||
@use "@openedx/frontend-base/shell/app.scss";
|
||||
@use "./sass/style";
|
||||
43
src/app.ts
43
src/app.ts
@@ -1,43 +0,0 @@
|
||||
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,48 +1,50 @@
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index';
|
||||
|
||||
describe('Default Layout tests', () => {
|
||||
it('should display the form passed as a child in SmallScreenLayout', () => {
|
||||
render(
|
||||
const smallScreen = mount(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<DefaultSmallLayout />
|
||||
<form aria-label="form">
|
||||
<form>
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByRole('form')).toBeDefined();
|
||||
expect(smallScreen.find('form').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should display the form passed as a child in MediumScreenLayout', () => {
|
||||
render(
|
||||
const mediumScreen = mount(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<DefaultMediumLayout />
|
||||
<form aria-label="form">
|
||||
<form>
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByRole('form')).toBeDefined();
|
||||
expect(mediumScreen.find('form').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should display the form passed as a child in LargeScreenLayout', () => {
|
||||
render(
|
||||
const largeScreen = mount(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<DefaultLargeLayout />
|
||||
<form aria-label="form">
|
||||
<form>
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByRole('form')).toBeDefined();
|
||||
expect(largeScreen.find('form').exists()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
@@ -10,20 +13,20 @@ const LargeLayout = () => {
|
||||
return (
|
||||
<div className="w-50 d-flex">
|
||||
<div className="col-md-9 bg-primary-400">
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo position-absolute" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo position-absolute" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="min-vh-100 d-flex align-items-center">
|
||||
<div className={classNames({ 'large-yellow-line mr-n4.5': getSiteConfig().siteName === 'edX' })} />
|
||||
<div className={classNames({ 'large-yellow-line mr-n4.5': getConfig().SITE_NAME === 'edX' })} />
|
||||
<h1
|
||||
className={classNames(
|
||||
'display-2 text-white mw-xs',
|
||||
{ 'ml-6': getSiteConfig().siteName !== 'edX' },
|
||||
{ 'ml-6': getConfig().SITE_NAME !== 'edX' },
|
||||
)}
|
||||
>
|
||||
{formatMessage(messages['start.learning'])}
|
||||
<div className="text-accent-a">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getSiteConfig().siteName })}
|
||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
@@ -12,22 +15,22 @@ const MediumLayout = () => {
|
||||
<div className="w-100 medium-screen-top-stripe" />
|
||||
<div className="w-100 p-0 mb-3 d-flex">
|
||||
<div className="col-md-10 bg-primary-400">
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getSiteConfig().siteName} className="logo" src={useAppConfig().LOGO_WHITE_URL} />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex align-items-center justify-content-center mb-4 ">
|
||||
<div className={classNames({ 'mt-1 medium-yellow-line': getSiteConfig().siteName === 'edX' })} />
|
||||
<div className={classNames({ 'mt-1 medium-yellow-line': getConfig().SITE_NAME === 'edX' })} />
|
||||
<div>
|
||||
<h1
|
||||
className={classNames(
|
||||
'display-1 text-white mt-5 mb-5 mr-2 main-heading',
|
||||
{ 'ml-4.5': getSiteConfig().siteName !== 'edX' },
|
||||
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{formatMessage(messages['start.learning'])}{' '}
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getSiteConfig().siteName })}
|
||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
@@ -11,11 +14,11 @@ const SmallLayout = () => {
|
||||
<span className="bg-primary-400 w-100">
|
||||
<div className="col-md-12 small-screen-top-stripe" />
|
||||
<div>
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo-small" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex align-items-center m-3.5">
|
||||
<div className={classNames({ 'small-yellow-line mr-n2.5': getSiteConfig().siteName === 'edX' })} />
|
||||
<div className={classNames({ 'small-yellow-line mr-n2.5': getConfig().SITE_NAME === 'edX' })} />
|
||||
<h1
|
||||
className={classNames(
|
||||
'text-white mt-3.5 mb-3.5',
|
||||
@@ -24,7 +27,7 @@ const SmallLayout = () => {
|
||||
<span>
|
||||
{formatMessage(messages['start.learning'])}{' '}
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getSiteConfig().siteName })}
|
||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'start.learning': {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -9,10 +12,10 @@ const ExtraSmallLayout = () => {
|
||||
return (
|
||||
<span
|
||||
className="w-100 bg-primary-500 banner__image extra-small-layout"
|
||||
style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_EXTRA_SMALL})` }}
|
||||
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_EXTRA_SMALL})` }}
|
||||
>
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="company-logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="company-logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="ml-4.5 mr-1 pb-3.5 pt-3.5">
|
||||
<h1 className="banner__heading">
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
|
||||
import './index.scss';
|
||||
import messages from './messages';
|
||||
@@ -10,10 +13,10 @@ const LargeLayout = () => {
|
||||
return (
|
||||
<div
|
||||
className="w-50 bg-primary-500 banner__image large-layout"
|
||||
style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_LARGE})` }}
|
||||
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_LARGE})` }}
|
||||
>
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="company-logo position-absolute" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="company-logo position-absolute" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<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">
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
|
||||
import './index.scss';
|
||||
import messages from './messages';
|
||||
@@ -10,10 +13,10 @@ const MediumLayout = () => {
|
||||
return (
|
||||
<div
|
||||
className="w-100 mb-3 bg-primary-500 banner__image medium-layout"
|
||||
style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_MEDIUM})` }}
|
||||
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_MEDIUM})` }}
|
||||
>
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="company-logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="company-logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="ml-5 pb-4 pt-4">
|
||||
<h1 className="display-2 banner__heading">
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -9,10 +12,10 @@ const SmallLayout = () => {
|
||||
return (
|
||||
<span
|
||||
className="w-100 bg-primary-500 banner__image small-layout"
|
||||
style={{ backgroundImage: `url(${useAppConfig().BANNER_IMAGE_SMALL})` }}
|
||||
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_SMALL})` }}
|
||||
>
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="company-logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_WHITE_URL} />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="company-logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
|
||||
</Hyperlink>
|
||||
<div className="ml-5 mr-1 pb-3.5 pt-3.5">
|
||||
<h1 className="display-2">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'your.career.turning.point': {
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const LargeLayout = ({ fullName }) => {
|
||||
const LargeLayout = ({ username }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="w-50 d-flex">
|
||||
<div className="col-md-10 bg-light-200 p-0">
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo position-absolute" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_URL} />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo position-absolute" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
||||
</Hyperlink>
|
||||
<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>
|
||||
<h1 className="welcome-to-platform data-hj-suppress">
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getSiteConfig().siteName, fullName })}
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||
</h1>
|
||||
<h2 className="complete-your-profile">
|
||||
{formatMessage(messages['complete.your.profile.1'])}
|
||||
@@ -40,7 +43,7 @@ const LargeLayout = ({ fullName }) => {
|
||||
};
|
||||
|
||||
LargeLayout.propTypes = {
|
||||
fullName: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default LargeLayout;
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const MediumLayout = ({ fullName }) => {
|
||||
const MediumLayout = ({ username }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
@@ -12,14 +15,14 @@ const MediumLayout = ({ fullName }) => {
|
||||
<div className="w-100 medium-screen-top-stripe" />
|
||||
<div className="w-100 p-0 mb-3 d-flex">
|
||||
<div className="col-md-10 bg-light-200">
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_URL} />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex align-items-center justify-content-center mb-4 ml-5">
|
||||
<div className="medium-yellow-line mt-5 mr-n2" />
|
||||
<div>
|
||||
<h1 className="h3 data-hj-suppress mw-320">
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getSiteConfig().siteName, fullName })}
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||
</h1>
|
||||
<h2 className="display-1">
|
||||
{formatMessage(messages['complete.your.profile.1'])}
|
||||
@@ -43,7 +46,7 @@ const MediumLayout = ({ fullName }) => {
|
||||
};
|
||||
|
||||
MediumLayout.propTypes = {
|
||||
fullName: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default MediumLayout;
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SmallLayout = ({ fullName }) => {
|
||||
const SmallLayout = ({ username }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="min-vw-100 bg-light-200">
|
||||
<div className="col-md-12 small-screen-top-stripe" />
|
||||
<Hyperlink destination={useAppConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo-small" alt={getSiteConfig().siteName} src={useAppConfig().LOGO_URL} />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
||||
</Hyperlink>
|
||||
<div className="d-flex align-items-center m-3.5">
|
||||
<div className="small-yellow-line mt-4.5" />
|
||||
<div>
|
||||
<h1 className="h5 data-hj-suppress">
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getSiteConfig().siteName, fullName })}
|
||||
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||
</h1>
|
||||
<h2 className="h1">
|
||||
{formatMessage(messages['complete.your.profile.1'])}
|
||||
@@ -32,7 +35,7 @@ const SmallLayout = ({ fullName }) => {
|
||||
};
|
||||
|
||||
SmallLayout.propTypes = {
|
||||
fullName: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default SmallLayout;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'welcome.to.platform': {
|
||||
id: 'welcome.to.platform',
|
||||
defaultMessage: 'Welcome to {siteName}, {fullName}!',
|
||||
defaultMessage: 'Welcome to {siteName}, {username}!',
|
||||
description: 'Welcome message that appears on progressive profile page',
|
||||
},
|
||||
'complete.your.profile.1': {
|
||||
|
||||
4
src/base-container/data/constants.js
Normal file
4
src/base-container/data/constants.js
Normal file
@@ -0,0 +1,4 @@
|
||||
const IMAGE_LAYOUT = 'image-layout';
|
||||
const DEFAULT_LAYOUT = 'default-layout';
|
||||
|
||||
export { DEFAULT_LAYOUT, IMAGE_LAYOUT };
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useAppConfig } from '@openedx/frontend-base';
|
||||
import { breakpoints } from '@openedx/paragon';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { breakpoints } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import MediaQuery from 'react-responsive';
|
||||
@@ -9,24 +10,40 @@ import {
|
||||
ImageExtraSmallLayout, ImageLargeLayout, ImageMediumLayout, ImageSmallLayout,
|
||||
} from './components/image-layout';
|
||||
import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout';
|
||||
import { DEFAULT_LAYOUT, IMAGE_LAYOUT } from './data/constants';
|
||||
|
||||
const BaseContainer = ({ children, showWelcomeBanner, fullName }) => {
|
||||
const enableImageLayout = useAppConfig().ENABLE_IMAGE_LAYOUT;
|
||||
const BaseContainer = ({ children, showWelcomeBanner, username }) => {
|
||||
const [baseContainerVersion, setBaseContainerVersion] = useState(DEFAULT_LAYOUT);
|
||||
|
||||
if (enableImageLayout) {
|
||||
useEffect(() => {
|
||||
const initRebrandExperiment = () => {
|
||||
if (window.experiments?.rebrandExperiment) {
|
||||
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
|
||||
} else {
|
||||
window.experiments = window.experiments || {};
|
||||
window.experiments.rebrandExperiment = {};
|
||||
window.experiments.rebrandExperiment.handleLoaded = () => {
|
||||
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
|
||||
};
|
||||
}
|
||||
};
|
||||
initRebrandExperiment();
|
||||
}, []);
|
||||
|
||||
if (baseContainerVersion === IMAGE_LAYOUT) {
|
||||
return (
|
||||
<div className="layout">
|
||||
<MediaQuery maxWidth={breakpoints.extraSmall.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <ImageExtraSmallLayout />}
|
||||
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <ImageExtraSmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.small.minWidth} maxWidth={breakpoints.small.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <ImageSmallLayout />}
|
||||
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <ImageSmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthMediumLayout fullName={fullName} /> : <ImageMediumLayout />}
|
||||
{showWelcomeBanner ? <AuthMediumLayout username={username} /> : <ImageMediumLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
|
||||
{showWelcomeBanner ? <AuthLargeLayout fullName={fullName} /> : <ImageLargeLayout />}
|
||||
{showWelcomeBanner ? <AuthLargeLayout username={username} /> : <ImageLargeLayout />}
|
||||
</MediaQuery>
|
||||
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
|
||||
{children}
|
||||
@@ -40,13 +57,13 @@ const BaseContainer = ({ children, showWelcomeBanner, fullName }) => {
|
||||
<div className="col-md-12 extra-large-screen-top-stripe" />
|
||||
<div className="layout">
|
||||
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <DefaultSmallLayout />}
|
||||
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <DefaultSmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthMediumLayout fullName={fullName} /> : <DefaultMediumLayout />}
|
||||
{showWelcomeBanner ? <AuthMediumLayout username={username} /> : <DefaultMediumLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
|
||||
{showWelcomeBanner ? <AuthLargeLayout fullName={fullName} /> : <DefaultLargeLayout />}
|
||||
{showWelcomeBanner ? <AuthLargeLayout username={username} /> : <DefaultLargeLayout />}
|
||||
</MediaQuery>
|
||||
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
|
||||
{children}
|
||||
@@ -58,13 +75,13 @@ const BaseContainer = ({ children, showWelcomeBanner, fullName }) => {
|
||||
|
||||
BaseContainer.defaultProps = {
|
||||
showWelcomeBanner: false,
|
||||
fullName: null,
|
||||
username: null,
|
||||
};
|
||||
|
||||
BaseContainer.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
showWelcomeBanner: PropTypes.bool,
|
||||
fullName: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
};
|
||||
|
||||
export default BaseContainer;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { IntlProvider, mergeAppConfig } from '@openedx/frontend-base';
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { Context as ResponsiveContext } from 'react-responsive';
|
||||
|
||||
import BaseContainer from '../index';
|
||||
import { appId } from '../../constants';
|
||||
|
||||
const LargeScreen = {
|
||||
wrappingComponent: ResponsiveContext.Provider,
|
||||
@@ -11,34 +12,32 @@ const LargeScreen = {
|
||||
};
|
||||
|
||||
describe('Base component tests', () => {
|
||||
it('should show default layout', () => {
|
||||
const { container } = render(
|
||||
it('should should default layout', () => {
|
||||
const baseContainer = mount(
|
||||
<IntlProvider locale="en">
|
||||
<BaseContainer>
|
||||
<div>Test Content</div>
|
||||
</BaseContainer>
|
||||
<BaseContainer />
|
||||
</IntlProvider>,
|
||||
LargeScreen,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.banner__image')).toBeNull();
|
||||
expect(container.querySelector('.large-screen-svg-primary')).toBeDefined();
|
||||
expect(baseContainer.find('.banner__image').exists()).toBeFalsy();
|
||||
expect(baseContainer.find('.large-screen-svg-primary').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders Image layout when ENABLE_IMAGE_LAYOUT configuration is enabled', () => {
|
||||
mergeAppConfig(appId, {
|
||||
ENABLE_IMAGE_LAYOUT: true,
|
||||
});
|
||||
it('[experiment] should show image layout for treatment group', () => {
|
||||
window.experiments = {
|
||||
rebrandExperiment: {
|
||||
variation: 'image-layout',
|
||||
},
|
||||
};
|
||||
|
||||
const { container } = render(
|
||||
const baseContainer = mount(
|
||||
<IntlProvider locale="en">
|
||||
<BaseContainer showWelcomeBanner={false}>
|
||||
<div>Test Content</div>
|
||||
</BaseContainer>
|
||||
<BaseContainer />
|
||||
</IntlProvider>,
|
||||
LargeScreen,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.banner__image')).toBeDefined();
|
||||
expect(baseContainer.find('.banner__image').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useAppConfig, getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Form,
|
||||
Icon,
|
||||
} from '@openedx/paragon';
|
||||
import { Login } from '@openedx/paragon/icons';
|
||||
} from '@edx/paragon';
|
||||
import { Login } from '@edx/paragon/icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
|
||||
/**
|
||||
* This component renders the Single sign-on (SSO) button only for the tpa provider passed
|
||||
@@ -16,12 +19,11 @@ import messages from './messages';
|
||||
const EnterpriseSSO = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const tpaProvider = props.provider;
|
||||
const hideRegistrationLink = useAppConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false
|
||||
|| useAppConfig().SHOW_REGISTRATION_LINKS === false;
|
||||
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
|
||||
|
||||
const handleSubmit = (e, url) => {
|
||||
e.preventDefault();
|
||||
window.location.href = getSiteConfig().lmsBaseUrl + url;
|
||||
window.location.href = getConfig().LMS_BASE_URL + url;
|
||||
};
|
||||
|
||||
const handleClick = (e) => {
|
||||
@@ -47,7 +49,7 @@ const EnterpriseSSO = (props) => {
|
||||
{tpaProvider.iconImage ? (
|
||||
<div aria-hidden="true">
|
||||
<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>
|
||||
)
|
||||
: (
|
||||
@@ -59,7 +61,7 @@ const EnterpriseSSO = (props) => {
|
||||
<Icon className="h-75" src={Login} />
|
||||
)}
|
||||
</div>
|
||||
<span className="pl-2" aria-hidden="true">{tpaProvider.name}</span>
|
||||
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
@@ -72,7 +74,7 @@ const EnterpriseSSO = (props) => {
|
||||
className="w-100"
|
||||
onClick={(e) => handleClick(e)}
|
||||
>
|
||||
{hideRegistrationLink
|
||||
{disablePublicAccountCreation
|
||||
? formatMessage(messages['enterprisetpa.login.button.text.public.account.creation.disabled'])
|
||||
: formatMessage(messages['enterprisetpa.login.button.text'])}
|
||||
</Button>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Form, TransitionReplace,
|
||||
} from '@openedx/paragon';
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FormGroup = (props) => {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Button, Hyperlink, Icon } from '@openedx/paragon';
|
||||
import { Institution } from '@openedx/paragon/icons';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Hyperlink, Icon } from '@edx/paragon';
|
||||
import { Institution } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
@@ -28,7 +31,7 @@ export const RenderInstitutionButton = props => {
|
||||
* This component renders the page list of available institutions for login
|
||||
* */
|
||||
const InstitutionLogistration = props => {
|
||||
const lmsBaseUrl = getSiteConfig().lmsBaseUrl;
|
||||
const lmsBaseUrl = getConfig().LMS_BASE_URL;
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
secondaryProviders,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { FormattedMessage } from '@openedx/frontend-base';
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const NotFoundPage = () => (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
import { useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@openedx/frontend-base';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
|
||||
} from '@openedx/paragon';
|
||||
} from '@edx/paragon';
|
||||
import {
|
||||
Check, Remove, Visibility, VisibilityOff,
|
||||
} from '@openedx/paragon/icons';
|
||||
} from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
|
||||
import { validatePasswordField } from '../register/data/utils';
|
||||
import messages from './messages';
|
||||
|
||||
const noopFn = () => {};
|
||||
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
|
||||
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../register/data/actions';
|
||||
import { validatePasswordField } from '../register/data/utils';
|
||||
|
||||
const PasswordField = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
|
||||
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const {
|
||||
validationApiRateLimited = false,
|
||||
clearRegistrationBackendError = noopFn,
|
||||
validateField = noopFn,
|
||||
} = props;
|
||||
|
||||
const handleBlur = (e) => {
|
||||
const { name, value } = e.target;
|
||||
if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
|
||||
@@ -53,7 +50,7 @@ const PasswordField = (props) => {
|
||||
if (fieldError) {
|
||||
props.handleErrorChange('password', fieldError);
|
||||
} else if (!validationApiRateLimited) {
|
||||
validateField({ password: passwordValue });
|
||||
dispatch(fetchRealtimeValidations({ password: passwordValue }));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -68,7 +65,7 @@ const PasswordField = (props) => {
|
||||
}
|
||||
if (props.handleErrorChange) {
|
||||
props.handleErrorChange('password', '');
|
||||
clearRegistrationBackendError('password');
|
||||
dispatch(clearRegistrationBackendError('password'));
|
||||
}
|
||||
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
|
||||
};
|
||||
@@ -141,7 +138,7 @@ const PasswordField = (props) => {
|
||||
{props.errorMessage !== '' && (
|
||||
<Form.Control.Feedback key="error" className="form-text-size" hasIcon={false} feedback-for={props.name} type="invalid">
|
||||
{props.errorMessage}
|
||||
{props.showScreenReaderText && <span className="sr-only">{formatMessage(messages['password.sr.only.helping.text'])}</span>}
|
||||
<span className="sr-only">{formatMessage(messages['password.sr.only.helping.text'])}</span>
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
@@ -156,11 +153,7 @@ PasswordField.defaultProps = {
|
||||
handleChange: () => {},
|
||||
handleErrorChange: null,
|
||||
showRequirements: true,
|
||||
showScreenReaderText: true,
|
||||
autoComplete: null,
|
||||
clearRegistrationBackendError: noopFn,
|
||||
validateField: noopFn,
|
||||
validationApiRateLimited: false,
|
||||
};
|
||||
|
||||
PasswordField.propTypes = {
|
||||
@@ -175,10 +168,6 @@ PasswordField.propTypes = {
|
||||
showRequirements: PropTypes.bool,
|
||||
value: PropTypes.string.isRequired,
|
||||
autoComplete: PropTypes.string,
|
||||
showScreenReaderText: PropTypes.bool,
|
||||
clearRegistrationBackendError: PropTypes.func,
|
||||
validateField: PropTypes.func,
|
||||
validationApiRateLimited: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default PasswordField;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useAppConfig, getSiteConfig } from '@openedx/frontend-base';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
AUTHN_PROGRESSIVE_PROFILING, REDIRECT,
|
||||
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
|
||||
} from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
|
||||
@@ -15,6 +15,7 @@ const RedirectLogistration = (props) => {
|
||||
redirectToProgressiveProfilingPage,
|
||||
success,
|
||||
optionalFields,
|
||||
redirectToRecommendationsPage,
|
||||
educationLevel,
|
||||
userId,
|
||||
registrationEmbedded,
|
||||
@@ -28,7 +29,7 @@ const RedirectLogistration = (props) => {
|
||||
// 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
|
||||
if (finishAuthUrl && !redirectUrl.includes(finishAuthUrl)) {
|
||||
finalRedirectUrl = getSiteConfig().lmsBaseUrl + finishAuthUrl;
|
||||
finalRedirectUrl = getConfig().LMS_BASE_URL + finishAuthUrl;
|
||||
} else {
|
||||
finalRedirectUrl = redirectUrl;
|
||||
}
|
||||
@@ -36,12 +37,12 @@ const RedirectLogistration = (props) => {
|
||||
// Redirect to Progressive Profiling after successful registration
|
||||
if (redirectToProgressiveProfilingPage) {
|
||||
// TODO: Do we still need this cookie?
|
||||
setCookie('van-504-returning-user', true, useAppConfig().SESSION_COOKIE_DOMAIN);
|
||||
setCookie('van-504-returning-user', true);
|
||||
|
||||
if (registrationEmbedded) {
|
||||
window.parent.postMessage({
|
||||
action: REDIRECT,
|
||||
redirectUrl: useAppConfig().POST_REGISTRATION_REDIRECT_URL,
|
||||
redirectUrl: getConfig().POST_REGISTRATION_REDIRECT_URL,
|
||||
}, host);
|
||||
return null;
|
||||
}
|
||||
@@ -59,6 +60,22 @@ 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;
|
||||
}
|
||||
|
||||
@@ -73,6 +90,7 @@ RedirectLogistration.defaultProps = {
|
||||
redirectUrl: '',
|
||||
redirectToProgressiveProfilingPage: false,
|
||||
optionalFields: {},
|
||||
redirectToRecommendationsPage: false,
|
||||
userId: null,
|
||||
registrationEmbedded: false,
|
||||
host: '',
|
||||
@@ -86,6 +104,7 @@ RedirectLogistration.propTypes = {
|
||||
redirectUrl: PropTypes.string,
|
||||
redirectToProgressiveProfilingPage: PropTypes.bool,
|
||||
optionalFields: PropTypes.shape({}),
|
||||
redirectToRecommendationsPage: PropTypes.bool,
|
||||
userId: PropTypes.number,
|
||||
registrationEmbedded: PropTypes.bool,
|
||||
host: PropTypes.string,
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon } from '@edx/paragon';
|
||||
import { Login } from '@edx/paragon/icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Icon } from '@openedx/paragon';
|
||||
import { Login } from '@openedx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
|
||||
const SocialAuthProviders = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
@@ -15,7 +18,7 @@ const SocialAuthProviders = (props) => {
|
||||
e.preventDefault();
|
||||
|
||||
const url = e.currentTarget.dataset.providerUrl;
|
||||
window.location.href = getSiteConfig().lmsBaseUrl + url;
|
||||
window.location.href = getConfig().LMS_BASE_URL + url;
|
||||
}
|
||||
|
||||
const socialAuth = socialAuthProviders.map((provider, index) => (
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { getSiteConfig, useIntl } from '@openedx/frontend-base';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
|
||||
const ThirdPartyAuthAlert = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { currentProvider, referrer } = props;
|
||||
const platformName = getSiteConfig().siteName;
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
let message;
|
||||
|
||||
if (referrer === LOGIN_PAGE) {
|
||||
@@ -27,7 +30,7 @@ const ThirdPartyAuthAlert = (props) => {
|
||||
{referrer === REGISTER_PAGE ? (
|
||||
<Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
) : null}
|
||||
<p>{message}</p>
|
||||
<p>{ message }</p>
|
||||
</Alert>
|
||||
{referrer === REGISTER_PAGE ? (
|
||||
<h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { fetchAuthenticatedUser, getAuthenticatedUser, getSiteConfig } from '@openedx/frontend-base';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
@@ -24,7 +25,7 @@ const UnAuthOnlyRoute = ({ children }) => {
|
||||
|
||||
if (isReady) {
|
||||
if (authUser && authUser.username) {
|
||||
global.location.href = getSiteConfig().lmsBaseUrl.concat(DEFAULT_REDIRECT_URL);
|
||||
global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
61
src/common-components/Zendesk.jsx
Normal file
61
src/common-components/Zendesk.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
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;
|
||||
@@ -1,60 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,133 +0,0 @@
|
||||
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;
|
||||
};
|
||||
27
src/common-components/data/actions.js
Normal file
27
src/common-components/data/actions.js
Normal file
@@ -0,0 +1,27 @@
|
||||
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,19 +0,0 @@
|
||||
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,
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { appId } from '../../constants';
|
||||
|
||||
export const ThirdPartyAuthQueryKeys = {
|
||||
all: [appId, 'ThirdPartyAuth'] as const,
|
||||
byPage: (pageId: string, payload?: unknown) => [appId, 'ThirdPartyAuth', pageId, payload] as const,
|
||||
};
|
||||
65
src/common-components/data/reducers.js
Normal file
65
src/common-components/data/reducers.js
Normal file
@@ -0,0 +1,65 @@
|
||||
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: {
|
||||
fields: {},
|
||||
},
|
||||
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;
|
||||
32
src/common-components/data/sagas.js
Normal file
32
src/common-components/data/sagas.js
Normal file
@@ -0,0 +1,32 @@
|
||||
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);
|
||||
}
|
||||
28
src/common-components/data/selectors.js
Normal file
28
src/common-components/data/selectors.js
Normal file
@@ -0,0 +1,28 @@
|
||||
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,6 +1,8 @@
|
||||
import { getAuthenticatedHttpClient, getSiteConfig } from '@openedx/frontend-base';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
const getThirdPartyAuthContext = async (urlParams: string) => {
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function getThirdPartyAuthContext(urlParams) {
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
params: urlParams,
|
||||
@@ -9,16 +11,15 @@ const getThirdPartyAuthContext = async (urlParams: string) => {
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.get(
|
||||
`${getSiteConfig().lmsBaseUrl}/api/mfe_context`,
|
||||
`${getConfig().LMS_BASE_URL}/api/mfe_context`,
|
||||
requestConfig,
|
||||
);
|
||||
)
|
||||
.catch((e) => {
|
||||
throw (e);
|
||||
});
|
||||
return {
|
||||
fieldDescriptions: data.registrationFields || {},
|
||||
optionalFields: data.optionalFields || {},
|
||||
thirdPartyAuthContext: data.contextData || {},
|
||||
};
|
||||
};
|
||||
|
||||
export {
|
||||
getThirdPartyAuthContext,
|
||||
};
|
||||
}
|
||||
82
src/common-components/data/tests/reducer.test.js
Normal file
82
src/common-components/data/tests/reducer.test.js
Normal file
@@ -0,0 +1,82 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
71
src/common-components/data/tests/sagas.test.js
Normal file
71
src/common-components/data/tests/sagas.test.js
Normal file
@@ -0,0 +1,71 @@
|
||||
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,5 +7,9 @@ export { default as SocialAuthProviders } from './SocialAuthProviders';
|
||||
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';
|
||||
export { default as InstitutionLogistration } 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 PasswordField } from './PasswordField';
|
||||
export { default as Zendesk } from './Zendesk';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineMessages } from '@openedx/frontend-base';
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
// institution login strings
|
||||
@@ -85,42 +85,32 @@ const messages = defineMessages({
|
||||
'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} '
|
||||
+ 'account does not have a linked {platformName} account. To link your accounts, '
|
||||
+ 'sign in now using your {platformName} password.',
|
||||
+ 'account does not have a linked {platformName} account. To link your accounts, '
|
||||
+ 'sign in now using your {platformName} password.',
|
||||
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': {
|
||||
id: 'register.third.party.auth.account.not.linked',
|
||||
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 '
|
||||
+ 'but no associated platform account exists',
|
||||
+ 'but no associated platform account exists',
|
||||
},
|
||||
'registration.using.tpa.form.heading': {
|
||||
id: 'registration.using.tpa.form.heading',
|
||||
defaultMessage: 'Finish creating your account',
|
||||
description: 'Heading that appears above form when user is trying to create account using social auth',
|
||||
},
|
||||
'registration.other.options.heading': {
|
||||
id: 'registration.other.options.heading',
|
||||
defaultMessage: 'Or register with:',
|
||||
description: 'A message that appears above third party auth providers i.e saml, google, facebook etc',
|
||||
supportTitle: {
|
||||
id: 'zendesk.supportTitle',
|
||||
description: 'Title for the support button',
|
||||
defaultMessage: 'edX Support',
|
||||
},
|
||||
'institution.login.button': {
|
||||
id: 'institution.login.button',
|
||||
defaultMessage: 'Institution/campus credentials',
|
||||
description: 'shows institutions list',
|
||||
},
|
||||
'login.other.options.heading': {
|
||||
id: 'login.other.options.heading',
|
||||
defaultMessage: 'Or sign in with:',
|
||||
description: 'Text that appears above other sign in options like social auth buttons',
|
||||
},
|
||||
'enterprise.login.btn.text': {
|
||||
id: 'enterprise.login.btn.text',
|
||||
defaultMessage: 'Company or school credentials',
|
||||
description: 'Company or school login link text.',
|
||||
selectTicketForm: {
|
||||
id: 'zendesk.selectTicketForm',
|
||||
description: 'Select ticket form',
|
||||
defaultMessage: 'Please choose your request type:',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
/* eslint-disable import/no-import-module-exports */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import React from 'react';
|
||||
|
||||
import { getSiteConfig } from '@openedx/frontend-base';
|
||||
import { render } from '@testing-library/react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { REGISTER_EMBEDDED_PAGE } from '../../data/constants';
|
||||
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
|
||||
|
||||
import {
|
||||
MemoryRouter, Route, BrowserRouter as Router, Routes,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { PAGE_NOT_FOUND, REGISTER_EMBEDDED_PAGE } from '../../data/constants';
|
||||
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
|
||||
|
||||
const RRD = require('react-router-dom');
|
||||
// Just render plain div with its children
|
||||
// eslint-disable-next-line react/prop-types
|
||||
RRD.BrowserRouter = ({ children }) => <div>{children}</div>;
|
||||
RRD.BrowserRouter = ({ children }) => <div>{ children }</div>;
|
||||
module.exports = RRD;
|
||||
|
||||
const TestApp = () => (
|
||||
@@ -25,10 +27,6 @@ const TestApp = () => (
|
||||
path={REGISTER_EMBEDDED_PAGE}
|
||||
element={<EmbeddedRegistrationRoute><span>Embedded Register Page</span></EmbeddedRegistrationRoute>}
|
||||
/>
|
||||
<Route
|
||||
path={PAGE_NOT_FOUND}
|
||||
element={<span>Page not found</span>}
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>
|
||||
@@ -47,30 +45,28 @@ describe('EmbeddedRegistrationRoute', () => {
|
||||
|
||||
it('should not render embedded register page if host query param is not available in the url', async () => {
|
||||
let embeddedRegistrationPage = null;
|
||||
|
||||
await act(async () => {
|
||||
const { container } = await render(routerWrapper());
|
||||
embeddedRegistrationPage = container;
|
||||
embeddedRegistrationPage = await mount(routerWrapper());
|
||||
});
|
||||
|
||||
const renderedPage = embeddedRegistrationPage.querySelector('span');
|
||||
expect(renderedPage.textContent).toBe('Page not found');
|
||||
expect(embeddedRegistrationPage.find('span').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should render embedded register page if host query param is available in the url (embedded)', async () => {
|
||||
delete window.location;
|
||||
window.location = {
|
||||
href: getSiteConfig().baseUrl.concat(REGISTER_EMBEDDED_PAGE),
|
||||
href: getConfig().BASE_URL.concat(REGISTER_EMBEDDED_PAGE),
|
||||
search: '?host=http://localhost/host-websit',
|
||||
};
|
||||
|
||||
let embeddedRegistrationPage = null;
|
||||
|
||||
await act(async () => {
|
||||
const { container } = await render(routerWrapper());
|
||||
embeddedRegistrationPage = container;
|
||||
embeddedRegistrationPage = await mount(routerWrapper());
|
||||
});
|
||||
|
||||
const renderedPage = embeddedRegistrationPage.querySelector('span');
|
||||
expect(renderedPage).toBeTruthy();
|
||||
expect(renderedPage.textContent).toBe('Embedded Register Page');
|
||||
expect(embeddedRegistrationPage.find('span').exists()).toBeTruthy();
|
||||
expect(embeddedRegistrationPage.find('span').text()).toBe('Embedded Register Page');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
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 PasswordField from '../PasswordField';
|
||||
|
||||
|
||||
describe('FormGroup', () => {
|
||||
const props = {
|
||||
floatingLabel: 'Email',
|
||||
@@ -16,30 +21,36 @@ describe('FormGroup', () => {
|
||||
};
|
||||
|
||||
it('should show help text on field focus', () => {
|
||||
const { queryByText, getByLabelText } = render(<FormGroup {...props} />);
|
||||
const emailInput = getByLabelText('Email');
|
||||
const formGroup = mount(<FormGroup {...props} />);
|
||||
expect(formGroup.find('.pgn-transition-replace-group').find('div#email-1').exists()).toBeFalsy();
|
||||
|
||||
expect(queryByText('Email field help text')).toBeNull();
|
||||
|
||||
fireEvent.focus(emailInput);
|
||||
|
||||
const helpText = queryByText('Email field help text');
|
||||
|
||||
expect(helpText).toBeTruthy();
|
||||
expect(helpText.textContent).toEqual('Email field help text');
|
||||
formGroup.find('input#email').simulate('focus');
|
||||
expect(formGroup.find('.pgn-transition-replace-group').find('div#email-1').text()).toEqual('Email field help text');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PasswordField', () => {
|
||||
const mockStore = configureStore();
|
||||
const IntlPasswordField = injectIntl(PasswordField);
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const wrapper = children => (
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
{children}
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</MemoryRouter>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const initialState = {
|
||||
register: {
|
||||
validationApiRateLimited: false,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(initialState);
|
||||
props = {
|
||||
floatingLabel: 'Password',
|
||||
name: 'password',
|
||||
@@ -49,29 +60,25 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should show/hide password on icon click', () => {
|
||||
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
const showPasswordButton = getByLabelText('Show password');
|
||||
fireEvent.click(showPasswordButton);
|
||||
expect(passwordInput.type).toBe('text');
|
||||
passwordField.find('button[aria-label="Show password"]').simulate('click');
|
||||
expect(passwordField.find('input').prop('type')).toEqual('text');
|
||||
|
||||
const hidePasswordButton = getByLabelText('Hide password');
|
||||
fireEvent.click(hidePasswordButton);
|
||||
expect(passwordInput.type).toBe('password');
|
||||
passwordField.find('button[aria-label="Hide password"]').simulate('click');
|
||||
expect(passwordField.find('input').prop('type')).toEqual('password');
|
||||
});
|
||||
|
||||
it('should show password requirement tooltip on focus', async () => {
|
||||
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
jest.useFakeTimers();
|
||||
await act(async () => {
|
||||
fireEvent.focus(passwordInput);
|
||||
passwordField.find('input').simulate('focus');
|
||||
jest.runAllTimers();
|
||||
});
|
||||
const passwordRequirementTooltip = document.querySelector('#password-requirement-left');
|
||||
passwordField.update();
|
||||
|
||||
expect(passwordRequirementTooltip).toBeTruthy();
|
||||
expect(passwordField.find('#password-requirement-left').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should show all password requirement checks as failed', async () => {
|
||||
@@ -79,66 +86,48 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
value: '',
|
||||
};
|
||||
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
|
||||
jest.useFakeTimers();
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
await act(async () => {
|
||||
fireEvent.focus(passwordInput);
|
||||
passwordField.find('input').simulate('focus');
|
||||
jest.runAllTimers();
|
||||
});
|
||||
passwordField.update();
|
||||
|
||||
const letterCheckIcon = document.querySelector('#letter-check span');
|
||||
const numberCheckIcon = document.querySelector('#number-check span');
|
||||
const charactersCheckIcon = document.querySelector('#characters-check span');
|
||||
|
||||
expect(letterCheckIcon).toBeTruthy();
|
||||
expect(letterCheckIcon.className).toContain('pgn__icon mr-1 text-light-700');
|
||||
|
||||
expect(numberCheckIcon).toBeTruthy();
|
||||
expect(numberCheckIcon.className).toContain('pgn__icon mr-1 text-light-700');
|
||||
|
||||
expect(charactersCheckIcon).toBeTruthy();
|
||||
expect(charactersCheckIcon.className).toContain('pgn__icon mr-1 text-light-700');
|
||||
expect(passwordField.find('#letter-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
|
||||
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
|
||||
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
|
||||
});
|
||||
|
||||
it('should update password requirement checks', async () => {
|
||||
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
jest.useFakeTimers();
|
||||
await act(async () => {
|
||||
fireEvent.focus(passwordInput);
|
||||
passwordField.find('input').simulate('focus');
|
||||
jest.runAllTimers();
|
||||
});
|
||||
passwordField.update();
|
||||
|
||||
const letterCheckIcon = document.querySelector('#letter-check span');
|
||||
const numberCheckIcon = document.querySelector('#number-check span');
|
||||
const charactersCheckIcon = document.querySelector('#characters-check span');
|
||||
|
||||
expect(letterCheckIcon).toBeTruthy();
|
||||
expect(letterCheckIcon.className).toContain('pgn__icon text-success mr-1');
|
||||
|
||||
expect(numberCheckIcon).toBeTruthy();
|
||||
expect(numberCheckIcon.className).toContain('pgn__icon text-success mr-1');
|
||||
|
||||
expect(charactersCheckIcon).toBeTruthy();
|
||||
expect(charactersCheckIcon.className).toContain('pgn__icon text-success mr-1');
|
||||
expect(passwordField.find('#letter-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
|
||||
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
|
||||
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
|
||||
});
|
||||
|
||||
it('should not run validations when blur is fired on password icon click', () => {
|
||||
const { container, getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
fireEvent.blur(passwordInput, {
|
||||
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: 'invalid',
|
||||
},
|
||||
relatedTarget: passwordIcon,
|
||||
relatedTarget: {
|
||||
name: 'passwordIcon',
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.querySelector('div[feedback-for="password"]')).toBeNull();
|
||||
expect(passwordField.find('div[feedback-for="password"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should call props handle blur if available', () => {
|
||||
@@ -146,10 +135,9 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
handleBlur: jest.fn(),
|
||||
};
|
||||
const { container } = render(wrapper(<PasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
fireEvent.blur(passwordInput, {
|
||||
passwordField.find('input#password').simulate('blur', {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: '',
|
||||
@@ -164,10 +152,9 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
const { container } = render(wrapper(<PasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
fireEvent.blur(passwordInput, {
|
||||
passwordField.find('input#password').simulate('blur', {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: '',
|
||||
@@ -187,11 +174,9 @@ describe('PasswordField', () => {
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
|
||||
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
fireEvent.focus(passwordIcon, {
|
||||
passwordField.find('button[aria-label="Show password"]').simulate('focus', {
|
||||
target: {
|
||||
name: 'passwordIcon',
|
||||
value: '',
|
||||
@@ -207,11 +192,9 @@ describe('PasswordField', () => {
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
|
||||
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
fireEvent.focus(passwordIcon, {
|
||||
passwordField.find('button[aria-label="Show password"]').simulate('focus', {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: 'invalid',
|
||||
@@ -226,36 +209,34 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should run backend validations when frontend validations pass on blur when rendered from register page', () => {
|
||||
const mockValidateField = jest.fn();
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
props = {
|
||||
...props,
|
||||
handleErrorChange: jest.fn(),
|
||||
validateField: mockValidateField,
|
||||
};
|
||||
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||
const passwordField = getByLabelText('Password');
|
||||
fireEvent.blur(passwordField, {
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: 'password123',
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockValidateField).toHaveBeenCalledWith({ password: 'password123' });
|
||||
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ password: 'password123' }));
|
||||
});
|
||||
|
||||
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,
|
||||
value: 'testPassword',
|
||||
handleErrorChange: jest.fn(),
|
||||
handleBlur: jest.fn(),
|
||||
};
|
||||
const { getByLabelText } = render(wrapper(<PasswordField {...props} />));
|
||||
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
fireEvent.blur(passwordIcon, {
|
||||
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
|
||||
target: {
|
||||
name: 'passwordIcon',
|
||||
value: undefined,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import React from 'react';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import registerIcons from '../RegisterFaIcons';
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { IntlProvider } from '@openedx/frontend-base';
|
||||
import React from 'react';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import { PENDING_STATE, REGISTER_PAGE } from '../../data/constants';
|
||||
import { REGISTER_PAGE } from '../../data/constants';
|
||||
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
|
||||
|
||||
describe('ThirdPartyAuthAlert', () => {
|
||||
@@ -36,19 +38,4 @@ describe('ThirdPartyAuthAlert', () => {
|
||||
).toJSON();
|
||||
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,18 +1,19 @@
|
||||
/* eslint-disable import/no-import-module-exports */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import React from 'react';
|
||||
|
||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@openedx/frontend-base';
|
||||
import { render } from '@testing-library/react';
|
||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
MemoryRouter, Route, BrowserRouter as Router, Routes,
|
||||
} from 'react-router-dom';
|
||||
|
||||
import { UnAuthOnlyRoute } from '..';
|
||||
import { REGISTER_PAGE } from '../../data/constants';
|
||||
|
||||
jest.mock('@openedx/frontend-base', () => ({
|
||||
...jest.requireActual('@openedx/frontend-base'),
|
||||
import {
|
||||
MemoryRouter, Route, BrowserRouter as Router, Routes,
|
||||
} from 'react-router-dom';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedUser: jest.fn(),
|
||||
fetchAuthenticatedUser: jest.fn(),
|
||||
}));
|
||||
@@ -54,7 +55,7 @@ describe('UnAuthOnlyRoute', () => {
|
||||
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(user));
|
||||
|
||||
await act(async () => {
|
||||
await render(routerWrapper());
|
||||
await mount(routerWrapper());
|
||||
});
|
||||
|
||||
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: true });
|
||||
@@ -65,7 +66,7 @@ describe('UnAuthOnlyRoute', () => {
|
||||
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(null));
|
||||
|
||||
await act(async () => {
|
||||
await render(routerWrapper());
|
||||
await mount(routerWrapper());
|
||||
});
|
||||
|
||||
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: false });
|
||||
|
||||
17
src/common-components/tests/Zendesk.test.jsx
Normal file
17
src/common-components/tests/Zendesk.test.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -66,14 +66,14 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
|
||||
data-prefix="fab"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={{}}
|
||||
style={Object {}}
|
||||
viewBox="0 0 488 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M488 261.8C488 403.3 391.1 504 248 504 110.8 504 0 393.2 0 256S110.8 8 248 8c66.8 0 123 24.5 166.3 64.9l-67.5 64.9C258.5 52.6 94.3 116.6 94.3 256c0 86.5 69.1 156.6 153.7 156.6 98.2 0 135-70.4 140.8-106.9H248v-85.3h236.1c2.3 12.7 3.9 24.9 3.9 41.4z"
|
||||
fill="currentColor"
|
||||
style={{}}
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -93,7 +93,7 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
|
||||
`;
|
||||
|
||||
exports[`SocialAuthProviders should match social auth provider with iconImage snapshot 1`] = `
|
||||
[
|
||||
Array [
|
||||
<button
|
||||
className="btn-social btn-oa2-apple-id mr-3"
|
||||
data-provider-url="/auth/login/apple-id/?auth_entry=login&next=/dashboard"
|
||||
|
||||
@@ -1,25 +1,5 @@
|
||||
// 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`] = `
|
||||
<div
|
||||
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
|
||||
@@ -33,7 +13,7 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
|
||||
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.
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,7 +21,7 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
|
||||
`;
|
||||
|
||||
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
|
||||
[
|
||||
Array [
|
||||
<div
|
||||
className="fade alert-content alert-success mt-n2 mb-5 alert show"
|
||||
id="tpa-alert"
|
||||
@@ -59,7 +39,7 @@ exports[`ThirdPartyAuthAlert should match register page third party auth alert m
|
||||
Almost done!
|
||||
</div>
|
||||
<p>
|
||||
You've successfully signed into Google! We just need a little more information before you start learning with Test Site.
|
||||
You've successfully signed into Google! We just need a little more information before you start learning with Your Platform Name Here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// 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={
|
||||
Object {
|
||||
"answerBot": Object {
|
||||
"avatar": Object {
|
||||
"name": Object {
|
||||
"*": "edX Support",
|
||||
},
|
||||
"url": undefined,
|
||||
},
|
||||
"contactOnlyAfterQuery": true,
|
||||
"suppress": false,
|
||||
"title": Object {
|
||||
"*": "edX Support",
|
||||
},
|
||||
},
|
||||
"chat": Object {
|
||||
"departments": Object {
|
||||
"enabled": Array [
|
||||
"account settings",
|
||||
"billing and payments",
|
||||
"certificates",
|
||||
"deadlines",
|
||||
"errors and technical issues",
|
||||
"other",
|
||||
"proctoring",
|
||||
],
|
||||
},
|
||||
"suppress": false,
|
||||
},
|
||||
"contactForm": Object {
|
||||
"attachments": true,
|
||||
"selectTicketForm": Object {
|
||||
"*": "Please choose your request type:",
|
||||
},
|
||||
"ticketForms": Array [
|
||||
Object {
|
||||
"fields": Array [
|
||||
Object {
|
||||
"id": "description",
|
||||
"prefill": Object {
|
||||
"*": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
"id": 360003368814,
|
||||
"subject": false,
|
||||
},
|
||||
],
|
||||
},
|
||||
"contactOptions": Object {
|
||||
"enabled": false,
|
||||
},
|
||||
"helpCenter": Object {
|
||||
"originalArticleButton": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
37
src/config/index.js
Normal file
37
src/config/index.js
Normal file
@@ -0,0 +1,37 @@
|
||||
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_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,
|
||||
// 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 +0,0 @@
|
||||
export const appId = 'org.openedx.frontend.app.authn';
|
||||
20
src/data/algolia.js
Normal file
20
src/data/algolia.js
Normal file
@@ -0,0 +1,20 @@
|
||||
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,
|
||||
};
|
||||
33
src/data/configureStore.js
Normal file
33
src/data/configureStore.js
Normal file
@@ -0,0 +1,33 @@
|
||||
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,6 +5,7 @@ export const REGISTER_EMBEDDED_PAGE = '/register-embedded';
|
||||
export const RESET_PAGE = '/reset';
|
||||
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
|
||||
export const DEFAULT_REDIRECT_URL = '/dashboard';
|
||||
export const RECOMMENDATIONS = '/recommendations';
|
||||
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
|
||||
export const PAGE_NOT_FOUND = '/notfound';
|
||||
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
||||
@@ -28,9 +29,9 @@ export const EMBEDDED = 'embedded';
|
||||
export const LETTER_REGEX = /[a-zA-Z]/;
|
||||
export const NUMBER_REGEX = /\d/;
|
||||
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
|
||||
+ '|^"([\\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})'
|
||||
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
||||
+ '|^"([\\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})'
|
||||
+ '|\\[(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
|
||||
// things like auto-enrollment upon login and registration.
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
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 }));
|
||||
}
|
||||
17
src/data/optimizely.js
Normal file
17
src/data/optimizely.js
Normal file
@@ -0,0 +1,17 @@
|
||||
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();
|
||||
36
src/data/reducers.js
Executable file
36
src/data/reducers.js
Executable file
@@ -0,0 +1,36 @@
|
||||
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;
|
||||
19
src/data/sagas.js
Normal file
19
src/data/sagas.js
Normal file
@@ -0,0 +1,19 @@
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
16
src/data/tests/algolia.test.js
Normal file
16
src/data/tests/algolia.test.js
Normal file
@@ -0,0 +1,16 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user