Compare commits
2 Commits
temp-main
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8dbf20b3d2 | ||
|
|
4ae9ead191 |
31
.env
31
.env
@@ -13,31 +13,20 @@ ORDER_HISTORY_URL=null
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=null
|
||||
INFO_EMAIL=''
|
||||
# ***** Cookies *****
|
||||
USER_RETENTION_COOKIE_NAME=null
|
||||
# ***** Links *****
|
||||
USER_INFO_COOKIE_NAME=null
|
||||
AUTHN_MINIMAL_HEADER=true
|
||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
|
||||
POST_REGISTRATION_REDIRECT_URL=''
|
||||
SEARCH_CATALOG_URL=''
|
||||
# ***** Features flags *****
|
||||
USER_SURVEY_COOKIE_NAME=null
|
||||
COOKIE_DOMAIN=null
|
||||
WELCOME_PAGE_SUPPORT_LINK=null
|
||||
INFO_EMAIL=''
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
ENABLE_AUTO_GENERATED_USERNAME=''
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
|
||||
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''
|
||||
REGISTER_CONVERSION_COOKIE_NAME=null
|
||||
ENABLE_PROGRESSIVE_PROFILING=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS=''
|
||||
ENABLE_IMAGE_LAYOUT=''
|
||||
# ***** Zendesk related keys *****
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
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=''
|
||||
ENABLE_COOKIE_POLICY_BANNER=''
|
||||
|
||||
@@ -18,26 +18,21 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
INFO_EMAIL='info@example.com'
|
||||
# ***** Features *****
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS='true'
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
|
||||
# ***** Cookies *****
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
# ***** Links *****
|
||||
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
|
||||
AUTHN_MINIMAL_HEADER=true
|
||||
LOGIN_ISSUE_SUPPORT_LINK='/login-issue-support-url'
|
||||
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
||||
TOS_LINK='http://localhost:18000/tos'
|
||||
PRIVACY_POLICY='http://localhost:18000/privacy'
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
# ***** Base Container Images *****
|
||||
BANNER_IMAGE_LARGE=''
|
||||
BANNER_IMAGE_MEDIUM=''
|
||||
BANNER_IMAGE_SMALL=''
|
||||
BANNER_IMAGE_EXTRA_SMALL=''
|
||||
# ***** Miscellaneous *****
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
||||
COOKIE_DOMAIN='localhost'
|
||||
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
INFO_EMAIL='info@edx.org'
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
# Copy these to the .env.private to enable edX specific functionality on local system
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
|
||||
MARKETING_EMAILS_OPT_IN='true'
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS='true'
|
||||
10
.env.test
10
.env.test
@@ -16,5 +16,15 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
LOGIN_ISSUE_SUPPORT_LINK='https://login-issue-support-url.com'
|
||||
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
||||
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
coverage/*
|
||||
dist/
|
||||
docs
|
||||
node_modules/
|
||||
__mocks__/
|
||||
__snapshots__/
|
||||
|
||||
16
.eslintrc.js
16
.eslintrc.js
@@ -1,17 +1,16 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
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
|
||||
// @openedx/frontend-build from v3 to v5:
|
||||
// to fail for no apparent reason since upgrading
|
||||
// @edx/frontend-build from v3 to v5:
|
||||
// - TypeError: Cannot read property 'range' of null
|
||||
indent: [
|
||||
'indent': [
|
||||
'error',
|
||||
2,
|
||||
{ ignoredNodes: ['TemplateLiteral', 'SwitchCase'] },
|
||||
{ 'ignoredNodes': ['TemplateLiteral', 'SwitchCase'] }
|
||||
],
|
||||
'template-curly-spacing': 'off',
|
||||
'jsx-a11y/label-has-associated-control': ['error', {
|
||||
@@ -19,9 +18,9 @@ module.exports = createConfig('eslint', {
|
||||
labelAttributes: [],
|
||||
controlComponents: [],
|
||||
assert: 'htmlFor',
|
||||
depth: 25,
|
||||
depth: 25
|
||||
}],
|
||||
'sort-imports': ['error', { ignoreCase: true, ignoreDeclarationSort: true }],
|
||||
'sort-imports': ['error', {ignoreCase: true, ignoreDeclarationSort: true}],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
@@ -47,6 +46,5 @@ module.exports = createConfig('eslint', {
|
||||
},
|
||||
},
|
||||
],
|
||||
'function-paren-newline': 'off',
|
||||
},
|
||||
});
|
||||
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
* @openedx/2U-infinity
|
||||
9
.github/pull_request_template.md
vendored
9
.github/pull_request_template.md
vendored
@@ -2,16 +2,11 @@
|
||||
|
||||
Include a description of your changes here, along with a link to any relevant Jira tickets and/or Github issues.
|
||||
|
||||
#### JIRA
|
||||
|
||||
[XXX-XXXX](https://2u-internal.atlassian.net/browse/XXX-XXXX)
|
||||
|
||||
#### How Has This Been Tested?
|
||||
|
||||
Please describe in detail how you tested your changes.
|
||||
|
||||
#### Screenshots/sandbox (optional):
|
||||
|
||||
Include a link to the sandbox for design changes or screenshot for before and after. **Remove this section if its not applicable.**
|
||||
|
||||
|Before|After|
|
||||
@@ -25,5 +20,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.
|
||||
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/vanguards** to do it.
|
||||
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
@@ -1,20 +0,0 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "label: " it tries to apply
|
||||
# the label indicated in rest of comment.
|
||||
# If the comment starts with "remove label: ", it tries
|
||||
# to remove the indicated label.
|
||||
# Note: Labels are allowed to have spaces and this script does
|
||||
# not parse spaces (as often a space is legitimate), so the command
|
||||
# "label: really long lots of words label" will apply the
|
||||
# label "really long lots of words label"
|
||||
|
||||
name: Allows for the adding and removing of labels via comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
add_remove_labels:
|
||||
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||
|
||||
25
.github/workflows/autoupdate-pull-request.yml
vendored
25
.github/workflows/autoupdate-pull-request.yml
vendored
@@ -1,25 +0,0 @@
|
||||
name: autoupdate
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
types: [ labeled ]
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
autoupdate:
|
||||
name: autoupdate
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: docker://chinthakagodawita/autoupdate-action:v1
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
DRY_RUN: "false"
|
||||
PR_FILTER: "labelled"
|
||||
PR_LABELS: "autoupdate"
|
||||
EXCLUDED_LABELS: "dependencies,wontfix"
|
||||
MERGE_MSG: "Branch was auto-updated."
|
||||
RETRY_COUNT: "5"
|
||||
RETRY_SLEEP: "300"
|
||||
MERGE_CONFLICT_ACTION: "fail"
|
||||
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@@ -13,13 +13,13 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [18, 20]
|
||||
|
||||
node: [16]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
@@ -42,7 +42,4 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run Code Coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
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/lockfileversion-check.yml@master
|
||||
|
||||
12
.github/workflows/self-assign-issue.yml
vendored
12
.github/workflows/self-assign-issue.yml
vendored
@@ -1,12 +0,0 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "assign me" it assigns the author to the
|
||||
# ticket (case insensitive)
|
||||
|
||||
name: Assign comment author to ticket if they say "assign me"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
self_assign_by_comment:
|
||||
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -5,7 +5,6 @@ node_modules
|
||||
npm-debug.log
|
||||
coverage
|
||||
module.config.js
|
||||
.env.private
|
||||
|
||||
dist/
|
||||
src/i18n/transifex_input.json
|
||||
@@ -18,4 +17,3 @@ temp/babel-plugin-react-intl
|
||||
*~
|
||||
/temp
|
||||
/.vscode
|
||||
src/i18n/messages
|
||||
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
|
||||
* @edx/vanguards
|
||||
|
||||
45
Makefile
45
Makefile
@@ -1,17 +1,19 @@
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
export TRANSIFEX_RESOURCE = frontend-app-authn
|
||||
transifex_langs = "ar,fr,es_419,zh_CN,it_IT,pt_PT,de_DE,uk,ru,hi"
|
||||
|
||||
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
|
||||
npm install
|
||||
|
||||
i18n.extract:
|
||||
# Pulling display strings from .jsx files into .json files...
|
||||
@@ -29,31 +31,22 @@ 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
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/frontend-app-authn/src/i18n/messages:frontend-app-authn
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
|
||||
$(intl_imports) paragon frontend-platform frontend-app-authn
|
||||
|
||||
# This target is used by Travis.
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
# Checking for package-lock.json changes...
|
||||
git diff --exit-code package-lock.json
|
||||
|
||||
.PHONY: validate
|
||||
validate:
|
||||
make validate-no-uncommitted-package-lock-changes
|
||||
npm run i18n_extract
|
||||
npm run lint -- --max-warnings 0
|
||||
npm run test
|
||||
npm run build
|
||||
|
||||
.PHONY: validate.ci
|
||||
validate.ci:
|
||||
npm ci
|
||||
make validate
|
||||
|
||||
107
README.rst
107
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,12 +22,9 @@ This is a micro-frontend application responsible for the login, registration and
|
||||
|
||||
- Progressive profiling page
|
||||
|
||||
***************
|
||||
Getting Started
|
||||
***************
|
||||
|
||||
Installation
|
||||
============
|
||||
------------
|
||||
|
||||
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
|
||||
|
||||
@@ -49,7 +46,7 @@ This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see
|
||||
**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://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
|
||||
@@ -75,8 +72,8 @@ The authentication micro-frontend also requires the following additional variabl
|
||||
- The fully-qualified URL to the password reset support page in the target environment.
|
||||
- ``https://support.example.com``
|
||||
|
||||
* - ``AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK``
|
||||
- The fully-qualified URL to the progressive profiling support page in the target environment.
|
||||
* - ``WELCOME_PAGE_SUPPORT_LINK``
|
||||
- The fully-qualified URL to the welcome support page in the target environment.
|
||||
- ``https://support.example.com``
|
||||
|
||||
* - ``TOS_AND_HONOR_CODE``
|
||||
@@ -99,7 +96,7 @@ The authentication micro-frontend also requires the following additional variabl
|
||||
- Enables support for configurable registration fields on the MFE. This flag must be enabled to show any required registration field besides the default fields (name, email, username, password).
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
* - ``ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN``
|
||||
* - ``ENABLE_PROGRESSIVE_PROFILING``
|
||||
- Enables support for progressive profiling. If enabled, users are redirected to a second page where data for optional registration fields can be collected.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
@@ -107,21 +104,8 @@ The authentication micro-frontend also requires the following additional variabl
|
||||
- Disables the enterprise login from Authn MFE.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
* - ``MFE_CONFIG_API_URL``
|
||||
- Link of the API to get runtime mfe configuration variables from the site configuration or django settings.
|
||||
- ``/api/v1/mfe_config`` | ``''`` (empty strings are falsy)
|
||||
|
||||
* - ``APP_ID``
|
||||
- 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.
|
||||
|
||||
@@ -137,77 +121,14 @@ 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://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://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.
|
||||
|
||||
PR description template should be automatically applied if you are sending PR from github interface; otherwise you
|
||||
can find it it at `PULL_REQUEST_TEMPLATE.md <https://github.com/openedx/frontend-app-authn/blob/master/.github/pull_request_template.md>`_
|
||||
|
||||
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
|
||||
============================
|
||||
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``
|
||||
file in this repo.
|
||||
|
||||
Reporting Security Issues
|
||||
=========================
|
||||
|
||||
Please do not report security issues in public. Please email security@openedx.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.
|
||||
|
||||
|
||||
==============================
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# This file records information about this repo. Its use is described in OEP-55:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
|
||||
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: 'frontend-app-authn'
|
||||
description: "Micro-frontend for authentication service. It contains views for login, registration and password reset functionality."
|
||||
links:
|
||||
- url: 'https://github.com/openedx/frontend-app-authn/blob/master/README.rst'
|
||||
title: 'Documentation'
|
||||
icon: 'Article'
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
spec:
|
||||
owner: group:2u-infinity
|
||||
type: 'service'
|
||||
lifecycle: 'production'
|
||||
@@ -91,7 +91,7 @@ In the data sub-directory, the file names describe what each piece of code does.
|
||||
/ProfilePhotoUploader.jsx // supporting view
|
||||
/data // Note: most files here are named with a plural, as they contain many of the things in question.
|
||||
/actions.js
|
||||
/mockedData.js
|
||||
/constants.js
|
||||
/reducers.js
|
||||
/sagas.js
|
||||
/selectors.js
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFiles: [
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
nick: Authn MFE
|
||||
oeps: {}
|
||||
owner: openedx/2u-infinity
|
||||
owner: edx/vanguards
|
||||
openedx-release:
|
||||
ref: master
|
||||
|
||||
50416
package-lock.json
generated
50416
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
71
package.json
71
package.json
@@ -11,7 +11,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"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",
|
||||
@@ -32,54 +32,57 @@
|
||||
"url": "https://github.com/openedx/frontend-app-authn/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-platform": "^8.0.0",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@openedx/paragon": "^22.1.1",
|
||||
"@optimizely/react-sdk": "^2.9.1",
|
||||
"@redux-devtools/extension": "3.3.0",
|
||||
"@testing-library/react": "^12.1.5",
|
||||
"@testing-library/react-hooks": "^8.0.1",
|
||||
"algoliasearch": "^4.14.3",
|
||||
"algoliasearch-helper": "^3.14.0",
|
||||
"classnames": "2.5.1",
|
||||
"core-js": "3.38.1",
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-cookie-policy-banner": "2.2.0",
|
||||
"@edx/frontend-platform": "3.1.1",
|
||||
"@edx/paragon": "20.20.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@redux-devtools/extension": "3.2.3",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.26.1",
|
||||
"extract-react-intl-messages": "4.1.1",
|
||||
"fastest-levenshtein": "1.0.16",
|
||||
"form-urlencoded": "6.1.5",
|
||||
"form-urlencoded": "4.2.1",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"query-string": "5.1.1",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-loading-skeleton": "3.4.0",
|
||||
"react-loading-skeleton": "2.2.0",
|
||||
"react-onclickoutside": "6.12.2",
|
||||
"react-redux": "7.2.9",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-router": "6.26.1",
|
||||
"react-router-dom": "6.26.1",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.2.1",
|
||||
"react-router": "5.3.4",
|
||||
"react-router-dom": "5.3.4",
|
||||
"redux": "4.2.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"redux-saga": "1.3.0",
|
||||
"redux-saga": "1.2.1",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.14.1",
|
||||
"reselect": "4.1.8",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"reselect": "4.1.7",
|
||||
"sanitize-html": "2.7.3",
|
||||
"semver-regex": "3.1.4",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "11.0.2",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"@openedx/frontend-build": "^14.0.3",
|
||||
"babel-plugin-formatjs": "10.5.16",
|
||||
"eslint-plugin-import": "2.29.1",
|
||||
"babel-plugin-formatjs": "10.3.31",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.7",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"glob": "7.2.3",
|
||||
"history": "5.3.0",
|
||||
"husky": "7.0.4",
|
||||
"jest": "29.7.0",
|
||||
"react-test-renderer": "^17.0.2"
|
||||
"jest": "27.5.1",
|
||||
"react-test-renderer": "16.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></title>
|
||||
<title>Authn | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon"/>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.4.4/iframeResizer.contentWindow.min.js"
|
||||
integrity="sha512-IWwZFBvHzN41wNI6etRLLuLrDDj/6AwJcPt7cmKJAzluYTIHHQ1PF8wh0rSy05jxEvvjflVvH2MxeV6riyEEXg=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer">
|
||||
</script>
|
||||
<% if (process.env.OPTIMIZELY_URL) { %>
|
||||
<script
|
||||
src="<%= process.env.OPTIMIZELY_URL %>"
|
||||
@@ -19,6 +13,58 @@
|
||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
||||
></script>
|
||||
<% } %>
|
||||
<% if (process.env.ZENDESK_KEY) { %>
|
||||
<script
|
||||
id="ze-snippet"
|
||||
src="https://static.zdassets.com/ekr/snippet.js?key=<%= process.env.ZENDESK_KEY %>"
|
||||
>
|
||||
</script>
|
||||
<script type="text/javascript">
|
||||
window.zESettings = {
|
||||
cookies: true,
|
||||
webWidget: {
|
||||
contactOptions: {
|
||||
enabled: false,
|
||||
},
|
||||
chat: {
|
||||
suppress: false,
|
||||
},
|
||||
contactForm: {
|
||||
ticketForms: [
|
||||
{
|
||||
id: 360003368814,
|
||||
subject: false,
|
||||
fields: [
|
||||
{
|
||||
id: 'description',
|
||||
prefill: {
|
||||
'*': '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
selectTicketForm: {
|
||||
'*': 'Please choose your request type:',
|
||||
},
|
||||
attachments: true,
|
||||
},
|
||||
helpCenter: {
|
||||
originalArticleButton: true,
|
||||
},
|
||||
answerBot: {
|
||||
suppress: false,
|
||||
contactOnlyAfterQuery: true,
|
||||
title: { '*': 'edX Support' },
|
||||
avatar: {
|
||||
url: '<%= process.env.ZENDESK_LOGO_URL %>',
|
||||
name: { '*': 'edX Support' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<% } %>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -3,30 +3,19 @@ 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 { Redirect, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
|
||||
Logistration, NotFoundPage, registerIcons, UnAuthOnlyRoute,
|
||||
} 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,
|
||||
LOGIN_PAGE, PAGE_NOT_FOUND, PASSWORD_RESET_CONFIRM, REGISTER_PAGE, RESET_PAGE, WELCOME_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 ForgotPasswordPage from './forgot-password';
|
||||
import ResetPasswordPage from './reset-password';
|
||||
import { ProgressiveProfiling } from './welcome';
|
||||
import './index.scss';
|
||||
|
||||
registerIcons();
|
||||
@@ -36,27 +25,20 @@ const MainApp = () => (
|
||||
<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>
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Redirect to={updatePathWithQueryParams(REGISTER_PAGE)} />
|
||||
</Route>
|
||||
<UnAuthOnlyRoute exact path={LOGIN_PAGE} render={() => <Logistration selectedPage={LOGIN_PAGE} />} />
|
||||
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={Logistration} />
|
||||
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
|
||||
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
|
||||
<Route exact path={WELCOME_PAGE} component={ProgressiveProfiling} />
|
||||
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
|
||||
<Route path="*">
|
||||
<Redirect to={PAGE_NOT_FOUND} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
|
||||
46
src/base-component/AuthLargeLayout.jsx
Normal file
46
src/base-component/AuthLargeLayout.jsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AuthLargeLayout = ({ intl, username }) => (
|
||||
<div className="w-50 d-flex">
|
||||
<div className="col-md-10 bg-light-200 p-0">
|
||||
<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">
|
||||
{intl.formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||
</h1>
|
||||
<h2 className="complete-your-profile">
|
||||
{intl.formatMessage(messages['complete.your.profile.1'])}
|
||||
<div className="text-accent-a">
|
||||
{intl.formatMessage(messages['complete.your.profile.2'])}
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 bg-white p-0">
|
||||
<svg className="m1-n1 w-100 h-100 large-screen-svg-light" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(171.6)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
AuthLargeLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AuthLargeLayout);
|
||||
49
src/base-component/AuthMediumLayout.jsx
Normal file
49
src/base-component/AuthMediumLayout.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AuthMediumLayout = ({ intl, username }) => (
|
||||
<>
|
||||
<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={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">
|
||||
{intl.formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||
</h1>
|
||||
<h2 className="display-1">
|
||||
{intl.formatMessage(messages['complete.your.profile.1'])}
|
||||
<div className="text-accent-a">
|
||||
{intl.formatMessage(messages['complete.your.profile.2'])}
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 bg-white p-0">
|
||||
<svg className="w-100 h-100 medium-screen-svg-light" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(168)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
AuthMediumLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AuthMediumLayout);
|
||||
38
src/base-component/AuthSmallLayout.jsx
Normal file
38
src/base-component/AuthSmallLayout.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const AuthSmallLayout = ({ intl, username }) => (
|
||||
<div className="min-vw-100 bg-light-200">
|
||||
<div className="col-md-12 small-screen-top-stripe" />
|
||||
<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 mb-3 mt-3 mr-3">
|
||||
<div className="small-yellow-line mt-4.5" />
|
||||
<div>
|
||||
<h1 className="h5 data-hj-suppress">
|
||||
{intl.formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
|
||||
</h1>
|
||||
<h2 className="h1">
|
||||
{intl.formatMessage(messages['complete.your.profile.1'])}
|
||||
<div className="text-accent-a">
|
||||
{intl.formatMessage(messages['complete.your.profile.2'])}
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
AuthSmallLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AuthSmallLayout);
|
||||
55
src/base-component/BaseComponent.jsx
Normal file
55
src/base-component/BaseComponent.jsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
|
||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { getLocale } from '@edx/frontend-platform/i18n';
|
||||
import { breakpoints } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import MediaQuery from 'react-responsive';
|
||||
|
||||
import AuthLargeLayout from './AuthLargeLayout';
|
||||
import AuthMediumLayout from './AuthMediumLayout';
|
||||
import AuthSmallLayout from './AuthSmallLayout';
|
||||
import LargeLayout from './LargeLayout';
|
||||
import MediumLayout from './MediumLayout';
|
||||
import SmallLayout from './SmallLayout';
|
||||
|
||||
const BaseComponent = ({ children, showWelcomeBanner }) => {
|
||||
const authenticatedUser = showWelcomeBanner ? getAuthenticatedUser() : null;
|
||||
const username = authenticatedUser ? authenticatedUser.username : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{getConfig().ENABLE_COOKIE_POLICY_BANNER ? <CookiePolicyBanner languageCode={getLocale()} /> : null}
|
||||
<div className="col-md-12 extra-large-screen-top-stripe" />
|
||||
<div className="layout">
|
||||
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
|
||||
{authenticatedUser ? <AuthSmallLayout username={username} /> : <SmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
||||
{authenticatedUser ? <AuthMediumLayout username={username} /> : <MediumLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth} maxWidth={breakpoints.extraExtraLarge.maxWidth}>
|
||||
{authenticatedUser ? <AuthLargeLayout username={username} /> : <LargeLayout />}
|
||||
</MediaQuery>
|
||||
|
||||
<div className={classNames('content', { 'align-items-center mt-0': authenticatedUser })}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BaseComponent.defaultProps = {
|
||||
showWelcomeBanner: false,
|
||||
};
|
||||
|
||||
BaseComponent.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
showWelcomeBanner: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default BaseComponent;
|
||||
45
src/base-component/LargeLayout.jsx
Normal file
45
src/base-component/LargeLayout.jsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const LargeLayout = ({ intl }) => (
|
||||
<div className="w-50 d-flex">
|
||||
<div className="col-md-9 bg-primary-400">
|
||||
<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': getConfig().SITE_NAME === 'edX' })} />
|
||||
<h1
|
||||
className={classNames(
|
||||
'display-2 text-white mw-xs',
|
||||
{ 'ml-6': getConfig().SITE_NAME !== 'edX' },
|
||||
)}
|
||||
>
|
||||
{intl.formatMessage(messages['start.learning'])}
|
||||
<div className="text-accent-a">
|
||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-3 bg-white p-0">
|
||||
<svg className="ml-n1 w-100 h-100 large-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(171.6)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
LargeLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LargeLayout);
|
||||
50
src/base-component/MediumLayout.jsx
Normal file
50
src/base-component/MediumLayout.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const MediumLayout = ({ intl }) => (
|
||||
<>
|
||||
<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={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': getConfig().SITE_NAME === 'edX' })} />
|
||||
<div>
|
||||
<h1
|
||||
className={classNames(
|
||||
'display-1 text-white mt-5 mb-5 mr-2',
|
||||
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
||||
)}
|
||||
>
|
||||
<span className="mr-2">{intl.formatMessage(messages['start.learning'])}</span>
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 bg-white p-0">
|
||||
<svg className="w-100 h-100 medium-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(168)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
MediumLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(MediumLayout);
|
||||
39
src/base-component/SmallLayout.jsx
Normal file
39
src/base-component/SmallLayout.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SmallLayout = ({ intl }) => (
|
||||
<span className="bg-primary-400 w-100">
|
||||
<div className="col-md-12 small-screen-top-stripe" />
|
||||
<div>
|
||||
<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 mb-3 mt-3 mr-3">
|
||||
<div className={classNames({ 'small-yellow-line mr-n2.5': getConfig().SITE_NAME === 'edX' })} />
|
||||
<h1
|
||||
className={classNames(
|
||||
'text-white mt-3.5 mb-3.5',
|
||||
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
|
||||
)}
|
||||
>
|
||||
<span className="mr-1">{intl.formatMessage(messages['start.learning'])}</span>
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
|
||||
SmallLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SmallLayout);
|
||||
1
src/base-component/index.jsx
Normal file
1
src/base-component/index.jsx
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './BaseComponent';
|
||||
@@ -1,11 +1,17 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'welcome.to.platform': {
|
||||
id: 'welcome.to.platform',
|
||||
defaultMessage: 'Welcome to {siteName}, {fullName}!',
|
||||
description: 'Welcome message that appears on progressive profile page',
|
||||
'start.learning': {
|
||||
id: 'start.learning',
|
||||
defaultMessage: 'Start learning',
|
||||
description: 'Header text for logistration MFE pages',
|
||||
},
|
||||
'with.site.name': {
|
||||
id: 'with.site.name',
|
||||
defaultMessage: 'with {siteName}',
|
||||
description: 'Header text with site name for logistration MFE pages',
|
||||
},
|
||||
// authenticated user base component text
|
||||
'complete.your.profile.1': {
|
||||
id: 'complete.your.profile.1',
|
||||
defaultMessage: 'Complete',
|
||||
@@ -16,6 +22,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'your profile',
|
||||
description: 'part of text "complete your profile"',
|
||||
},
|
||||
'welcome.to.platform': {
|
||||
id: 'welcome.to.platform',
|
||||
defaultMessage: 'Welcome to {siteName}, {username}!',
|
||||
description: 'Welcome message that appears on progressive profile page',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
52
src/base-component/tests/BaseComponent.test.jsx
Normal file
52
src/base-component/tests/BaseComponent.test.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import LargeLayout from '../LargeLayout';
|
||||
import MediumLayout from '../MediumLayout';
|
||||
import SmallLayout from '../SmallLayout';
|
||||
|
||||
describe('ScreenLayout', () => {
|
||||
it('should display the form, pass as a child in SmallScreenLayout', () => {
|
||||
const smallScreen = mount(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<SmallLayout />
|
||||
<form>
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(smallScreen.find('form').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should display the form, pass as a child in MediumScreenLayout', () => {
|
||||
const mediumScreen = mount(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<MediumLayout />
|
||||
<form>
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(mediumScreen.find('form').exists()).toEqual(true);
|
||||
});
|
||||
|
||||
it('should display the form, pass as a child in LargeScreenLayout', () => {
|
||||
const largeScreen = mount(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<LargeLayout />
|
||||
<form>
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(largeScreen.find('form').exists()).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index';
|
||||
|
||||
describe('Default Layout tests', () => {
|
||||
it('should display the form passed as a child in SmallScreenLayout', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<DefaultSmallLayout />
|
||||
<form aria-label="form">
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByRole('form')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display the form passed as a child in MediumScreenLayout', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<DefaultMediumLayout />
|
||||
<form aria-label="form">
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByRole('form')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display the form passed as a child in LargeScreenLayout', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<div>
|
||||
<DefaultLargeLayout />
|
||||
<form aria-label="form">
|
||||
<input type="text" />
|
||||
</form>
|
||||
</div>
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByRole('form')).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const LargeLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="w-50 d-flex">
|
||||
<div className="col-md-9 bg-primary-400">
|
||||
<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': getConfig().SITE_NAME === 'edX' })} />
|
||||
<h1
|
||||
className={classNames(
|
||||
'display-2 text-white mw-xs',
|
||||
{ 'ml-6': getConfig().SITE_NAME !== 'edX' },
|
||||
)}
|
||||
>
|
||||
{formatMessage(messages['start.learning'])}
|
||||
<div className="text-accent-a">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</div>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-3 bg-white p-0">
|
||||
<svg className="ml-n1 w-100 h-100 large-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(171.6)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LargeLayout;
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const MediumLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<>
|
||||
<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={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': getConfig().SITE_NAME === 'edX' })} />
|
||||
<div>
|
||||
<h1
|
||||
className={classNames(
|
||||
'display-1 text-white mt-5 mb-5 mr-2 main-heading',
|
||||
{ '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: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 bg-white p-0">
|
||||
<svg className="w-100 h-100 medium-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(168)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediumLayout;
|
||||
@@ -1,40 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SmallLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<span className="bg-primary-400 w-100">
|
||||
<div className="col-md-12 small-screen-top-stripe" />
|
||||
<div>
|
||||
<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': getConfig().SITE_NAME === 'edX' })} />
|
||||
<h1
|
||||
className={classNames(
|
||||
'text-white mt-3.5 mb-3.5',
|
||||
)}
|
||||
>
|
||||
<span>
|
||||
{formatMessage(messages['start.learning'])}{' '}
|
||||
<span className="text-accent-a d-inline-block">
|
||||
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
|
||||
</span>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmallLayout;
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as DefaultLargeLayout } from './LargeLayout';
|
||||
export { default as DefaultMediumLayout } from './MediumLayout';
|
||||
export { default as DefaultSmallLayout } from './SmallLayout';
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'start.learning': {
|
||||
id: 'start.learning',
|
||||
defaultMessage: 'Start learning',
|
||||
description: 'Header text for logistration MFE pages',
|
||||
},
|
||||
'with.site.name': {
|
||||
id: 'with.site.name',
|
||||
defaultMessage: 'with {siteName}',
|
||||
description: 'Header text with site name for logistration MFE pages',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ExtraSmallLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<span
|
||||
className="w-100 bg-primary-500 banner__image extra-small-layout"
|
||||
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_EXTRA_SMALL})` }}
|
||||
>
|
||||
<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">
|
||||
<span className="text-light-500">
|
||||
{formatMessage(messages['your.career.turning.point'])}{' '}
|
||||
</span>
|
||||
<span className="text-warning-300">
|
||||
{formatMessage(messages['is.here'])}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExtraSmallLayout;
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
|
||||
import './index.scss';
|
||||
import messages from './messages';
|
||||
|
||||
const LargeLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-50 bg-primary-500 banner__image large-layout"
|
||||
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_LARGE})` }}
|
||||
>
|
||||
<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">
|
||||
<span className="text-light-500">
|
||||
{formatMessage(messages['your.career.turning.point'])}
|
||||
</span>
|
||||
<span className="text-warning-300">
|
||||
{formatMessage(messages['is.here'])}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LargeLayout;
|
||||
@@ -1,35 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
|
||||
import './index.scss';
|
||||
import messages from './messages';
|
||||
|
||||
const MediumLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-100 mb-3 bg-primary-500 banner__image medium-layout"
|
||||
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_MEDIUM})` }}
|
||||
>
|
||||
<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">
|
||||
<span className="text-light-500">
|
||||
{formatMessage(messages['your.career.turning.point'])}{' '}
|
||||
</span>
|
||||
<span className="text-warning-300 d-inline-block">
|
||||
{formatMessage(messages['is.here'])}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MediumLayout;
|
||||
@@ -1,34 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SmallLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<span
|
||||
className="w-100 bg-primary-500 banner__image small-layout"
|
||||
style={{ backgroundImage: `url(${getConfig().BANNER_IMAGE_SMALL})` }}
|
||||
>
|
||||
<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">
|
||||
<span className="text-light-500">
|
||||
{formatMessage(messages['your.career.turning.point'])}{' '}
|
||||
</span>
|
||||
<span className="text-warning-300">
|
||||
{formatMessage(messages['is.here'])}
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default SmallLayout;
|
||||
@@ -1,4 +0,0 @@
|
||||
export { default as ImageLargeLayout } from './LargeLayout';
|
||||
export { default as ImageMediumLayout } from './MediumLayout';
|
||||
export { default as ImageSmallLayout } from './SmallLayout';
|
||||
export { default as ImageExtraSmallLayout } from './ExtraSmallLayout';
|
||||
@@ -1,37 +0,0 @@
|
||||
.company-logo {
|
||||
width: 71px;
|
||||
margin-top: 2rem;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.company-logo {
|
||||
width: 44.67px;
|
||||
margin-top: 1.25rem;
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.banner__image {
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
border:none;
|
||||
}
|
||||
|
||||
@media (min-width: 464px) and (max-width: 575.98px) {
|
||||
.banner__heading {
|
||||
font-size: 60px;
|
||||
font-weight: 700;
|
||||
line-height: 60px;
|
||||
letter-spacing: -1.2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 800px) {
|
||||
.banner__heading {
|
||||
font-size: 60px !important;
|
||||
font-weight: 700 !important;
|
||||
line-height: 60px !important;
|
||||
letter-spacing: -2px !important;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'your.career.turning.point': {
|
||||
id: 'your.career.turning.point',
|
||||
defaultMessage: 'Your career turning point',
|
||||
description: 'Part of the heading "Your career turning point is here." shown on Authn MFE',
|
||||
},
|
||||
'is.here': {
|
||||
id: 'is.here',
|
||||
defaultMessage: 'is here.',
|
||||
description: 'Part of the heading "Your career turning point is here." shown on Authn MFE',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const LargeLayout = ({ fullName }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="w-50 d-flex">
|
||||
<div className="col-md-10 bg-light-200 p-0">
|
||||
<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: getConfig().SITE_NAME, fullName })}
|
||||
</h1>
|
||||
<h2 className="complete-your-profile">
|
||||
{formatMessage(messages['complete.your.profile.1'])}
|
||||
<div className="text-accent-a">
|
||||
{formatMessage(messages['complete.your.profile.2'])}
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 bg-white p-0">
|
||||
<svg className="m1-n1 w-100 h-100 large-screen-svg-light" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(171.6)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LargeLayout.propTypes = {
|
||||
fullName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default LargeLayout;
|
||||
@@ -1,52 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const MediumLayout = ({ fullName }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<>
|
||||
<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={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: getConfig().SITE_NAME, fullName })}
|
||||
</h1>
|
||||
<h2 className="display-1">
|
||||
{formatMessage(messages['complete.your.profile.1'])}
|
||||
<div className="text-accent-a">
|
||||
{formatMessage(messages['complete.your.profile.2'])}
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-md-2 bg-white p-0">
|
||||
<svg className="w-100 h-100 medium-screen-svg-light" preserveAspectRatio="xMaxYMin meet">
|
||||
<g transform="skewX(168)">
|
||||
<rect x="0" y="0" height="100%" width="100%" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
MediumLayout.propTypes = {
|
||||
fullName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default MediumLayout;
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const SmallLayout = ({ fullName }) => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<div className="min-vw-100 bg-light-200">
|
||||
<div className="col-md-12 small-screen-top-stripe" />
|
||||
<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: getConfig().SITE_NAME, fullName })}
|
||||
</h1>
|
||||
<h2 className="h1">
|
||||
{formatMessage(messages['complete.your.profile.1'])}
|
||||
<div className="text-accent-a">
|
||||
{formatMessage(messages['complete.your.profile.2'])}
|
||||
</div>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SmallLayout.propTypes = {
|
||||
fullName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default SmallLayout;
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as AuthLargeLayout } from './LargeLayout';
|
||||
export { default as AuthMediumLayout } from './MediumLayout';
|
||||
export { default as AuthSmallLayout } from './SmallLayout';
|
||||
@@ -1,72 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { breakpoints } from '@openedx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import MediaQuery from 'react-responsive';
|
||||
|
||||
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './components/default-layout';
|
||||
import {
|
||||
ImageExtraSmallLayout, ImageLargeLayout, ImageMediumLayout, ImageSmallLayout,
|
||||
} from './components/image-layout';
|
||||
import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout';
|
||||
|
||||
const BaseContainer = ({ children, showWelcomeBanner, fullName }) => {
|
||||
const enableImageLayout = getConfig().ENABLE_IMAGE_LAYOUT;
|
||||
|
||||
if (enableImageLayout) {
|
||||
return (
|
||||
<div className="layout">
|
||||
<MediaQuery maxWidth={breakpoints.extraSmall.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <ImageExtraSmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.small.minWidth} maxWidth={breakpoints.small.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <ImageSmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthMediumLayout fullName={fullName} /> : <ImageMediumLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
|
||||
{showWelcomeBanner ? <AuthLargeLayout fullName={fullName} /> : <ImageLargeLayout />}
|
||||
</MediaQuery>
|
||||
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="col-md-12 extra-large-screen-top-stripe" />
|
||||
<div className="layout">
|
||||
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthSmallLayout fullName={fullName} /> : <DefaultSmallLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
|
||||
{showWelcomeBanner ? <AuthMediumLayout fullName={fullName} /> : <DefaultMediumLayout />}
|
||||
</MediaQuery>
|
||||
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
|
||||
{showWelcomeBanner ? <AuthLargeLayout fullName={fullName} /> : <DefaultLargeLayout />}
|
||||
</MediaQuery>
|
||||
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BaseContainer.defaultProps = {
|
||||
showWelcomeBanner: false,
|
||||
fullName: null,
|
||||
};
|
||||
|
||||
BaseContainer.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
showWelcomeBanner: PropTypes.bool,
|
||||
fullName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default BaseContainer;
|
||||
@@ -1,46 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { mergeConfig } from '@edx/frontend-platform';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Context as ResponsiveContext } from 'react-responsive';
|
||||
|
||||
import BaseContainer from '../index';
|
||||
|
||||
const LargeScreen = {
|
||||
wrappingComponent: ResponsiveContext.Provider,
|
||||
wrappingComponentProps: { value: { width: 1200 } },
|
||||
};
|
||||
|
||||
describe('Base component tests', () => {
|
||||
it('should show default layout', () => {
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<BaseContainer>
|
||||
<div>Test Content</div>
|
||||
</BaseContainer>
|
||||
</IntlProvider>,
|
||||
LargeScreen,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.banner__image')).toBeNull();
|
||||
expect(container.querySelector('.large-screen-svg-primary')).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders Image layout when ENABLE_IMAGE_LAYOUT configuration is enabled', () => {
|
||||
mergeConfig({
|
||||
ENABLE_IMAGE_LAYOUT: true,
|
||||
});
|
||||
|
||||
const { container } = render(
|
||||
<IntlProvider locale="en">
|
||||
<BaseContainer showWelcomeBanner={false}>
|
||||
<div>Test Content</div>
|
||||
</BaseContainer>
|
||||
</IntlProvider>,
|
||||
LargeScreen,
|
||||
);
|
||||
|
||||
expect(container.querySelector('.banner__image')).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
import { PAGE_NOT_FOUND } from '../data/constants';
|
||||
import { isHostAvailableInQueryParams } from '../data/utils';
|
||||
|
||||
/**
|
||||
* This wrapper redirects the requester to embedded register page only if host
|
||||
* query param is present.
|
||||
*/
|
||||
const EmbeddedRegistrationRoute = ({ children }) => {
|
||||
const registrationEmbedded = isHostAvailableInQueryParams();
|
||||
|
||||
// Show registration page for embedded experience even if the user is authenticated
|
||||
if (registrationEmbedded) {
|
||||
return children;
|
||||
}
|
||||
|
||||
return <Navigate to={PAGE_NOT_FOUND} replace />;
|
||||
};
|
||||
|
||||
EmbeddedRegistrationRoute.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default EmbeddedRegistrationRoute;
|
||||
@@ -1,26 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Form,
|
||||
Icon,
|
||||
} from '@openedx/paragon';
|
||||
import { Login } from '@openedx/paragon/icons';
|
||||
} from '@edx/paragon';
|
||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* This component renders the Single sign-on (SSO) button only for the tpa provider passed
|
||||
* */
|
||||
const EnterpriseSSO = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { intl } = props;
|
||||
const tpaProvider = props.provider;
|
||||
const hideRegistrationLink = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false
|
||||
|| getConfig().SHOW_REGISTRATION_LINKS === false;
|
||||
|
||||
const handleSubmit = (e, url) => {
|
||||
e.preventDefault();
|
||||
@@ -38,7 +32,7 @@ const EnterpriseSSO = (props) => {
|
||||
<div className="d-flex flex-column">
|
||||
<div className="mw-450">
|
||||
<Form className="m-0">
|
||||
<p>{formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
|
||||
<p>{intl.formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
|
||||
<Button
|
||||
id={tpaProvider.id}
|
||||
key={tpaProvider.id}
|
||||
@@ -49,18 +43,16 @@ const EnterpriseSSO = (props) => {
|
||||
>
|
||||
{tpaProvider.iconImage ? (
|
||||
<div aria-hidden="true">
|
||||
<img className="btn-tpa__image-icon" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
|
||||
<img className="icon-image" src={tpaProvider.iconImage} alt={`icon ${tpaProvider.name}`} />
|
||||
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div className="btn-tpa__font-container" aria-hidden="true">
|
||||
{SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? (
|
||||
<FontAwesomeIcon icon={['fab', tpaProvider.iconClass]} />)
|
||||
: (
|
||||
<Icon className="h-75" src={Login} />
|
||||
)}
|
||||
<div className="font-container" aria-hidden="true">
|
||||
<FontAwesomeIcon
|
||||
icon={SUPPORTED_ICON_CLASSES.includes(tpaProvider.iconClass) ? ['fab', tpaProvider.iconClass] : faSignInAlt}
|
||||
/>
|
||||
</div>
|
||||
<span className="pl-2" aria-hidden="true">{ tpaProvider.name }</span>
|
||||
</>
|
||||
@@ -69,15 +61,12 @@ const EnterpriseSSO = (props) => {
|
||||
<div className="mb-4" />
|
||||
<Button
|
||||
type="submit"
|
||||
id="other-ways-to-sign-in"
|
||||
variant="outline-primary"
|
||||
state="Complete"
|
||||
className="w-100"
|
||||
onClick={(e) => handleClick(e)}
|
||||
>
|
||||
{hideRegistrationLink
|
||||
? formatMessage(messages['enterprisetpa.login.button.text.public.account.creation.disabled'])
|
||||
: formatMessage(messages['enterprisetpa.login.button.text'])}
|
||||
{intl.formatMessage(messages['enterprisetpa.login.button.text'])}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
@@ -108,6 +97,7 @@ EnterpriseSSO.propTypes = {
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
}),
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default EnterpriseSSO;
|
||||
export default injectIntl(EnterpriseSSO);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
Form, TransitionReplace,
|
||||
} from '@openedx/paragon';
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const FormGroup = (props) => {
|
||||
@@ -27,7 +27,7 @@ const FormGroup = (props) => {
|
||||
readOnly={props.readOnly}
|
||||
type={props.type}
|
||||
aria-invalid={props.errorMessage !== ''}
|
||||
className="form-group__form-field"
|
||||
className="form-field"
|
||||
autoComplete={props.autoComplete}
|
||||
spellCheck={props.spellCheck}
|
||||
name={props.name}
|
||||
@@ -37,6 +37,7 @@ const FormGroup = (props) => {
|
||||
onClick={handleClick}
|
||||
onChange={props.handleChange}
|
||||
controlClassName={props.borderClass}
|
||||
|
||||
trailingElement={props.trailingElement}
|
||||
floatingLabel={props.floatingLabel}
|
||||
>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Hyperlink, Icon } from '@openedx/paragon';
|
||||
import { Institution } from '@openedx/paragon/icons';
|
||||
import { injectIntl, intlShape } 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';
|
||||
|
||||
/**
|
||||
* This component renders the Institution login button
|
||||
* */
|
||||
export const RenderInstitutionButton = props => {
|
||||
const { onSubmitHandler, buttonTitle } = props;
|
||||
|
||||
@@ -27,13 +24,10 @@ export const RenderInstitutionButton = props => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This component renders the page list of available institutions for login
|
||||
* */
|
||||
const InstitutionLogistration = props => {
|
||||
const lmsBaseUrl = getConfig().LMS_BASE_URL;
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
intl,
|
||||
secondaryProviders,
|
||||
headingTitle,
|
||||
} = props;
|
||||
@@ -42,11 +36,11 @@ const InstitutionLogistration = props => {
|
||||
<>
|
||||
<div className="d-flex justify-content-left mb-4 mt-2">
|
||||
<div className="flex-column">
|
||||
<h4 className="mb-2 font-weight-bold institutions__heading">
|
||||
<h4 className="mb-2 font-weight-bold institute-heading">
|
||||
{headingTitle}
|
||||
</h4>
|
||||
<p className="mb-2">
|
||||
{formatMessage(messages['institution.login.page.sub.heading'])}
|
||||
{intl.formatMessage(messages['institution.login.page.sub.heading'])}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,10 +51,10 @@ const InstitutionLogistration = props => {
|
||||
<tr key={provider} className="pgn__data-table-row">
|
||||
<td>
|
||||
<Hyperlink
|
||||
className="btn nav-item p-0 mb-1 institutions--provider-link"
|
||||
className="btn nav-item p-0 mb-1 secondary-provider-link"
|
||||
destination={lmsBaseUrl + provider.loginUrl}
|
||||
>
|
||||
{provider?.name}
|
||||
{provider.name}
|
||||
</Hyperlink>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -95,6 +89,7 @@ RenderInstitutionButton.defaultProps = {
|
||||
|
||||
InstitutionLogistration.propTypes = {
|
||||
...LogistrationProps,
|
||||
intl: intlShape.isRequired,
|
||||
headingTitle: PropTypes.string,
|
||||
};
|
||||
InstitutionLogistration.defaultProps = {
|
||||
@@ -102,4 +97,4 @@ InstitutionLogistration.defaultProps = {
|
||||
headingTitle: '',
|
||||
};
|
||||
|
||||
export default InstitutionLogistration;
|
||||
export default injectIntl(InstitutionLogistration);
|
||||
|
||||
104
src/common-components/Logistration.jsx
Normal file
104
src/common-components/Logistration.jsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthService } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Icon,
|
||||
Tab,
|
||||
Tabs,
|
||||
} from '@edx/paragon';
|
||||
import { ChevronLeft } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import BaseComponent from '../base-component';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
import { getTpaHint, updatePathWithQueryParams } from '../data/utils';
|
||||
import { LoginPage } from '../login';
|
||||
import { RegistrationPage } from '../register';
|
||||
import messages from './messages';
|
||||
|
||||
const Logistration = (props) => {
|
||||
const { intl, selectedPage } = props;
|
||||
const tpa = getTpaHint();
|
||||
const [institutionLogin, setInstitutionLogin] = useState(false);
|
||||
const [key, setKey] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const authService = getAuthService();
|
||||
if (authService) {
|
||||
authService.getCsrfTokenService().getCsrfToken(getConfig().LMS_BASE_URL);
|
||||
}
|
||||
});
|
||||
|
||||
const handleInstitutionLogin = (e) => {
|
||||
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
|
||||
if (typeof e === 'string') {
|
||||
sendPageEvent('login_and_registration', e === '/login' ? 'login' : 'register');
|
||||
} else {
|
||||
sendPageEvent('login_and_registration', e.target.dataset.eventName);
|
||||
}
|
||||
|
||||
setInstitutionLogin(!institutionLogin);
|
||||
};
|
||||
|
||||
const handleOnSelect = (tabKey) => {
|
||||
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
|
||||
setKey(tabKey);
|
||||
};
|
||||
|
||||
const tabTitle = (
|
||||
<div className="d-flex">
|
||||
<Icon src={ChevronLeft} className="left-icon" />
|
||||
<span className="ml-2">
|
||||
{selectedPage === LOGIN_PAGE
|
||||
? intl.formatMessage(messages['logistration.sign.in'])
|
||||
: intl.formatMessage(messages['logistration.register'])}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseComponent>
|
||||
<div>
|
||||
{institutionLogin
|
||||
? (
|
||||
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
|
||||
<Tab title={tabTitle} eventKey={selectedPage === LOGIN_PAGE ? LOGIN_PAGE : REGISTER_PAGE} />
|
||||
</Tabs>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
{!tpa && (
|
||||
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={handleOnSelect}>
|
||||
<Tab title={intl.formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
|
||||
<Tab title={intl.formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
|
||||
</Tabs>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{ key && (
|
||||
<Redirect to={updatePathWithQueryParams(key)} />
|
||||
)}
|
||||
<div id="main-content" className="main-content">
|
||||
{selectedPage === LOGIN_PAGE
|
||||
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
|
||||
: <RegistrationPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />}
|
||||
</div>
|
||||
</div>
|
||||
</BaseComponent>
|
||||
);
|
||||
};
|
||||
|
||||
Logistration.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
selectedPage: PropTypes.string,
|
||||
};
|
||||
|
||||
Logistration.defaultProps = {
|
||||
selectedPage: REGISTER_PAGE,
|
||||
};
|
||||
|
||||
export default injectIntl(Logistration);
|
||||
@@ -2,16 +2,16 @@ 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">
|
||||
<p className="my-0 py-5 text-muted mw-32em">
|
||||
<FormattedMessage
|
||||
id="error.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default NotFoundPage;
|
||||
export default function NotFoundPage() {
|
||||
return (
|
||||
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
|
||||
<p className="my-0 py-5 text-muted mw-32em">
|
||||
<FormattedMessage
|
||||
id="error.notfound.message"
|
||||
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
|
||||
description="error message when a page does not exist"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,103 +1,41 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } 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 messages from './messages';
|
||||
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
|
||||
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../register/data/actions';
|
||||
import { validatePasswordField } from '../register/data/utils';
|
||||
import messages from './messages';
|
||||
|
||||
const PasswordField = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
|
||||
const { formatMessage } = props.intl;
|
||||
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
const handleBlur = (e) => {
|
||||
const { name, value } = e.target;
|
||||
if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
|
||||
return; // Do not run validations on password icon click
|
||||
}
|
||||
|
||||
let passwordValue = value;
|
||||
if (name === 'passwordIcon') {
|
||||
// To validate actual password value when onBlur is triggered by focusing out the password icon
|
||||
passwordValue = props.value;
|
||||
}
|
||||
|
||||
if (props.handleBlur) {
|
||||
props.handleBlur({
|
||||
target: {
|
||||
name: props.name,
|
||||
value: passwordValue,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (props.handleBlur) { props.handleBlur(e); }
|
||||
setShowTooltip(props.showRequirements && false);
|
||||
if (props.handleErrorChange) { // If rendering from register page
|
||||
const fieldError = validatePasswordField(passwordValue, formatMessage);
|
||||
if (fieldError) {
|
||||
props.handleErrorChange('password', fieldError);
|
||||
} else if (!validationApiRateLimited) {
|
||||
dispatch(fetchRealtimeValidations({ password: passwordValue }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleFocus = (e) => {
|
||||
if (e.target?.name === 'passwordIcon') {
|
||||
return; // Do not clear error on password icon focus
|
||||
}
|
||||
|
||||
if (props.handleFocus) {
|
||||
props.handleFocus(e);
|
||||
}
|
||||
if (props.handleErrorChange) {
|
||||
props.handleErrorChange('password', '');
|
||||
dispatch(clearRegistrationBackendError('password'));
|
||||
}
|
||||
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
|
||||
};
|
||||
|
||||
const HideButton = (
|
||||
<IconButton
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
name="passwordIcon"
|
||||
src={VisibilityOff}
|
||||
iconAs={Icon}
|
||||
onClick={setHiddenTrue}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt={formatMessage(messages['hide.password'])}
|
||||
/>
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
|
||||
);
|
||||
|
||||
const ShowButton = (
|
||||
<IconButton
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
name="passwordIcon"
|
||||
src={Visibility}
|
||||
iconAs={Icon}
|
||||
onClick={setHiddenFalse}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt={formatMessage(messages['show.password'])}
|
||||
/>
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
|
||||
);
|
||||
|
||||
const placement = window.innerWidth < 768 ? 'top' : 'left';
|
||||
const tooltip = (
|
||||
<Tooltip id={`password-requirement-${placement}`}>
|
||||
@@ -121,7 +59,7 @@ const PasswordField = (props) => {
|
||||
<OverlayTrigger key="tooltip" placement={placement} overlay={tooltip} show={showTooltip}>
|
||||
<Form.Control
|
||||
as="input"
|
||||
className="form-group__form-field"
|
||||
className="form-field"
|
||||
type={isPasswordHidden ? 'password' : 'text'}
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
@@ -138,7 +76,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>
|
||||
@@ -151,9 +89,7 @@ PasswordField.defaultProps = {
|
||||
handleBlur: null,
|
||||
handleFocus: null,
|
||||
handleChange: () => {},
|
||||
handleErrorChange: null,
|
||||
showRequirements: true,
|
||||
showScreenReaderText: true,
|
||||
autoComplete: null,
|
||||
};
|
||||
|
||||
@@ -164,12 +100,11 @@ PasswordField.propTypes = {
|
||||
handleBlur: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
handleErrorChange: PropTypes.func,
|
||||
intl: intlShape.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
showRequirements: PropTypes.bool,
|
||||
value: PropTypes.string.isRequired,
|
||||
autoComplete: PropTypes.string,
|
||||
showScreenReaderText: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default PasswordField;
|
||||
export default injectIntl(PasswordField);
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT,
|
||||
} from '../data/constants';
|
||||
import setCookie from '../data/utils/cookies';
|
||||
import { WELCOME_PAGE } from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
|
||||
const RedirectLogistration = (props) => {
|
||||
function RedirectLogistration(props) {
|
||||
const {
|
||||
authenticatedUser,
|
||||
finishAuthUrl,
|
||||
redirectUrl,
|
||||
redirectToProgressiveProfilingPage,
|
||||
success,
|
||||
optionalFields,
|
||||
redirectToRecommendationsPage,
|
||||
educationLevel,
|
||||
userId,
|
||||
registrationEmbedded,
|
||||
host,
|
||||
finishAuthUrl, redirectUrl, redirectToWelcomePage, success, optionalFields,
|
||||
} = props;
|
||||
let finalRedirectUrl = '';
|
||||
|
||||
@@ -34,80 +24,42 @@ const RedirectLogistration = (props) => {
|
||||
finalRedirectUrl = redirectUrl;
|
||||
}
|
||||
|
||||
// Redirect to Progressive Profiling after successful registration
|
||||
if (redirectToProgressiveProfilingPage) {
|
||||
// TODO: Do we still need this cookie?
|
||||
if (redirectToWelcomePage) {
|
||||
setCookie('van-504-returning-user', true);
|
||||
|
||||
if (registrationEmbedded) {
|
||||
window.parent.postMessage({
|
||||
action: REDIRECT,
|
||||
redirectUrl: getConfig().POST_REGISTRATION_REDIRECT_URL,
|
||||
}, host);
|
||||
return null;
|
||||
}
|
||||
// use this component to redirect WelcomePage after successful registration
|
||||
// return <Redirect to={WELCOME_PAGE} />;
|
||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||
return (
|
||||
<Navigate
|
||||
to={AUTHN_PROGRESSIVE_PROFILING}
|
||||
state={{
|
||||
<Redirect to={{
|
||||
pathname: WELCOME_PAGE,
|
||||
state: {
|
||||
registrationResult,
|
||||
optionalFields,
|
||||
authenticatedUser,
|
||||
}}
|
||||
replace
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Redirect to Recommendation page
|
||||
if (redirectToRecommendationsPage) {
|
||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||
return (
|
||||
<Navigate
|
||||
to={RECOMMENDATIONS}
|
||||
state={{
|
||||
registrationResult,
|
||||
educationLevel,
|
||||
userId,
|
||||
}}
|
||||
replace
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
window.location.href = finalRedirectUrl;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
return <></>;
|
||||
}
|
||||
|
||||
RedirectLogistration.defaultProps = {
|
||||
authenticatedUser: {},
|
||||
educationLevel: null,
|
||||
finishAuthUrl: null,
|
||||
success: false,
|
||||
redirectUrl: '',
|
||||
redirectToProgressiveProfilingPage: false,
|
||||
redirectToWelcomePage: false,
|
||||
optionalFields: {},
|
||||
redirectToRecommendationsPage: false,
|
||||
userId: null,
|
||||
registrationEmbedded: false,
|
||||
host: '',
|
||||
};
|
||||
|
||||
RedirectLogistration.propTypes = {
|
||||
authenticatedUser: PropTypes.shape({}),
|
||||
educationLevel: PropTypes.string,
|
||||
finishAuthUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
redirectUrl: PropTypes.string,
|
||||
redirectToProgressiveProfilingPage: PropTypes.bool,
|
||||
redirectToWelcomePage: PropTypes.bool,
|
||||
optionalFields: PropTypes.shape({}),
|
||||
redirectToRecommendationsPage: PropTypes.bool,
|
||||
userId: PropTypes.number,
|
||||
registrationEmbedded: PropTypes.bool,
|
||||
host: PropTypes.string,
|
||||
};
|
||||
|
||||
export default RedirectLogistration;
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
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, REGISTER_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
|
||||
const SocialAuthProviders = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { referrer, socialAuthProviders } = props;
|
||||
const registrationFields = useSelector(state => state.register.registrationFormData);
|
||||
function SocialAuthProviders(props) {
|
||||
const { intl, referrer, socialAuthProviders } = props;
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (referrer === REGISTER_PAGE) {
|
||||
setCookie('marketingEmailsOptIn', registrationFields?.configurableFormFields?.marketingEmailsOptIn);
|
||||
}
|
||||
const url = e.currentTarget.dataset.providerUrl;
|
||||
window.location.href = getConfig().LMS_BASE_URL + url;
|
||||
}
|
||||
@@ -38,30 +30,29 @@ const SocialAuthProviders = (props) => {
|
||||
>
|
||||
{provider.iconImage ? (
|
||||
<div aria-hidden="true">
|
||||
<img className="btn-tpa__image-icon" src={provider.iconImage} alt={`icon ${provider.name}`} />
|
||||
<img className="icon-image" src={provider.iconImage} alt={`icon ${provider.name}`} />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="btn-tpa__font-container" aria-hidden="true">
|
||||
{SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? (
|
||||
<FontAwesomeIcon icon={['fab', provider.iconClass]} />)
|
||||
: (
|
||||
<Icon className="h-75" src={Login} />
|
||||
)}
|
||||
</div>
|
||||
<>
|
||||
<div className="font-container" aria-hidden="true">
|
||||
<FontAwesomeIcon
|
||||
icon={SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? ['fab', provider.iconClass] : faSignInAlt}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<span id="provider-name" className="notranslate mr-auto pl-2" aria-hidden="true">{provider.name}</span>
|
||||
<span className="sr-only">
|
||||
{referrer === LOGIN_PAGE
|
||||
? formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
||||
: formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
|
||||
? intl.formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
||||
: intl.formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
|
||||
</span>
|
||||
</button>
|
||||
));
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{socialAuth}</>;
|
||||
};
|
||||
}
|
||||
|
||||
SocialAuthProviders.defaultProps = {
|
||||
referrer: LOGIN_PAGE,
|
||||
@@ -69,6 +60,7 @@ SocialAuthProviders.defaultProps = {
|
||||
};
|
||||
|
||||
SocialAuthProviders.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
referrer: PropTypes.string,
|
||||
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
@@ -77,8 +69,7 @@ SocialAuthProviders.propTypes = {
|
||||
iconImage: PropTypes.string,
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
skipRegistrationForm: PropTypes.bool,
|
||||
})),
|
||||
};
|
||||
|
||||
export default SocialAuthProviders;
|
||||
export default injectIntl(SocialAuthProviders);
|
||||
|
||||
62
src/common-components/SwitchContent.jsx
Normal file
62
src/common-components/SwitchContent.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import React from 'react';
|
||||
|
||||
import { TransitionReplace } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const onChildExit = (htmlNode) => {
|
||||
// If the leaving child has focus, take control and redirect it
|
||||
if (htmlNode.contains(document.activeElement)) {
|
||||
// Get the newly entering sibling.
|
||||
// It's the previousSibling, but not for any explicit reason. So checking for both.
|
||||
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
|
||||
|
||||
// There's no replacement, do nothing.
|
||||
if (!enteringChild) return; // eslint-disable-line curly
|
||||
|
||||
// Get all the focusable elements in the entering child and focus the first one
|
||||
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (focusableElements.length) {
|
||||
focusableElements[0].focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function SwitchContent({ expression, cases, className }) {
|
||||
const getContent = (caseKey) => {
|
||||
if (cases[caseKey]) {
|
||||
if (typeof cases[caseKey] === 'string') {
|
||||
return getContent(cases[caseKey]);
|
||||
}
|
||||
return React.cloneElement(cases[caseKey], { key: caseKey });
|
||||
} else if (cases.default) { // eslint-disable-line no-else-return
|
||||
if (typeof cases.default === 'string') {
|
||||
return getContent(cases.default);
|
||||
}
|
||||
React.cloneElement(cases.default, { key: 'default' });
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<TransitionReplace
|
||||
className={className}
|
||||
onChildExit={onChildExit}
|
||||
>
|
||||
{getContent(expression)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
}
|
||||
|
||||
SwitchContent.propTypes = {
|
||||
expression: PropTypes.string,
|
||||
cases: PropTypes.objectOf(PropTypes.node).isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
SwitchContent.defaultProps = {
|
||||
expression: null,
|
||||
className: null,
|
||||
};
|
||||
|
||||
export default SwitchContent;
|
||||
@@ -1,126 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Hyperlink, Icon,
|
||||
} from '@openedx/paragon';
|
||||
import { Institution } from '@openedx/paragon/icons';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import messages from './messages';
|
||||
import {
|
||||
ENTERPRISE_LOGIN_URL, LOGIN_PAGE, PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
|
||||
import {
|
||||
RenderInstitutionButton,
|
||||
SocialAuthProviders,
|
||||
} from './index';
|
||||
|
||||
/**
|
||||
* This component renders the Single sign-on (SSO) buttons for the providers passed.
|
||||
* */
|
||||
const ThirdPartyAuth = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
providers,
|
||||
secondaryProviders,
|
||||
currentProvider,
|
||||
handleInstitutionLogin,
|
||||
thirdPartyAuthApiStatus,
|
||||
isLoginPage,
|
||||
} = props;
|
||||
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
|
||||
const isSocialAuthActive = !!providers.length && !currentProvider;
|
||||
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
|
||||
const enterpriseLoginURL = getConfig().LMS_BASE_URL + ENTERPRISE_LOGIN_URL;
|
||||
const isThirdPartyAuthActive = isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive);
|
||||
|
||||
return (
|
||||
<>
|
||||
{((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive) && (
|
||||
<div className="mt-4 mb-3 h4">
|
||||
{isLoginPage
|
||||
? formatMessage(messages['login.other.options.heading'])
|
||||
: formatMessage(messages['registration.other.options.heading'])}
|
||||
</div>
|
||||
)}
|
||||
{(isLoginPage && !isEnterpriseLoginDisabled && isSocialAuthActive) && (
|
||||
<Hyperlink
|
||||
className={classNames(
|
||||
'btn btn-link btn-sm text-body p-0',
|
||||
{ 'mb-0': thirdPartyAuthApiStatus === PENDING_STATE },
|
||||
{ 'mb-4': thirdPartyAuthApiStatus !== PENDING_STATE },
|
||||
)}
|
||||
destination={enterpriseLoginURL}
|
||||
>
|
||||
<Icon src={Institution} className="institute-icon" />
|
||||
{formatMessage(messages['enterprise.login.btn.text'])}
|
||||
</Hyperlink>
|
||||
)}
|
||||
|
||||
{thirdPartyAuthApiStatus === PENDING_STATE && isThirdPartyAuthActive ? (
|
||||
<div className="mt-4">
|
||||
<Skeleton className="tpa-skeleton" height={36} count={2} />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
|
||||
<RenderInstitutionButton
|
||||
onSubmitHandler={handleInstitutionLogin}
|
||||
buttonTitle={formatMessage(messages['institution.login.button'])}
|
||||
/>
|
||||
)}
|
||||
{isSocialAuthActive && (
|
||||
<div className="row m-0">
|
||||
<SocialAuthProviders
|
||||
socialAuthProviders={providers}
|
||||
referrer={isLoginPage ? LOGIN_PAGE : REGISTER_PAGE}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ThirdPartyAuth.defaultProps = {
|
||||
currentProvider: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
isLoginPage: false,
|
||||
};
|
||||
|
||||
ThirdPartyAuth.propTypes = {
|
||||
currentProvider: PropTypes.string,
|
||||
handleInstitutionLogin: PropTypes.func.isRequired,
|
||||
providers: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
iconClass: PropTypes.string,
|
||||
iconImage: PropTypes.string,
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
}),
|
||||
),
|
||||
secondaryProviders: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
iconClass: PropTypes.string,
|
||||
iconImage: PropTypes.string,
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
}),
|
||||
),
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
isLoginPage: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default ThirdPartyAuth;
|
||||
@@ -1,56 +1,42 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
import setCookie from '../data/utils/cookies';
|
||||
import messages from './messages';
|
||||
|
||||
const ThirdPartyAuthAlert = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { currentProvider, referrer } = props;
|
||||
const { currentProvider, intl, referrer } = props;
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
let message;
|
||||
|
||||
if (referrer === LOGIN_PAGE) {
|
||||
message = formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
message = intl.formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
} else {
|
||||
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
}
|
||||
|
||||
if (currentProvider) {
|
||||
// Setting this cookie to capture marketingEmailsOptIn for SSO flow on the onboarding component
|
||||
setCookie('ssoPipelineRedirectionDone', true);
|
||||
} else {
|
||||
return null;
|
||||
message = intl.formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2 mb-5' : 'alert-warning mt-n2 mb-5'}>
|
||||
{referrer === REGISTER_PAGE ? (
|
||||
<Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
) : null}
|
||||
<p>{ message }</p>
|
||||
</Alert>
|
||||
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2' : 'alert-warning mt-n2'}>
|
||||
{referrer === REGISTER_PAGE ? (
|
||||
<h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
||||
<Alert.Heading>{intl.formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
) : null}
|
||||
</>
|
||||
<p>{ message }</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
ThirdPartyAuthAlert.defaultProps = {
|
||||
currentProvider: '',
|
||||
referrer: LOGIN_PAGE,
|
||||
};
|
||||
|
||||
ThirdPartyAuthAlert.propTypes = {
|
||||
currentProvider: PropTypes.string,
|
||||
currentProvider: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
referrer: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ThirdPartyAuthAlert;
|
||||
export default injectIntl(ThirdPartyAuthAlert);
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
import { RESET_PAGE } from '../data/constants';
|
||||
import { updatePathWithQueryParams } from '../data/utils';
|
||||
import { DEFAULT_REDIRECT_URL } from '../data/constants';
|
||||
|
||||
/**
|
||||
* This wrapper redirects the requester to our default redirect url if they are
|
||||
* already authenticated.
|
||||
*/
|
||||
const UnAuthOnlyRoute = ({ children }) => {
|
||||
const UnAuthOnlyRoute = (props) => {
|
||||
const [authUser, setAuthUser] = useState({});
|
||||
const [isReady, setIsReady] = useState(false);
|
||||
|
||||
@@ -24,23 +23,14 @@ const UnAuthOnlyRoute = ({ children }) => {
|
||||
|
||||
if (isReady) {
|
||||
if (authUser && authUser.username) {
|
||||
const updatedPath = updatePathWithQueryParams(window.location.pathname);
|
||||
if (updatedPath.startsWith(RESET_PAGE)) {
|
||||
global.location.href = getConfig().LMS_BASE_URL;
|
||||
return null;
|
||||
}
|
||||
global.location.href = getConfig().LMS_BASE_URL.concat(updatedPath);
|
||||
global.location.href = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
return null;
|
||||
}
|
||||
|
||||
return children;
|
||||
return <Route {...props} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
UnAuthOnlyRoute.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default UnAuthOnlyRoute;
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import Zendesk from 'react-zendesk';
|
||||
|
||||
import messages from './messages';
|
||||
import { REGISTER_EMBEDDED_PAGE } from '../data/constants';
|
||||
|
||||
const ZendeskHelp = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const setting = {
|
||||
cookies: true,
|
||||
webWidget: {
|
||||
contactOptions: {
|
||||
enabled: false,
|
||||
},
|
||||
chat: {
|
||||
suppress: false,
|
||||
departments: {
|
||||
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring'],
|
||||
},
|
||||
},
|
||||
contactForm: {
|
||||
ticketForms: [
|
||||
{
|
||||
id: 360003368814,
|
||||
subject: false,
|
||||
fields: [{ id: 'description', prefill: { '*': '' } }],
|
||||
},
|
||||
],
|
||||
selectTicketForm: {
|
||||
'*': formatMessage(messages.selectTicketForm),
|
||||
},
|
||||
attachments: true,
|
||||
},
|
||||
helpCenter: {
|
||||
originalArticleButton: true,
|
||||
},
|
||||
answerBot: {
|
||||
suppress: false,
|
||||
contactOnlyAfterQuery: true,
|
||||
title: { '*': formatMessage(messages.supportTitle) },
|
||||
avatar: {
|
||||
url: getConfig().ZENDESK_LOGO_URL,
|
||||
name: { '*': formatMessage(messages.supportTitle) },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (window.location.pathname === REGISTER_EMBEDDED_PAGE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ZendeskHelp;
|
||||
@@ -1,7 +1,6 @@
|
||||
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) => ({
|
||||
@@ -21,7 +20,3 @@ export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalField
|
||||
export const getThirdPartyAuthContextFailure = () => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
|
||||
});
|
||||
|
||||
export const clearThirdPartyAuthContextErrorMessage = () => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
|
||||
});
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
export const registerFields = {
|
||||
fields: {
|
||||
country: {
|
||||
name: 'country',
|
||||
error_message: 'Select your country or region of residence',
|
||||
},
|
||||
honor_code: {
|
||||
name: 'honor_code',
|
||||
type: 'tos_and_honor_code',
|
||||
error_message: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const progressiveProfilingFields = {
|
||||
extended_profile: [],
|
||||
fields: {
|
||||
level_of_education: {
|
||||
name: 'level_of_education',
|
||||
type: 'select',
|
||||
label: 'Highest level of education completed',
|
||||
error_message: '',
|
||||
options: [
|
||||
[
|
||||
'p',
|
||||
'Doctorate',
|
||||
],
|
||||
[
|
||||
'm',
|
||||
"Master's or professional degree",
|
||||
],
|
||||
[
|
||||
'b',
|
||||
"Bachelor's degree",
|
||||
],
|
||||
[
|
||||
'a',
|
||||
'Associate degree',
|
||||
],
|
||||
[
|
||||
'hs',
|
||||
'Secondary/high school',
|
||||
],
|
||||
[
|
||||
'jhs',
|
||||
'Junior secondary/junior high/middle school',
|
||||
],
|
||||
[
|
||||
'none',
|
||||
'No formal education',
|
||||
],
|
||||
[
|
||||
'other',
|
||||
'Other education',
|
||||
],
|
||||
],
|
||||
},
|
||||
gender: {
|
||||
name: 'gender',
|
||||
type: 'select',
|
||||
label: 'Gender',
|
||||
error_message: '',
|
||||
options: [
|
||||
[
|
||||
'm',
|
||||
'Male',
|
||||
],
|
||||
[
|
||||
'f',
|
||||
'Female',
|
||||
],
|
||||
[
|
||||
'o',
|
||||
'Other/Prefer Not to Say',
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,59 +1,33 @@
|
||||
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions';
|
||||
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
|
||||
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
|
||||
import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
|
||||
|
||||
export const defaultState = {
|
||||
extendedProfile: [],
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {
|
||||
fields: {},
|
||||
extended_profile: [],
|
||||
},
|
||||
optionalFields: {},
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
autoSubmitRegForm: false,
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
errorMessage: null,
|
||||
welcomePageRedirectUrl: null,
|
||||
},
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
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: {
|
||||
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
fieldDescriptions: action.payload.fieldDescriptions?.fields,
|
||||
extendedProfile: action.payload.fieldDescriptions.extendedProfile,
|
||||
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,
|
||||
},
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
||||
@@ -1,37 +1,28 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { call, put, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
|
||||
import {
|
||||
getThirdPartyAuthContextBegin,
|
||||
getThirdPartyAuthContextFailure,
|
||||
getThirdPartyAuthContextSuccess,
|
||||
THIRD_PARTY_AUTH_CONTEXT,
|
||||
} from './actions';
|
||||
import { progressiveProfilingFields, registerFields } from './constants';
|
||||
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);
|
||||
const { fieldDescriptions, optionalFields, thirdPartyAuthContext } = yield call(
|
||||
getThirdPartyAuthContext, action.payload.urlParams,
|
||||
);
|
||||
|
||||
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
|
||||
// hard code country field, level of education and gender fields
|
||||
if (getConfig().ENABLE_HARD_CODE_OPTIONAL_FIELDS) {
|
||||
yield put(getThirdPartyAuthContextSuccess(
|
||||
registerFields,
|
||||
progressiveProfilingFields,
|
||||
thirdPartyAuthContext,
|
||||
));
|
||||
} else {
|
||||
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
|
||||
}
|
||||
yield put(getThirdPartyAuthContextSuccess(
|
||||
fieldDescriptions, optionalFields, thirdPartyAuthContext,
|
||||
));
|
||||
} catch (e) {
|
||||
yield put(getThirdPartyAuthContextFailure());
|
||||
logError(e);
|
||||
|
||||
@@ -14,15 +14,12 @@ export const fieldDescriptionSelector = createSelector(
|
||||
commonComponents => commonComponents.fieldDescriptions,
|
||||
);
|
||||
|
||||
export const extendedProfileSelector = createSelector(
|
||||
commonComponentsSelector,
|
||||
commonComponents => commonComponents.extendedProfile,
|
||||
);
|
||||
|
||||
export const optionalFieldsSelector = createSelector(
|
||||
commonComponentsSelector,
|
||||
commonComponents => commonComponents.optionalFields,
|
||||
);
|
||||
|
||||
export const tpaProvidersSelector = createSelector(
|
||||
commonComponentsSelector,
|
||||
commonComponents => ({
|
||||
providers: commonComponents.thirdPartyAuthContext.providers,
|
||||
secondaryProviders: commonComponents.thirdPartyAuthContext.secondaryProviders,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { camelCaseObject, convertKeyNames, getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
@@ -18,8 +18,12 @@ export async function getThirdPartyAuthContext(urlParams) {
|
||||
throw (e);
|
||||
});
|
||||
return {
|
||||
fieldDescriptions: data.registrationFields || {},
|
||||
optionalFields: data.optionalFields || {},
|
||||
thirdPartyAuthContext: data.contextData || {},
|
||||
fieldDescriptions: data.registration_fields || {},
|
||||
optionalFields: data.optional_fields || {},
|
||||
// For backward compatibility with the API, once https://github.com/openedx/edx-platform/pull/30198 is merged
|
||||
// and deployed update it to use data.context_data
|
||||
thirdPartyAuthContext: camelCaseObject(
|
||||
convertKeyNames(data.context_data || data, { fullname: 'name' }),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { PENDING_STATE } from '../../../data/constants';
|
||||
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from '../actions';
|
||||
import reducer from '../reducers';
|
||||
|
||||
describe('common components reducer', () => {
|
||||
it('test mfe context response', () => {
|
||||
const state = {
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
errorMessage: null,
|
||||
},
|
||||
};
|
||||
const fieldDescriptions = {
|
||||
fields: [],
|
||||
};
|
||||
const optionalFields = {
|
||||
fields: [],
|
||||
extended_profile: {},
|
||||
};
|
||||
const thirdPartyAuthContext = { ...state.thirdPartyAuthContext };
|
||||
const action = {
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
|
||||
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
).toEqual(
|
||||
{
|
||||
...state,
|
||||
fieldDescriptions: [],
|
||||
optionalFields: {
|
||||
fields: [],
|
||||
extended_profile: {},
|
||||
},
|
||||
thirdPartyAuthApiStatus: 'complete',
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear tpa context error message', () => {
|
||||
const state = {
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
errorMessage: 'An error occurred',
|
||||
},
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
).toEqual(
|
||||
{
|
||||
...state,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
...state.thirdPartyAuthContext,
|
||||
errorMessage: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
export { default as RedirectLogistration } from './RedirectLogistration';
|
||||
export { default as registerIcons } from './RegisterFaIcons';
|
||||
export { default as EmbeddedRegistrationRoute } from './EmbeddedRegistrationRoute';
|
||||
export { default as UnAuthOnlyRoute } from './UnAuthOnlyRoute';
|
||||
export { default as NotFoundPage } from './NotFoundPage';
|
||||
export { default as SocialAuthProviders } from './SocialAuthProviders';
|
||||
@@ -12,4 +11,4 @@ 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';
|
||||
export { default as Logistration } from './Logistration';
|
||||
|
||||
@@ -1,13 +1,29 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
// institution login strings
|
||||
'institution.login.page.sub.heading': {
|
||||
id: 'institution.login.page.sub.heading',
|
||||
defaultMessage: 'Choose your institution from the list below',
|
||||
description: 'Heading of the institutions list',
|
||||
},
|
||||
// logistration strings
|
||||
// Confirmation Alert Message
|
||||
'forgot.password.confirmation.title': {
|
||||
id: 'forgot.password.confirmation.title',
|
||||
defaultMessage: 'Check your email',
|
||||
description: 'Forgot password confirmation message title',
|
||||
},
|
||||
'forgot.password.confirmation.support.link': {
|
||||
id: 'forgot.password.confirmation.support.link',
|
||||
defaultMessage: 'contact technical support',
|
||||
description: 'Technical support link text',
|
||||
},
|
||||
'forgot.password.confirmation.info': {
|
||||
id: 'forgot.password.confirmation.info',
|
||||
defaultMessage: 'If you do not receive a password reset message after 1 minute, verify that you entered the correct '
|
||||
+ 'email address, or check your spam folder.',
|
||||
description: 'Part of message that appears after user requests password change',
|
||||
},
|
||||
// Logistration strinsg
|
||||
'logistration.sign.in': {
|
||||
id: 'logistration.sign.in',
|
||||
defaultMessage: 'Sign in',
|
||||
@@ -18,22 +34,32 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Register',
|
||||
description: 'Text that appears on the tab to switch between login and register',
|
||||
},
|
||||
'internal.server.error.message': {
|
||||
id: 'internal.server.error.message',
|
||||
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
|
||||
description: 'Error message that appears when server responds with 500 error code',
|
||||
},
|
||||
'server.ratelimit.error.message': {
|
||||
id: 'server.ratelimit.error.message',
|
||||
defaultMessage: 'An error has occurred because of too many requests. Please try again after some time.',
|
||||
description: 'Error message that appears when server responds with 429 error code',
|
||||
},
|
||||
// enterprise sso strings
|
||||
'enterprisetpa.title.heading': {
|
||||
id: 'enterprisetpa.title.heading',
|
||||
defaultMessage: 'Would you like to sign in using your {providerName} credentials?',
|
||||
description: 'Header text used in enterprise third party authentication',
|
||||
},
|
||||
'enterprisetpa.sso.button.title': {
|
||||
id: 'enterprisetpa.sso.button.title',
|
||||
defaultMessage: 'Sign in using {providerName}',
|
||||
description: 'Text for third party auth provider buttons',
|
||||
},
|
||||
'enterprisetpa.login.button.text': {
|
||||
id: 'enterprisetpa.login.button.text',
|
||||
defaultMessage: 'Show me other ways to sign in or register',
|
||||
description: 'Button text for login',
|
||||
},
|
||||
'enterprisetpa.login.button.text.public.account.creation.disabled': {
|
||||
id: 'enterprisetpa.login.button.text.public.account.creation.disabled',
|
||||
defaultMessage: 'Show me other ways to sign in',
|
||||
description: 'Button text for login when account creation is disabled',
|
||||
},
|
||||
// social auth providers
|
||||
'sso.sign.in.with': {
|
||||
id: 'sso.sign.in.with',
|
||||
@@ -97,41 +123,6 @@ const messages = defineMessages({
|
||||
description: 'Message that appears on register page if user has successfully authenticated with TPA '
|
||||
+ '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',
|
||||
},
|
||||
supportTitle: {
|
||||
id: 'zendesk.supportTitle',
|
||||
description: 'Title for the support button',
|
||||
defaultMessage: 'edX Support',
|
||||
},
|
||||
selectTicketForm: {
|
||||
id: 'zendesk.selectTicketForm',
|
||||
description: 'Select ticket form',
|
||||
defaultMessage: 'Please choose your request type:',
|
||||
},
|
||||
'registration.other.options.heading': {
|
||||
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',
|
||||
},
|
||||
'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.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
/* eslint-disable import/no-import-module-exports */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { render } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
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>;
|
||||
module.exports = RRD;
|
||||
|
||||
const TestApp = () => (
|
||||
<Router>
|
||||
<div>
|
||||
<Routes>
|
||||
<Route
|
||||
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>
|
||||
);
|
||||
|
||||
describe('EmbeddedRegistrationRoute', () => {
|
||||
const routerWrapper = () => (
|
||||
<MemoryRouter initialEntries={[REGISTER_EMBEDDED_PAGE]}>
|
||||
<TestApp />
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
|
||||
const renderedPage = embeddedRegistrationPage.querySelector('span');
|
||||
expect(renderedPage.textContent).toBe('Page not found');
|
||||
});
|
||||
|
||||
it('should render embedded register page if host query param is available in the url (embedded)', async () => {
|
||||
delete window.location;
|
||||
window.location = {
|
||||
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;
|
||||
});
|
||||
|
||||
const renderedPage = embeddedRegistrationPage.querySelector('span');
|
||||
expect(renderedPage).toBeTruthy();
|
||||
expect(renderedPage.textContent).toBe('Embedded Register Page');
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
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';
|
||||
|
||||
@@ -21,42 +17,19 @@ 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 reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</MemoryRouter>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const initialState = {
|
||||
register: {
|
||||
validationApiRateLimited: false,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(initialState);
|
||||
props = {
|
||||
floatingLabel: 'Password',
|
||||
name: 'password',
|
||||
@@ -66,29 +39,25 @@ describe('PasswordField', () => {
|
||||
});
|
||||
|
||||
it('should show/hide password on icon click', () => {
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
|
||||
|
||||
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(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
|
||||
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 () => {
|
||||
@@ -96,195 +65,31 @@ describe('PasswordField', () => {
|
||||
...props,
|
||||
value: '',
|
||||
};
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
|
||||
jest.useFakeTimers();
|
||||
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
|
||||
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(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = getByLabelText('Password');
|
||||
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
|
||||
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');
|
||||
});
|
||||
|
||||
it('should not run validations when blur is fired on password icon click', () => {
|
||||
const { container, getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
fireEvent.blur(passwordInput, {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: 'invalid',
|
||||
},
|
||||
relatedTarget: passwordIcon,
|
||||
});
|
||||
|
||||
expect(container.querySelector('div[feedback-for="password"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('should call props handle blur if available', () => {
|
||||
props = {
|
||||
...props,
|
||||
handleBlur: jest.fn(),
|
||||
};
|
||||
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
fireEvent.blur(passwordInput, {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(props.handleBlur).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should run validations on blur event when rendered from register page', () => {
|
||||
props = {
|
||||
...props,
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
const { container } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordInput = container.querySelector('input[name="password"]');
|
||||
|
||||
fireEvent.blur(passwordInput, {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'password',
|
||||
'Password criteria has not been met',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not clear error when focus is fired on password icon click when rendered from register page', () => {
|
||||
props = {
|
||||
...props,
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
fireEvent.focus(passwordIcon, {
|
||||
target: {
|
||||
name: 'passwordIcon',
|
||||
value: '',
|
||||
},
|
||||
});
|
||||
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should clear error when focus is fired on password icon click when rendered from register page', () => {
|
||||
props = {
|
||||
...props,
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
fireEvent.focus(passwordIcon, {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: 'invalid',
|
||||
},
|
||||
});
|
||||
|
||||
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleErrorChange).toHaveBeenCalledWith(
|
||||
'password',
|
||||
'',
|
||||
);
|
||||
});
|
||||
|
||||
it('should run backend validations when frontend validations pass on blur when rendered from register page', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
props = {
|
||||
...props,
|
||||
handleErrorChange: jest.fn(),
|
||||
};
|
||||
const { getByLabelText } = render(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
const passwordField = getByLabelText('Password');
|
||||
fireEvent.blur(passwordField, {
|
||||
target: {
|
||||
name: 'password',
|
||||
value: '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(reduxWrapper(<IntlPasswordField {...props} />));
|
||||
|
||||
const passwordIcon = getByLabelText('Show password');
|
||||
|
||||
fireEvent.blur(passwordIcon, {
|
||||
target: {
|
||||
name: 'passwordIcon',
|
||||
value: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(props.handleBlur).toHaveBeenCalledTimes(1);
|
||||
expect(props.handleBlur).toHaveBeenCalledWith({
|
||||
target: {
|
||||
name: 'password',
|
||||
value: 'testPassword',
|
||||
},
|
||||
});
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
181
src/common-components/tests/Logistration.test.jsx
Normal file
181
src/common-components/tests/Logistration.test.jsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { COMPLETE_STATE, LOGIN_PAGE } from '../../data/constants';
|
||||
import { RenderInstitutionButton } from '../InstitutionLogistration';
|
||||
import Logistration from '../Logistration';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
analytics.sendPageEvent = jest.fn();
|
||||
|
||||
const mockStore = configureStore();
|
||||
const IntlLogistration = injectIntl(Logistration);
|
||||
|
||||
describe('Logistration', () => {
|
||||
let store = {};
|
||||
|
||||
const secondaryProviders = {
|
||||
id: 'saml-test',
|
||||
name: 'Test University',
|
||||
loginUrl: '/dummy-auth',
|
||||
registerUrl: '/dummy_auth',
|
||||
};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</MemoryRouter>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'edX' }));
|
||||
});
|
||||
it('should render registration page', () => {
|
||||
configure({
|
||||
loggingService: { logError: jest.fn() },
|
||||
config: {
|
||||
ENVIRONMENT: 'production',
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
|
||||
},
|
||||
messages: { 'es-419': {}, de: {}, 'en-us': {} },
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
register: {
|
||||
registrationResult: { success: false, redirectUrl: '' },
|
||||
registrationError: {},
|
||||
},
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
},
|
||||
});
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration />));
|
||||
|
||||
expect(logistration.find('#main-content').find('RegistrationPage').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render login page', () => {
|
||||
store = mockStore({
|
||||
login: {
|
||||
loginResult: { success: false, redirectUrl: '' },
|
||||
},
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
},
|
||||
});
|
||||
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
|
||||
|
||||
expect(logistration.find('#main-content').find('LoginPage').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display institution login option when secondary providers are present', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: 'true',
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
login: {
|
||||
loginResult: { success: false, redirectUrl: '' },
|
||||
},
|
||||
commonComponents: {
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
providers: [],
|
||||
secondaryProviders: [secondaryProviders],
|
||||
},
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
|
||||
expect(logistration.text().includes('Institution/campus credentials')).toBe(true);
|
||||
|
||||
// on clicking "Institution/campus credentials" button, it should display institution login page
|
||||
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
|
||||
expect(logistration.text().includes('Test University')).toBe(true);
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('send tracking and page events when institutional login button is clicked', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: 'true',
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
login: {
|
||||
loginResult: { success: false, redirectUrl: '' },
|
||||
},
|
||||
commonComponents: {
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
providers: [],
|
||||
secondaryProviders: [secondaryProviders],
|
||||
},
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
|
||||
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
|
||||
|
||||
expect(analytics.sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
|
||||
expect(analytics.sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not display institution register button', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: 'true',
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
register: {
|
||||
registrationResult: { success: false, redirectUrl: '' },
|
||||
registrationError: {},
|
||||
},
|
||||
commonComponents: {
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
providers: [],
|
||||
secondaryProviders: [secondaryProviders],
|
||||
},
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
delete window.location;
|
||||
window.location = { hostname: getConfig().SITE_NAME, href: getConfig().BASE_URL };
|
||||
|
||||
const root = mount(reduxWrapper(<IntlLogistration />));
|
||||
root.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
|
||||
expect(root.text().includes('Test University')).toBe(true);
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,35 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import renderer from 'react-test-renderer';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import registerIcons from '../RegisterFaIcons';
|
||||
import SocialAuthProviders from '../SocialAuthProviders';
|
||||
|
||||
registerIcons();
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('SocialAuthProviders', () => {
|
||||
let props = {};
|
||||
|
||||
const initialState = {
|
||||
register: {
|
||||
registrationFormData: {
|
||||
configurableFormFields: {
|
||||
marketingEmailsOptIn: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const store = mockStore(initialState);
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const appleProvider = {
|
||||
id: 'oa2-apple-id',
|
||||
name: 'Apple',
|
||||
@@ -49,11 +30,11 @@ describe('SocialAuthProviders', () => {
|
||||
it('should match social auth provider with iconImage snapshot', () => {
|
||||
props = { socialAuthProviders: [appleProvider, facebookProvider] };
|
||||
|
||||
const tree = renderer.create(reduxWrapper(
|
||||
const tree = renderer.create(
|
||||
<IntlProvider locale="en">
|
||||
<SocialAuthProviders {...props} />
|
||||
</IntlProvider>,
|
||||
)).toJSON();
|
||||
).toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
@@ -67,11 +48,11 @@ describe('SocialAuthProviders', () => {
|
||||
}],
|
||||
};
|
||||
|
||||
const tree = renderer.create(reduxWrapper(
|
||||
const tree = renderer.create(
|
||||
<IntlProvider locale="en">
|
||||
<SocialAuthProviders {...props} />
|
||||
</IntlProvider>,
|
||||
)).toJSON();
|
||||
).toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
@@ -85,11 +66,11 @@ describe('SocialAuthProviders', () => {
|
||||
}],
|
||||
};
|
||||
|
||||
const tree = renderer.create(reduxWrapper(
|
||||
const tree = renderer.create(
|
||||
<IntlProvider locale="en">
|
||||
<SocialAuthProviders {...props} />
|
||||
</IntlProvider>,
|
||||
)).toJSON();
|
||||
).toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
/* eslint-disable import/no-import-module-exports */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import React from 'react';
|
||||
|
||||
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { render } from '@testing-library/react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import {
|
||||
MemoryRouter, Route, BrowserRouter as Router, Routes,
|
||||
} from 'react-router-dom';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { mount } from 'enzyme';
|
||||
import { MemoryRouter, BrowserRouter as Router, Switch } from 'react-router-dom';
|
||||
|
||||
import { UnAuthOnlyRoute } from '..';
|
||||
import { REGISTER_PAGE } from '../../data/constants';
|
||||
import { LOGIN_PAGE } from '../../data/constants';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedUser: jest.fn(),
|
||||
fetchAuthenticatedUser: jest.fn(),
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const RRD = require('react-router-dom');
|
||||
// Just render plain div with its children
|
||||
@@ -26,16 +18,16 @@ module.exports = RRD;
|
||||
const TestApp = () => (
|
||||
<Router>
|
||||
<div>
|
||||
<Routes>
|
||||
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><span>Register Page</span></UnAuthOnlyRoute>} />
|
||||
</Routes>
|
||||
<Switch>
|
||||
<UnAuthOnlyRoute path={LOGIN_PAGE} render={() => (<span>Login Page</span>)} />
|
||||
</Switch>
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
|
||||
describe('UnAuthOnlyRoute', () => {
|
||||
const routerWrapper = () => (
|
||||
<MemoryRouter initialEntries={[REGISTER_PAGE]}>
|
||||
<MemoryRouter initialEntries={[LOGIN_PAGE]}>
|
||||
<TestApp />
|
||||
</MemoryRouter>
|
||||
);
|
||||
@@ -44,30 +36,25 @@ describe('UnAuthOnlyRoute', () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should have called with forceRefresh true', async () => {
|
||||
it('should have called with forceRefresh true', () => {
|
||||
const user = {
|
||||
username: 'gonzo',
|
||||
other: 'data',
|
||||
};
|
||||
auth.getAuthenticatedUser = jest.fn(() => user);
|
||||
auth.fetchAuthenticatedUser = jest.fn(() => ({ then: () => auth.getAuthenticatedUser() }));
|
||||
|
||||
getAuthenticatedUser.mockReturnValue(user);
|
||||
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(user));
|
||||
mount(routerWrapper());
|
||||
|
||||
await act(async () => {
|
||||
await render(routerWrapper());
|
||||
});
|
||||
|
||||
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: true });
|
||||
expect(auth.fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: true });
|
||||
});
|
||||
|
||||
it('should have called with forceRefresh false', async () => {
|
||||
getAuthenticatedUser.mockReturnValue(null);
|
||||
fetchAuthenticatedUser.mockReturnValueOnce(Promise.resolve(null));
|
||||
it('should have called with forceRefresh false', () => {
|
||||
auth.getAuthenticatedUser = jest.fn(() => null);
|
||||
auth.fetchAuthenticatedUser = jest.fn(() => ({ then: () => auth.getAuthenticatedUser() }));
|
||||
|
||||
await act(async () => {
|
||||
await render(routerWrapper());
|
||||
});
|
||||
mount(routerWrapper());
|
||||
|
||||
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: false });
|
||||
expect(auth.fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: false });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import Zendesk from '../Zendesk';
|
||||
|
||||
jest.mock('react-zendesk', () => 'Zendesk');
|
||||
|
||||
describe('Zendesk Help', () => {
|
||||
it('should match login page third party auth alert message snapshot', () => {
|
||||
const tree = renderer.create(
|
||||
<IntlProvider locale="en">
|
||||
<Zendesk />
|
||||
</IntlProvider>,
|
||||
).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -10,27 +10,25 @@ exports[`SocialAuthProviders should match social auth provider with default icon
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="btn-tpa__font-container"
|
||||
className="font-container"
|
||||
>
|
||||
<span
|
||||
className="pgn__icon h-75"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-right-to-bracket "
|
||||
data-icon="right-to-bracket"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M11 7 9.6 8.4l2.6 2.6H2v2h10.2l-2.6 2.6L11 17l5-5-5-5zm9 12h-8v2h10V3H12v2h8v14z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<path
|
||||
d="M352 96h64c17.7 0 32 14.3 32 32V384c0 17.7-14.3 32-32 32H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h64c53 0 96-43 96-96V128c0-53-43-96-96-96H352c-17.7 0-32 14.3-32 32s14.3 32 32 32zm-7.5 177.4c4.8-4.5 7.5-10.8 7.5-17.4s-2.7-12.9-7.5-17.4l-144-136c-7-6.6-17.2-8.4-26-4.6s-14.5 12.5-14.5 22v72H32c-17.7 0-32 14.3-32 32v64c0 17.7 14.3 32 32 32H160v72c0 9.6 5.7 18.2 14.5 22s19 2 26-4.6l144-136z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
@@ -57,7 +55,7 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="btn-tpa__font-container"
|
||||
className="font-container"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@@ -66,14 +64,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 +91,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"
|
||||
@@ -106,7 +104,7 @@ exports[`SocialAuthProviders should match social auth provider with iconImage sn
|
||||
>
|
||||
<img
|
||||
alt="icon Apple"
|
||||
className="btn-tpa__image-icon"
|
||||
className="icon-image"
|
||||
src="https://edx.devstack.lms/logo.png"
|
||||
/>
|
||||
</div>
|
||||
@@ -135,7 +133,7 @@ exports[`SocialAuthProviders should match social auth provider with iconImage sn
|
||||
>
|
||||
<img
|
||||
alt="icon Facebook"
|
||||
className="btn-tpa__image-icon"
|
||||
className="icon-image"
|
||||
src="https://edx.devstack.lms/facebook-logo.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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"
|
||||
className="fade alert-content alert-warning mt-n2 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
>
|
||||
@@ -21,33 +21,26 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
|
||||
`;
|
||||
|
||||
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
|
||||
[
|
||||
<div
|
||||
className="fade alert-content alert-success mt-n2 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
>
|
||||
<div
|
||||
className="fade alert-content alert-success mt-n2 mb-5 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
className="pgn__alert-message-wrapper"
|
||||
>
|
||||
<div
|
||||
className="pgn__alert-message-wrapper"
|
||||
className="alert-message-content"
|
||||
>
|
||||
<div
|
||||
className="alert-message-content"
|
||||
className="alert-heading h4"
|
||||
>
|
||||
<div
|
||||
className="alert-heading h4"
|
||||
>
|
||||
Almost done!
|
||||
</div>
|
||||
<p>
|
||||
You've successfully signed into Google! We just need a little more information before you start learning with Your Platform Name Here.
|
||||
</p>
|
||||
Almost done!
|
||||
</div>
|
||||
<p>
|
||||
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>,
|
||||
<h4
|
||||
className="mt-4 mb-4"
|
||||
>
|
||||
Finish creating your account
|
||||
</h4>,
|
||||
]
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Zendesk Help should match login page third party auth alert message snapshot 1`] = `
|
||||
<Zendesk
|
||||
cookies={true}
|
||||
defer={true}
|
||||
webWidget={
|
||||
{
|
||||
"answerBot": {
|
||||
"avatar": {
|
||||
"name": {
|
||||
"*": "edX Support",
|
||||
},
|
||||
"url": undefined,
|
||||
},
|
||||
"contactOnlyAfterQuery": true,
|
||||
"suppress": false,
|
||||
"title": {
|
||||
"*": "edX Support",
|
||||
},
|
||||
},
|
||||
"chat": {
|
||||
"departments": {
|
||||
"enabled": [
|
||||
"account settings",
|
||||
"billing and payments",
|
||||
"certificates",
|
||||
"deadlines",
|
||||
"errors and technical issues",
|
||||
"other",
|
||||
"proctoring",
|
||||
],
|
||||
},
|
||||
"suppress": false,
|
||||
},
|
||||
"contactForm": {
|
||||
"attachments": true,
|
||||
"selectTicketForm": {
|
||||
"*": "Please choose your request type:",
|
||||
},
|
||||
"ticketForms": [
|
||||
{
|
||||
"fields": [
|
||||
{
|
||||
"id": "description",
|
||||
"prefill": {
|
||||
"*": "",
|
||||
},
|
||||
},
|
||||
],
|
||||
"id": 360003368814,
|
||||
"subject": false,
|
||||
},
|
||||
],
|
||||
},
|
||||
"contactOptions": {
|
||||
"enabled": false,
|
||||
},
|
||||
"helpCenter": {
|
||||
"originalArticleButton": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
@@ -1,42 +0,0 @@
|
||||
const configuration = {
|
||||
// Cookies related configs
|
||||
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN,
|
||||
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
|
||||
// Features
|
||||
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
|
||||
ENABLE_AUTO_GENERATED_USERNAME: process.env.ENABLE_AUTO_GENERATED_USERNAME || false,
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
|
||||
ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,
|
||||
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
|
||||
SHOW_REGISTRATION_LINKS: process.env.SHOW_REGISTRATION_LINKS !== 'false',
|
||||
ENABLE_HARD_CODE_OPTIONAL_FIELDS: process.env.ENABLE_HARD_CODE_OPTIONAL_FIELDS || false,
|
||||
ENABLE_IMAGE_LAYOUT: process.env.ENABLE_IMAGE_LAYOUT || false,
|
||||
// Links
|
||||
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
|
||||
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 || '',
|
||||
AUTO_GENERATED_USERNAME_EXPERIMENT_ID: process.env.AUTO_GENERATED_USERNAME_EXPERIMENT_ID || '',
|
||||
};
|
||||
|
||||
export default configuration;
|
||||
@@ -1,20 +0,0 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import algoliasearch from 'algoliasearch';
|
||||
|
||||
// initialize Algolia workers
|
||||
const initializeSearchClient = () => algoliasearch(
|
||||
getConfig().ALGOLIA_APP_ID,
|
||||
getConfig().ALGOLIA_SEARCH_API_KEY,
|
||||
);
|
||||
|
||||
const getLocationRestrictionFilter = (userCountry) => {
|
||||
if (userCountry) {
|
||||
return `NOT blocked_in:"${userCountry}" AND (allowed_in:"null" OR allowed_in:"${userCountry}")`;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export {
|
||||
initializeSearchClient,
|
||||
getLocationRestrictionFilter,
|
||||
};
|
||||
@@ -1,11 +1,9 @@
|
||||
// URL Paths
|
||||
export const LOGIN_PAGE = '/login';
|
||||
export const REGISTER_PAGE = '/register';
|
||||
export const REGISTER_EMBEDDED_PAGE = '/register-embedded';
|
||||
export const RESET_PAGE = '/reset';
|
||||
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
|
||||
export const WELCOME_PAGE = '/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';
|
||||
@@ -24,17 +22,16 @@ export const PENDING_STATE = 'pending';
|
||||
export const COMPLETE_STATE = 'complete';
|
||||
export const FAILURE_STATE = 'failure';
|
||||
export const FORBIDDEN_STATE = 'forbidden';
|
||||
export const EMBEDDED = 'embedded';
|
||||
|
||||
export const LETTER_REGEX = /[a-zA-Z]/;
|
||||
export const NUMBER_REGEX = /\d/;
|
||||
// Regex
|
||||
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}\\]$';
|
||||
export const LETTER_REGEX = /[a-zA-Z]/;
|
||||
export const NUMBER_REGEX = /\d/;
|
||||
export const VALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
|
||||
|
||||
// Query string parameters that can be passed to LMS to manage
|
||||
// things like auto-enrollment upon login and registration.
|
||||
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'register_for_free', 'track', 'is_account_recovery', 'variant', 'host', 'cta'];
|
||||
export const REDIRECT = 'redirect';
|
||||
export const APP_NAME = 'authn_mfe';
|
||||
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'save_for_later', 'register_for_free'];
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
const isOneTrustFunctionalCookieEnabled = () => !!window?.OnetrustActiveGroups?.includes('C0003');
|
||||
|
||||
export default isOneTrustFunctionalCookieEnabled;
|
||||
@@ -1,17 +0,0 @@
|
||||
import {
|
||||
createInstance,
|
||||
} from '@optimizely/react-sdk';
|
||||
|
||||
const OPTIMIZELY_SDK_KEY = process.env.OPTIMIZELY_FULL_STACK_SDK_KEY;
|
||||
|
||||
const getOptimizelyInstance = () => {
|
||||
if (OPTIMIZELY_SDK_KEY) {
|
||||
return createInstance({
|
||||
sdkKey: OPTIMIZELY_SDK_KEY,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default getOptimizelyInstance();
|
||||
@@ -12,10 +12,6 @@ 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,
|
||||
@@ -24,6 +20,10 @@ import {
|
||||
reducer as resetPasswordReducer,
|
||||
storeName as resetPasswordStoreName,
|
||||
} from '../reset-password';
|
||||
import {
|
||||
reducer as welcomePageReducers,
|
||||
storeName as welcomePageStoreName,
|
||||
} from '../welcome';
|
||||
|
||||
const createRootReducer = () => combineReducers({
|
||||
[loginStoreName]: loginReducer,
|
||||
@@ -31,6 +31,6 @@ const createRootReducer = () => combineReducers({
|
||||
[commonComponentsStoreName]: commonComponentsReducer,
|
||||
[forgotPasswordStoreName]: forgotPasswordReducer,
|
||||
[resetPasswordStoreName]: resetPasswordReducer,
|
||||
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
|
||||
[welcomePageStoreName]: welcomePageReducers,
|
||||
});
|
||||
export default createRootReducer;
|
||||
|
||||
@@ -3,9 +3,9 @@ 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';
|
||||
import { saga as welcomePageSaga } from '../welcome';
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield all([
|
||||
@@ -14,6 +14,6 @@ export default function* rootSaga() {
|
||||
commonComponentsSaga(),
|
||||
forgotPasswordSaga(),
|
||||
resetPasswordSaga(),
|
||||
authnProgressiveProfilingSaga(),
|
||||
welcomePageSaga(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
|
||||
import { APP_NAME } from '../constants';
|
||||
|
||||
export const LINK_TIMEOUT = 300;
|
||||
|
||||
/**
|
||||
* Creates an event tracker function that sends a tracking event with the given name and options.
|
||||
*
|
||||
* @param {string} name - The name of the event to be tracked.
|
||||
* @param {object} [options={}] - Additional options to be included with the event.
|
||||
* @returns {function} - A function that, when called, sends the tracking event.
|
||||
*/
|
||||
export const createEventTracker = (name, options = {}) => () => sendTrackEvent(
|
||||
name,
|
||||
{ ...options, app_name: APP_NAME },
|
||||
);
|
||||
|
||||
/**
|
||||
* Creates an event tracker function that sends a tracking event with the given name and options.
|
||||
*
|
||||
* @param {string} name - The name of the event to be tracked.
|
||||
* @param {object} [options={}] - Additional options to be included with the event.
|
||||
* @returns {function} - A function that, when called, sends the tracking event.
|
||||
*/
|
||||
export const createPageEventTracker = (name, options = null) => () => sendPageEvent(
|
||||
name,
|
||||
options,
|
||||
{ app_name: APP_NAME },
|
||||
);
|
||||
|
||||
export const createLinkTracker = (tracker, href) => (e) => {
|
||||
e.preventDefault();
|
||||
tracker();
|
||||
return setTimeout(() => { window.location.href = href; }, LINK_TIMEOUT);
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user