Compare commits

..

10 Commits

Author SHA1 Message Date
Sagirov Eugeniy
1cca916784 chore: update frontend-platform version to v4.2.0 2023-05-09 18:21:28 -03:00
Shahbaz Shabbir
c5ef676e65 feat: hide signup page on the bases of flag (#837)
Co-authored-by: attiyaishaque <atiya.ishaq@arbisoft.com>
2023-04-10 17:18:22 +05:00
Dmytro
ed4fbaaca5 feat: displaying a support link on the welcome page (#763)
This update adds a display dependency
support links on welcome page if variable
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK non-empty
in MFE configuration.
2023-03-08 10:08:59 +05:00
Stanislav
b676a058b1 fix: Remove horizontal scroll in windows (#759) 2023-03-02 16:15:55 +05:00
Dmytro
ce6cc34bde fix: link to the password reset page (#753)
When using two different deployment
approaches, with one of them we get an
incorrectly working link to the password
reset page.
2023-03-02 16:15:38 +05:00
Ghassan Maslamani
c5deeb7f99 chore(i18n): update translations (#761)
Co-authored-by: Jenkins <sre+jenkins@edx.org>
2023-03-01 12:25:31 +05:00
Dmytro
dabf556e2f fix: display or sign in with (#750) 2023-02-20 13:36:20 +05:00
Dmytro
fd4404ec28 feat: don't show support button (#746)
Do not show the button "Need help logging in?"
if there is no support page for sign-in issues.
2023-02-14 15:54:57 +05:00
Zainab Amir
8dbf20b3d2 fix: add policy banner settings to config (#708) 2022-12-19 18:07:57 +00:00
Zainab Amir
4ae9ead191 feat: make policy banner configurable (#707) 2022-12-19 18:07:57 +00:00
231 changed files with 40664 additions and 19510 deletions

29
.env
View File

@@ -13,29 +13,20 @@ ORDER_HISTORY_URL=null
REFRESH_ACCESS_TOKEN_ENDPOINT=null
SEGMENT_KEY=''
SITE_NAME=null
INFO_EMAIL=''
# ***** Cookies *****
REGISTER_CONVERSION_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_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=''
# ***** 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=''

View File

@@ -18,24 +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'
# ***** Cookies *****
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
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=''

View File

@@ -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'

View File

@@ -16,6 +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=''

View File

@@ -1,6 +1,5 @@
coverage/*
dist/
docs
node_modules/
__mocks__/
__snapshots__/

View File

@@ -1,17 +1,16 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('eslint', {
rules: {
// Temporarily update the 'indent', 'template-curly-spacing' and
// 'no-multiple-empty-lines' rules since they are causing eslint
// to fail for no apparent reason since upgrading
// 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',
},
});

View File

@@ -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|
@@ -26,4 +21,4 @@ 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/vanguards** to do it.
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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"

View File

@@ -11,16 +11,17 @@ on:
jobs:
tests:
runs-on: ubuntu-20.04
strategy:
matrix:
node: [16]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
uses: actions/checkout@v2
- name: Setup Nodejs
uses: actions/setup-node@v3
uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VER }}
node-version: ${{ matrix.node }}
- name: Install Dependencies
run: npm ci

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master

View File

@@ -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

1
.gitignore vendored
View File

@@ -5,7 +5,6 @@ node_modules
npm-debug.log
coverage
module.config.js
.env.private
dist/
src/i18n/transifex_input.json

1
.nvmrc
View File

@@ -1 +0,0 @@
18

View File

@@ -1,2 +1,2 @@
# The following users are the owners of all frontend-app-authn files
* @openedx/vanguards
* @edx/vanguards

View File

@@ -1,7 +1,6 @@
export TRANSIFEX_RESOURCE = frontend-app-authn
transifex_langs = "ar,de,de_DE,es_419,fa_IR,fr,fr_CA,hi,it,it_IT,pt,pt_PT,ru,uk,zh_CN"
transifex_langs = "ar,fr,es_419,zh_CN,it_IT,pt_PT,de_DE,uk,ru,hi"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
@@ -43,37 +42,11 @@ push_translations:
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull --filter=$(transifex_langs) \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-app-authn/src/i18n/messages:frontend-app-authn
$(intl_imports) paragon frontend-app-authn
endif
# 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

View File

@@ -72,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``
@@ -96,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)
@@ -104,15 +104,6 @@ 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`` | ``''``
edX-specific Environment Variables
**********************************
@@ -130,52 +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.
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/vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml``
file in this repo.
Reporting Security Issues
-------------------------
Please do not report security issues in public. Please email security@edx.org.
Known Issues
------------
None
License
-------
The code in this repository is licensed under the GNU Affero General Public License v3.0, unless
otherwise noted.
Please see `LICENSE <https://github.com/openedx/frontend-app-authn/blob/master/LICENSE>`_ for details.
==============================

View File

@@ -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:vanguards
type: 'service'
lifecycle: 'production'

View File

@@ -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

View File

@@ -3,6 +3,6 @@
nick: Authn MFE
oeps: {}
owner: openedx/vanguards
owner: edx/vanguards
openedx-release:
ref: master

41874
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -32,55 +32,57 @@
"url": "https://github.com/openedx/frontend-app-authn/issues"
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-platform": "^5.0.0",
"@edx/paragon": "20.46.2",
"@fortawesome/fontawesome-svg-core": "6.4.2",
"@fortawesome/free-brands-svg-icons": "6.4.2",
"@fortawesome/free-solid-svg-icons": "6.4.2",
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-cookie-policy-banner": "2.2.0",
"@edx/frontend-platform": "4.2.0",
"@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",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.2.5",
"@testing-library/react": "^12.1.5",
"@testing-library/react-hooks": "^8.0.1",
"algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.14.0",
"@redux-devtools/extension": "3.2.3",
"classnames": "2.3.2",
"core-js": "3.32.0",
"core-js": "3.26.1",
"extract-react-intl-messages": "4.1.1",
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.0",
"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.3.1",
"react-loading-skeleton": "2.2.0",
"react-onclickoutside": "6.12.2",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router": "6.15.0",
"react-router-dom": "6.15.0",
"react-zendesk": "^0.1.13",
"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.2.3",
"redux-saga": "1.2.1",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.0",
"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": "12.9.8",
"@edx/frontend-build": "11.0.2",
"@edx/reactifex": "1.1.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.8.0",
"babel-plugin-formatjs": "10.5.3",
"babel-plugin-formatjs": "10.3.31",
"enzyme": "3.11.0",
"eslint-plugin-import": "2.28.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.6.2",
"react-test-renderer": "^17.0.2"
"jest": "27.5.1",
"react-test-renderer": "16.14.0"
}
}

View File

@@ -4,11 +4,6 @@
<title>Authn | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.6/iframeResizer.contentWindow.min.js"
integrity="sha512-R7Piufj0/o6jG9ZKrAvS2dblFr2kkuG4XVQwStX+/4P+KwOLUXn2DXy0l1AJDxxqGhkM/FJllZHG2PKOAheYzg=="
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<% if (process.env.OPTIMIZELY_URL) { %>
<script
src="<%= process.env.OPTIMIZELY_URL %>"
@@ -18,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>

View File

@@ -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 { EmbeddableRegistrationPage } 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><EmbeddableRegistrationPage /></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>
);

View 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);

View 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);

View 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);

View 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;

View 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);

View 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);

View 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);

View File

@@ -0,0 +1 @@
export { default } from './BaseComponent';

View File

@@ -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}, {username}!',
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;

View File

@@ -3,14 +3,16 @@ import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { DefaultLargeLayout, DefaultMediumLayout, DefaultSmallLayout } from './index';
import LargeLayout from '../LargeLayout';
import MediumLayout from '../MediumLayout';
import SmallLayout from '../SmallLayout';
describe('Default Layout tests', () => {
it('should display the form passed as a child in SmallScreenLayout', () => {
describe('ScreenLayout', () => {
it('should display the form, pass as a child in SmallScreenLayout', () => {
const smallScreen = mount(
<IntlProvider locale="en">
<div>
<DefaultSmallLayout />
<SmallLayout />
<form>
<input type="text" />
</form>
@@ -20,11 +22,11 @@ describe('Default Layout tests', () => {
expect(smallScreen.find('form').exists()).toEqual(true);
});
it('should display the form passed as a child in MediumScreenLayout', () => {
it('should display the form, pass as a child in MediumScreenLayout', () => {
const mediumScreen = mount(
<IntlProvider locale="en">
<div>
<DefaultMediumLayout />
<MediumLayout />
<form>
<input type="text" />
</form>
@@ -34,11 +36,11 @@ describe('Default Layout tests', () => {
expect(mediumScreen.find('form').exists()).toEqual(true);
});
it('should display the form passed as a child in LargeScreenLayout', () => {
it('should display the form, pass as a child in LargeScreenLayout', () => {
const largeScreen = mount(
<IntlProvider locale="en">
<div>
<DefaultLargeLayout />
<LargeLayout />
<form>
<input type="text" />
</form>

View File

@@ -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 '@edx/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;

View File

@@ -1,50 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import classNames from 'classnames';
import messages from './messages';
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',
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
)}
>
<span className="mr-2">{formatMessage(messages['start.learning'])}</span>
<span className="text-accent-a d-inline-block">
{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>
</>
);
};
export default MediumLayout;

View File

@@ -1,39 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import classNames from 'classnames';
import messages from './messages';
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 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">{formatMessage(messages['start.learning'])}</span>
<span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</h1>
</div>
</div>
</span>
);
};
export default SmallLayout;

View File

@@ -1,3 +0,0 @@
export { default as DefaultLargeLayout } from './LargeLayout';
export { default as DefaultMediumLayout } from './MediumLayout';
export { default as DefaultSmallLayout } from './SmallLayout';

View File

@@ -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;

View File

@@ -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 '@edx/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;

View File

@@ -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 '@edx/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;

View File

@@ -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 '@edx/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;

View File

@@ -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 '@edx/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;

View File

@@ -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';

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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 '@edx/paragon';
import PropTypes from 'prop-types';
import messages from './messages';
const LargeLayout = ({ username }) => {
const { formatMessage } = useIntl();
return (
<div className="w-50 d-flex">
<div className="col-md-10 bg-light-200 p-0">
<Hyperlink destination={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, username })}
</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 = {
username: PropTypes.string.isRequired,
};
export default LargeLayout;

View File

@@ -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 '@edx/paragon';
import PropTypes from 'prop-types';
import messages from './messages';
const MediumLayout = ({ username }) => {
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, username })}
</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 = {
username: PropTypes.string.isRequired,
};
export default MediumLayout;

View File

@@ -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 '@edx/paragon';
import PropTypes from 'prop-types';
import messages from './messages';
const SmallLayout = ({ username }) => {
const { formatMessage } = useIntl();
return (
<div className="min-vw-100 bg-light-200">
<div className="col-md-12 small-screen-top-stripe" />
<Hyperlink destination={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">
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
</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 = {
username: PropTypes.string.isRequired,
};
export default SmallLayout;

View File

@@ -1,3 +0,0 @@
export { default as AuthLargeLayout } from './LargeLayout';
export { default as AuthMediumLayout } from './MediumLayout';
export { default as AuthSmallLayout } from './SmallLayout';

View File

@@ -1,4 +0,0 @@
const IMAGE_LAYOUT = 'image-layout';
const DEFAULT_LAYOUT = 'default-layout';
export { DEFAULT_LAYOUT, IMAGE_LAYOUT };

View File

@@ -1,87 +0,0 @@
import React, { useEffect, useState } from 'react';
import { breakpoints } from '@edx/paragon';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import MediaQuery from 'react-responsive';
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';
import { DEFAULT_LAYOUT, IMAGE_LAYOUT } from './data/constants';
const BaseContainer = ({ children, showWelcomeBanner, username }) => {
const [baseContainerVersion, setBaseContainerVersion] = useState(DEFAULT_LAYOUT);
useEffect(() => {
const initRebrandExperiment = () => {
if (window.experiments?.rebrandExperiment) {
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
} else {
window.experiments = window.experiments || {};
window.experiments.rebrandExperiment = {};
window.experiments.rebrandExperiment.handleLoaded = () => {
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
};
}
};
initRebrandExperiment();
}, []);
if (baseContainerVersion === IMAGE_LAYOUT) {
return (
<div className="layout">
<MediaQuery maxWidth={breakpoints.extraSmall.maxWidth - 1}>
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <ImageExtraSmallLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.small.minWidth} maxWidth={breakpoints.small.maxWidth - 1}>
{showWelcomeBanner ? <AuthSmallLayout username={username} /> : <ImageSmallLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
{showWelcomeBanner ? <AuthMediumLayout username={username} /> : <ImageMediumLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
{showWelcomeBanner ? <AuthLargeLayout username={username} /> : <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 username={username} /> : <DefaultSmallLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
{showWelcomeBanner ? <AuthMediumLayout username={username} /> : <DefaultMediumLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.extraLarge.minWidth}>
{showWelcomeBanner ? <AuthLargeLayout username={username} /> : <DefaultLargeLayout />}
</MediaQuery>
<div className={classNames('content', { 'align-items-center mt-0': showWelcomeBanner })}>
{children}
</div>
</div>
</>
);
};
BaseContainer.defaultProps = {
showWelcomeBanner: false,
username: null,
};
BaseContainer.propTypes = {
children: PropTypes.node.isRequired,
showWelcomeBanner: PropTypes.bool,
username: PropTypes.string,
};
export default BaseContainer;

View File

@@ -1,43 +0,0 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { Context as ResponsiveContext } from 'react-responsive';
import BaseContainer from '../index';
const LargeScreen = {
wrappingComponent: ResponsiveContext.Provider,
wrappingComponentProps: { value: { width: 1200 } },
};
describe('Base component tests', () => {
it('should should default layout', () => {
const baseContainer = mount(
<IntlProvider locale="en">
<BaseContainer />
</IntlProvider>,
LargeScreen,
);
expect(baseContainer.find('.banner__image').exists()).toBeFalsy();
expect(baseContainer.find('.large-screen-svg-primary').exists()).toBeTruthy();
});
it('[experiment] should show image layout for treatment group', () => {
window.experiments = {
rebrandExperiment: {
variation: 'image-layout',
},
};
const baseContainer = mount(
<IntlProvider locale="en">
<BaseContainer />
</IntlProvider>,
LargeScreen,
);
expect(baseContainer.find('.banner__image').exists()).toBeTruthy();
});
});

View File

@@ -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;

View File

@@ -1,23 +1,19 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Button, Form,
Icon,
} from '@edx/paragon';
import { Login } from '@edx/paragon/icons';
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 disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
@@ -37,7 +33,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}
@@ -48,18 +44,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>
</>
@@ -75,8 +69,8 @@ const EnterpriseSSO = (props) => {
onClick={(e) => handleClick(e)}
>
{disablePublicAccountCreation
? formatMessage(messages['enterprisetpa.login.button.text.public.account.creation.disabled'])
: formatMessage(messages['enterprisetpa.login.button.text'])}
? intl.formatMessage(messages['enterprisetpa.login.button.text.public.account.creation.disabled'])
: intl.formatMessage(messages['enterprisetpa.login.button.text'])}
</Button>
</Form>
</div>
@@ -107,6 +101,7 @@ EnterpriseSSO.propTypes = {
loginUrl: PropTypes.string,
registerUrl: PropTypes.string,
}),
intl: intlShape.isRequired,
};
export default EnterpriseSSO;
export default injectIntl(EnterpriseSSO);

View File

@@ -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}
>

View File

@@ -1,16 +1,13 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
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,7 +51,7 @@ 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}
@@ -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);

View File

@@ -1,10 +1,9 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthService } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Icon,
Tab,
@@ -12,32 +11,20 @@ import {
} from '@edx/paragon';
import { ChevronLeft } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import { Navigate, useNavigate } from 'react-router-dom';
import { Redirect } from 'react-router-dom';
import BaseContainer from '../base-container';
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
import {
tpaProvidersSelector,
} from '../common-components/data/selectors';
import messages from '../common-components/messages';
import BaseComponent from '../base-component';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import {
getTpaHint, getTpaProvider, updatePathWithQueryParams,
} from '../data/utils';
import { getTpaHint, updatePathWithQueryParams } from '../data/utils';
import { LoginPage } from '../login';
import { RegistrationPage } from '../register';
import { backupRegistrationForm } from '../register/data/actions';
import messages from './messages';
const Logistration = (props) => {
const { selectedPage, tpaProviders } = props;
const tpaHint = getTpaHint();
const {
providers, secondaryProviders,
} = tpaProviders;
const { formatMessage } = useIntl();
const { intl, selectedPage } = props;
const tpa = getTpaHint();
const [institutionLogin, setInstitutionLogin] = useState(false);
const [key, setKey] = useState('');
const navigate = useNavigate();
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
useEffect(() => {
@@ -47,12 +34,6 @@ const Logistration = (props) => {
}
});
useEffect(() => {
if (disablePublicAccountCreation) {
navigate(updatePathWithQueryParams(LOGIN_PAGE));
}
}, [navigate, disablePublicAccountCreation]);
const handleInstitutionLogin = (e) => {
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
if (typeof e === 'string') {
@@ -66,10 +47,6 @@ const Logistration = (props) => {
const handleOnSelect = (tabKey) => {
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
props.clearThirdPartyAuthContextErrorMessage();
if (tabKey === LOGIN_PAGE) {
props.backupRegistrationForm();
}
setKey(tabKey);
};
@@ -78,23 +55,19 @@ const Logistration = (props) => {
<Icon src={ChevronLeft} className="left-icon" />
<span className="ml-2">
{selectedPage === LOGIN_PAGE
? formatMessage(messages['logistration.sign.in'])
: formatMessage(messages['logistration.register'])}
? intl.formatMessage(messages['logistration.sign.in'])
: intl.formatMessage(messages['logistration.register'])}
</span>
</div>
);
const isValidTpaHint = () => {
const { provider } = getTpaProvider(tpaHint, providers, secondaryProviders);
return !!provider;
};
return (
<BaseContainer>
<BaseComponent>
<div>
{disablePublicAccountCreation
? (
<>
<Redirect to={updatePathWithQueryParams(LOGIN_PAGE)} />
{institutionLogin && (
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
<Tab title={tabTitle} eventKey={LOGIN_PAGE} />
@@ -102,7 +75,7 @@ const Logistration = (props) => {
)}
<div id="main-content" className="main-content">
{!institutionLogin && (
<h3 className="mb-4.5">{formatMessage(messages['logistration.sign.in'])}</h3>
<h3 className="mb-4.5">{intl.formatMessage(messages['logistration.sign.in'])}</h3>
)}
<LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
</div>
@@ -116,14 +89,18 @@ const Logistration = (props) => {
<Tab title={tabTitle} eventKey={selectedPage === LOGIN_PAGE ? LOGIN_PAGE : REGISTER_PAGE} />
</Tabs>
)
: (!isValidTpaHint() && (
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={handleOnSelect}>
<Tab title={formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
<Tab title={formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_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 && (
<Navigate to={updatePathWithQueryParams(key)} replace />
<Redirect to={updatePathWithQueryParams(key)} />
)}
<div id="main-content" className="main-content">
{selectedPage === LOGIN_PAGE
@@ -138,39 +115,17 @@ const Logistration = (props) => {
</div>
)}
</div>
</BaseContainer>
</BaseComponent>
);
};
Logistration.propTypes = {
intl: intlShape.isRequired,
selectedPage: PropTypes.string,
backupRegistrationForm: PropTypes.func.isRequired,
clearThirdPartyAuthContextErrorMessage: PropTypes.func.isRequired,
tpaProviders: PropTypes.shape({
providers: PropTypes.arrayOf(PropTypes.shape({})),
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
}),
};
Logistration.defaultProps = {
tpaProviders: {
providers: [],
secondaryProviders: [],
},
};
Logistration.defaultProps = {
selectedPage: REGISTER_PAGE,
};
const mapStateToProps = state => ({
tpaProviders: tpaProvidersSelector(state),
});
export default connect(
mapStateToProps,
{
backupRegistrationForm,
clearThirdPartyAuthContextErrorMessage,
},
)(Logistration);
export default injectIntl(Logistration);

View File

@@ -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>
);
}

View File

@@ -1,7 +1,6 @@
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 '@edx/paragon';
@@ -10,94 +9,33 @@ import {
} 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}
@@ -151,7 +89,6 @@ PasswordField.defaultProps = {
handleBlur: null,
handleFocus: null,
handleChange: () => {},
handleErrorChange: null,
showRequirements: true,
autoComplete: null,
};
@@ -163,11 +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,
};
export default PasswordField;
export default injectIntl(PasswordField);

View File

@@ -1,23 +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,
} from '../data/constants';
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,
finishAuthUrl, redirectUrl, redirectToWelcomePage, success, optionalFields,
} = props;
let finalRedirectUrl = '';
@@ -32,69 +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);
// 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,
};
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,
};
export default RedirectLogistration;

View File

@@ -1,18 +1,16 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon } from '@edx/paragon';
import { Login } from '@edx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
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';
const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl();
const { referrer, socialAuthProviders } = props;
function SocialAuthProviders(props) {
const { intl, referrer, socialAuthProviders } = props;
function handleSubmit(e) {
e.preventDefault();
@@ -32,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,
@@ -63,6 +60,7 @@ SocialAuthProviders.defaultProps = {
};
SocialAuthProviders.propTypes = {
intl: intlShape.isRequired,
referrer: PropTypes.string,
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
@@ -71,8 +69,7 @@ SocialAuthProviders.propTypes = {
iconImage: PropTypes.string,
loginUrl: PropTypes.string,
registerUrl: PropTypes.string,
skipRegistrationForm: PropTypes.bool,
})),
};
export default SocialAuthProviders;
export default injectIntl(SocialAuthProviders);

View 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;

View File

@@ -1,52 +1,42 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
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 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) {
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);

View File

@@ -1,18 +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 {
DEFAULT_REDIRECT_URL,
} from '../data/constants';
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);
@@ -29,14 +27,10 @@ const UnAuthOnlyRoute = ({ children }) => {
return null;
}
return children;
return <Route {...props} />;
}
return null;
};
UnAuthOnlyRoute.propTypes = {
children: PropTypes.node.isRequired,
return <></>;
};
export default UnAuthOnlyRoute;

View File

@@ -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;

View File

@@ -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,
});

View File

@@ -1,61 +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 = {
fieldDescriptions: {
fields: {},
},
optionalFields: {
fields: {},
extended_profile: [],
},
extendedProfile: [],
fieldDescriptions: {},
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,
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;

View File

@@ -1,6 +1,7 @@
import { logError } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
import {
getThirdPartyAuthContextBegin,
getThirdPartyAuthContextFailure,
@@ -10,17 +11,18 @@ import {
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));
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
yield put(getThirdPartyAuthContextSuccess(
fieldDescriptions, optionalFields, thirdPartyAuthContext,
));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);

View File

@@ -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,
}),
);

View File

@@ -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' }),
),
};
}

View File

@@ -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,
},
},
);
});
});

View File

@@ -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';

View File

@@ -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,12 +34,27 @@ 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',
@@ -97,21 +128,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:',
},
});
export default messages;

View File

@@ -1,72 +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 { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { REGISTER_EMBEDDED_PAGE } from '../../data/constants';
import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute';
import {
MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom';
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>}
/>
</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 () => {
embeddedRegistrationPage = await mount(routerWrapper());
});
expect(embeddedRegistrationPage.find('span').exists()).toBeFalsy();
});
it('should render embedded register page if host query param is available in the url (embedded)', async () => {
delete window.location;
window.location = {
href: getConfig().BASE_URL.concat(REGISTER_EMBEDDED_PAGE),
search: '?host=http://localhost/host-websit',
};
let embeddedRegistrationPage = null;
await act(async () => {
embeddedRegistrationPage = await mount(routerWrapper());
});
expect(embeddedRegistrationPage.find('span').exists()).toBeTruthy();
expect(embeddedRegistrationPage.find('span').text()).toBe('Embedded Register Page');
});
});

View File

@@ -1,13 +1,9 @@
import React from 'react';
import { Provider } from 'react-redux';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import { fetchRealtimeValidations } from '../../register/data/actions';
import FormGroup from '../FormGroup';
import PasswordField from '../PasswordField';
@@ -30,27 +26,10 @@ describe('FormGroup', () => {
});
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',
@@ -60,7 +39,7 @@ describe('PasswordField', () => {
});
it('should show/hide password on icon click', () => {
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
passwordField.find('button[aria-label="Show password"]').simulate('click');
expect(passwordField.find('input').prop('type')).toEqual('text');
@@ -70,7 +49,7 @@ describe('PasswordField', () => {
});
it('should show password requirement tooltip on focus', async () => {
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
jest.useFakeTimers();
await act(async () => {
passwordField.find('input').simulate('focus');
@@ -88,7 +67,7 @@ describe('PasswordField', () => {
};
jest.useFakeTimers();
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
await act(async () => {
passwordField.find('input').simulate('focus');
jest.runAllTimers();
@@ -101,7 +80,7 @@ describe('PasswordField', () => {
});
it('should update password requirement checks', async () => {
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
const passwordField = mount(<IntlProvider locale="en"><IntlPasswordField {...props} /></IntlProvider>);
jest.useFakeTimers();
await act(async () => {
passwordField.find('input').simulate('focus');
@@ -113,142 +92,4 @@ describe('PasswordField', () => {
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon text-success mr-1');
});
it('should not run validations when blur is fired on password icon click', () => {
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
target: {
name: 'password',
value: 'invalid',
},
relatedTarget: {
name: 'passwordIcon',
},
});
expect(passwordField.find('div[feedback-for="password"]').exists()).toBeFalsy();
});
it('should call props handle blur if available', () => {
props = {
...props,
handleBlur: jest.fn(),
};
const passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('input#password').simulate('blur', {
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 passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('input#password').simulate('blur', {
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 passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('button[aria-label="Show password"]').simulate('focus', {
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 passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('button[aria-label="Show password"]').simulate('focus', {
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 passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
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 passwordField = mount(reduxWrapper(<IntlPasswordField {...props} />));
passwordField.find('button[aria-label="Show password"]').simulate('blur', {
target: {
name: 'passwordIcon',
value: undefined,
},
});
expect(props.handleBlur).toHaveBeenCalledTimes(1);
expect(props.handleBlur).toHaveBeenCalledWith({
target: {
name: 'password',
value: 'testPassword',
},
});
});
});

View File

@@ -2,25 +2,20 @@ import React from 'react';
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
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 Logistration from './Logistration';
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
import { RenderInstitutionButton } from '../common-components/InstitutionLogistration';
import {
COMPLETE_STATE, LOGIN_PAGE,
} from '../data/constants';
import { backupRegistrationForm } from '../register/data/actions';
import { COMPLETE_STATE, LOGIN_PAGE } from '../../data/constants';
import { RenderInstitutionButton } from '../InstitutionLogistration';
import Logistration from '../Logistration';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-platform/auth');
analytics.sendPageEvent = jest.fn();
const mockStore = configureStore();
const IntlLogistration = injectIntl(Logistration);
@@ -43,47 +38,10 @@ describe('Logistration', () => {
</IntlProvider>
);
const initialState = {
register: {
registrationFormData: {
configurableFormFields: {
marketingEmailsOptIn: true,
},
formFields: {
name: '', email: '', username: '', password: '',
},
emailSuggestion: {
suggestion: '', type: '',
},
errors: {
name: '', email: '', username: '', password: '',
},
},
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
usernameSuggestions: [],
validationApiRateLimited: false,
},
commonComponents: {
thirdPartyAuthContext: {
providers: [],
secondaryProviders: [],
},
},
login: {
loginResult: { success: false, redirectUrl: '' },
},
};
beforeEach(() => {
store = mockStore(initialState);
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(() => ({
userId: 3,
username: 'test-user',
})),
}));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'edX' }));
});
it('should render registration page', () => {
configure({
loggingService: { logError: jest.fn() },
config: {
@@ -92,19 +50,34 @@ describe('Logistration', () => {
},
messages: { 'es-419': {}, de: {}, 'en-us': {} },
});
});
it('should render registration page', () => {
mergeConfig({
ALLOW_PUBLIC_ACCOUNT_CREATION: true,
});
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} />));
@@ -118,7 +91,9 @@ describe('Logistration', () => {
});
store = mockStore({
...initialState,
login: {
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
@@ -148,7 +123,9 @@ describe('Logistration', () => {
});
store = mockStore({
...initialState,
login: {
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
@@ -179,7 +156,9 @@ describe('Logistration', () => {
});
store = mockStore({
...initialState,
login: {
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
@@ -195,8 +174,8 @@ describe('Logistration', () => {
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
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: '',
@@ -209,7 +188,10 @@ describe('Logistration', () => {
});
store = mockStore({
...initialState,
register: {
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
},
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
@@ -232,18 +214,4 @@ describe('Logistration', () => {
DISABLE_ENTERPRISE_LOGIN: '',
});
});
it('should fire action to backup registration form on tab click', () => {
store.dispatch = jest.fn(store.dispatch);
const logistration = mount(reduxWrapper(<IntlLogistration />));
logistration.find('a[data-rb-event-key="/login"]').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm());
});
it('should clear tpa context errorMessage tab click', () => {
store.dispatch = jest.fn(store.dispatch);
const logistration = mount(reduxWrapper(<IntlLogistration />));
logistration.find('a[data-rb-event-key="/login"]').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage());
});
});

View File

@@ -1,22 +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 * as auth from '@edx/frontend-platform/auth';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
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';
import {
MemoryRouter, Route, BrowserRouter as Router, Routes,
} from 'react-router-dom';
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
@@ -27,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>
);
@@ -45,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 mount(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 mount(routerWrapper());
});
mount(routerWrapper());
expect(fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: false });
expect(auth.fetchAuthenticatedUser).toBeCalledWith({ forceRefresh: false });
});
});

View File

@@ -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();
});
});

View File

@@ -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"
@@ -106,7 +104,7 @@ Array [
>
<img
alt="icon Apple"
className="btn-tpa__image-icon"
className="icon-image"
src="https://edx.devstack.lms/logo.png"
/>
</div>
@@ -135,7 +133,7 @@ Array [
>
<img
alt="icon Facebook"
className="btn-tpa__image-icon"
className="icon-image"
src="https://edx.devstack.lms/facebook-logo.png"
/>
</div>

View File

@@ -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`] = `
Array [
<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>
`;

View File

@@ -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={
Object {
"answerBot": Object {
"avatar": Object {
"name": Object {
"*": "edX Support",
},
"url": undefined,
},
"contactOnlyAfterQuery": true,
"suppress": false,
"title": Object {
"*": "edX Support",
},
},
"chat": Object {
"departments": Object {
"enabled": Array [
"account settings",
"billing and payments",
"certificates",
"deadlines",
"errors and technical issues",
"other",
"proctoring",
],
},
"suppress": false,
},
"contactForm": Object {
"attachments": true,
"selectTicketForm": Object {
"*": "Please choose your request type:",
},
"ticketForms": Array [
Object {
"fields": Array [
Object {
"id": "description",
"prefill": Object {
"*": "",
},
},
],
"id": 360003368814,
"subject": false,
},
],
},
"contactOptions": Object {
"enabled": false,
},
"helpCenter": Object {
"originalArticleButton": true,
},
}
}
/>
`;

View File

@@ -1,37 +0,0 @@
const configuration = {
// Cookies related configs
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN,
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null,
// Features
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
// Links
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: process.env.AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK || null,
LOGIN_ISSUE_SUPPORT_LINK: process.env.LOGIN_ISSUE_SUPPORT_LINK || null,
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || null,
POST_REGISTRATION_REDIRECT_URL: process.env.POST_REGISTRATION_REDIRECT_URL || '',
PRIVACY_POLICY: process.env.PRIVACY_POLICY || null,
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null,
TOS_LINK: process.env.TOS_LINK || null,
// Base container images
BANNER_IMAGE_LARGE: process.env.BANNER_IMAGE_LARGE || '',
BANNER_IMAGE_MEDIUM: process.env.BANNER_IMAGE_MEDIUM || '',
BANNER_IMAGE_SMALL: process.env.BANNER_IMAGE_SMALL || '',
BANNER_IMAGE_EXTRA_SMALL: process.env.BANNER_IMAGE_EXTRA_SMALL || '',
// Recommendation constants
GENERAL_RECOMMENDATIONS: process.env.GENERAL_RECOMMENDATIONS || '[]',
// Miscellaneous
INFO_EMAIL: process.env.INFO_EMAIL || '',
ZENDESK_KEY: process.env.ZENDESK_KEY,
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
ALGOLIA_APP_ID: process.env.ALGOLIA_APP_ID || '',
ALGOLIA_SEARCH_API_KEY: process.env.ALGOLIA_SEARCH_API_KEY || '',
};
export default configuration;

View File

@@ -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,
};

View File

@@ -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,16 +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 AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'save_for_later', 'register_for_free'];

View File

@@ -1,3 +0,0 @@
const isOneTrustFunctionalCookieEnabled = () => !!window?.OnetrustActiveGroups?.includes('C0003');
export default isOneTrustFunctionalCookieEnabled;

View File

@@ -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();

View File

@@ -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;

View File

@@ -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(),
]);
}

View File

@@ -1,16 +0,0 @@
import { getLocationRestrictionFilter } from '../algolia';
describe('algoliaUtilsTests', () => {
it('test getLocationRestrictionFilter returns filter if country is passed', () => {
const countryCode = 'PK';
const filter = getLocationRestrictionFilter(countryCode);
const expectedFilter = `NOT blocked_in:"${countryCode}" AND (allowed_in:"null" OR allowed_in:"${countryCode}")`;
expect(filter).toEqual(expectedFilter);
});
it('test getLocationRestrictionFilter returns empty string if country is not passed', () => {
const countryCode = '';
const filter = getLocationRestrictionFilter(countryCode);
const expectedFilter = '';
expect(filter).toEqual(expectedFilter);
});
});

View File

@@ -1,11 +1,21 @@
import { getConfig } from '@edx/frontend-platform';
import Cookies from 'universal-cookie';
export default function setCookie(cookieName, cookieValue, cookieExpiry) {
export function setCookie(cookieName, cookieValue, cookieExpiry) {
const cookies = new Cookies();
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
const options = { domain: getConfig().COOKIE_DOMAIN, path: '/' };
if (cookieExpiry) {
options.expires = cookieExpiry;
}
cookies.set(cookieName, cookieValue, options);
}
export default function setSurveyCookie(surveyType) {
const cookieName = getConfig().USER_SURVEY_COOKIE_NAME;
if (cookieName) {
const signupTimestamp = (new Date()).getTime();
// set expiry to exactly 24 hours from now
const cookieExpiry = new Date(signupTimestamp + 1 * 864e5);
setCookie(cookieName, surveyType, cookieExpiry);
}
}

View File

@@ -49,8 +49,8 @@ export const updatePathWithQueryParams = (path) => {
return `${path}${queryParams}`;
};
export const getAllPossibleQueryParams = (locationURl = null) => {
const urlParams = locationURl ? QueryString.parseUrl(locationURl).query : QueryString.parse(window.location.search);
export const getAllPossibleQueryParam = () => {
const urlParams = QueryString.parse(window.location.search);
const params = {};
Object.entries(urlParams).forEach(([key, value]) => {
if (AUTH_PARAMS.indexOf(key) > -1) {
@@ -76,8 +76,3 @@ export const windowScrollTo = (options) => {
return window.scrollTo(options.top, options.left);
};
export const isHostAvailableInQueryParams = () => {
const queryParams = getAllPossibleQueryParams();
return 'host' in queryParams;
};

View File

@@ -1,5 +1,5 @@
import { LOGIN_PAGE } from '../constants';
import { updatePathWithQueryParams } from '../utils/dataUtils';
import { updatePathWithQueryParams } from './dataUtils';
describe('updatePathWithQueryParams', () => {
it('should append query params into the path', () => {

View File

@@ -1,11 +1,10 @@
export {
getTpaProvider,
getTpaHint,
getAllPossibleQueryParams,
getActivationStatus,
isHostAvailableInQueryParams,
updatePathWithQueryParams,
getAllPossibleQueryParam,
getActivationStatus,
windowScrollTo,
} from './dataUtils';
export { default as AsyncActionType } from './reduxUtils';
export { default as setCookie } from './cookies';
export { default as setSurveyCookie, setCookie } from './cookies';

View File

@@ -1,4 +1,4 @@
import AsyncActionType from '../utils/reduxUtils';
import AsyncActionType from './reduxUtils';
describe('AsyncActionType', () => {
it('should return well formatted action strings', () => {

View File

@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
const FormFieldRenderer = (props) => {
let formField = null;
const {
className, errorMessage, fieldData, onChangeHandler, isRequired, value,
errorMessage, fieldData, onChangeHandler, isRequired, value,
} = props;
const handleFocus = (e) => {
@@ -26,7 +26,6 @@ const FormFieldRenderer = (props) => {
formField = (
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
<Form.Control
className={className}
as="select"
name={fieldData.name}
value={value}
@@ -55,7 +54,6 @@ const FormFieldRenderer = (props) => {
formField = (
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
<Form.Control
className={className}
as="textarea"
name={fieldData.name}
value={value}
@@ -78,7 +76,6 @@ const FormFieldRenderer = (props) => {
formField = (
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
<Form.Control
className={className}
name={fieldData.name}
value={value}
aria-invalid={isRequired && Boolean(errorMessage)}
@@ -100,7 +97,6 @@ const FormFieldRenderer = (props) => {
formField = (
<Form.Group isInvalid={!!(isRequired && errorMessage)}>
<Form.Checkbox
className={className}
id={fieldData.name}
checked={!!value}
name={fieldData.name}
@@ -128,7 +124,6 @@ const FormFieldRenderer = (props) => {
return formField;
};
FormFieldRenderer.defaultProps = {
className: '',
value: '',
handleBlur: null,
handleFocus: null,
@@ -137,22 +132,17 @@ FormFieldRenderer.defaultProps = {
};
FormFieldRenderer.propTypes = {
className: PropTypes.string,
fieldData: PropTypes.shape({
type: PropTypes.string,
label: PropTypes.string,
name: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
}).isRequired,
onChangeHandler: PropTypes.func.isRequired,
handleBlur: PropTypes.func,
handleFocus: PropTypes.func,
errorMessage: PropTypes.string,
isRequired: PropTypes.bool,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
]),
value: PropTypes.string,
};
export default FormFieldRenderer;

View File

@@ -1,2 +1 @@
/* eslint-disable import/prefer-default-export */
export { default as FormFieldRenderer } from './FieldRenderer';
export { default } from './FieldRenderer';

View File

@@ -1,6 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { mount } from 'enzyme';
import FieldRenderer from '../FieldRenderer';
@@ -25,7 +24,7 @@ describe('FieldRendererTests', () => {
type: 'select',
label: 'Year of Birth',
name: 'yob-field',
options: [['1997', '1997'], ['1998', '1998']],
options: [['1997', 1997], ['1998', 1998]],
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
@@ -83,7 +82,7 @@ describe('FieldRendererTests', () => {
it('should render checkbox field', () => {
const fieldData = {
type: 'checkbox',
label: `I agree that ${getConfig().SITE_NAME} may send me marketing messages.`,
label: 'I agree that edX may send me marketing messages.',
name: 'marketing-emails-opt-in-field',
};
@@ -92,7 +91,7 @@ describe('FieldRendererTests', () => {
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
expect(field.prop('type')).toEqual('checkbox');
expect(fieldRenderer.find('label').text()).toEqual(fieldData.label);
expect(fieldRenderer.find('label').text()).toEqual('I agree that edX may send me marketing messages.');
expect(value).toEqual(true);
});

View File

@@ -1,22 +1,21 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import { CheckCircle, Error } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';
import {
COMPLETE_STATE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR,
} from '../data/constants';
import { PASSWORD_RESET } from '../reset-password/data/constants';
import messages from './messages';
const ForgotPasswordAlert = (props) => {
const { formatMessage } = useIntl();
const { email, emailError } = props;
const { email, emailError, intl } = props;
let message = '';
let heading = formatMessage(messages['forgot.password.error.alert.title']);
let heading = intl.formatMessage(messages['forgot.password.error.alert.title']);
let { status } = props;
if (emailError) {
@@ -25,7 +24,7 @@ const ForgotPasswordAlert = (props) => {
switch (status) {
case COMPLETE_STATE:
heading = formatMessage(messages['confirmation.message.title']);
heading = intl.formatMessage(messages['confirmation.message.title']);
message = (
<FormattedMessage
id="forgot.password.confirmation.message"
@@ -37,7 +36,7 @@ const ForgotPasswordAlert = (props) => {
email: <span className="data-hj-suppress">{email}</span>,
supportLink: (
<Alert.Link href={getConfig().PASSWORD_RESET_SUPPORT_LINK} target="_blank">
{formatMessage(messages['confirmation.support.link'])}
{intl.formatMessage(messages['confirmation.support.link'])}
</Alert.Link>
),
}}
@@ -45,26 +44,26 @@ const ForgotPasswordAlert = (props) => {
);
break;
case INTERNAL_SERVER_ERROR:
message = formatMessage(messages['internal.server.error']);
message = intl.formatMessage(messages['internal.server.error']);
break;
case FORBIDDEN_STATE:
heading = formatMessage(messages['forgot.password.error.message.title']);
message = formatMessage(messages['forgot.password.request.in.progress.message']);
heading = intl.formatMessage(messages['forgot.password.error.message.title']);
message = intl.formatMessage(messages['forgot.password.request.in.progress.message']);
break;
case FORM_SUBMISSION_ERROR:
message = formatMessage(messages['extend.field.errors'], { emailError });
message = intl.formatMessage(messages['extend.field.errors'], { emailError });
break;
case PASSWORD_RESET.INVALID_TOKEN:
heading = formatMessage(messages['invalid.token.heading']);
message = formatMessage(messages['invalid.token.error.message']);
heading = intl.formatMessage(messages['invalid.token.heading']);
message = intl.formatMessage(messages['invalid.token.error.message']);
break;
case PASSWORD_RESET.FORBIDDEN_REQUEST:
heading = formatMessage(messages['token.validation.rate.limit.error.heading']);
message = formatMessage(messages['token.validation.rate.limit.error']);
heading = intl.formatMessage(messages['token.validation.rate.limit.error.heading']);
message = intl.formatMessage(messages['token.validation.rate.limit.error']);
break;
case PASSWORD_RESET.INTERNAL_SERVER_ERROR:
heading = formatMessage(messages['token.validation.internal.sever.error.heading']);
message = formatMessage(messages['token.validation.internal.sever.error']);
heading = intl.formatMessage(messages['token.validation.internal.sever.error.heading']);
message = intl.formatMessage(messages['token.validation.internal.sever.error']);
break;
default:
break;
@@ -94,7 +93,8 @@ ForgotPasswordAlert.defaultProps = {
ForgotPasswordAlert.propTypes = {
status: PropTypes.string.isRequired,
email: PropTypes.string,
intl: intlShape.isRequired,
emailError: PropTypes.string,
};
export default ForgotPasswordAlert;
export default injectIntl(ForgotPasswordAlert);

Some files were not shown because too many files have changed in this diff Show More