Compare commits
73 Commits
ahtesham/V
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfb839d617 | ||
|
|
ef66eb1c31 | ||
|
|
ec8b256852 | ||
|
|
5a715b2fb5 | ||
|
|
e80578e682 | ||
|
|
155a73dc39 | ||
|
|
f5d0b50d90 | ||
|
|
b0745de672 | ||
|
|
d54fdbf84f | ||
|
|
0a6432c393 | ||
|
|
9e91c382b3 | ||
|
|
2cf24761c0 | ||
|
|
c2bdc31a03 | ||
|
|
9d487d7b61 | ||
|
|
a2ab6c196a | ||
|
|
6a5b02e8ad | ||
|
|
e76f214024 | ||
|
|
cb47717b09 | ||
|
|
85dbc9a6ca | ||
|
|
4aebeaffa7 | ||
|
|
6a84e2d5b6 | ||
|
|
e26620e350 | ||
|
|
1cabd2a514 | ||
|
|
06dd70078e | ||
|
|
4b13866e1d | ||
|
|
11142fda25 | ||
|
|
b4057f9588 | ||
|
|
9524f030d1 | ||
|
|
3f10dce04f | ||
|
|
5f8802272d | ||
|
|
0d486c2774 | ||
|
|
e78a1583c0 | ||
|
|
ea966c48b9 | ||
|
|
810b8d46b9 | ||
|
|
8a00b74863 | ||
|
|
94fafe661d | ||
|
|
7d58a124ab | ||
|
|
378a8d95f9 | ||
|
|
3a2e39af97 | ||
|
|
6f6d725126 | ||
|
|
3d8eb34d80 | ||
|
|
2768fc02ea | ||
|
|
0902467fa6 | ||
|
|
1dc999070f | ||
|
|
7a169715ea | ||
|
|
361f6781ee | ||
|
|
42190a89dd | ||
|
|
2d4c6a1d3b | ||
|
|
1dd88795c3 | ||
|
|
7cff7311e1 | ||
|
|
bf93959350 | ||
|
|
94151c2668 | ||
|
|
bf650e6d4c | ||
|
|
575f195970 | ||
|
|
c6bf6c92c1 | ||
|
|
b86c31bff8 | ||
|
|
fc37bbec1d | ||
|
|
6525c66600 | ||
|
|
145234c5c3 | ||
|
|
a7f816f49a | ||
|
|
694b0a5381 | ||
|
|
8a0947faf3 | ||
|
|
d1c4b20160 | ||
|
|
d81d8419a0 | ||
|
|
c6acdab7c6 | ||
|
|
0374143148 | ||
|
|
2d3c5ed761 | ||
|
|
a611451233 | ||
|
|
93dcd8f16e | ||
|
|
294519c7a5 | ||
|
|
f11df1f513 | ||
|
|
563609e10a | ||
|
|
b4d4e36f72 |
@@ -33,3 +33,5 @@ AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
# ***** Miscellaneous *****
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
|
||||
14
.eslintrc.js
14
.eslintrc.js
@@ -1,16 +1,17 @@
|
||||
// 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', {
|
||||
@@ -18,9 +19,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',
|
||||
{
|
||||
@@ -46,5 +47,8 @@ module.exports = createConfig('eslint', {
|
||||
},
|
||||
},
|
||||
],
|
||||
'function-paren-newline': 'off',
|
||||
'no-import-assign': 'off',
|
||||
'react/no-unstable-nested-components': 'off',
|
||||
},
|
||||
});
|
||||
|
||||
7
.github/pull_request_template.md
vendored
7
.github/pull_request_template.md
vendored
@@ -2,11 +2,16 @@
|
||||
|
||||
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|
|
||||
@@ -21,4 +26,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.
|
||||
|
||||
21
.github/workflows/autoupdate.yml
vendored
Normal file
21
.github/workflows/autoupdate.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: autoupdate
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
autoupdate:
|
||||
name: autoupdate
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: docker://chinthakagodawita/autoupdate-action:v1
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.CC_GITHUB_TOKEN }}"
|
||||
DRY_RUN: "false"
|
||||
PR_FILTER: "labelled"
|
||||
PR_LABELS: "autoupdate"
|
||||
EXCLUDED_LABELS: "dependencies,wontfix"
|
||||
MERGE_MSG: "Branch was auto-updated."
|
||||
RETRY_COUNT: "5"
|
||||
RETRY_SLEEP: "300"
|
||||
MERGE_CONFLICT_ACTION: "fail"
|
||||
13
.github/workflows/ci.yml
vendored
13
.github/workflows/ci.yml
vendored
@@ -11,17 +11,16 @@ on:
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
16
README.rst
16
README.rst
@@ -104,6 +104,18 @@ The authentication micro-frontend also requires the following additional variabl
|
||||
- Disables the enterprise login from Authn MFE.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
* - ``MFE_CONFIG_API_URL``
|
||||
- Link of the API to get runtime mfe configuration variables from the site configuration or django settings.
|
||||
- ``/api/v1/mfe_config`` | ``''`` (empty strings are falsy)
|
||||
|
||||
* - ``APP_ID``
|
||||
- Name of MFE, this will be used by the API to get runtime configurations for the specific micro frontend. For a frontend repo `frontend-app-appName`, use `appName` as APP_ID.
|
||||
- ``authn`` | ``''``
|
||||
|
||||
* - ``ENABLE_COOKIE_POLICY_BANNER``
|
||||
- Enables support for displaying the cookies acceptance banner.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
edX-specific Environment Variables
|
||||
**********************************
|
||||
|
||||
@@ -121,6 +133,10 @@ 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>`__.
|
||||
|
||||
|
||||
33717
package-lock.json
generated
33717
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@@ -32,22 +32,23 @@
|
||||
"url": "https://github.com/openedx/frontend-app-authn/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-cookie-policy-banner": "2.2.0",
|
||||
"@edx/frontend-platform": "3.2.0",
|
||||
"@edx/paragon": "20.28.4",
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
|
||||
"@edx/frontend-component-cookie-policy-banner": "2.2.2",
|
||||
"@edx/frontend-platform": "4.2.0",
|
||||
"@edx/paragon": "20.30.1",
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||
"@fortawesome/free-brands-svg-icons": "6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||
"@fortawesome/react-fontawesome": "0.2.0",
|
||||
"@redux-devtools/extension": "3.2.3",
|
||||
"@optimizely/react-sdk": "^2.9.1",
|
||||
"@redux-devtools/extension": "3.2.5",
|
||||
"algoliasearch": "^4.14.3",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.26.1",
|
||||
"core-js": "3.30.0",
|
||||
"extract-react-intl-messages": "4.1.1",
|
||||
"fastest-levenshtein": "1.0.16",
|
||||
"form-urlencoded": "4.2.1",
|
||||
"form-urlencoded": "6.1.0",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"prop-types": "15.8.1",
|
||||
@@ -55,35 +56,36 @@
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-helmet": "6.1.0",
|
||||
"react-loading-skeleton": "2.2.0",
|
||||
"react-onclickoutside": "6.12.2",
|
||||
"react-loading-skeleton": "3.2.0",
|
||||
"react-onclickoutside": "6.13.0",
|
||||
"react-redux": "7.2.9",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-router": "5.3.4",
|
||||
"react-router-dom": "5.3.4",
|
||||
"react-zendesk": "^0.1.13",
|
||||
"redux": "4.2.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"redux-saga": "1.2.2",
|
||||
"redux-saga": "1.2.3",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"reselect": "4.1.7",
|
||||
"sanitize-html": "2.8.0",
|
||||
"sanitize-html": "2.10.0",
|
||||
"semver-regex": "3.1.4",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "11.0.2",
|
||||
"@edx/frontend-build": "12.8.6",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"babel-plugin-formatjs": "10.3.35",
|
||||
"babel-plugin-formatjs": "10.4.0",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.7",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
"glob": "7.2.3",
|
||||
"history": "5.3.0",
|
||||
"husky": "7.0.4",
|
||||
"jest": "27.5.1",
|
||||
"jest": "29.5.0",
|
||||
"react-test-renderer": "16.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,61 +13,6 @@
|
||||
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,
|
||||
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: {
|
||||
'*': '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>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Helmet } from 'react-helmet';
|
||||
import { Redirect, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Logistration, NotFoundPage, registerIcons, UnAuthOnlyRoute,
|
||||
Logistration, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
|
||||
} from './common-components';
|
||||
import configureStore from './data/configureStore';
|
||||
import {
|
||||
@@ -19,10 +19,10 @@ import {
|
||||
RESET_PAGE,
|
||||
} from './data/constants';
|
||||
import { updatePathWithQueryParams } from './data/utils';
|
||||
import ForgotPasswordPage from './forgot-password';
|
||||
import { ForgotPasswordPage } from './forgot-password';
|
||||
import { ProgressiveProfiling } from './progressive-profiling';
|
||||
import RecommendationsPage from './recommendations';
|
||||
import ResetPasswordPage from './reset-password';
|
||||
import { RecommendationsPage } from './recommendations';
|
||||
import { ResetPasswordPage } from './reset-password';
|
||||
import './index.scss';
|
||||
|
||||
registerIcons();
|
||||
@@ -32,6 +32,7 @@ const MainApp = () => (
|
||||
<Helmet>
|
||||
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
|
||||
</Helmet>
|
||||
{getConfig().ZENDESK_KEY && <Zendesk />}
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Redirect to={updatePathWithQueryParams(REGISTER_PAGE)} />
|
||||
|
||||
@@ -1,46 +1,49 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } 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>
|
||||
const AuthLargeLayout = ({ 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>
|
||||
<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: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AuthLargeLayout);
|
||||
export default AuthLargeLayout;
|
||||
|
||||
@@ -1,49 +1,52 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } 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>
|
||||
const AuthMediumLayout = ({ 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>
|
||||
<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: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AuthMediumLayout);
|
||||
export default AuthMediumLayout;
|
||||
|
||||
@@ -1,38 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } 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>
|
||||
const AuthSmallLayout = ({ 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>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
AuthSmallLayout.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AuthSmallLayout);
|
||||
export default AuthSmallLayout;
|
||||
|
||||
@@ -1,46 +1,45 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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>
|
||||
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>
|
||||
<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: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(LargeLayout);
|
||||
export default LargeLayout;
|
||||
|
||||
@@ -1,51 +1,50 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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>
|
||||
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>
|
||||
<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: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default injectIntl(MediumLayout);
|
||||
export default MediumLayout;
|
||||
|
||||
@@ -1,40 +1,39 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
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>
|
||||
);
|
||||
const SmallLayout = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
SmallLayout.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
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 injectIntl(SmallLayout);
|
||||
export default SmallLayout;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default } from './BaseComponent';
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as BaseComponent } from './BaseComponent';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Form,
|
||||
} from '@edx/paragon';
|
||||
@@ -16,8 +16,9 @@ import messages from './messages';
|
||||
* This component renders the Single sign-on (SSO) button only for the tpa provider passed
|
||||
* */
|
||||
const EnterpriseSSO = (props) => {
|
||||
const { intl } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const tpaProvider = props.provider;
|
||||
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
|
||||
|
||||
const handleSubmit = (e, url) => {
|
||||
e.preventDefault();
|
||||
@@ -35,7 +36,7 @@ const EnterpriseSSO = (props) => {
|
||||
<div className="d-flex flex-column">
|
||||
<div className="mw-450">
|
||||
<Form className="m-0">
|
||||
<p>{intl.formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
|
||||
<p>{formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
|
||||
<Button
|
||||
id={tpaProvider.id}
|
||||
key={tpaProvider.id}
|
||||
@@ -64,12 +65,15 @@ const EnterpriseSSO = (props) => {
|
||||
<div className="mb-4" />
|
||||
<Button
|
||||
type="submit"
|
||||
id="other-ways-to-sign-in"
|
||||
variant="outline-primary"
|
||||
state="Complete"
|
||||
className="w-100"
|
||||
onClick={(e) => handleClick(e)}
|
||||
>
|
||||
{intl.formatMessage(messages['enterprisetpa.login.button.text'])}
|
||||
{disablePublicAccountCreation
|
||||
? formatMessage(messages['enterprisetpa.login.button.text.public.account.creation.disabled'])
|
||||
: formatMessage(messages['enterprisetpa.login.button.text'])}
|
||||
</Button>
|
||||
</Form>
|
||||
</div>
|
||||
@@ -100,7 +104,6 @@ EnterpriseSSO.propTypes = {
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
}),
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(EnterpriseSSO);
|
||||
export default EnterpriseSSO;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Hyperlink, Icon } from '@edx/paragon';
|
||||
import { Institution } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -32,8 +32,8 @@ export const RenderInstitutionButton = props => {
|
||||
* */
|
||||
const InstitutionLogistration = props => {
|
||||
const lmsBaseUrl = getConfig().LMS_BASE_URL;
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
intl,
|
||||
secondaryProviders,
|
||||
headingTitle,
|
||||
} = props;
|
||||
@@ -46,7 +46,7 @@ const InstitutionLogistration = props => {
|
||||
{headingTitle}
|
||||
</h4>
|
||||
<p className="mb-2">
|
||||
{intl.formatMessage(messages['institution.login.page.sub.heading'])}
|
||||
{formatMessage(messages['institution.login.page.sub.heading'])}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,7 +95,6 @@ RenderInstitutionButton.defaultProps = {
|
||||
|
||||
InstitutionLogistration.propTypes = {
|
||||
...LogistrationProps,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
headingTitle: PropTypes.string,
|
||||
};
|
||||
InstitutionLogistration.defaultProps = {
|
||||
@@ -103,4 +102,4 @@ InstitutionLogistration.defaultProps = {
|
||||
headingTitle: '',
|
||||
};
|
||||
|
||||
export default injectIntl(InstitutionLogistration);
|
||||
export default InstitutionLogistration;
|
||||
|
||||
@@ -4,7 +4,7 @@ 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 { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Icon,
|
||||
Tab,
|
||||
@@ -14,25 +14,28 @@ import { ChevronLeft } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import BaseComponent from '../base-component';
|
||||
import { BaseComponent } from '../base-component';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
import { getTpaHint, getTpaProvider, updatePathWithQueryParams } from '../data/utils';
|
||||
import { LoginPage } from '../login';
|
||||
import { RegistrationPage } from '../register';
|
||||
import { backupRegistrationForm } from '../register/data/actions';
|
||||
import { clearThirdPartyAuthContextErrorMessage } from './data/actions';
|
||||
import {
|
||||
tpaProvidersSelector,
|
||||
} from './data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
const Logistration = (props) => {
|
||||
const { intl, selectedPage, tpaProviders } = props;
|
||||
const { selectedPage, tpaProviders } = props;
|
||||
const tpaHint = getTpaHint();
|
||||
const {
|
||||
providers, secondaryProviders,
|
||||
} = tpaProviders;
|
||||
const { formatMessage } = useIntl();
|
||||
const [institutionLogin, setInstitutionLogin] = useState(false);
|
||||
const [key, setKey] = useState('');
|
||||
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
|
||||
|
||||
useEffect(() => {
|
||||
const authService = getAuthService();
|
||||
@@ -54,6 +57,7 @@ const Logistration = (props) => {
|
||||
|
||||
const handleOnSelect = (tabKey) => {
|
||||
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
|
||||
props.clearThirdPartyAuthContextErrorMessage();
|
||||
if (tabKey === LOGIN_PAGE) {
|
||||
props.backupRegistrationForm();
|
||||
}
|
||||
@@ -65,8 +69,8 @@ const Logistration = (props) => {
|
||||
<Icon src={ChevronLeft} className="left-icon" />
|
||||
<span className="ml-2">
|
||||
{selectedPage === LOGIN_PAGE
|
||||
? intl.formatMessage(messages['logistration.sign.in'])
|
||||
: intl.formatMessage(messages['logistration.register'])}
|
||||
? formatMessage(messages['logistration.sign.in'])
|
||||
: formatMessage(messages['logistration.register'])}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
@@ -79,40 +83,64 @@ const Logistration = (props) => {
|
||||
return (
|
||||
<BaseComponent>
|
||||
<div>
|
||||
{institutionLogin
|
||||
{disablePublicAccountCreation
|
||||
? (
|
||||
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
|
||||
<Tab title={tabTitle} eventKey={selectedPage === LOGIN_PAGE ? LOGIN_PAGE : REGISTER_PAGE} />
|
||||
</Tabs>
|
||||
)
|
||||
: (!isValidTpaHint() && (
|
||||
<>
|
||||
<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>
|
||||
<Redirect to={updatePathWithQueryParams(LOGIN_PAGE)} />
|
||||
{institutionLogin && (
|
||||
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
|
||||
<Tab title={tabTitle} eventKey={LOGIN_PAGE} />
|
||||
</Tabs>
|
||||
)}
|
||||
<div id="main-content" className="main-content">
|
||||
{!institutionLogin && (
|
||||
<h3 className="mb-4.5">{formatMessage(messages['logistration.sign.in'])}</h3>
|
||||
)}
|
||||
<LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
{ key && (
|
||||
<Redirect to={updatePathWithQueryParams(key)} />
|
||||
)}
|
||||
<div id="main-content" className="main-content">
|
||||
{selectedPage === LOGIN_PAGE
|
||||
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
|
||||
: <RegistrationPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div>
|
||||
{institutionLogin
|
||||
? (
|
||||
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
|
||||
<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>
|
||||
))}
|
||||
{ key && (
|
||||
<Redirect to={updatePathWithQueryParams(key)} />
|
||||
)}
|
||||
<div id="main-content" className="main-content">
|
||||
{selectedPage === LOGIN_PAGE
|
||||
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
|
||||
: (
|
||||
<RegistrationPage
|
||||
institutionLogin={institutionLogin}
|
||||
handleInstitutionLogin={handleInstitutionLogin}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</BaseComponent>
|
||||
);
|
||||
};
|
||||
|
||||
Logistration.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
selectedPage: PropTypes.string,
|
||||
backupRegistrationForm: PropTypes.func.isRequired,
|
||||
clearThirdPartyAuthContextErrorMessage: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.shape({
|
||||
providers: PropTypes.array,
|
||||
secondaryProviders: PropTypes.array,
|
||||
providers: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -135,5 +163,6 @@ export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
backupRegistrationForm,
|
||||
clearThirdPartyAuthContextErrorMessage,
|
||||
},
|
||||
)(injectIntl(Logistration));
|
||||
)(Logistration);
|
||||
|
||||
@@ -2,16 +2,16 @@ import React from 'react';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
|
||||
} from '@edx/paragon';
|
||||
@@ -13,7 +13,7 @@ import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
const PasswordField = (props) => {
|
||||
const { formatMessage } = props.intl;
|
||||
const { formatMessage } = useIntl();
|
||||
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
|
||||
const [showTooltip, setShowTooltip] = useState(false);
|
||||
|
||||
@@ -100,11 +100,10 @@ PasswordField.propTypes = {
|
||||
handleBlur: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
showRequirements: PropTypes.bool,
|
||||
value: PropTypes.string.isRequired,
|
||||
autoComplete: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(PasswordField);
|
||||
export default PasswordField;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Redirect } from 'react-router-dom';
|
||||
import { AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS } from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
|
||||
function RedirectLogistration(props) {
|
||||
const RedirectLogistration = (props) => {
|
||||
const {
|
||||
finishAuthUrl,
|
||||
redirectUrl,
|
||||
@@ -66,8 +66,9 @@ function RedirectLogistration(props) {
|
||||
|
||||
window.location.href = finalRedirectUrl;
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
RedirectLogistration.defaultProps = {
|
||||
educationLevel: null,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -9,8 +9,9 @@ import PropTypes from 'prop-types';
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
function SocialAuthProviders(props) {
|
||||
const { intl, referrer, socialAuthProviders } = props;
|
||||
const SocialAuthProviders = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const { referrer, socialAuthProviders } = props;
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
@@ -34,25 +35,24 @@ function SocialAuthProviders(props) {
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<>
|
||||
<div className="font-container" aria-hidden="true">
|
||||
<FontAwesomeIcon
|
||||
icon={SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? ['fab', provider.iconClass] : faSignInAlt}
|
||||
/>
|
||||
</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
|
||||
? intl.formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
||||
: intl.formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
|
||||
? formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
|
||||
: 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,
|
||||
@@ -60,7 +60,6 @@ SocialAuthProviders.defaultProps = {
|
||||
};
|
||||
|
||||
SocialAuthProviders.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
referrer: PropTypes.string,
|
||||
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
@@ -69,7 +68,8 @@ SocialAuthProviders.propTypes = {
|
||||
iconImage: PropTypes.string,
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
skipRegistrationForm: PropTypes.bool,
|
||||
})),
|
||||
};
|
||||
|
||||
export default injectIntl(SocialAuthProviders);
|
||||
export default SocialAuthProviders;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -9,14 +9,15 @@ import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
const ThirdPartyAuthAlert = (props) => {
|
||||
const { currentProvider, intl, referrer } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const { currentProvider, referrer } = props;
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
let message;
|
||||
|
||||
if (referrer === LOGIN_PAGE) {
|
||||
message = intl.formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
message = formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
} else {
|
||||
message = intl.formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
}
|
||||
|
||||
if (!currentProvider) {
|
||||
@@ -25,14 +26,14 @@ const ThirdPartyAuthAlert = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2' : 'alert-warning mt-n2'}>
|
||||
<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>{intl.formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
<Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
) : null}
|
||||
<p>{ message }</p>
|
||||
</Alert>
|
||||
{referrer === REGISTER_PAGE ? (
|
||||
<h4 className="mt-4 mb-4">{intl.formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
||||
<h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
@@ -45,8 +46,7 @@ ThirdPartyAuthAlert.defaultProps = {
|
||||
|
||||
ThirdPartyAuthAlert.propTypes = {
|
||||
currentProvider: PropTypes.string,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
referrer: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(ThirdPartyAuthAlert);
|
||||
export default ThirdPartyAuthAlert;
|
||||
|
||||
@@ -30,7 +30,7 @@ const UnAuthOnlyRoute = (props) => {
|
||||
return <Route {...props} />;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
return null;
|
||||
};
|
||||
|
||||
export default UnAuthOnlyRoute;
|
||||
|
||||
53
src/common-components/Zendesk.jsx
Normal file
53
src/common-components/Zendesk.jsx
Normal file
@@ -0,0 +1,53 @@
|
||||
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';
|
||||
|
||||
const ZendeskHelp = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
const setting = {
|
||||
cookies: true,
|
||||
webWidget: {
|
||||
contactOptions: {
|
||||
enabled: false,
|
||||
},
|
||||
chat: {
|
||||
suppress: false,
|
||||
},
|
||||
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) },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} />
|
||||
);
|
||||
};
|
||||
|
||||
export default ZendeskHelp;
|
||||
@@ -1,6 +1,7 @@
|
||||
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) => ({
|
||||
@@ -20,3 +21,7 @@ export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalField
|
||||
export const getThirdPartyAuthContextFailure = () => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
|
||||
});
|
||||
|
||||
export const clearThirdPartyAuthContextErrorMessage = () => ({
|
||||
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
|
||||
import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
|
||||
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions';
|
||||
|
||||
export const defaultState = {
|
||||
fieldDescriptions: {},
|
||||
@@ -12,10 +12,11 @@ export const defaultState = {
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
errorMessage: null,
|
||||
},
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
|
||||
return {
|
||||
@@ -36,6 +37,15 @@ const reducer = (state = defaultState, action) => {
|
||||
...state,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
};
|
||||
case THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG:
|
||||
return {
|
||||
...state,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
...state.thirdPartyAuthContext,
|
||||
errorMessage: null,
|
||||
},
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -15,14 +15,12 @@ import {
|
||||
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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { camelCaseObject, convertKeyNames, getConfig } from '@edx/frontend-platform';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
@@ -18,10 +18,8 @@ export async function getThirdPartyAuthContext(urlParams) {
|
||||
throw (e);
|
||||
});
|
||||
return {
|
||||
fieldDescriptions: data.registration_fields || {},
|
||||
optionalFields: data.optional_fields || {},
|
||||
thirdPartyAuthContext: camelCaseObject(
|
||||
convertKeyNames(data.context_data, { fullname: 'name' }),
|
||||
),
|
||||
fieldDescriptions: data.registrationFields || data.registration_fields,
|
||||
optionalFields: data.optionalFields || data.optional_fields,
|
||||
thirdPartyAuthContext: data.contextData || data.context_data,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { THIRD_PARTY_AUTH_CONTEXT } from '../actions';
|
||||
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', () => {
|
||||
@@ -14,6 +15,7 @@ describe('common components reducer', () => {
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
errorMessage: null,
|
||||
},
|
||||
};
|
||||
const fieldDescriptions = {
|
||||
@@ -43,4 +45,38 @@ describe('common components reducer', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
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 occured',
|
||||
},
|
||||
};
|
||||
|
||||
const action = {
|
||||
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
).toEqual(
|
||||
{
|
||||
...state,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
...state.thirdPartyAuthContext,
|
||||
errorMessage: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,3 +12,4 @@ export { storeName } from './data/selectors';
|
||||
export { default as FormGroup } from './FormGroup';
|
||||
export { default as PasswordField } from './PasswordField';
|
||||
export { default as Logistration } from './Logistration';
|
||||
export { default as Zendesk } from './Zendesk';
|
||||
|
||||
@@ -29,6 +29,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Show me other ways to sign in or register',
|
||||
description: 'Button text for login',
|
||||
},
|
||||
'enterprisetpa.login.button.text.public.account.creation.disabled': {
|
||||
id: 'enterprisetpa.login.button.text.public.account.creation.disabled',
|
||||
defaultMessage: 'Show me other ways to sign in',
|
||||
description: 'Button text for login when account creation is disabled',
|
||||
},
|
||||
// social auth providers
|
||||
'sso.sign.in.with': {
|
||||
id: 'sso.sign.in.with',
|
||||
@@ -97,6 +102,16 @@ const messages = defineMessages({
|
||||
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;
|
||||
|
||||
@@ -11,6 +11,7 @@ import configureStore from 'redux-mock-store';
|
||||
|
||||
import { COMPLETE_STATE, LOGIN_PAGE } from '../../data/constants';
|
||||
import { backupRegistrationForm } from '../../register/data/actions';
|
||||
import { clearThirdPartyAuthContextErrorMessage } from '../data/actions';
|
||||
import { RenderInstitutionButton } from '../InstitutionLogistration';
|
||||
import Logistration from '../Logistration';
|
||||
|
||||
@@ -52,6 +53,9 @@ describe('Logistration', () => {
|
||||
});
|
||||
|
||||
it('should render registration page', () => {
|
||||
mergeConfig({
|
||||
ALLOW_PUBLIC_ACCOUNT_CREATION: true,
|
||||
});
|
||||
store = mockStore({
|
||||
register: {
|
||||
registrationResult: { success: false, redirectUrl: '' },
|
||||
@@ -88,9 +92,42 @@ describe('Logistration', () => {
|
||||
expect(logistration.find('#main-content').find('LoginPage').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render only login page when public account creation is disabled', () => {
|
||||
mergeConfig({
|
||||
ALLOW_PUBLIC_ACCOUNT_CREATION: false,
|
||||
DISABLE_ENTERPRISE_LOGIN: 'true',
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
login: {
|
||||
loginResult: { success: false, redirectUrl: '' },
|
||||
},
|
||||
commonComponents: {
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
providers: [],
|
||||
secondaryProviders: [secondaryProviders],
|
||||
},
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
const props = { selectedPage: LOGIN_PAGE };
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
|
||||
|
||||
// verifying sign in heading for institution login false
|
||||
expect(logistration.find('#main-content').find('h3').text()).toEqual('Sign in');
|
||||
|
||||
// verifying tabs heading for institution login true
|
||||
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
|
||||
expect(logistration.find('#controlled-tab').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display institution login option when secondary providers are present', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: 'true',
|
||||
ALLOW_PUBLIC_ACCOUNT_CREATION: 'true',
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
@@ -208,4 +245,27 @@ describe('Logistration', () => {
|
||||
logistration.find('a[data-rb-event-key="/login"]').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm());
|
||||
});
|
||||
|
||||
it('should clear tpa context errorMessage tab click', () => {
|
||||
store = mockStore({
|
||||
login: {
|
||||
loginResult: { success: false, redirectUrl: '' },
|
||||
},
|
||||
register: {
|
||||
registrationResult: { success: false, redirectUrl: '' },
|
||||
registrationError: {},
|
||||
},
|
||||
commonComponents: {
|
||||
thirdPartyAuthContext: {
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
/* eslint-disable import/no-import-module-exports */
|
||||
/* eslint-disable react/function-component-definition */
|
||||
import React from 'react';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { mount } from 'enzyme';
|
||||
import { MemoryRouter, BrowserRouter as Router, Switch } from 'react-router-dom';
|
||||
|
||||
import { UnAuthOnlyRoute } from '..';
|
||||
import { LOGIN_PAGE } from '../../data/constants';
|
||||
|
||||
import { MemoryRouter, BrowserRouter as Router, Switch } from 'react-router-dom';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const RRD = require('react-router-dom');
|
||||
|
||||
17
src/common-components/tests/Zendesk.test.jsx
Normal file
17
src/common-components/tests/Zendesk.test.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import Zendesk from '../Zendesk';
|
||||
|
||||
jest.mock('react-zendesk', () => 'Zendesk');
|
||||
|
||||
describe('Zendesk Help', () => {
|
||||
it('should match login page third party auth alert message snapshot', () => {
|
||||
const tree = renderer.create(
|
||||
<IntlProvider locale="en">
|
||||
<Zendesk />
|
||||
</IntlProvider>,
|
||||
).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -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 alert show"
|
||||
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
>
|
||||
@@ -23,7 +23,7 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
|
||||
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
|
||||
Array [
|
||||
<div
|
||||
className="fade alert-content alert-success mt-n2 alert show"
|
||||
className="fade alert-content alert-success mt-n2 mb-5 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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 {
|
||||
"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,
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
`;
|
||||
@@ -22,6 +22,8 @@ const configuration = {
|
||||
// Miscellaneous
|
||||
GENERAL_RECOMMENDATIONS: process.env.GENERAL_RECOMMENDATIONS || '[]',
|
||||
INFO_EMAIL: process.env.INFO_EMAIL || '',
|
||||
ZENDESK_KEY: process.env.ZENDESK_KEY,
|
||||
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
|
||||
};
|
||||
|
||||
export default configuration;
|
||||
|
||||
@@ -35,4 +35,4 @@ export const INVALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{
|
||||
|
||||
// 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', 'save_for_later', 'register_for_free'];
|
||||
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'save_for_later', 'register_for_free', 'track', 'is_account_recovery'];
|
||||
|
||||
11
src/data/optimizely.js
Normal file
11
src/data/optimizely.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import {
|
||||
createInstance,
|
||||
} from '@optimizely/react-sdk';
|
||||
|
||||
const OPTIMIZELY_SDK_KEY = process.env.OPTIMIZELY_FULL_STACK_SDK_KEY;
|
||||
|
||||
const optimizely = createInstance({
|
||||
sdkKey: OPTIMIZELY_SDK_KEY,
|
||||
});
|
||||
|
||||
export default optimizely;
|
||||
@@ -142,6 +142,7 @@ FormFieldRenderer.propTypes = {
|
||||
type: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
options: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
|
||||
}).isRequired,
|
||||
onChangeHandler: PropTypes.func.isRequired,
|
||||
handleBlur: PropTypes.func,
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default } from './FieldRenderer';
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as FormFieldRenderer } from './FieldRenderer';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { CheckCircle, Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -13,9 +13,10 @@ import { PASSWORD_RESET } from '../reset-password/data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
const ForgotPasswordAlert = (props) => {
|
||||
const { email, emailError, intl } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const { email, emailError } = props;
|
||||
let message = '';
|
||||
let heading = intl.formatMessage(messages['forgot.password.error.alert.title']);
|
||||
let heading = formatMessage(messages['forgot.password.error.alert.title']);
|
||||
let { status } = props;
|
||||
|
||||
if (emailError) {
|
||||
@@ -24,7 +25,7 @@ const ForgotPasswordAlert = (props) => {
|
||||
|
||||
switch (status) {
|
||||
case COMPLETE_STATE:
|
||||
heading = intl.formatMessage(messages['confirmation.message.title']);
|
||||
heading = formatMessage(messages['confirmation.message.title']);
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id="forgot.password.confirmation.message"
|
||||
@@ -36,7 +37,7 @@ const ForgotPasswordAlert = (props) => {
|
||||
email: <span className="data-hj-suppress">{email}</span>,
|
||||
supportLink: (
|
||||
<Alert.Link href={getConfig().PASSWORD_RESET_SUPPORT_LINK} target="_blank">
|
||||
{intl.formatMessage(messages['confirmation.support.link'])}
|
||||
{formatMessage(messages['confirmation.support.link'])}
|
||||
</Alert.Link>
|
||||
),
|
||||
}}
|
||||
@@ -44,26 +45,26 @@ const ForgotPasswordAlert = (props) => {
|
||||
);
|
||||
break;
|
||||
case INTERNAL_SERVER_ERROR:
|
||||
message = intl.formatMessage(messages['internal.server.error']);
|
||||
message = formatMessage(messages['internal.server.error']);
|
||||
break;
|
||||
case FORBIDDEN_STATE:
|
||||
heading = intl.formatMessage(messages['forgot.password.error.message.title']);
|
||||
message = intl.formatMessage(messages['forgot.password.request.in.progress.message']);
|
||||
heading = formatMessage(messages['forgot.password.error.message.title']);
|
||||
message = formatMessage(messages['forgot.password.request.in.progress.message']);
|
||||
break;
|
||||
case FORM_SUBMISSION_ERROR:
|
||||
message = intl.formatMessage(messages['extend.field.errors'], { emailError });
|
||||
message = formatMessage(messages['extend.field.errors'], { emailError });
|
||||
break;
|
||||
case PASSWORD_RESET.INVALID_TOKEN:
|
||||
heading = intl.formatMessage(messages['invalid.token.heading']);
|
||||
message = intl.formatMessage(messages['invalid.token.error.message']);
|
||||
heading = formatMessage(messages['invalid.token.heading']);
|
||||
message = formatMessage(messages['invalid.token.error.message']);
|
||||
break;
|
||||
case PASSWORD_RESET.FORBIDDEN_REQUEST:
|
||||
heading = intl.formatMessage(messages['token.validation.rate.limit.error.heading']);
|
||||
message = intl.formatMessage(messages['token.validation.rate.limit.error']);
|
||||
heading = formatMessage(messages['token.validation.rate.limit.error.heading']);
|
||||
message = formatMessage(messages['token.validation.rate.limit.error']);
|
||||
break;
|
||||
case PASSWORD_RESET.INTERNAL_SERVER_ERROR:
|
||||
heading = intl.formatMessage(messages['token.validation.internal.sever.error.heading']);
|
||||
message = intl.formatMessage(messages['token.validation.internal.sever.error']);
|
||||
heading = formatMessage(messages['token.validation.internal.sever.error.heading']);
|
||||
message = formatMessage(messages['token.validation.internal.sever.error']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -93,8 +94,7 @@ ForgotPasswordAlert.defaultProps = {
|
||||
ForgotPasswordAlert.propTypes = {
|
||||
status: PropTypes.string.isRequired,
|
||||
email: PropTypes.string,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
emailError: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(ForgotPasswordAlert);
|
||||
export default ForgotPasswordAlert;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form,
|
||||
Hyperlink,
|
||||
@@ -17,7 +17,7 @@ import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import BaseComponent from '../base-component';
|
||||
import { BaseComponent } from '../base-component';
|
||||
import { FormGroup } from '../common-components';
|
||||
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
|
||||
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||
@@ -30,9 +30,10 @@ const ForgotPasswordPage = (props) => {
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||
const {
|
||||
intl, status, submitState, emailValidationError,
|
||||
status, submitState, emailValidationError,
|
||||
} = props;
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
const [email, setEmail] = useState(props.email);
|
||||
const [bannerEmail, setBannerEmail] = useState('');
|
||||
const [formErrors, setFormErrors] = useState('');
|
||||
@@ -58,9 +59,9 @@ const ForgotPasswordPage = (props) => {
|
||||
let error = '';
|
||||
|
||||
if (value === '') {
|
||||
error = intl.formatMessage(messages['forgot.password.empty.email.field.error']);
|
||||
error = formatMessage(messages['forgot.password.empty.email.field.error']);
|
||||
} else if (!emailRegex.test(value)) {
|
||||
error = intl.formatMessage(messages['forgot.password.page.invalid.email.message']);
|
||||
error = formatMessage(messages['forgot.password.page.invalid.email.message']);
|
||||
}
|
||||
|
||||
return error;
|
||||
@@ -89,14 +90,14 @@ const ForgotPasswordPage = (props) => {
|
||||
const tabTitle = (
|
||||
<div className="d-inline-flex flex-wrap align-items-center">
|
||||
<Icon src={ChevronLeft} />
|
||||
<span className="ml-2">{intl.formatMessage(messages['sign.in.text'])}</span>
|
||||
<span className="ml-2">{formatMessage(messages['sign.in.text'])}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<BaseComponent>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['forgot.password.page.title'],
|
||||
<title>{formatMessage(messages['forgot.password.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
@@ -111,13 +112,13 @@ const ForgotPasswordPage = (props) => {
|
||||
<Form id="forget-password-form" name="forget-password-form" className="mw-xs">
|
||||
<ForgotPasswordAlert email={bannerEmail} emailError={formErrors} status={status} />
|
||||
<h2 className="h4">
|
||||
{intl.formatMessage(messages['forgot.password.page.heading'])}
|
||||
{formatMessage(messages['forgot.password.page.heading'])}
|
||||
</h2>
|
||||
<p className="mb-4">
|
||||
{intl.formatMessage(messages['forgot.password.page.instructions'])}
|
||||
{formatMessage(messages['forgot.password.page.instructions'])}
|
||||
</p>
|
||||
<FormGroup
|
||||
floatingLabel={intl.formatMessage(messages['forgot.password.page.email.field.label'])}
|
||||
floatingLabel={formatMessage(messages['forgot.password.page.email.field.label'])}
|
||||
name="email"
|
||||
value={email}
|
||||
autoComplete="on"
|
||||
@@ -125,7 +126,7 @@ const ForgotPasswordPage = (props) => {
|
||||
handleChange={(e) => setEmail(e.target.value)}
|
||||
handleBlur={handleBlur}
|
||||
handleFocus={handleFocus}
|
||||
helpText={[intl.formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
|
||||
helpText={[formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
|
||||
/>
|
||||
<StatefulButton
|
||||
id="submit-forget-password"
|
||||
@@ -135,7 +136,7 @@ const ForgotPasswordPage = (props) => {
|
||||
className="forgot-password-button-width"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['forgot.password.page.submit.button']),
|
||||
default: formatMessage(messages['forgot.password.page.submit.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
@@ -150,11 +151,11 @@ const ForgotPasswordPage = (props) => {
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{intl.formatMessage(messages['need.help.sign.in.text'])}
|
||||
{formatMessage(messages['need.help.sign.in.text'])}
|
||||
</Hyperlink>
|
||||
)}
|
||||
<p className="mt-5.5 small text-gray-700">
|
||||
{intl.formatMessage(messages['additional.help.text'], { platformName })}
|
||||
{formatMessage(messages['additional.help.text'], { platformName })}
|
||||
<span>
|
||||
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
|
||||
</span>
|
||||
@@ -170,7 +171,6 @@ ForgotPasswordPage.propTypes = {
|
||||
email: PropTypes.string,
|
||||
emailValidationError: PropTypes.string,
|
||||
forgotPassword: PropTypes.func.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
setForgotPasswordFormData: PropTypes.func.isRequired,
|
||||
status: PropTypes.string,
|
||||
submitState: PropTypes.string,
|
||||
@@ -189,4 +189,4 @@ export default connect(
|
||||
forgotPassword,
|
||||
setForgotPasswordFormData,
|
||||
},
|
||||
)(injectIntl(ForgotPasswordPage));
|
||||
)(ForgotPasswordPage);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { default } from './ForgotPasswordPage';
|
||||
export { default as ForgotPasswordPage } from './ForgotPasswordPage';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { FORGOT_PASSWORD } from './data/actions';
|
||||
export { default as saga } from './data/sagas';
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"logistration.register": "التسجيل",
|
||||
"enterprisetpa.title.heading": "هل ترغب في تسجيل الدخول باستخدام بيانات {providerName} الخاصة بك؟",
|
||||
"enterprisetpa.login.button.text": "أرِني وسائل أخرى لتسجيل الدخول أو للتسجيل",
|
||||
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
|
||||
"sso.sign.in.with": "تسجيل الدخول باستخدام {providerName}",
|
||||
"sso.create.account.using": "إنشاء حساب باستخدام {providerName}",
|
||||
"show.password": "إظهار كلمة المرور",
|
||||
@@ -21,6 +22,8 @@
|
||||
"login.third.party.auth.account.not.linked": "لقد نجحت في تسجيل الدخول إلى {currentProvider}، لكن حسابك على {currentProvider} غير موصول بأي حساب على {platformName}. لوصل حساباتك، سجّل الدخول الآن باستخدام كلمة مرورك على {platformName}.",
|
||||
"register.third.party.auth.account.not.linked": "لقد سجلت دخولك بنجاح إلى {currentProvider}! نحتاج فقط قليلاً بعدُ من المعلومات قبل أن تبدأ التعلم مع {platformName}.",
|
||||
"registration.using.tpa.form.heading": "إتمام إنشاء حسابك",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجددًا.",
|
||||
"forgot.password.confirmation.message": "لقد أرسلنا بريدًا إلكترونيًا إلى {email} به إرشادات لإعادة ضبط كلمة المرور الخاصة بك. إن لم تستلم رسالة إعادة ضبط كلمة المرور بعد دقيقة واحدة، فتحقق من إدخال عنوان البريد الإلكتروني الصحيح، أو تفقد مجلد الرسائل غير المرغوب فيها. إن احتجت مزيدًا من المساعدة، {supportLink}.",
|
||||
"forgot.password.page.title": "نسيت كلمة المرور | {siteName}",
|
||||
@@ -93,7 +96,7 @@
|
||||
"password.security.block.body": "اكتشف نظامنا أن كلمة مرورك صعيفة. غيّر كلمة مرورك حتى يظل حسابك آمنًا.",
|
||||
"password.security.close.button": "إغلاق",
|
||||
"password.security.redirect.to.reset.password.button": "إعادة ضبط كلمة المرور",
|
||||
"progressive.profiling.page.title": "الحقول الاختيارية | {siteName}",
|
||||
"progressive.profiling.page.title": "Welcome | {siteName}",
|
||||
"progressive.profiling.page.heading": "بعض الأسئلة الموجهة لك ستساعدنا كي نزداد ذكاءً.",
|
||||
"optional.fields.information.link": "معرفة المزيد عن كيفية استخدامنا لهذه المعلومات.",
|
||||
"optional.fields.submit.button": "إرسال",
|
||||
@@ -104,7 +107,7 @@
|
||||
"modal.description": "إن غيرت رأيك، قيمكنك إكمال ملفك الشخصي ضمن الإعدادات في أي وقت.",
|
||||
"welcome.page.error.heading": "لم نتمكن من تحديث ملفك الشخصي",
|
||||
"welcome.page.error.message": "حدث خطأ ما. يمكنك إكمال ملفك الشخصي ضمن الإعدادات في أي وقت.",
|
||||
"recommendation.page.title": "Recommendations| {siteName}",
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "التسجيل | {siteName}",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"logistration.register": "Registrieren",
|
||||
"enterprisetpa.title.heading": "Möchten Sie sich mit Ihren {providerName}-Anmeldedaten anmelden?",
|
||||
"enterprisetpa.login.button.text": "Andere Möglichkeiten für die Anmeldung oder Registrierung",
|
||||
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
|
||||
"sso.sign.in.with": "Melden Sie sich mit {providerName} an",
|
||||
"sso.create.account.using": "Erstellen Sie ein Konto mit {providerName}",
|
||||
"show.password": "Passwort anzeigen",
|
||||
@@ -21,6 +22,8 @@
|
||||
"login.third.party.auth.account.not.linked": "Sie haben sich erfolgreich bei {currentProvider} angemeldet, aber Ihr {currentProvider}-Konto hat kein verknüpftes {platformName}-Konto. Um Ihre Konten zu verknüpfen, melden Sie sich jetzt mit Ihrem {platformName}-Passwort an.",
|
||||
"register.third.party.auth.account.not.linked": "Sie haben sich erfolgreich bei {currentProvider} angemeldet! Wir brauchen nur ein paar mehr Informationen, bevor Sie anfangen, mit {platformName} zu lernen.",
|
||||
"registration.using.tpa.form.heading": "Beenden Sie die Erstellung Ihres Kontos",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "Die gesuchte Seite ist nicht verfügbar oder es liegt ein Fehler in der URL vor. Bitte überprüfen Sie die URL und versuchen Sie es erneut.",
|
||||
"forgot.password.confirmation.message": "Wir haben eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts an {email} gesendet. Wenn Sie nach 1 Minute keine Nachricht zum Zurücksetzen des Passworts erhalten, überprüfen Sie, ob Sie die richtige E-Mail-Adresse eingegeben haben, oder überprüfen Sie Ihren Spam-Ordner. Wenn Sie weitere Hilfe benötigen, {supportLink}.",
|
||||
"forgot.password.page.title": "Passwort vergessen | {siteName}",
|
||||
@@ -93,20 +96,20 @@
|
||||
"password.security.block.body": "Unser System hat festgestellt, dass Ihr Passwort angreifbar ist. Ändern Sie Ihr Passwort, damit Ihr Konto sicher bleibt.",
|
||||
"password.security.close.button": "Schließen",
|
||||
"password.security.redirect.to.reset.password.button": "Setzen Sie Ihr Passwort zurück",
|
||||
"progressive.profiling.page.title": "Optionale Felder | {siteName}",
|
||||
"progressive.profiling.page.title": "Welcome | {siteName}",
|
||||
"progressive.profiling.page.heading": "Ein paar Fragen an Sie helfen uns, schlauer zu werden.",
|
||||
"optional.fields.information.link": "Erfahren Sie mehr darüber, wie wir diese Informationen verwenden.",
|
||||
"optional.fields.submit.button": "Einreichen",
|
||||
"optional.fields.skip.button": "Überspringen",
|
||||
"optional.fields.next.button": "Next",
|
||||
"optional.fields.next.button": "Weiter",
|
||||
"continue.to.platform": "Weiter zu {platformName}",
|
||||
"modal.title": "Danke, dass Sie uns das mitteilen.",
|
||||
"modal.description": "Sie können Ihr Profil jederzeit in den Einstellungen vervollständigen, wenn Sie Ihre Meinung ändern.",
|
||||
"welcome.page.error.heading": "Wir konnten Ihr Profil nicht aktualisieren",
|
||||
"welcome.page.error.message": "Ein Fehler ist aufgetreten. Sie können Ihr Profil jederzeit in den Einstellungen vervollständigen.",
|
||||
"recommendation.page.title": "Recommendations| {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"recommendation.page.title": "Empfehlungen | {siteName}",
|
||||
"recommendation.page.heading": "Wir haben ein paar Empfehlungen für den Einstieg.",
|
||||
"recommendation.skip.button": "Überspringen",
|
||||
"register.page.title": "Registrieren | {siteName}",
|
||||
"registration.fullname.label": "Vollständiger Name",
|
||||
"registration.email.label": "E-Mail-Adresse",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"logistration.register": "Registrarse",
|
||||
"enterprisetpa.title.heading": "¿Deseas iniciar sesión con tus credenciales de {providerName}?",
|
||||
"enterprisetpa.login.button.text": "Mostrar otras formas de iniciar sesión o de registrarme",
|
||||
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
|
||||
"sso.sign.in.with": "Inicio de sesión con {providerName}",
|
||||
"sso.create.account.using": "Crear una cuenta con {providerName}",
|
||||
"show.password": "Mostrar contraseña",
|
||||
@@ -21,6 +22,8 @@
|
||||
"login.third.party.auth.account.not.linked": "Te has registrado correctamente en {currentProvider}, pero tu cuenta de {currentProvider} no tiene una cuenta de {platformName} asociada. Para asociar tus cuentas, inicia sesión ahora usando tu contraseña de {platformName}.",
|
||||
"register.third.party.auth.account.not.linked": "¡Has iniciado sesión con éxito en {currentProvider}! Sólo necesitamos un poco más de información antes de que empieces a aprender con {platformName}.",
|
||||
"registration.using.tpa.form.heading": "Termina de crear tu cuenta",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, verifica la URL y vuelve a intentarlo.",
|
||||
"forgot.password.confirmation.message": "Hemos enviado un correo electrónico a {email} con instrucciones para restablecer tu contraseña.\n Si no recibes un mensaje de restablecimiento de contraseña después de 1 minuto, verifica que has introducido\n la dirección de correo electrónico correcta, o comprueba tu carpeta de correo no deseado. Si necesitas más ayuda, {supportLink}.",
|
||||
"forgot.password.page.title": "Olvidé la contraseña | {siteName}",
|
||||
@@ -93,20 +96,20 @@
|
||||
"password.security.block.body": "Nuestro sistema detectó que su contraseña es vulnerable. Cambie su contraseña para que su cuenta permanezca segura.",
|
||||
"password.security.close.button": "Cerrar",
|
||||
"password.security.redirect.to.reset.password.button": "Restablece tu contraseña",
|
||||
"progressive.profiling.page.title": "Campos opcionales | {siteName}",
|
||||
"progressive.profiling.page.title": "Welcome | {siteName}",
|
||||
"progressive.profiling.page.heading": "Unas cuantas preguntas para ti nos ayudarán a mejorar.",
|
||||
"optional.fields.information.link": "Aprende más sobre cómo usamos esta información.",
|
||||
"optional.fields.submit.button": "Enviar",
|
||||
"optional.fields.skip.button": "Saltar por ahora ",
|
||||
"optional.fields.next.button": "Next",
|
||||
"optional.fields.next.button": "Siguiente",
|
||||
"continue.to.platform": "Continuar a {platformName}",
|
||||
"modal.title": "Gracias por informarnos.",
|
||||
"modal.description": "Puedes completar tu perfil en los ajustes en cualquier momento si cambias de opinión.",
|
||||
"welcome.page.error.heading": "No hemos podido actualizar tu perfil",
|
||||
"welcome.page.error.message": "Se ha producido un error. Puedes completar tu perfil en los ajustes en cualquier momento.",
|
||||
"recommendation.page.title": "Recommendations| {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"recommendation.page.title": "Recomendaciones | {siteName}",
|
||||
"recommendation.page.heading": "Tenemos algunas recomendaciones para empezar.",
|
||||
"recommendation.skip.button": "Saltar por ahora ",
|
||||
"register.page.title": "Register | {siteName}",
|
||||
"registration.fullname.label": "Nombre completo",
|
||||
"registration.email.label": "Correo electrónico",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"logistration.register": "S'inscrire",
|
||||
"enterprisetpa.title.heading": "Souhaitez-vous vous connecter à l'aide de vos identifiants {providerName} ?",
|
||||
"enterprisetpa.login.button.text": "Montrez-moi d'autres méthodes pour me connecter ou m'inscrire",
|
||||
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
|
||||
"sso.sign.in.with": "Connectez-vous avec {providerName}",
|
||||
"sso.create.account.using": "Créer un compte avec {providerName}",
|
||||
"show.password": "Afficher le mot de passe",
|
||||
@@ -21,6 +22,8 @@
|
||||
"login.third.party.auth.account.not.linked": "Vous vous êtes connecté avec succès à {currentProvider}, mais votre compte {currentProvider} n'a pas de compte relié à {platformName}. Pour lier vos comptes, connectez-vous en utilisant votre mot de passe {platformName}.",
|
||||
"register.third.party.auth.account.not.linked": "Vous vous êtes connecté avec succès à {currentProvider} ! Nous avons juste besoin d'un peu plus d'informations avant que vous commenciez à apprendre avec {platformName}.",
|
||||
"registration.using.tpa.form.heading": "Terminer la création de votre compte",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
|
||||
"forgot.password.confirmation.message": "Nous avons envoyé un courriel à {email} avec des instructions pour réinitialiser votre mot de passe.\n Si vous ne recevez pas de message de réinitialisation de mot de passe après 1 minute, vérifiez que vous avez saisi\nl'adresse courriel correctement, ou vérifiez votre dossier de courriel indésirable. Si vous avez besoin d'aide supplémentaire, {supportLink}.",
|
||||
"forgot.password.page.title": " Mot de passe oublié | {siteName}",
|
||||
@@ -93,7 +96,7 @@
|
||||
"password.security.block.body": "Notre système a détecté que votre mot de passe est vulnérable. Changez votre mot de passe afin que votre compte reste sécurisé.",
|
||||
"password.security.close.button": "Fermer",
|
||||
"password.security.redirect.to.reset.password.button": "Réinitialiser votre mot de passe",
|
||||
"progressive.profiling.page.title": "Champs optionnels | {siteName}",
|
||||
"progressive.profiling.page.title": "Welcome | {siteName}",
|
||||
"progressive.profiling.page.heading": "Quelques questions pour vous nous aideront à devenir plus intelligents.",
|
||||
"optional.fields.information.link": "En savoir plus sur la façon dont nous utilisons ces informations.",
|
||||
"optional.fields.submit.button": "Envoyez",
|
||||
@@ -104,7 +107,7 @@
|
||||
"modal.description": "Vous pouvez compléter votre profil dans les paramètres à tout moment si vous changez d'avis.",
|
||||
"welcome.page.error.heading": "Nous n'avons pas pu mettre à jour votre profil",
|
||||
"welcome.page.error.message": "Une erreur s'est produite. Vous pouvez compléter votre profil dans les paramètres à tout moment.",
|
||||
"recommendation.page.title": "Recommendations| {siteName}",
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "S'inscrire | {siteName}",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"logistration.register": "Register",
|
||||
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
|
||||
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
|
||||
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
|
||||
"sso.sign.in.with": "Sign in with {providerName}",
|
||||
"sso.create.account.using": "Create account using {providerName}",
|
||||
"show.password": "Show password",
|
||||
@@ -21,6 +22,8 @@
|
||||
"login.third.party.auth.account.not.linked": "You have successfully signed into {currentProvider}, but your {currentProvider} account does not have a linked {platformName} account. To link your accounts, sign in now using your {platformName} password.",
|
||||
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Forgot Password | {siteName}",
|
||||
@@ -93,7 +96,7 @@
|
||||
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
|
||||
"password.security.close.button": "Close",
|
||||
"password.security.redirect.to.reset.password.button": "Reset your password",
|
||||
"progressive.profiling.page.title": "Optional Fields | {siteName}",
|
||||
"progressive.profiling.page.title": "Welcome | {siteName}",
|
||||
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
|
||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
||||
"optional.fields.submit.button": "Submit",
|
||||
@@ -104,7 +107,7 @@
|
||||
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
|
||||
"welcome.page.error.heading": "We couldn't update your profile",
|
||||
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time.",
|
||||
"recommendation.page.title": "Recommendations| {siteName}",
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "Register | {siteName}",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"logistration.register": "Registrazione",
|
||||
"enterprisetpa.title.heading": "Vuoi accedere utilizzando le credenziali {providerName}?",
|
||||
"enterprisetpa.login.button.text": "Mostrami altre modalità di accesso o registrazione",
|
||||
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
|
||||
"sso.sign.in.with": "Accedi con {providerName}",
|
||||
"sso.create.account.using": "Crea un account utilizzando {providerName}",
|
||||
"show.password": "Mostra password",
|
||||
@@ -21,6 +22,8 @@
|
||||
"login.third.party.auth.account.not.linked": "Hai correttamente effettuato l'accesso in {currentProvider}, ma il tuo account {currentProvider} non ha un account {platformName} ad esso abbinato. Per collegare i tuoi account accesi utilizzando la password {platformName}. ",
|
||||
"register.third.party.auth.account.not.linked": "Hai eseguito correttamente l'accesso a {a03f0f8cfb85cz0}! Abbiamo solo bisogno di un po' più di informazioni prima di iniziare a imparare con {platformName}.",
|
||||
"registration.using.tpa.form.heading": "Completa la creazione del tuo account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "La pagina che stai cercando non è disponibile o si è verificato un errore nell'URL. Controlla l'URL e riprova. ",
|
||||
"forgot.password.confirmation.message": "Abbiamo inviato un'email a {email} con le istruzioni per reimpostare la password. Se non ricevi un messaggio di reimpostazione della password dopo 1 minuto, verifica di aver inserito l'indirizzo e-mail corretto o controlla la cartella spam. Se hai bisogno di ulteriore assistenza, {supportLink}.",
|
||||
"forgot.password.page.title": "Dimenticato la password | {siteName}",
|
||||
@@ -93,7 +96,7 @@
|
||||
"password.security.block.body": "Il nostro sistema ha rilevato che la tua password è vulnerabile. Cambia la tua password in modo che il tuo account rimanga sicuro.",
|
||||
"password.security.close.button": "Chiudi",
|
||||
"password.security.redirect.to.reset.password.button": "Ripristina la tua password",
|
||||
"progressive.profiling.page.title": "Campi facoltativi | {siteName}",
|
||||
"progressive.profiling.page.title": "Welcome | {siteName}",
|
||||
"progressive.profiling.page.heading": "Alcune domande per te ci aiuteranno a diventare più intelligenti.",
|
||||
"optional.fields.information.link": "Ulteriori informazioni su come utilizziamo queste informazioni.",
|
||||
"optional.fields.submit.button": "Invia",
|
||||
@@ -104,7 +107,7 @@
|
||||
"modal.description": "Puoi completare il tuo profilo nelle impostazioni in qualsiasi momento se cambi idea.",
|
||||
"welcome.page.error.heading": "Impossibile aggiornare il tuo profilo",
|
||||
"welcome.page.error.message": "Si è verificato un errore. Puoi completare il tuo profilo nelle impostazioni in qualsiasi momento.",
|
||||
"recommendation.page.title": "Recommendations| {siteName}",
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "Registrazione | {siteName}",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"logistration.register": "Registe-se",
|
||||
"enterprisetpa.title.heading": "Gostaria de iniciar sessão usando as suas {providerName} credenciais?",
|
||||
"enterprisetpa.login.button.text": "Mostre-me outras formas de iniciar sessão ou registar-se",
|
||||
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
|
||||
"sso.sign.in.with": "Inicie sessão com {providerName}",
|
||||
"sso.create.account.using": "Criar conta usando {providerName}",
|
||||
"show.password": "Show password",
|
||||
@@ -21,6 +22,8 @@
|
||||
"login.third.party.auth.account.not.linked": "Iniciou sessão com sucesso em {currentProvider}, mas a sua conta {currentProvider} não está vinculada a uma conta {platformName}. Para vincular as suas contas, inicie sessão através da sua palavra-passe em {platformName}.",
|
||||
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "A página que procura não está disponível ou há um erro no URL. Por favor, verifique o URL e tente novamente.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Esqueceu a Senha | {siteName}",
|
||||
@@ -93,7 +96,7 @@
|
||||
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
|
||||
"password.security.close.button": "Close",
|
||||
"password.security.redirect.to.reset.password.button": "Reset your password",
|
||||
"progressive.profiling.page.title": "Optional Fields | {siteName}",
|
||||
"progressive.profiling.page.title": "Welcome | {siteName}",
|
||||
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
|
||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
||||
"optional.fields.submit.button": "Submit",
|
||||
@@ -104,7 +107,7 @@
|
||||
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
|
||||
"welcome.page.error.heading": "We couldn't update your profile",
|
||||
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time.",
|
||||
"recommendation.page.title": "Recommendations| {siteName}",
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "Registar | {siteName}",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"logistration.register": "Register",
|
||||
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
|
||||
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
|
||||
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
|
||||
"sso.sign.in.with": "Sign in with {providerName}",
|
||||
"sso.create.account.using": "Create account using {providerName}",
|
||||
"show.password": "Show password",
|
||||
@@ -21,6 +22,8 @@
|
||||
"login.third.party.auth.account.not.linked": "You have successfully signed into {currentProvider}, but your {currentProvider} account does not have a linked {platformName} account. To link your accounts, sign in now using your {platformName} password.",
|
||||
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Forgot Password | {siteName}",
|
||||
@@ -93,7 +96,7 @@
|
||||
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
|
||||
"password.security.close.button": "Close",
|
||||
"password.security.redirect.to.reset.password.button": "Reset your password",
|
||||
"progressive.profiling.page.title": "Optional Fields | {siteName}",
|
||||
"progressive.profiling.page.title": "Welcome | {siteName}",
|
||||
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
|
||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
||||
"optional.fields.submit.button": "Submit",
|
||||
@@ -104,7 +107,7 @@
|
||||
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
|
||||
"welcome.page.error.heading": "We couldn't update your profile",
|
||||
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time.",
|
||||
"recommendation.page.title": "Recommendations| {siteName}",
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "Register | {siteName}",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"logistration.register": "Register",
|
||||
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
|
||||
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
|
||||
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
|
||||
"sso.sign.in.with": "Sign in with {providerName}",
|
||||
"sso.create.account.using": "Create account using {providerName}",
|
||||
"show.password": "Show password",
|
||||
@@ -21,6 +22,8 @@
|
||||
"login.third.party.auth.account.not.linked": "You have successfully signed into {currentProvider}, but your {currentProvider} account does not have a linked {platformName} account. To link your accounts, sign in now using your {platformName} password.",
|
||||
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Forgot Password | {siteName}",
|
||||
@@ -93,7 +96,7 @@
|
||||
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
|
||||
"password.security.close.button": "Close",
|
||||
"password.security.redirect.to.reset.password.button": "Reset your password",
|
||||
"progressive.profiling.page.title": "Optional Fields | {siteName}",
|
||||
"progressive.profiling.page.title": "Welcome | {siteName}",
|
||||
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
|
||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
||||
"optional.fields.submit.button": "Submit",
|
||||
@@ -104,7 +107,7 @@
|
||||
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
|
||||
"welcome.page.error.heading": "We couldn't update your profile",
|
||||
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time.",
|
||||
"recommendation.page.title": "Recommendations| {siteName}",
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "Register | {siteName}",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"logistration.register": "Register",
|
||||
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
|
||||
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
|
||||
"enterprisetpa.login.button.text.public.account.creation.disabled": "Show me other ways to sign in",
|
||||
"sso.sign.in.with": "Sign in with {providerName}",
|
||||
"sso.create.account.using": "Create account using {providerName}",
|
||||
"show.password": "Show password",
|
||||
@@ -21,6 +22,8 @@
|
||||
"login.third.party.auth.account.not.linked": "You have successfully signed into {currentProvider}, but your {currentProvider} account does not have a linked {platformName} account. To link your accounts, sign in now using your {platformName} password.",
|
||||
"register.third.party.auth.account.not.linked": "You've successfully signed into {currentProvider}! We just need a little more information before you start learning with {platformName}.",
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"zendesk.supportTitle": "edX Support",
|
||||
"zendesk.selectTicketForm": "Please choose your request type:",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"forgot.password.confirmation.message": "We sent an email to {email} with instructions to reset your password.\n If you do not receive a password reset message after 1 minute, verify that you entered\n the correct email address, or check your spam folder. If you need further assistance, {supportLink}.",
|
||||
"forgot.password.page.title": "Forgot Password | {siteName}",
|
||||
@@ -93,7 +96,7 @@
|
||||
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
|
||||
"password.security.close.button": "Close",
|
||||
"password.security.redirect.to.reset.password.button": "Reset your password",
|
||||
"progressive.profiling.page.title": "Optional Fields | {siteName}",
|
||||
"progressive.profiling.page.title": "Welcome | {siteName}",
|
||||
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
|
||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
||||
"optional.fields.submit.button": "Submit",
|
||||
@@ -104,7 +107,7 @@
|
||||
"modal.description": "You can complete your profile in settings at any time if you change your mind.",
|
||||
"welcome.page.error.heading": "We couldn't update your profile",
|
||||
"welcome.page.error.message": "An error occurred. You can complete your profile in settings at any time.",
|
||||
"recommendation.page.title": "Recommendations| {siteName}",
|
||||
"recommendation.page.title": "Recommendations | {siteName}",
|
||||
"recommendation.page.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "Register | {siteName}",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { CheckCircle, Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -10,7 +10,8 @@ import { ACCOUNT_ACTIVATION_MESSAGE } from './data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
const AccountActivationMessage = (props) => {
|
||||
const { intl, messageType } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const { messageType } = props;
|
||||
const variant = messageType === ACCOUNT_ACTIVATION_MESSAGE.ERROR ? 'danger' : messageType;
|
||||
|
||||
const activationOrVerification = getConfig().MARKETING_EMAILS_OPT_IN ? 'confirmation' : 'activation';
|
||||
@@ -25,22 +26,22 @@ const AccountActivationMessage = (props) => {
|
||||
|
||||
switch (messageType) {
|
||||
case ACCOUNT_ACTIVATION_MESSAGE.SUCCESS: {
|
||||
heading = intl.formatMessage(messages[`account.${activationOrVerification}.success.message.title`]);
|
||||
activationMessage = <span>{intl.formatMessage(messages[`account.${activationOrVerification}.success.message`])}</span>;
|
||||
heading = formatMessage(messages[`account.${activationOrVerification}.success.message.title`]);
|
||||
activationMessage = <span>{formatMessage(messages[`account.${activationOrVerification}.success.message`])}</span>;
|
||||
break;
|
||||
}
|
||||
case ACCOUNT_ACTIVATION_MESSAGE.INFO: {
|
||||
activationMessage = intl.formatMessage(messages[`account.${activationOrVerification}.info.message`]);
|
||||
activationMessage = formatMessage(messages[`account.${activationOrVerification}.info.message`]);
|
||||
break;
|
||||
}
|
||||
case ACCOUNT_ACTIVATION_MESSAGE.ERROR: {
|
||||
const supportLink = (
|
||||
<Alert.Link href={getConfig().ACTIVATION_EMAIL_SUPPORT_LINK}>
|
||||
{intl.formatMessage(messages['account.activation.support.link'])}
|
||||
{formatMessage(messages['account.activation.support.link'])}
|
||||
</Alert.Link>
|
||||
);
|
||||
|
||||
heading = intl.formatMessage(messages[`account.${activationOrVerification}.error.message.title`]);
|
||||
heading = formatMessage(messages[`account.${activationOrVerification}.error.message.title`]);
|
||||
activationMessage = (
|
||||
<FormattedMessage
|
||||
id="account.activation.error.message"
|
||||
@@ -70,7 +71,6 @@ const AccountActivationMessage = (props) => {
|
||||
|
||||
AccountActivationMessage.propTypes = {
|
||||
messageType: PropTypes.string.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AccountActivationMessage);
|
||||
export default AccountActivationMessage;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, ModalDialog, useToggle,
|
||||
} from '@edx/paragon';
|
||||
@@ -14,7 +14,7 @@ import { updatePathWithQueryParams } from '../data/utils';
|
||||
import useMobileResponsive from '../data/utils/useMobileResponsive';
|
||||
import messages from './messages';
|
||||
|
||||
const ChangePasswordPrompt = ({ intl, variant, redirectUrl }) => {
|
||||
const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
|
||||
const isMobileView = useMobileResponsive();
|
||||
const [redirectToResetPasswordPage, setRedirectToResetPasswordPage] = useState(false);
|
||||
const handlers = {
|
||||
@@ -28,6 +28,7 @@ const ChangePasswordPrompt = ({ intl, variant, redirectUrl }) => {
|
||||
};
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [isOpen, open, close] = useToggle(true, handlers);
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
if (redirectToResetPasswordPage) {
|
||||
return <Redirect to={updatePathWithQueryParams(RESET_PAGE)} />;
|
||||
@@ -42,11 +43,11 @@ const ChangePasswordPrompt = ({ intl, variant, redirectUrl }) => {
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
{intl.formatMessage(messages[`password.security.${variant}.title`])}
|
||||
{formatMessage(messages[`password.security.${variant}.title`])}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
<ModalDialog.Body>
|
||||
{intl.formatMessage(messages[`password.security.${variant}.body`])}
|
||||
{formatMessage(messages[`password.security.${variant}.body`])}
|
||||
</ModalDialog.Body>
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow className={classNames(
|
||||
@@ -55,7 +56,7 @@ const ChangePasswordPrompt = ({ intl, variant, redirectUrl }) => {
|
||||
>
|
||||
{variant === 'nudge' ? (
|
||||
<ModalDialog.CloseButton id="password-security-close" variant="tertiary">
|
||||
{intl.formatMessage(messages['password.security.close.button'])}
|
||||
{formatMessage(messages['password.security.close.button'])}
|
||||
</ModalDialog.CloseButton>
|
||||
) : null}
|
||||
<Link
|
||||
@@ -66,7 +67,7 @@ const ChangePasswordPrompt = ({ intl, variant, redirectUrl }) => {
|
||||
)}
|
||||
to={updatePathWithQueryParams(RESET_PAGE)}
|
||||
>
|
||||
{intl.formatMessage(messages['password.security.redirect.to.reset.password.button'])}
|
||||
{formatMessage(messages['password.security.redirect.to.reset.password.button'])}
|
||||
</Link>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
@@ -80,9 +81,8 @@ ChangePasswordPrompt.defaultProps = {
|
||||
};
|
||||
|
||||
ChangePasswordPrompt.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
variant: PropTypes.oneOf(['nudge', 'block']),
|
||||
redirectUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(ChangePasswordPrompt);
|
||||
export default ChangePasswordPrompt;
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthService } from '@edx/frontend-platform/auth';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Hyperlink } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -20,17 +20,19 @@ import {
|
||||
NON_COMPLIANT_PASSWORD_EXCEPTION,
|
||||
NUDGE_PASSWORD_CHANGE,
|
||||
REQUIRE_PASSWORD_CHANGE,
|
||||
TPA_AUTHENTICATION_FAILURE,
|
||||
} from './data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
const LoginFailureMessage = (props) => {
|
||||
const { intl } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const { context, errorCode } = props.loginError;
|
||||
|
||||
const authService = getAuthService();
|
||||
let errorList;
|
||||
let resetLink = (
|
||||
<Hyperlink destination="/reset" isInline>
|
||||
{intl.formatMessage(messages['login.incorrect.credentials.error.reset.link.text'])}
|
||||
<Hyperlink destination="reset" isInline>
|
||||
{formatMessage(messages['login.incorrect.credentials.error.reset.link.text'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
@@ -38,19 +40,19 @@ const LoginFailureMessage = (props) => {
|
||||
case NON_COMPLIANT_PASSWORD_EXCEPTION: {
|
||||
errorList = (
|
||||
<>
|
||||
<strong>{intl.formatMessage(messages['non.compliant.password.title'])}</strong>
|
||||
<p>{intl.formatMessage(messages['non.compliant.password.message'])}</p>
|
||||
<strong>{formatMessage(messages['non.compliant.password.title'])}</strong>
|
||||
<p>{formatMessage(messages['non.compliant.password.message'])}</p>
|
||||
</>
|
||||
);
|
||||
break;
|
||||
}
|
||||
case FORBIDDEN_REQUEST:
|
||||
errorList = <p>{intl.formatMessage(messages['login.rate.limit.reached.message'])}</p>;
|
||||
errorList = <p>{formatMessage(messages['login.rate.limit.reached.message'])}</p>;
|
||||
break;
|
||||
case INACTIVE_USER: {
|
||||
const supportLink = (
|
||||
<a href={context.supportLink}>
|
||||
{intl.formatMessage(messages['contact.support.link'], { platformName: context.platformName })}
|
||||
{formatMessage(messages['contact.support.link'], { platformName: context.platformName })}
|
||||
</a>
|
||||
);
|
||||
errorList = (
|
||||
@@ -74,7 +76,7 @@ const LoginFailureMessage = (props) => {
|
||||
const url = `${getConfig().LMS_BASE_URL}/dashboard/?tpa_hint=${context.tpaHint}`;
|
||||
const tpaLink = (
|
||||
<a href={url}>
|
||||
{intl.formatMessage(messages['tpa.account.link'], { provider: context.provider })}
|
||||
{formatMessage(messages['tpa.account.link'], { provider: context.provider })}
|
||||
</a>
|
||||
);
|
||||
errorList = (
|
||||
@@ -90,12 +92,12 @@ const LoginFailureMessage = (props) => {
|
||||
break;
|
||||
}
|
||||
case INVALID_FORM:
|
||||
errorList = <p>{intl.formatMessage(messages['login.form.invalid.error.message'])}</p>;
|
||||
errorList = <p>{formatMessage(messages['login.form.invalid.error.message'])}</p>;
|
||||
break;
|
||||
case FAILED_LOGIN_ATTEMPT: {
|
||||
resetLink = (
|
||||
<Hyperlink destination="/reset" isInline>
|
||||
{intl.formatMessage(messages['login.incorrect.credentials.error.before.account.blocked.text'])}
|
||||
<Hyperlink destination="reset" isInline>
|
||||
{formatMessage(messages['login.incorrect.credentials.error.before.account.blocked.text'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
errorList = (
|
||||
@@ -124,7 +126,7 @@ const LoginFailureMessage = (props) => {
|
||||
case ACCOUNT_LOCKED_OUT: {
|
||||
errorList = (
|
||||
<>
|
||||
<p>{intl.formatMessage(messages['account.locked.out.message.1'])}</p>
|
||||
<p>{formatMessage(messages['account.locked.out.message.1'])}</p>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="account.locked.out.message.2"
|
||||
@@ -139,7 +141,7 @@ const LoginFailureMessage = (props) => {
|
||||
}
|
||||
case INCORRECT_EMAIL_PASSWORD:
|
||||
if (context.failureCount <= 1) {
|
||||
errorList = <p>{intl.formatMessage(messages['login.incorrect.credentials.error'])}</p>;
|
||||
errorList = <p>{formatMessage(messages['login.incorrect.credentials.error'])}</p>;
|
||||
} else if (context.failureCount === 2) {
|
||||
errorList = (
|
||||
<p>
|
||||
@@ -165,15 +167,25 @@ const LoginFailureMessage = (props) => {
|
||||
);
|
||||
case REQUIRE_PASSWORD_CHANGE:
|
||||
return <ChangePasswordPrompt />;
|
||||
case TPA_AUTHENTICATION_FAILURE:
|
||||
errorList = (
|
||||
<p>{formatMessage(messages['login.tpa.authentication.failure'], {
|
||||
platform_name: getConfig().SITE_NAME,
|
||||
lineBreak: <br />,
|
||||
errorMessage: context.errorMessage,
|
||||
})}
|
||||
</p>
|
||||
);
|
||||
break;
|
||||
case INTERNAL_SERVER_ERROR:
|
||||
default:
|
||||
errorList = <p>{intl.formatMessage(messages['internal.server.error.message'])}</p>;
|
||||
errorList = <p>{formatMessage(messages['internal.server.error.message'])}</p>;
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert id="login-failure-alert" className="mb-5" variant="danger" icon={Error}>
|
||||
<Alert.Heading>{intl.formatMessage(messages['login.failure.header.title'])}</Alert.Heading>
|
||||
<Alert.Heading>{formatMessage(messages['login.failure.header.title'])}</Alert.Heading>
|
||||
{ errorList }
|
||||
</Alert>
|
||||
);
|
||||
@@ -183,17 +195,26 @@ LoginFailureMessage.defaultProps = {
|
||||
loginError: {
|
||||
redirectUrl: null,
|
||||
errorCode: null,
|
||||
errorMessage: null,
|
||||
},
|
||||
};
|
||||
|
||||
LoginFailureMessage.propTypes = {
|
||||
loginError: PropTypes.shape({
|
||||
context: PropTypes.object,
|
||||
context: PropTypes.shape({
|
||||
supportLink: PropTypes.string,
|
||||
platformName: PropTypes.string,
|
||||
tpaHint: PropTypes.string,
|
||||
provider: PropTypes.string,
|
||||
allowedDomain: PropTypes.string,
|
||||
remainingAttempts: PropTypes.number,
|
||||
failureCount: PropTypes.number,
|
||||
errorMessage: PropTypes.string,
|
||||
}),
|
||||
email: PropTypes.string,
|
||||
errorCode: PropTypes.string,
|
||||
redirectUrl: PropTypes.string,
|
||||
}),
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LoginFailureMessage);
|
||||
export default LoginFailureMessage;
|
||||
|
||||
@@ -37,7 +37,7 @@ import AccountActivationMessage from './AccountActivationMessage';
|
||||
import {
|
||||
loginRemovePasswordResetBanner, loginRequest, loginRequestFailure, loginRequestReset, setLoginFormData,
|
||||
} from './data/actions';
|
||||
import { INVALID_FORM } from './data/constants';
|
||||
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
|
||||
import { loginErrorSelector, loginFormDataSelector, loginRequestSelector } from './data/selectors';
|
||||
import LoginFailureMessage from './LoginFailure';
|
||||
import messages from './messages';
|
||||
@@ -122,7 +122,7 @@ class LoginPage extends React.Component {
|
||||
email_or_username: emailOrUsername, password, ...this.queryParams,
|
||||
};
|
||||
this.props.loginRequest(payload);
|
||||
}
|
||||
};
|
||||
|
||||
handleOnFocus = (e) => {
|
||||
const { errors } = this.state;
|
||||
@@ -130,14 +130,14 @@ class LoginPage extends React.Component {
|
||||
this.props.setLoginFormData({
|
||||
errors,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleOnBlur = (e) => {
|
||||
const payload = {
|
||||
[e.target.name]: e.target.value,
|
||||
};
|
||||
this.props.setLoginFormData(payload);
|
||||
}
|
||||
};
|
||||
|
||||
handleForgotPasswordLinkClickEvent = () => {
|
||||
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
|
||||
@@ -223,7 +223,13 @@ class LoginPage extends React.Component {
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const tpaAuthenticationError = {};
|
||||
if (thirdPartyAuthContext.errorMessage) {
|
||||
tpaAuthenticationError.context = {
|
||||
errorMessage: thirdPartyAuthContext.errorMessage,
|
||||
};
|
||||
tpaAuthenticationError.errorCode = TPA_AUTHENTICATION_FAILURE;
|
||||
}
|
||||
if (this.props.loginResult.success) {
|
||||
setSurveyCookie('login');
|
||||
|
||||
@@ -253,6 +259,7 @@ class LoginPage extends React.Component {
|
||||
platformName={thirdPartyAuthContext.platformName}
|
||||
/>
|
||||
{this.props.loginError ? <LoginFailureMessage loginError={this.props.loginError} /> : null}
|
||||
{thirdPartyAuthContext.errorMessage ? <LoginFailureMessage loginError={tpaAuthenticationError} /> : null}
|
||||
{submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null}
|
||||
{activationMsgType && <AccountActivationMessage messageType={activationMsgType} />}
|
||||
{this.props.resetPassword && !this.props.loginError ? <ResetPasswordSuccess /> : null}
|
||||
@@ -361,6 +368,7 @@ LoginPage.defaultProps = {
|
||||
thirdPartyAuthApiStatus: 'pending',
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
errorMessage: null,
|
||||
finishAuthUrl: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
@@ -369,8 +377,10 @@ LoginPage.defaultProps = {
|
||||
|
||||
LoginPage.propTypes = {
|
||||
getThirdPartyAuthContext: PropTypes.func.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
loginError: PropTypes.objectOf(PropTypes.any),
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func,
|
||||
}).isRequired,
|
||||
loginError: PropTypes.shape({}),
|
||||
loginRequest: PropTypes.func.isRequired,
|
||||
loginRequestFailure: PropTypes.func.isRequired,
|
||||
loginRequestReset: PropTypes.func.isRequired,
|
||||
@@ -393,9 +403,10 @@ LoginPage.propTypes = {
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
thirdPartyAuthContext: PropTypes.shape({
|
||||
currentProvider: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
platformName: PropTypes.string,
|
||||
providers: PropTypes.array,
|
||||
secondaryProviders: PropTypes.array,
|
||||
providers: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
|
||||
finishAuthUrl: PropTypes.string,
|
||||
}),
|
||||
institutionLogin: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -10,6 +10,7 @@ export const INCORRECT_EMAIL_PASSWORD = 'incorrect-email-or-password';
|
||||
export const NUDGE_PASSWORD_CHANGE = 'nudge-password-change';
|
||||
export const REQUIRE_PASSWORD_CHANGE = 'require-password-change';
|
||||
export const ALLOWED_DOMAIN_LOGIN_ERROR = 'allowed-domain-login-error';
|
||||
export const TPA_AUTHENTICATION_FAILURE = 'tpa-authentication-failure';
|
||||
|
||||
// Account Activation Message
|
||||
export const ACCOUNT_ACTIVATION_MESSAGE = {
|
||||
|
||||
@@ -16,7 +16,7 @@ export const defaultState = {
|
||||
},
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case LOGIN_REQUEST.BEGIN:
|
||||
return {
|
||||
|
||||
@@ -209,6 +209,13 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Reset your password',
|
||||
description: 'Button to redirect users to Reset Password page',
|
||||
},
|
||||
'login.tpa.authentication.failure': {
|
||||
id: 'login.tpa.authentication.failure',
|
||||
defaultMessage: 'We are sorry, you are not authorized to access {platform_name} via this channel. '
|
||||
+ 'Please contact your learning administrator or manager in order to access {platform_name}.'
|
||||
+ '{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}',
|
||||
description: 'Error message third party authentication pipeline fails',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
NON_COMPLIANT_PASSWORD_EXCEPTION,
|
||||
NUDGE_PASSWORD_CHANGE,
|
||||
REQUIRE_PASSWORD_CHANGE,
|
||||
TPA_AUTHENTICATION_FAILURE,
|
||||
} from '../data/constants';
|
||||
import LoginFailureMessage from '../LoginFailure';
|
||||
|
||||
@@ -219,6 +220,28 @@ describe('LoginFailureMessage', () => {
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match tpa authentication failed error message', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
errorCode: TPA_AUTHENTICATION_FAILURE,
|
||||
context: {
|
||||
errorMessage: 'An error occured',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const loginFailureMessage = mount(
|
||||
<IntlProvider locale="en">
|
||||
<IntlLoginFailureMessage {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const expectedMessageSubstring = 'We are sorry, you are not authorized to access';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toContain(expectedMessageSubstring);
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toContain('An error occured');
|
||||
});
|
||||
|
||||
it('should show modal that nudges users to change password', () => {
|
||||
props = {
|
||||
loginError: {
|
||||
|
||||
@@ -441,6 +441,23 @@ describe('LoginPage', () => {
|
||||
expect(loginPage.find('#tpa-alert').find('p').text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should show tpa authentication fails error message', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
currentProvider: null,
|
||||
errorMessage: 'An error occured',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('#login-failure-alert').find('p').text()).toContain('An error occured');
|
||||
});
|
||||
|
||||
it('should match invalid login form error message', () => {
|
||||
const errorMessage = 'Please fill in the fields below.';
|
||||
store = mockStore({
|
||||
@@ -611,6 +628,51 @@ describe('LoginPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should render other ways to sign in button', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [ssoProvider],
|
||||
},
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat('/login'), search: `?tpa_hint=${ssoProvider.id}` };
|
||||
ssoProvider.iconImage = null;
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('button#other-ways-to-sign-in').text()).toEqual('Show me other ways to sign in or register');
|
||||
});
|
||||
|
||||
it('should render other ways to sign in button when public account creation disabled', () => {
|
||||
mergeConfig({
|
||||
ALLOW_PUBLIC_ACCOUNT_CREATION: false,
|
||||
});
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [ssoProvider],
|
||||
},
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
},
|
||||
});
|
||||
|
||||
delete window.location;
|
||||
window.location = { href: getConfig().BASE_URL.concat('/login'), search: `?tpa_hint=${ssoProvider.id}` };
|
||||
ssoProvider.iconImage = null;
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.find('button#other-ways-to-sign-in').text()).toEqual('Show me other ways to sign in');
|
||||
});
|
||||
|
||||
// ******** miscellaneous tests ********
|
||||
|
||||
it('should render cookie banner', () => {
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { identifyAuthenticatedUser, sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
AxiosJwtAuthService,
|
||||
configure as configureAuth,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
getAuthenticatedUser,
|
||||
hydrateAuthenticatedUser,
|
||||
} from '@edx/frontend-platform/auth';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getLoggingService } from '@edx/frontend-platform/logging';
|
||||
import {
|
||||
Alert,
|
||||
@@ -22,15 +22,17 @@ import { Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import BaseComponent from '../base-component';
|
||||
import { BaseComponent } from '../base-component';
|
||||
import { RedirectLogistration } from '../common-components';
|
||||
import {
|
||||
DEFAULT_REDIRECT_URL, DEFAULT_STATE, FAILURE_STATE,
|
||||
} from '../data/constants';
|
||||
import { getAllPossibleQueryParams } from '../data/utils';
|
||||
import FormFieldRenderer from '../field-renderer';
|
||||
import { activateRecommendationsExperiment, isUserInVariation } from '../recommendations/optimizelyExperiment';
|
||||
import { trackRecommendationsViewed } from '../recommendations/track';
|
||||
import { FormFieldRenderer } from '../field-renderer';
|
||||
import {
|
||||
activateRecommendationsExperiment, RECOMMENDATIONS_EXP_VARIATION, trackRecommendationViewedOptimizely,
|
||||
} from '../recommendations/optimizelyExperiment';
|
||||
import { trackRecommendationsGroup, trackRecommendationsViewed } from '../recommendations/track';
|
||||
import { saveUserProfile } from './data/actions';
|
||||
import { welcomePageSelector } from './data/selectors';
|
||||
import messages from './messages';
|
||||
@@ -38,10 +40,12 @@ import ProgressiveProfilingPageModal from './ProgressiveProfilingPageModal';
|
||||
|
||||
const ProgressiveProfiling = (props) => {
|
||||
const {
|
||||
formRenderState, intl, submitState, showError, location,
|
||||
formRenderState, submitState, showError, location,
|
||||
} = props;
|
||||
const enablePersonalizedRecommendations = getConfig().ENABLE_PERSONALIZED_RECOMMENDATIONS;
|
||||
const registrationResponse = location.state?.registrationResult;
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
const [ready, setReady] = useState(false);
|
||||
const [registrationResult, setRegistrationResult] = useState({ redirectUrl: '' });
|
||||
const [values, setValues] = useState({});
|
||||
@@ -62,27 +66,32 @@ const ProgressiveProfiling = (props) => {
|
||||
|
||||
if (registrationResponse) {
|
||||
setRegistrationResult(registrationResponse);
|
||||
sendPageEvent('login_and_registration', 'welcome');
|
||||
}
|
||||
}, [DASHBOARD_URL, registrationResponse]);
|
||||
|
||||
useEffect(() => {
|
||||
let queryParams = {};
|
||||
let timer = null;
|
||||
if (registrationResponse) {
|
||||
queryParams = getAllPossibleQueryParams(registrationResponse.redirectUrl);
|
||||
if (ready && authenticatedUser?.userId) {
|
||||
identifyAuthenticatedUser(authenticatedUser.userId);
|
||||
sendPageEvent('login_and_registration', 'welcome');
|
||||
}
|
||||
}, [authenticatedUser, ready]);
|
||||
|
||||
useEffect(() => {
|
||||
if (registrationResponse && authenticatedUser?.userId) {
|
||||
const queryParams = getAllPossibleQueryParams(registrationResponse.redirectUrl);
|
||||
if (enablePersonalizedRecommendations && !('enrollment_action' in queryParams)) {
|
||||
activateRecommendationsExperiment();
|
||||
timer = setTimeout(() => {
|
||||
const showRecommendations = isUserInVariation();
|
||||
setShowRecommendationsPage(showRecommendations);
|
||||
if (!showRecommendations) {
|
||||
trackRecommendationsViewed([], true, authenticatedUser?.userId);
|
||||
}
|
||||
}, 500);
|
||||
const userIdStr = authenticatedUser.userId.toString();
|
||||
const variation = activateRecommendationsExperiment(userIdStr);
|
||||
const showRecommendations = variation === RECOMMENDATIONS_EXP_VARIATION;
|
||||
|
||||
trackRecommendationsGroup(variation, authenticatedUser.userId);
|
||||
trackRecommendationViewedOptimizely(userIdStr);
|
||||
setShowRecommendationsPage(showRecommendations);
|
||||
if (!showRecommendations) {
|
||||
trackRecommendationsViewed([], true, authenticatedUser.userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [authenticatedUser, enablePersonalizedRecommendations, registrationResponse]);
|
||||
|
||||
if (!location.state || !location.state.registrationResult || formRenderState === FAILURE_STATE) {
|
||||
@@ -150,36 +159,36 @@ const ProgressiveProfiling = (props) => {
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseComponent showWelcomeBanner>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['progressive.profiling.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
<ProgressiveProfilingPageModal isOpen={openDialog} redirectUrl={registrationResult.redirectUrl} />
|
||||
{props.shouldRedirect ? (
|
||||
<RedirectLogistration
|
||||
success
|
||||
redirectUrl={registrationResult.redirectUrl}
|
||||
redirectToRecommendationsPage={showRecommendationsPage}
|
||||
educationLevel={values?.level_of_education}
|
||||
userId={authenticatedUser?.userId}
|
||||
/>
|
||||
<BaseComponent showWelcomeBanner>
|
||||
<Helmet>
|
||||
<title>{formatMessage(messages['progressive.profiling.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
<ProgressiveProfilingPageModal isOpen={openDialog} redirectUrl={registrationResult.redirectUrl} />
|
||||
{props.shouldRedirect ? (
|
||||
<RedirectLogistration
|
||||
success
|
||||
redirectUrl={registrationResult.redirectUrl}
|
||||
redirectToRecommendationsPage={showRecommendationsPage}
|
||||
educationLevel={values?.level_of_education}
|
||||
userId={authenticatedUser?.userId}
|
||||
/>
|
||||
) : null}
|
||||
<div className="mw-xs m-4 pp-page-content">
|
||||
<div>
|
||||
<h2 className="pp-page-heading text-primary">{formatMessage(messages['progressive.profiling.page.heading'])}</h2>
|
||||
</div>
|
||||
<hr className="border-light-700 mb-4" />
|
||||
{showError ? (
|
||||
<Alert id="pp-page-errors" className="mb-3" variant="danger" icon={Error}>
|
||||
<Alert.Heading>{formatMessage(messages['welcome.page.error.heading'])}</Alert.Heading>
|
||||
<p>{formatMessage(messages['welcome.page.error.message'])}</p>
|
||||
</Alert>
|
||||
) : null}
|
||||
<div className="mw-xs pp-page-content">
|
||||
<div>
|
||||
<h2 className="pp-page-heading text-primary">{intl.formatMessage(messages['progressive.profiling.page.heading'])}</h2>
|
||||
</div>
|
||||
<hr className="border-light-700 mb-4" />
|
||||
{showError ? (
|
||||
<Alert id="pp-page-errors" className="mb-3" variant="danger" icon={Error}>
|
||||
<Alert.Heading>{intl.formatMessage(messages['welcome.page.error.heading'])}</Alert.Heading>
|
||||
<p>{intl.formatMessage(messages['welcome.page.error.message'])}</p>
|
||||
</Alert>
|
||||
) : null}
|
||||
<Form>
|
||||
{formFields}
|
||||
<Form>
|
||||
{formFields}
|
||||
{(getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK) && (
|
||||
<span className="progressive-profiling-support">
|
||||
<Hyperlink
|
||||
isInline
|
||||
@@ -189,45 +198,52 @@ const ProgressiveProfiling = (props) => {
|
||||
showLaunchIcon={false}
|
||||
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
|
||||
>
|
||||
{intl.formatMessage(messages['optional.fields.information.link'])}
|
||||
{formatMessage(messages['optional.fields.information.link'])}
|
||||
</Hyperlink>
|
||||
</span>
|
||||
<div className="d-flex mt-4 mb-3">
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="login-button-width"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: showRecommendationsPage ? intl.formatMessage(messages['optional.fields.next.button']) : intl.formatMessage(messages['optional.fields.submit.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
<StatefulButton
|
||||
className="text-gray-700 font-weight-500"
|
||||
type="submit"
|
||||
variant="link"
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['optional.fields.skip.button']),
|
||||
}}
|
||||
onClick={handleSkip}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</BaseComponent>
|
||||
</>
|
||||
)}
|
||||
<div className="d-flex mt-4 mb-3">
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="login-button-width"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: showRecommendationsPage ? formatMessage(messages['optional.fields.next.button']) : formatMessage(messages['optional.fields.submit.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
<StatefulButton
|
||||
className="text-gray-700 font-weight-500"
|
||||
type="submit"
|
||||
variant="link"
|
||||
labels={{
|
||||
default: formatMessage(messages['optional.fields.skip.button']),
|
||||
}}
|
||||
onClick={handleSkip}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</BaseComponent>
|
||||
);
|
||||
};
|
||||
|
||||
ProgressiveProfiling.propTypes = {
|
||||
formRenderState: PropTypes.string.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
location: PropTypes.shape({
|
||||
state: PropTypes.object,
|
||||
state: PropTypes.shape({
|
||||
registrationResult: PropTypes.shape({
|
||||
redirectUrl: PropTypes.string,
|
||||
}),
|
||||
optionalFields: PropTypes.shape({
|
||||
extended_profile: PropTypes.arrayOf(PropTypes.string),
|
||||
fields: PropTypes.shape({}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
saveUserProfile: PropTypes.func.isRequired,
|
||||
showError: PropTypes.bool,
|
||||
@@ -254,4 +270,4 @@ export default connect(
|
||||
{
|
||||
saveUserProfile,
|
||||
},
|
||||
)(injectIntl(ProgressiveProfiling));
|
||||
)(ProgressiveProfiling);
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { ActionRow, Button, ModalDialog } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ProgressiveProfilingPageModal = (props) => {
|
||||
const { intl, isOpen, redirectUrl } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const { isOpen, redirectUrl } = props;
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
@@ -18,7 +19,7 @@ const ProgressiveProfilingPageModal = (props) => {
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
title={intl.formatMessage(messages['modal.title'])}
|
||||
title={formatMessage(messages['modal.title'])}
|
||||
isOpen={isOpen}
|
||||
onClose={() => {}}
|
||||
size="sm"
|
||||
@@ -27,18 +28,18 @@ const ProgressiveProfilingPageModal = (props) => {
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
{intl.formatMessage(messages['modal.title'])}
|
||||
{formatMessage(messages['modal.title'])}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
|
||||
<ModalDialog.Body>
|
||||
{intl.formatMessage(messages['modal.description'])}
|
||||
{formatMessage(messages['modal.description'])}
|
||||
</ModalDialog.Body>
|
||||
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<Button onClick={handleSubmit} variant="primary">
|
||||
{intl.formatMessage(messages['continue.to.platform'], { platformName })}
|
||||
{formatMessage(messages['continue.to.platform'], { platformName })}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
@@ -47,7 +48,6 @@ const ProgressiveProfilingPageModal = (props) => {
|
||||
};
|
||||
|
||||
ProgressiveProfilingPageModal.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
isOpen: PropTypes.bool,
|
||||
redirectUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -56,4 +56,4 @@ ProgressiveProfilingPageModal.defaultProps = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
export default injectIntl(ProgressiveProfilingPageModal);
|
||||
export default ProgressiveProfilingPageModal;
|
||||
|
||||
@@ -12,7 +12,7 @@ export const defaultState = {
|
||||
showError: false,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case SAVE_USER_PROFILE.BEGIN:
|
||||
return {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
const messages = defineMessages({
|
||||
'progressive.profiling.page.title': {
|
||||
id: 'progressive.profiling.page.title',
|
||||
defaultMessage: 'Optional Fields | {siteName}',
|
||||
defaultMessage: 'Welcome | {siteName}',
|
||||
description: 'progressive profiling page title',
|
||||
},
|
||||
'progressive.profiling.page.heading': {
|
||||
|
||||
@@ -27,6 +27,7 @@ jest.mock('@edx/frontend-platform/logging');
|
||||
|
||||
analytics.sendTrackEvent = jest.fn();
|
||||
analytics.sendPageEvent = jest.fn();
|
||||
analytics.identifyAuthenticatedUser = jest.fn();
|
||||
logging.getLoggingService = jest.fn();
|
||||
|
||||
auth.configure = jest.fn();
|
||||
@@ -76,7 +77,7 @@ describe('ProgressiveProfilingTests', () => {
|
||||
));
|
||||
await act(async () => {
|
||||
await Promise.resolve(progressiveProfilingPage);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
await new Promise(resolve => { setImmediate(resolve); });
|
||||
progressiveProfilingPage.update();
|
||||
});
|
||||
|
||||
@@ -104,11 +105,36 @@ describe('ProgressiveProfilingTests', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('not should display button "Learn more about how we use this information."', async () => {
|
||||
mergeConfig({
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: '',
|
||||
});
|
||||
const progressiveProfilingPage = await getProgressiveProfilingPage();
|
||||
|
||||
expect(progressiveProfilingPage.find('a.pgn__hyperlink').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should display button "Learn more about how we use this information."', async () => {
|
||||
mergeConfig({
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/support',
|
||||
});
|
||||
const progressiveProfilingPage = await getProgressiveProfilingPage();
|
||||
|
||||
expect(progressiveProfilingPage.find('a.pgn__hyperlink').text()).toEqual('Learn more about how we use this information.');
|
||||
});
|
||||
|
||||
it('should render fields returned by backend api', async () => {
|
||||
const progressiveProfilingPage = await getProgressiveProfilingPage();
|
||||
expect(progressiveProfilingPage.find('#gender').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should make identify call to segment on progressive profiling page', async () => {
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'abc123' }));
|
||||
await getProgressiveProfilingPage();
|
||||
expect(analytics.identifyAuthenticatedUser).toHaveBeenCalledWith(3);
|
||||
expect(analytics.identifyAuthenticatedUser).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should submit user profile details on form submission', async () => {
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'abc123' }));
|
||||
const formPayload = {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Card, Hyperlink } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { trackRecommendationCardClickOptimizely } from './optimizelyExperiment';
|
||||
import { trackRecommendationsClicked } from './track';
|
||||
|
||||
const RecommendationCard = (props) => {
|
||||
@@ -23,6 +23,7 @@ const RecommendationCard = (props) => {
|
||||
};
|
||||
|
||||
const handleCardClick = () => {
|
||||
trackRecommendationCardClickOptimizely(userId?.toString());
|
||||
trackRecommendationsClicked(
|
||||
recommendation.courseKey,
|
||||
false,
|
||||
@@ -38,6 +39,7 @@ const RecommendationCard = (props) => {
|
||||
<Hyperlink
|
||||
destination={recommendation.marketingUrl}
|
||||
target="_blank"
|
||||
className="card-box"
|
||||
showLaunchIcon={false}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
@@ -82,4 +84,4 @@ RecommendationCard.defaultProps = {
|
||||
userId: null,
|
||||
};
|
||||
|
||||
export default injectIntl(RecommendationCard);
|
||||
export default RecommendationCard;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Container } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -53,4 +52,4 @@ RecommendationsList.defaultProps = {
|
||||
userId: null,
|
||||
};
|
||||
|
||||
export default injectIntl(RecommendationsList);
|
||||
export default RecommendationsList;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Hyperlink, Image, Spinner, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
@@ -17,13 +17,15 @@ import RecommendationsList from './RecommendationsList';
|
||||
import { trackRecommendationsViewed } from './track';
|
||||
|
||||
const RecommendationsPage = (props) => {
|
||||
const { intl, location } = props;
|
||||
const { location } = props;
|
||||
const registrationResponse = location.state?.registrationResult;
|
||||
const userId = location.state?.userId;
|
||||
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [recommendations, setRecommendations] = useState([]);
|
||||
const [algoliaRecommendations, setAlgoliaRecommendations] = useState([]);
|
||||
const educationLevel = EDUCATION_LEVEL_MAPPING[location.state?.educationLevel];
|
||||
|
||||
useEffect(() => {
|
||||
@@ -35,6 +37,7 @@ const RecommendationsPage = (props) => {
|
||||
...course,
|
||||
courseKey: convertCourseRunKeytoCourseKey(course.activeRunKey),
|
||||
}));
|
||||
setAlgoliaRecommendations(coursesWithKeys.slice(0, RECOMMENDATIONS_COUNT));
|
||||
|
||||
if (coursesWithKeys.length >= RECOMMENDATIONS_COUNT) {
|
||||
setRecommendations(coursesWithKeys.slice(0, RECOMMENDATIONS_COUNT));
|
||||
@@ -55,12 +58,17 @@ const RecommendationsPage = (props) => {
|
||||
setRecommendations(generalRecommendations.slice(0, RECOMMENDATIONS_COUNT));
|
||||
setIsLoading(false);
|
||||
});
|
||||
// We only want to track the recommendations returned by Algolia
|
||||
const courseKeys = coursesWithKeys.map(course => course.courseKey);
|
||||
trackRecommendationsViewed(courseKeys.slice(0, RECOMMENDATIONS_COUNT), false, userId);
|
||||
}
|
||||
}, [registrationResponse, DASHBOARD_URL, educationLevel, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
// We only want to track the recommendations returned by Algolia
|
||||
const courseKeys = algoliaRecommendations.map(course => course.courseKey);
|
||||
trackRecommendationsViewed(courseKeys, false, userId);
|
||||
}
|
||||
}, [isLoading, algoliaRecommendations, userId]);
|
||||
|
||||
if (!registrationResponse) {
|
||||
global.location.assign(DASHBOARD_URL);
|
||||
return null;
|
||||
@@ -87,7 +95,7 @@ const RecommendationsPage = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['recommendation.page.title'],
|
||||
<title>{formatMessage(messages['recommendation.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
@@ -101,7 +109,7 @@ const RecommendationsPage = (props) => {
|
||||
{(!isLoading && recommendations.length === RECOMMENDATIONS_COUNT) ? (
|
||||
<div className="d-flex flex-column align-items-center justify-content-center flex-grow-1 p-1">
|
||||
<RecommendationsList
|
||||
title={intl.formatMessage(messages['recommendation.page.heading'])}
|
||||
title={formatMessage(messages['recommendation.page.heading'])}
|
||||
recommendations={recommendations}
|
||||
userId={userId}
|
||||
/>
|
||||
@@ -111,7 +119,7 @@ const RecommendationsPage = (props) => {
|
||||
type="submit"
|
||||
variant="brand"
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['recommendation.skip.button']),
|
||||
default: formatMessage(messages['recommendation.skip.button']),
|
||||
}}
|
||||
onClick={handleSkip}
|
||||
/>
|
||||
@@ -127,9 +135,14 @@ const RecommendationsPage = (props) => {
|
||||
};
|
||||
|
||||
RecommendationsPage.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
location: PropTypes.shape({
|
||||
state: PropTypes.object,
|
||||
state: PropTypes.shape({
|
||||
registrationResult: PropTypes.shape({
|
||||
redirectUrl: PropTypes.string,
|
||||
}),
|
||||
userId: PropTypes.number,
|
||||
educationLevel: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
|
||||
};
|
||||
@@ -138,4 +151,4 @@ RecommendationsPage.defaultProps = {
|
||||
location: { state: {} },
|
||||
};
|
||||
|
||||
export default injectIntl(RecommendationsPage);
|
||||
export default RecommendationsPage;
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export { default } from './RecommendationsPage';
|
||||
/* eslint-disable import/prefer-default-export */
|
||||
export { default as RecommendationsPage } from './RecommendationsPage';
|
||||
|
||||
@@ -1,21 +1,42 @@
|
||||
const RECOMMENDATIONS_EXP_ID = process.env.RECOMMENDATIONS_EXPERIMENT_ID;
|
||||
const RECOMMENDATIONS_EXP_VARIATION = 'show_recommendations_page';
|
||||
import optimizelyInstance from '../data/optimizely';
|
||||
|
||||
const activateRecommendationsExperiment = () => {
|
||||
if (window.optimizely) {
|
||||
window.optimizely.push({
|
||||
type: 'page',
|
||||
pageName: 'van_1294_personalized_recommendations_on_authn',
|
||||
});
|
||||
}
|
||||
const RECOMMENDATIONS_EXP_KEY = 'welcome_page_recommendations_exp';
|
||||
const RECOMMENDATIONS_EXP_VARIATION = 'welcome_page_recommendations_enabled';
|
||||
|
||||
export const eventNames = {
|
||||
recommendedCourseClicked: 'welcome_page_recommendation_card_click',
|
||||
recommendationsViewed: 'welcome_page_recommendations_viewed',
|
||||
};
|
||||
|
||||
const isUserInVariation = () => {
|
||||
if (window.optimizely) {
|
||||
const selectedVariant = window.optimizely.get('state').getVariationMap()[RECOMMENDATIONS_EXP_ID];
|
||||
return selectedVariant?.name === RECOMMENDATIONS_EXP_VARIATION;
|
||||
}
|
||||
return false;
|
||||
/**
|
||||
* Activate the post registration recommendations optimizely experiment
|
||||
* and return the true if the user is in variation else false.
|
||||
* @param {String} userId user id of authenticated user.
|
||||
* @return {string} true if the user is in variation else false
|
||||
*/
|
||||
const activateRecommendationsExperiment = (userId) => optimizelyInstance.activate(RECOMMENDATIONS_EXP_KEY, userId);
|
||||
|
||||
/**
|
||||
* Fire an optimizely track event for post registration recommended course card clicked.
|
||||
* @param {String} userId user id of authenticated user.
|
||||
* @param {Object} userAttributes Dictionary of user attributes (optional).
|
||||
*/
|
||||
const trackRecommendationCardClickOptimizely = (userId, userAttributes = {}) => {
|
||||
optimizelyInstance.track(eventNames.recommendedCourseClicked, userId, userAttributes);
|
||||
};
|
||||
|
||||
export { activateRecommendationsExperiment, isUserInVariation };
|
||||
/**
|
||||
* Fire an optimizely track event for post registration recommendation viewed.
|
||||
* @param {String} userId user id of authenticated user.
|
||||
* @param {Object} userAttributes Dictionary of user attributes (optional).
|
||||
*/
|
||||
const trackRecommendationViewedOptimizely = (userId, userAttributes = {}) => {
|
||||
optimizelyInstance.track(eventNames.recommendationsViewed, userId, userAttributes);
|
||||
};
|
||||
|
||||
export {
|
||||
RECOMMENDATIONS_EXP_VARIATION,
|
||||
activateRecommendationsExperiment,
|
||||
trackRecommendationCardClickOptimizely,
|
||||
trackRecommendationViewedOptimizely,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import configureStore from 'redux-mock-store';
|
||||
|
||||
import { DEFAULT_REDIRECT_URL } from '../../data/constants';
|
||||
import * as getPersonalizedRecommendations from '../data/service';
|
||||
import { trackRecommendationCardClickOptimizely } from '../optimizelyExperiment';
|
||||
import RecommendationsPage from '../RecommendationsPage';
|
||||
import { mockedGeneralRecommendations, mockedResponse } from './mockedData';
|
||||
|
||||
@@ -18,6 +19,9 @@ const mockStore = configureStore();
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
jest.mock('../data/service');
|
||||
jest.mock('../optimizelyExperiment', () => ({
|
||||
trackRecommendationCardClickOptimizely: jest.fn(),
|
||||
}));
|
||||
|
||||
analytics.sendTrackEvent = jest.fn();
|
||||
|
||||
@@ -29,7 +33,7 @@ describe('RecommendationsPageTests', () => {
|
||||
let defaultProps = {};
|
||||
let store = {};
|
||||
|
||||
const registrationResult = {
|
||||
let registrationResult = {
|
||||
redirectUrl: getConfig().LMS_BASE_URL.concat('/course-about-page-url'),
|
||||
success: true,
|
||||
};
|
||||
@@ -74,6 +78,32 @@ describe('RecommendationsPageTests', () => {
|
||||
expect(getPersonalizedRecommendations.default).toHaveBeenCalledTimes(0);
|
||||
expect(window.location.href).toEqual(DASHBOARD_URL);
|
||||
});
|
||||
it('redirects to dashboard if user click on skip button', async () => {
|
||||
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
registrationResult = {
|
||||
...registrationResult,
|
||||
redirectUrl: getConfig().LMS_BASE_URL.concat('/dashboard'),
|
||||
};
|
||||
const props = {
|
||||
location: {
|
||||
state: {
|
||||
registrationResult,
|
||||
userId: 111,
|
||||
},
|
||||
},
|
||||
};
|
||||
getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve(mockedResponse));
|
||||
const recommendationsPage = await getRecommendationsPage(props);
|
||||
recommendationsPage.find('button').simulate('click');
|
||||
expect(window.location.href).toEqual(DASHBOARD_URL);
|
||||
});
|
||||
|
||||
it('should call trackRecommendationCardClickOptimizely when card is clicked', async () => {
|
||||
getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve(mockedResponse));
|
||||
const recommendationsPage = await getRecommendationsPage();
|
||||
recommendationsPage.find('.card-box').first().simulate('click');
|
||||
expect(trackRecommendationCardClickOptimizely).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should show loading state to user', async () => {
|
||||
getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve(mockedResponse));
|
||||
@@ -109,6 +139,13 @@ describe('RecommendationsPageTests', () => {
|
||||
expect(recommendationsPage.find('#course-recommendations').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not display recommendations if error comes in while fetching the recommendations', async () => {
|
||||
getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.reject(mockedResponse));
|
||||
const recommendationsPage = await getRecommendationsPage();
|
||||
|
||||
expect(recommendationsPage.find('#recommendation-card').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should redirect if recommended courses count is less than RECOMMENDATIONS_COUNT', async () => {
|
||||
delete window.location;
|
||||
window.location = { assign: jest.fn() };
|
||||
|
||||
@@ -4,6 +4,7 @@ export const LINK_TIMEOUT = 300;
|
||||
|
||||
export const eventNames = {
|
||||
recommendedCourseClicked: 'edx.bi.user.recommended.course.click',
|
||||
recommendationsGroup: 'edx.bi.user.recommendations.group',
|
||||
recommendationsViewed: 'edx.bi.user.recommendations.viewed',
|
||||
};
|
||||
|
||||
@@ -43,7 +44,18 @@ export const trackRecommendationsViewed = (recommendedCourseKeys, isControl, use
|
||||
);
|
||||
};
|
||||
|
||||
export const trackRecommendationsGroup = (variation, userId) => {
|
||||
sendTrackEvent(
|
||||
eventNames.recommendationsGroup, {
|
||||
variation,
|
||||
page: 'authn_recommendations',
|
||||
user_id: userId,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
trackRecommendationsClicked,
|
||||
trackRecommendationsGroup,
|
||||
trackRecommendationsViewed,
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import FormFieldRenderer from '../field-renderer';
|
||||
import { FormFieldRenderer } from '../field-renderer';
|
||||
import { FIELDS } from './data/constants';
|
||||
import { validateCountryField } from './data/utils';
|
||||
import messages from './messages';
|
||||
@@ -24,13 +24,13 @@ import CountryField from './registrationFields/CountryField';
|
||||
* it for edX.
|
||||
* */
|
||||
const ConfigurableRegistrationForm = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
countryList,
|
||||
email,
|
||||
fieldDescriptions,
|
||||
fieldErrors,
|
||||
formFields,
|
||||
intl,
|
||||
setFieldErrors,
|
||||
setFocusedField,
|
||||
setFormFields,
|
||||
@@ -72,7 +72,7 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
let error = '';
|
||||
if (name === 'country') {
|
||||
const countryValidation = validateCountryField(
|
||||
value.trim(), countryList, intl.formatMessage(messages['empty.country.field.error']),
|
||||
value.trim(), countryList, formatMessage(messages['empty.country.field.error']),
|
||||
);
|
||||
const { countryCode, displayValue } = countryValidation;
|
||||
error = countryValidation.error;
|
||||
@@ -80,7 +80,7 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
} else if (!value || !value.trim()) {
|
||||
error = fieldDescriptions[name].error_message;
|
||||
} else if (name === 'confirm_email' && value !== email) {
|
||||
error = intl.formatMessage(messages['email.do.not.match']);
|
||||
error = formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
setFocusedField(null);
|
||||
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: error }));
|
||||
@@ -167,7 +167,7 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
<FormFieldRenderer
|
||||
fieldData={{
|
||||
type: 'checkbox',
|
||||
label: intl.formatMessage(messages['registration.opt.in.label'], { siteName: getConfig().SITE_NAME }),
|
||||
label: formatMessage(messages['registration.opt.in.label'], { siteName: getConfig().SITE_NAME }),
|
||||
name: 'marketingEmailsOptIn',
|
||||
}}
|
||||
value={formFields.marketingEmailsOptIn}
|
||||
@@ -199,7 +199,7 @@ const ConfigurableRegistrationForm = (props) => {
|
||||
};
|
||||
|
||||
ConfigurableRegistrationForm.propTypes = {
|
||||
countryList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
countryList: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
fieldDescriptions: PropTypes.shape({}),
|
||||
fieldErrors: PropTypes.shape({
|
||||
@@ -213,7 +213,6 @@ ConfigurableRegistrationForm.propTypes = {
|
||||
honor_code: PropTypes.bool,
|
||||
marketingEmailsOptIn: PropTypes.bool,
|
||||
}).isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
setFieldErrors: PropTypes.func.isRequired,
|
||||
setFocusedField: PropTypes.func.isRequired,
|
||||
setFormFields: PropTypes.func.isRequired,
|
||||
@@ -223,4 +222,4 @@ ConfigurableRegistrationForm.defaultProps = {
|
||||
fieldDescriptions: {},
|
||||
};
|
||||
|
||||
export default injectIntl(ConfigurableRegistrationForm);
|
||||
export default ConfigurableRegistrationForm;
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { windowScrollTo } from '../data/utils';
|
||||
import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_SESSION_EXPIRED } from './data/constants';
|
||||
import {
|
||||
FORBIDDEN_REQUEST,
|
||||
INTERNAL_SERVER_ERROR,
|
||||
TPA_AUTHENTICATION_FAILURE,
|
||||
TPA_SESSION_EXPIRED,
|
||||
} from './data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
const RegistrationFailureMessage = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
context, errorCode, failureCount, intl,
|
||||
context, errorCode, failureCount,
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -25,38 +32,48 @@ const RegistrationFailureMessage = (props) => {
|
||||
let errorMessage;
|
||||
switch (errorCode) {
|
||||
case INTERNAL_SERVER_ERROR:
|
||||
errorMessage = intl.formatMessage(messages['registration.request.server.error']);
|
||||
errorMessage = formatMessage(messages['registration.request.server.error']);
|
||||
break;
|
||||
case FORBIDDEN_REQUEST:
|
||||
errorMessage = intl.formatMessage(messages['registration.rate.limit.error']);
|
||||
errorMessage = formatMessage(messages['registration.rate.limit.error']);
|
||||
break;
|
||||
case TPA_AUTHENTICATION_FAILURE:
|
||||
errorMessage = formatMessage(messages['registration.tpa.authentication.failure'],
|
||||
{
|
||||
platform_name: getConfig().SITE_NAME,
|
||||
lineBreak: <br />,
|
||||
errorMessage: context.errorMessage,
|
||||
});
|
||||
break;
|
||||
case TPA_SESSION_EXPIRED:
|
||||
errorMessage = intl.formatMessage(messages['registration.tpa.session.expired'], { provider: context.provider });
|
||||
errorMessage = formatMessage(messages['registration.tpa.session.expired'], { provider: context.provider });
|
||||
break;
|
||||
default:
|
||||
errorMessage = intl.formatMessage(messages['registration.empty.form.submission.error']);
|
||||
errorMessage = formatMessage(messages['registration.empty.form.submission.error']);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert id="validation-errors" className="mb-5" variant="danger" icon={Error}>
|
||||
<Alert.Heading>{props.intl.formatMessage(messages['registration.request.failure.header'])}</Alert.Heading>
|
||||
<Alert.Heading>{formatMessage(messages['registration.request.failure.header'])}</Alert.Heading>
|
||||
<p>{errorMessage}</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
RegistrationFailureMessage.defaultProps = {
|
||||
context: {},
|
||||
context: {
|
||||
errorMessage: null,
|
||||
},
|
||||
};
|
||||
|
||||
RegistrationFailureMessage.propTypes = {
|
||||
context: PropTypes.shape({
|
||||
provider: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
}),
|
||||
errorCode: PropTypes.string.isRequired,
|
||||
failureCount: PropTypes.number.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(RegistrationFailureMessage);
|
||||
export default RegistrationFailureMessage;
|
||||
|
||||
@@ -6,9 +6,9 @@ import { connect } from 'react-redux';
|
||||
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
getCountryList, getLocale, injectIntl,
|
||||
getCountryList, getLocale, useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Form, StatefulButton } from '@edx/paragon';
|
||||
import { Form, Spinner, StatefulButton } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from '../common-components/data/selectors';
|
||||
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
||||
import {
|
||||
COMPLETE_STATE,
|
||||
DEFAULT_STATE, INVALID_NAME_REGEX, LETTER_REGEX, NUMBER_REGEX, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX,
|
||||
} from '../data/constants';
|
||||
import {
|
||||
@@ -30,16 +31,23 @@ import {
|
||||
import ConfigurableRegistrationForm from './ConfigurableRegistrationForm';
|
||||
import {
|
||||
backupRegistrationFormBegin,
|
||||
clearRegistertionBackendError,
|
||||
clearUsernameSuggestions,
|
||||
fetchRealtimeValidations,
|
||||
registerNewUser,
|
||||
setUserPipelineDataLoaded,
|
||||
} from './data/actions';
|
||||
import {
|
||||
COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, FORM_SUBMISSION_ERROR,
|
||||
COUNTRY_CODE_KEY,
|
||||
COUNTRY_DISPLAY_KEY,
|
||||
FIELDS,
|
||||
FORM_SUBMISSION_ERROR,
|
||||
TPA_AUTHENTICATION_FAILURE,
|
||||
} from './data/constants';
|
||||
import { registrationErrorSelector, validationsSelector } from './data/selectors';
|
||||
import { getSuggestionForInvalidEmail, validateCountryField, validateEmailAddress } from './data/utils';
|
||||
import {
|
||||
getSuggestionForInvalidEmail, validateCountryField, validateEmailAddress,
|
||||
} from './data/utils';
|
||||
import messages from './messages';
|
||||
import RegistrationFailure from './RegistrationFailure';
|
||||
import { EmailField, UsernameField } from './registrationFields';
|
||||
@@ -55,9 +63,9 @@ const RegistrationPage = (props) => {
|
||||
backendValidations,
|
||||
fieldDescriptions,
|
||||
handleInstitutionLogin,
|
||||
intl,
|
||||
institutionLogin,
|
||||
optionalFields,
|
||||
registrationError,
|
||||
registrationErrorCode,
|
||||
registrationResult,
|
||||
shouldBackupState,
|
||||
@@ -72,8 +80,10 @@ const RegistrationPage = (props) => {
|
||||
getRegistrationDataFromBackend,
|
||||
userPipelineDataLoaded,
|
||||
validateFromBackend,
|
||||
clearBackendError,
|
||||
} = props;
|
||||
|
||||
const { formatMessage } = useIntl();
|
||||
const countryList = useMemo(() => getCountryList(getLocale()), []);
|
||||
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||
const tpaHint = useMemo(() => getTpaHint(), []);
|
||||
@@ -87,7 +97,7 @@ const RegistrationPage = (props) => {
|
||||
const [configurableFormFields, setConfigurableFormFields] = useState({ ...backedUpFormData.configurableFormFields });
|
||||
const [errors, setErrors] = useState({ ...backedUpFormData.errors });
|
||||
const [emailSuggestion, setEmailSuggestion] = useState({ ...backedUpFormData.emailSuggestion });
|
||||
|
||||
const [autoSubmitRegisterForm, setAutoSubmitRegisterForm] = useState(false);
|
||||
const [errorCode, setErrorCode] = useState({ type: '', count: 0 });
|
||||
const [formStartTime, setFormStartTime] = useState(null);
|
||||
const [focusedField, setFocusedField] = useState(null);
|
||||
@@ -97,12 +107,36 @@ const RegistrationPage = (props) => {
|
||||
} = thirdPartyAuthContext;
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
|
||||
/**
|
||||
* If auto submitting register form, we will check tos and honor code fields if they exist for feature parity.
|
||||
*/
|
||||
const checkTOSandHonorCodeFields = () => {
|
||||
if (Object.keys(fieldDescriptions).includes(FIELDS.HONOR_CODE)) {
|
||||
setConfigurableFormFields(prevState => ({
|
||||
...prevState,
|
||||
[FIELDS.HONOR_CODE]: true,
|
||||
}));
|
||||
}
|
||||
if (Object.keys(fieldDescriptions).includes(FIELDS.TERMS_OF_SERVICE)) {
|
||||
setConfigurableFormFields(prevState => ({
|
||||
...prevState,
|
||||
[FIELDS.TERMS_OF_SERVICE]: true,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the userPipelineDetails data in formFields for only first time
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!userPipelineDataLoaded) {
|
||||
const { pipelineUserDetails } = thirdPartyAuthContext;
|
||||
if (!userPipelineDataLoaded && thirdPartyAuthApiStatus === COMPLETE_STATE) {
|
||||
const { autoSubmitRegForm, pipelineUserDetails, errorMessage } = thirdPartyAuthContext;
|
||||
if (errorMessage) {
|
||||
setErrorCode(prevState => ({ type: TPA_AUTHENTICATION_FAILURE, count: prevState.count + 1 }));
|
||||
} else if (autoSubmitRegForm) {
|
||||
checkTOSandHonorCodeFields();
|
||||
setAutoSubmitRegisterForm(true);
|
||||
}
|
||||
if (pipelineUserDetails && Object.keys(pipelineUserDetails).length !== 0) {
|
||||
const { name = '', username = '', email = '' } = pipelineUserDetails;
|
||||
setFormFields(prevState => ({
|
||||
@@ -111,7 +145,11 @@ const RegistrationPage = (props) => {
|
||||
setUserPipelineDetailsLoaded(true);
|
||||
}
|
||||
}
|
||||
}, [thirdPartyAuthContext, userPipelineDataLoaded, setUserPipelineDetailsLoaded]);
|
||||
}, [ // eslint-disable-line react-hooks/exhaustive-deps
|
||||
thirdPartyAuthContext,
|
||||
userPipelineDataLoaded,
|
||||
setUserPipelineDetailsLoaded,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!formStartTime) {
|
||||
@@ -152,21 +190,24 @@ const RegistrationPage = (props) => {
|
||||
}, [registrationErrorCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (backendCountryCode !== '') {
|
||||
const selectedCountry = countryList.find(
|
||||
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()),
|
||||
);
|
||||
if (selectedCountry) {
|
||||
setConfigurableFormFields(prevState => (
|
||||
{
|
||||
...prevState,
|
||||
country: {
|
||||
countryCode: selectedCountry[COUNTRY_CODE_KEY], displayValue: selectedCountry[COUNTRY_DISPLAY_KEY],
|
||||
},
|
||||
}
|
||||
));
|
||||
}
|
||||
let countryCode = '';
|
||||
let countryDisplayValue = '';
|
||||
|
||||
const selectedCountry = countryList.find(
|
||||
(country) => (country[COUNTRY_CODE_KEY].toLowerCase() === backendCountryCode.toLowerCase()),
|
||||
);
|
||||
if (selectedCountry) {
|
||||
countryCode = selectedCountry[COUNTRY_CODE_KEY];
|
||||
countryDisplayValue = selectedCountry[COUNTRY_DISPLAY_KEY];
|
||||
}
|
||||
setConfigurableFormFields(prevState => (
|
||||
{
|
||||
...prevState,
|
||||
country: {
|
||||
countryCode, displayValue: countryDisplayValue,
|
||||
},
|
||||
}
|
||||
));
|
||||
}, [backendCountryCode, countryList]);
|
||||
|
||||
/**
|
||||
@@ -209,24 +250,24 @@ const RegistrationPage = (props) => {
|
||||
switch (fieldName) {
|
||||
case 'name':
|
||||
if (!value.trim()) {
|
||||
fieldError = intl.formatMessage(messages['empty.name.field.error']);
|
||||
fieldError = formatMessage(messages['empty.name.field.error']);
|
||||
} else if (value && value.match(urlRegex)) {
|
||||
fieldError = intl.formatMessage(messages['name.validation.message']);
|
||||
fieldError = formatMessage(messages['name.validation.message']);
|
||||
} else if (value && !payload.username.trim() && shouldValidateFromBackend) {
|
||||
validateFromBackend(payload);
|
||||
}
|
||||
break;
|
||||
case 'email':
|
||||
if (!value) {
|
||||
fieldError = intl.formatMessage(messages['empty.email.field.error']);
|
||||
fieldError = formatMessage(messages['empty.email.field.error']);
|
||||
} else if (value.length <= 2) {
|
||||
fieldError = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
fieldError = formatMessage(messages['email.invalid.format.error']);
|
||||
} else {
|
||||
const [username, domainName] = value.split('@');
|
||||
// Check if email address is invalid. If we have a suggestion for invalid email
|
||||
// provide that along with the error message.
|
||||
if (!emailRegex.test(value)) {
|
||||
fieldError = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
fieldError = formatMessage(messages['email.invalid.format.error']);
|
||||
setEmailSuggestion({
|
||||
suggestion: getSuggestionForInvalidEmail(domainName, username),
|
||||
type: 'error',
|
||||
@@ -234,7 +275,7 @@ const RegistrationPage = (props) => {
|
||||
} else {
|
||||
const response = validateEmailAddress(value, username, domainName);
|
||||
if (response.hasError) {
|
||||
fieldError = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
fieldError = formatMessage(messages['email.invalid.format.error']);
|
||||
delete response.hasError;
|
||||
} else if (shouldValidateFromBackend) {
|
||||
validateFromBackend(payload);
|
||||
@@ -242,32 +283,32 @@ const RegistrationPage = (props) => {
|
||||
setEmailSuggestion({ ...response });
|
||||
|
||||
if (configurableFormFields.confirm_email && value !== configurableFormFields.confirm_email) {
|
||||
confirmEmailError = intl.formatMessage(messages['email.do.not.match']);
|
||||
confirmEmailError = formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'username':
|
||||
if (!value || value.length <= 1 || value.length > 30) {
|
||||
fieldError = intl.formatMessage(messages['username.validation.message']);
|
||||
fieldError = formatMessage(messages['username.validation.message']);
|
||||
} else if (!value.match(/^[a-zA-Z0-9_-]*$/i)) {
|
||||
fieldError = intl.formatMessage(messages['username.format.validation.message']);
|
||||
fieldError = formatMessage(messages['username.format.validation.message']);
|
||||
} else if (shouldValidateFromBackend) {
|
||||
validateFromBackend(payload);
|
||||
}
|
||||
break;
|
||||
case 'password':
|
||||
if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) {
|
||||
fieldError = intl.formatMessage(messages['password.validation.message']);
|
||||
fieldError = formatMessage(messages['password.validation.message']);
|
||||
} else if (shouldValidateFromBackend) {
|
||||
validateFromBackend(payload);
|
||||
}
|
||||
break;
|
||||
case 'country':
|
||||
if (flags.showConfigurableEdxFields || flags.showConfigurableRegistrationFields) {
|
||||
const { countryCode, displayValue, error } = validateCountryField(
|
||||
value.displayValue.trim(), countryList, intl.formatMessage(messages['empty.country.field.error']),
|
||||
);
|
||||
const {
|
||||
countryCode, displayValue, error,
|
||||
} = validateCountryField(value.displayValue.trim(), countryList, formatMessage(messages['empty.country.field.error']));
|
||||
fieldError = error;
|
||||
countryFieldCode = countryCode;
|
||||
setConfigurableFormFields(prevState => ({ ...prevState, country: { countryCode, displayValue } }));
|
||||
@@ -278,7 +319,7 @@ const RegistrationPage = (props) => {
|
||||
if (!value && fieldDescriptions[fieldName].error_message) {
|
||||
fieldError = fieldDescriptions[fieldName].error_message;
|
||||
} else if (fieldName === 'confirm_email' && formFields.email && value !== formFields.email) {
|
||||
fieldError = intl.formatMessage(messages['email.do.not.match']);
|
||||
fieldError = formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -298,7 +339,7 @@ const RegistrationPage = (props) => {
|
||||
let isValid = !focusedFieldError;
|
||||
Object.keys(payload).forEach(key => {
|
||||
if (!payload[key]) {
|
||||
fieldErrors[key] = intl.formatMessage(messages[`empty.${key}.field.error`]);
|
||||
fieldErrors[key] = formatMessage(messages[`empty.${key}.field.error`]);
|
||||
}
|
||||
if (fieldErrors[key]) {
|
||||
isValid = false;
|
||||
@@ -307,7 +348,7 @@ const RegistrationPage = (props) => {
|
||||
|
||||
if (flags.showConfigurableEdxFields) {
|
||||
if (!configurableFormFields.country.displayValue) {
|
||||
fieldErrors.country = intl.formatMessage(messages['empty.country.field.error']);
|
||||
fieldErrors.country = formatMessage(messages['empty.country.field.error']);
|
||||
}
|
||||
if (fieldErrors.country) {
|
||||
isValid = false;
|
||||
@@ -317,7 +358,7 @@ const RegistrationPage = (props) => {
|
||||
if (flags.showConfigurableRegistrationFields) {
|
||||
Object.keys(fieldDescriptions).forEach(key => {
|
||||
if (key === 'country' && !configurableFormFields.country.displayValue) {
|
||||
fieldErrors[key] = intl.formatMessage(messages['empty.country.field.error']);
|
||||
fieldErrors[key] = formatMessage(messages['empty.country.field.error']);
|
||||
} else if (!configurableFormFields[key]) {
|
||||
fieldErrors[key] = fieldDescriptions[key].error_message;
|
||||
}
|
||||
@@ -352,12 +393,16 @@ const RegistrationPage = (props) => {
|
||||
};
|
||||
|
||||
const handleEmailSuggestionClosed = () => setEmailSuggestion({ suggestion: '', type: '' });
|
||||
|
||||
const handleUsernameSuggestionClosed = () => props.resetUsernameSuggestions();
|
||||
|
||||
const handleOnChange = (event) => {
|
||||
const { name } = event.target;
|
||||
let value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
||||
|
||||
if (registrationError[name]) {
|
||||
clearBackendError(name);
|
||||
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||
}
|
||||
if (name === 'username') {
|
||||
if (value.length > 30) {
|
||||
return;
|
||||
@@ -387,6 +432,7 @@ const RegistrationPage = (props) => {
|
||||
const handleOnFocus = (event) => {
|
||||
const { name, value } = event.target;
|
||||
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||
clearBackendError(name);
|
||||
// Since we are removing the form errors from the focused field, we will
|
||||
// need to rerun the validation for focused field on form submission.
|
||||
setFocusedField(name);
|
||||
@@ -402,9 +448,7 @@ const RegistrationPage = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const registerUser = () => {
|
||||
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
|
||||
let payload = { ...formFields };
|
||||
|
||||
@@ -451,19 +495,30 @@ const RegistrationPage = (props) => {
|
||||
props.registerNewUser(payload);
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
registerUser();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (autoSubmitRegisterForm && userPipelineDataLoaded) {
|
||||
registerUser();
|
||||
}
|
||||
}, [autoSubmitRegisterForm, userPipelineDataLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const renderForm = () => {
|
||||
if (institutionLogin) {
|
||||
return (
|
||||
<InstitutionLogistration
|
||||
secondaryProviders={secondaryProviders}
|
||||
headingTitle={intl.formatMessage(messages['register.institution.login.page.title'])}
|
||||
headingTitle={formatMessage(messages['register.institution.login.page.title'])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}</title>
|
||||
<title>{formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}</title>
|
||||
</Helmet>
|
||||
<RedirectLogistration
|
||||
success={registrationResult.success}
|
||||
@@ -474,99 +529,106 @@ const RegistrationPage = (props) => {
|
||||
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && Object.keys(optionalFields).includes('fields')
|
||||
}
|
||||
/>
|
||||
<div className="mw-xs mt-3">
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={currentProvider}
|
||||
platformName={platformName}
|
||||
referrer={REGISTER_PAGE}
|
||||
/>
|
||||
<RegistrationFailure
|
||||
errorCode={errorCode.type}
|
||||
failureCount={errorCode.count}
|
||||
context={{ provider: currentProvider }}
|
||||
/>
|
||||
<Form id="registration-form" name="registration-form">
|
||||
<FormGroup
|
||||
name="name"
|
||||
value={formFields.name}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={errors.name}
|
||||
helpText={[intl.formatMessage(messages['help.text.name'])]}
|
||||
floatingLabel={intl.formatMessage(messages['registration.fullname.label'])}
|
||||
{autoSubmitRegisterForm && !errorCode.type ? (
|
||||
<div className="mw-xs mt-5 text-center">
|
||||
<Spinner animation="border" variant="primary" id="tpa-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="mw-xs mt-3">
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={currentProvider}
|
||||
platformName={platformName}
|
||||
referrer={REGISTER_PAGE}
|
||||
/>
|
||||
<EmailField
|
||||
name="email"
|
||||
value={formFields.email}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
handleSuggestionClick={(e) => handleSuggestionClick(e, 'email')}
|
||||
handleOnClose={handleEmailSuggestionClosed}
|
||||
emailSuggestion={emailSuggestion}
|
||||
errorMessage={errors.email}
|
||||
helpText={[intl.formatMessage(messages['help.text.email'])]}
|
||||
floatingLabel={intl.formatMessage(messages['registration.email.label'])}
|
||||
<RegistrationFailure
|
||||
errorCode={errorCode.type}
|
||||
failureCount={errorCode.count}
|
||||
context={{ provider: currentProvider, errorMessage: thirdPartyAuthContext.errorMessage }}
|
||||
/>
|
||||
<UsernameField
|
||||
name="username"
|
||||
spellCheck="false"
|
||||
value={formFields.username}
|
||||
handleBlur={handleOnBlur}
|
||||
handleChange={handleOnChange}
|
||||
handleFocus={handleOnFocus}
|
||||
handleSuggestionClick={handleSuggestionClick}
|
||||
handleUsernameSuggestionClose={handleUsernameSuggestionClosed}
|
||||
usernameSuggestions={usernameSuggestions}
|
||||
errorMessage={errors.username}
|
||||
helpText={[intl.formatMessage(messages['help.text.username.1']), intl.formatMessage(messages['help.text.username.2'])]}
|
||||
floatingLabel={intl.formatMessage(messages['registration.username.label'])}
|
||||
/>
|
||||
{!currentProvider && (
|
||||
<PasswordField
|
||||
name="password"
|
||||
value={formFields.password}
|
||||
<Form id="registration-form" name="registration-form">
|
||||
<FormGroup
|
||||
name="name"
|
||||
value={formFields.name}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={errors.password}
|
||||
floatingLabel={intl.formatMessage(messages['registration.password.label'])}
|
||||
errorMessage={errors.name}
|
||||
helpText={[formatMessage(messages['help.text.name'])]}
|
||||
floatingLabel={formatMessage(messages['registration.fullname.label'])}
|
||||
/>
|
||||
)}
|
||||
<ConfigurableRegistrationForm
|
||||
countryList={countryList}
|
||||
email={formFields.email}
|
||||
fieldErrors={errors}
|
||||
formFields={configurableFormFields}
|
||||
setFieldErrors={setErrors}
|
||||
setFormFields={setConfigurableFormFields}
|
||||
setFocusedField={setFocusedField}
|
||||
fieldDescriptions={fieldDescriptions}
|
||||
/>
|
||||
<StatefulButton
|
||||
id="register-user"
|
||||
name="register-user"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="register-stateful-button-width mt-4 mb-4"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['create.account.for.free.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
<ThirdPartyAuth
|
||||
currentProvider={currentProvider}
|
||||
providers={providers}
|
||||
secondaryProviders={secondaryProviders}
|
||||
handleInstitutionLogin={handleInstitutionLogin}
|
||||
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
<EmailField
|
||||
name="email"
|
||||
value={formFields.email}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
handleSuggestionClick={(e) => handleSuggestionClick(e, 'email')}
|
||||
handleOnClose={handleEmailSuggestionClosed}
|
||||
emailSuggestion={emailSuggestion}
|
||||
errorMessage={errors.email}
|
||||
helpText={[formatMessage(messages['help.text.email'])]}
|
||||
floatingLabel={formatMessage(messages['registration.email.label'])}
|
||||
/>
|
||||
<UsernameField
|
||||
name="username"
|
||||
spellCheck="false"
|
||||
value={formFields.username}
|
||||
handleBlur={handleOnBlur}
|
||||
handleChange={handleOnChange}
|
||||
handleFocus={handleOnFocus}
|
||||
handleSuggestionClick={handleSuggestionClick}
|
||||
handleUsernameSuggestionClose={handleUsernameSuggestionClosed}
|
||||
usernameSuggestions={usernameSuggestions}
|
||||
errorMessage={errors.username}
|
||||
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
|
||||
floatingLabel={formatMessage(messages['registration.username.label'])}
|
||||
/>
|
||||
{!currentProvider && (
|
||||
<PasswordField
|
||||
name="password"
|
||||
value={formFields.password}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={errors.password}
|
||||
floatingLabel={formatMessage(messages['registration.password.label'])}
|
||||
/>
|
||||
)}
|
||||
<ConfigurableRegistrationForm
|
||||
countryList={countryList}
|
||||
email={formFields.email}
|
||||
fieldErrors={errors}
|
||||
formFields={configurableFormFields}
|
||||
setFieldErrors={setErrors}
|
||||
setFormFields={setConfigurableFormFields}
|
||||
setFocusedField={setFocusedField}
|
||||
fieldDescriptions={fieldDescriptions}
|
||||
/>
|
||||
<StatefulButton
|
||||
id="register-user"
|
||||
name="register-user"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="register-stateful-button-width mt-4 mb-4"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: formatMessage(messages['create.account.for.free.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
<ThirdPartyAuth
|
||||
currentProvider={currentProvider}
|
||||
providers={providers}
|
||||
secondaryProviders={secondaryProviders}
|
||||
handleInstitutionLogin={handleInstitutionLogin}
|
||||
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -580,7 +642,7 @@ const RegistrationPage = (props) => {
|
||||
window.location.href = getConfig().LMS_BASE_URL + provider.registerUrl;
|
||||
return null;
|
||||
}
|
||||
return provider ? <EnterpriseSSO provider={provider} intl={intl} /> : renderForm();
|
||||
return provider ? <EnterpriseSSO provider={provider} /> : renderForm();
|
||||
}
|
||||
return (
|
||||
renderForm()
|
||||
@@ -595,6 +657,7 @@ const mapStateToProps = state => {
|
||||
backendValidations: validationsSelector(state),
|
||||
fieldDescriptions: fieldDescriptionSelector(state),
|
||||
optionalFields: optionalFieldsSelector(state),
|
||||
registrationError: registerPageState.registrationError,
|
||||
registrationErrorCode: registrationErrorSelector(state),
|
||||
registrationResult: registerPageState.registrationResult,
|
||||
shouldBackupState: registerPageState.shouldBackupState,
|
||||
@@ -623,8 +686,8 @@ RegistrationPage.propTypes = {
|
||||
}),
|
||||
fieldDescriptions: PropTypes.shape({}),
|
||||
institutionLogin: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
optionalFields: PropTypes.shape({}),
|
||||
registrationError: PropTypes.shape({}),
|
||||
registrationErrorCode: PropTypes.string,
|
||||
registrationResult: PropTypes.shape({
|
||||
redirectUrl: PropTypes.string,
|
||||
@@ -634,12 +697,11 @@ RegistrationPage.propTypes = {
|
||||
submitState: PropTypes.string,
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
thirdPartyAuthContext: PropTypes.shape({
|
||||
currentProvider: PropTypes.string,
|
||||
platformName: PropTypes.string,
|
||||
providers: PropTypes.array,
|
||||
secondaryProviders: PropTypes.array,
|
||||
finishAuthUrl: PropTypes.string,
|
||||
autoSubmitRegForm: PropTypes.bool,
|
||||
countryCode: PropTypes.string,
|
||||
currentProvider: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
finishAuthUrl: PropTypes.string,
|
||||
pipelineUserDetails: PropTypes.shape({
|
||||
email: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
@@ -647,12 +709,20 @@ RegistrationPage.propTypes = {
|
||||
lastName: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
}),
|
||||
platformName: PropTypes.string,
|
||||
providers: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
secondaryProviders: PropTypes.arrayOf(
|
||||
PropTypes.shape({}),
|
||||
),
|
||||
}),
|
||||
usernameSuggestions: PropTypes.arrayOf(PropTypes.string),
|
||||
userPipelineDataLoaded: PropTypes.bool,
|
||||
validationApiRateLimited: PropTypes.bool,
|
||||
// Actions
|
||||
backupFormState: PropTypes.func.isRequired,
|
||||
clearBackendError: PropTypes.func.isRequired,
|
||||
getRegistrationDataFromBackend: PropTypes.func.isRequired,
|
||||
handleInstitutionLogin: PropTypes.func.isRequired,
|
||||
registerNewUser: PropTypes.func.isRequired,
|
||||
@@ -680,18 +750,21 @@ RegistrationPage.defaultProps = {
|
||||
backendValidations: null,
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
registrationError: {},
|
||||
registrationErrorCode: '',
|
||||
registrationResult: null,
|
||||
shouldBackupState: false,
|
||||
submitState: DEFAULT_STATE,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
autoSubmitRegForm: false,
|
||||
countryCode: null,
|
||||
currentProvider: null,
|
||||
errorMessage: null,
|
||||
finishAuthUrl: null,
|
||||
pipelineUserDetails: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
},
|
||||
usernameSuggestions: [],
|
||||
userPipelineDataLoaded: false,
|
||||
@@ -702,10 +775,11 @@ export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
backupFormState: backupRegistrationFormBegin,
|
||||
clearBackendError: clearRegistertionBackendError,
|
||||
getRegistrationDataFromBackend: getThirdPartyAuthContext,
|
||||
resetUsernameSuggestions: clearUsernameSuggestions,
|
||||
validateFromBackend: fetchRealtimeValidations,
|
||||
registerNewUser,
|
||||
setUserPipelineDetailsLoaded: setUserPipelineDataLoaded,
|
||||
},
|
||||
)(injectIntl(RegistrationPage));
|
||||
)(RegistrationPage);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
@@ -18,8 +18,9 @@ import messages from './messages';
|
||||
* This component renders the Single sign-on (SSO) buttons for the providers passed.
|
||||
* */
|
||||
const ThirdPartyAuth = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
providers, secondaryProviders, currentProvider, handleInstitutionLogin, thirdPartyAuthApiStatus, intl,
|
||||
providers, secondaryProviders, currentProvider, handleInstitutionLogin, thirdPartyAuthApiStatus,
|
||||
} = props;
|
||||
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
|
||||
const isSocialAuthActive = !!providers.length && !currentProvider;
|
||||
@@ -29,7 +30,7 @@ const ThirdPartyAuth = (props) => {
|
||||
<>
|
||||
{((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive) && (
|
||||
<div className="mt-4 mb-3 h4">
|
||||
{intl.formatMessage(messages['registration.other.options.heading'])}
|
||||
{formatMessage(messages['registration.other.options.heading'])}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -40,7 +41,7 @@ const ThirdPartyAuth = (props) => {
|
||||
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
|
||||
<RenderInstitutionButton
|
||||
onSubmitHandler={handleInstitutionLogin}
|
||||
buttonTitle={intl.formatMessage(messages['register.institution.login.button'])}
|
||||
buttonTitle={formatMessage(messages['register.institution.login.button'])}
|
||||
/>
|
||||
)}
|
||||
{isSocialAuthActive && (
|
||||
@@ -64,10 +65,27 @@ ThirdPartyAuth.defaultProps = {
|
||||
ThirdPartyAuth.propTypes = {
|
||||
currentProvider: PropTypes.string,
|
||||
handleInstitutionLogin: PropTypes.func.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
providers: PropTypes.arrayOf(PropTypes.any),
|
||||
secondaryProviders: PropTypes.arrayOf(PropTypes.any),
|
||||
providers: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
iconClass: PropTypes.string,
|
||||
iconImage: PropTypes.string,
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
}),
|
||||
),
|
||||
secondaryProviders: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
iconClass: PropTypes.string,
|
||||
iconImage: PropTypes.string,
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
}),
|
||||
),
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(ThirdPartyAuth);
|
||||
export default ThirdPartyAuth;
|
||||
|
||||
@@ -4,6 +4,7 @@ export const BACKUP_REGISTRATION_DATA = new AsyncActionType('REGISTRATION', 'BAC
|
||||
export const REGISTER_FORM_VALIDATIONS = new AsyncActionType('REGISTRATION', 'GET_FORM_VALIDATIONS');
|
||||
export const REGISTER_NEW_USER = new AsyncActionType('REGISTRATION', 'REGISTER_NEW_USER');
|
||||
export const REGISTER_CLEAR_USERNAME_SUGGESTIONS = 'REGISTRATION_CLEAR_USERNAME_SUGGESTIONS';
|
||||
export const REGISTERATION_CLEAR_BACKEND_ERROR = 'REGISTERATION_CLEAR_BACKEND_ERROR';
|
||||
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
|
||||
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
|
||||
|
||||
@@ -61,6 +62,11 @@ export const clearUsernameSuggestions = () => ({
|
||||
type: REGISTER_CLEAR_USERNAME_SUGGESTIONS,
|
||||
});
|
||||
|
||||
export const clearRegistertionBackendError = (fieldName) => ({
|
||||
type: REGISTERATION_CLEAR_BACKEND_ERROR,
|
||||
payload: fieldName,
|
||||
});
|
||||
|
||||
export const setCountryFromThirdPartyAuthContext = (countryCode) => ({
|
||||
type: REGISTER_SET_COUNTRY_CODE,
|
||||
payload: { countryCode },
|
||||
|
||||
@@ -9,6 +9,7 @@ export const FIELDS = {
|
||||
export const FORBIDDEN_REQUEST = 'forbidden-request';
|
||||
export const FORM_SUBMISSION_ERROR = 'form-submission-error';
|
||||
export const INTERNAL_SERVER_ERROR = 'internal-server-error';
|
||||
export const TPA_AUTHENTICATION_FAILURE = 'tpa-authentication-failure';
|
||||
export const TPA_SESSION_EXPIRED = 'tpa-session-expired';
|
||||
|
||||
export const YEAR_OF_BIRTH_OPTIONS = (() => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
REGISTER_FORM_VALIDATIONS,
|
||||
REGISTER_NEW_USER,
|
||||
REGISTER_SET_COUNTRY_CODE, REGISTER_SET_USER_PIPELINE_DATA_LOADED,
|
||||
REGISTERATION_CLEAR_BACKEND_ERROR,
|
||||
} from './actions';
|
||||
|
||||
export const defaultState = {
|
||||
@@ -36,7 +37,7 @@ export const defaultState = {
|
||||
shouldBackupState: false,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
const reducer = (state = defaultState, action = {}) => {
|
||||
switch (action.type) {
|
||||
case BACKUP_REGISTRATION_DATA.BASE:
|
||||
return {
|
||||
@@ -72,6 +73,14 @@ const reducer = (state = defaultState, action) => {
|
||||
usernameSuggestions: usernameSuggestions || state.usernameSuggestions,
|
||||
};
|
||||
}
|
||||
case REGISTERATION_CLEAR_BACKEND_ERROR: {
|
||||
const registrationErrorTemp = state.registrationError;
|
||||
delete registrationErrorTemp[action.payload];
|
||||
return {
|
||||
...state,
|
||||
registrationError: { ...registrationErrorTemp },
|
||||
};
|
||||
}
|
||||
case REGISTER_FORM_VALIDATIONS.SUCCESS: {
|
||||
const { usernameSuggestions, ...validationWithoutUsernameSuggestions } = action.payload.validations;
|
||||
return {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { DEFAULT_STATE } from '../../../data/constants';
|
||||
import {
|
||||
BACKUP_REGISTRATION_DATA,
|
||||
@@ -5,6 +7,7 @@ import {
|
||||
REGISTER_FORM_VALIDATIONS,
|
||||
REGISTER_NEW_USER,
|
||||
REGISTER_SET_COUNTRY_CODE,
|
||||
REGISTERATION_CLEAR_BACKEND_ERROR,
|
||||
} from '../actions';
|
||||
import reducer from '../reducers';
|
||||
|
||||
@@ -102,6 +105,22 @@ describe('Registration Reducer Tests', () => {
|
||||
expect(reducer(state, action)).toEqual(state);
|
||||
});
|
||||
|
||||
it('should reset email error field data on focus of email field', () => {
|
||||
const state = {
|
||||
...defaultState,
|
||||
registrationError: { email: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account` },
|
||||
};
|
||||
const action = {
|
||||
type: REGISTERATION_CLEAR_BACKEND_ERROR,
|
||||
payload: 'email',
|
||||
};
|
||||
|
||||
expect(reducer(state, action)).toEqual({
|
||||
...state,
|
||||
registrationError: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should set country code', () => {
|
||||
const countryCode = 'PK';
|
||||
|
||||
|
||||
@@ -162,6 +162,13 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Registration using {provider} has timed out.',
|
||||
description: '',
|
||||
},
|
||||
'registration.tpa.authentication.failure': {
|
||||
id: 'registration.tpa.authentication.failure',
|
||||
defaultMessage: 'We are sorry, you are not authorized to access {platform_name} via this channel. '
|
||||
+ 'Please contact your learning administrator or manager in order to access {platform_name}.'
|
||||
+ '{lineBreak}{lineBreak}Error Details:{lineBreak}{errorMessage}',
|
||||
description: 'Error message third party authentication pipeline fails',
|
||||
},
|
||||
// Terms of Service and Honor Code
|
||||
'terms.of.service.and.honor.code': {
|
||||
id: 'terms.of.service.and.honor.code',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Icon, IconButton } from '@edx/paragon';
|
||||
import { ExpandLess, ExpandMore } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -10,11 +10,10 @@ import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY } from '../data/constants';
|
||||
import messages from '../messages';
|
||||
|
||||
const CountryField = (props) => {
|
||||
const {
|
||||
intl, countryList, selectedCountry,
|
||||
} = props;
|
||||
const { countryList, selectedCountry } = props;
|
||||
|
||||
const dropdownRef = useRef(null);
|
||||
const { formatMessage } = useIntl();
|
||||
const [errorMessage, setErrorMessage] = useState(props.errorMessage);
|
||||
const [dropDownItems, setDropDownItems] = useState([]);
|
||||
const [displayValue, setDisplayValue] = useState('');
|
||||
@@ -54,6 +53,10 @@ const CountryField = (props) => {
|
||||
value={countryName}
|
||||
key={country[COUNTRY_CODE_KEY]}
|
||||
onClick={(event) => onBlurHandler(event, true, countryName)}
|
||||
/* This event will prevent the blur event to be fired,
|
||||
as blur event is having higher priority than click event and restricts the click event.
|
||||
*/
|
||||
onMouseDown={(event) => event.preventDefault()}
|
||||
>
|
||||
{countryName.length > 30 ? countryName.substring(0, 30).concat('...') : countryName}
|
||||
</button>
|
||||
@@ -156,7 +159,7 @@ const CountryField = (props) => {
|
||||
name="country"
|
||||
autoComplete="chrome-off"
|
||||
className="mb-0"
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
floatingLabel={formatMessage(messages['registration.country.label'])}
|
||||
trailingElement={trailingIcon}
|
||||
value={displayValue}
|
||||
errorMessage={errorMessage}
|
||||
@@ -172,9 +175,13 @@ const CountryField = (props) => {
|
||||
};
|
||||
|
||||
CountryField.propTypes = {
|
||||
countryList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
countryList: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
code: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
}),
|
||||
).isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
onBlurHandler: PropTypes.func.isRequired,
|
||||
onChangeHandler: PropTypes.func.isRequired,
|
||||
onFocusHandler: PropTypes.func.isRequired,
|
||||
@@ -191,4 +198,4 @@ CountryField.defaultProps = {
|
||||
},
|
||||
};
|
||||
|
||||
export default injectIntl(CountryField);
|
||||
export default CountryField;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Icon } from '@edx/paragon';
|
||||
import { Close, Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -9,8 +9,8 @@ import { FormGroup } from '../../common-components';
|
||||
import messages from '../messages';
|
||||
|
||||
const EmailField = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
intl,
|
||||
emailSuggestion,
|
||||
handleSuggestionClick,
|
||||
handleOnClose,
|
||||
@@ -21,7 +21,7 @@ const EmailField = (props) => {
|
||||
return (
|
||||
<Alert variant="danger" className="email-error-alert mt-1" icon={Error}>
|
||||
<span className="alert-text">
|
||||
{intl.formatMessage(messages['did.you.mean.alert.text'])}{' '}
|
||||
{formatMessage(messages['did.you.mean.alert.text'])}{' '}
|
||||
<Alert.Link
|
||||
href="#"
|
||||
name="email"
|
||||
@@ -35,7 +35,7 @@ const EmailField = (props) => {
|
||||
}
|
||||
return (
|
||||
<span id="email-warning" className="small">
|
||||
{intl.formatMessage(messages['did.you.mean.alert.text'])}:{' '}
|
||||
{formatMessage(messages['did.you.mean.alert.text'])}:{' '}
|
||||
<Alert.Link
|
||||
href="#"
|
||||
name="email"
|
||||
@@ -73,10 +73,9 @@ EmailField.propTypes = {
|
||||
suggestion: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
}),
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
handleOnClose: PropTypes.func.isRequired,
|
||||
handleSuggestionClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(EmailField);
|
||||
export default EmailField;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Hyperlink } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
const HonorCode = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
intl, errorMessage, onChangeHandler, fieldType, value,
|
||||
errorMessage, onChangeHandler, fieldType, value,
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -30,12 +31,12 @@ const HonorCode = (props) => {
|
||||
platformName: getConfig().SITE_NAME,
|
||||
tosAndHonorCode: (
|
||||
<Hyperlink variant="muted" destination={getConfig().TOS_AND_HONOR_CODE || '#'} target="_blank">
|
||||
{intl.formatMessage(messages['terms.of.service.and.honor.code'])}
|
||||
{formatMessage(messages['terms.of.service.and.honor.code'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
privacyPolicy: (
|
||||
<Hyperlink variant="muted" destination={getConfig().PRIVACY_POLICY || '#'} target="_blank">
|
||||
{intl.formatMessage(messages['privacy.policy'])}
|
||||
{formatMessage(messages['privacy.policy'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
@@ -62,7 +63,7 @@ const HonorCode = (props) => {
|
||||
platformName: getConfig().SITE_NAME,
|
||||
tosAndHonorCode: (
|
||||
<Hyperlink variant="muted" destination={getConfig().TOS_AND_HONOR_CODE || '#'} target="_blank">
|
||||
{intl.formatMessage(messages['honor.code'])}
|
||||
{formatMessage(messages['honor.code'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
@@ -85,11 +86,10 @@ HonorCode.defaultProps = {
|
||||
};
|
||||
|
||||
HonorCode.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onChangeHandler: PropTypes.func,
|
||||
fieldType: PropTypes.string,
|
||||
value: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(HonorCode);
|
||||
export default HonorCode;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Hyperlink } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
const TermsOfService = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
intl, errorMessage, onChangeHandler, value,
|
||||
errorMessage, onChangeHandler, value,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
@@ -31,7 +32,7 @@ const TermsOfService = (props) => {
|
||||
platformName: getConfig().SITE_NAME,
|
||||
termsOfService: (
|
||||
<Hyperlink variant="muted" destination={getConfig().TOS_LINK || '#'} target="_blank">
|
||||
{intl.formatMessage(messages['terms.of.service'])}
|
||||
{formatMessage(messages['terms.of.service'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
@@ -52,10 +53,9 @@ TermsOfService.defaultProps = {
|
||||
};
|
||||
|
||||
TermsOfService.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
onChangeHandler: PropTypes.func.isRequired,
|
||||
value: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default injectIntl(TermsOfService);
|
||||
export default TermsOfService;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Icon, IconButton } from '@edx/paragon';
|
||||
import { Close } from '@edx/paragon/icons';
|
||||
import PropTypes, { string } from 'prop-types';
|
||||
@@ -9,15 +9,16 @@ import { FormGroup } from '../../common-components';
|
||||
import messages from '../messages';
|
||||
|
||||
const UsernameField = (props) => {
|
||||
const { formatMessage } = useIntl();
|
||||
const {
|
||||
intl, handleSuggestionClick, handleUsernameSuggestionClose, usernameSuggestions, errorMessage,
|
||||
handleSuggestionClick, handleUsernameSuggestionClose, usernameSuggestions, errorMessage,
|
||||
} = props;
|
||||
let className = '';
|
||||
let suggestedUsernameDiv = <></>;
|
||||
let iconButton = <></>;
|
||||
let suggestedUsernameDiv = null;
|
||||
let iconButton = null;
|
||||
const suggestedUsernames = () => (
|
||||
<div className={className}>
|
||||
<span className="text-gray username-suggestion-label">{intl.formatMessage(messages['registration.username.suggestion.label'])}</span>
|
||||
<span className="text-gray username-suggestion-label">{formatMessage(messages['registration.username.suggestion.label'])}</span>
|
||||
<div className="scroll-suggested-username">
|
||||
{usernameSuggestions.map((username, index) => (
|
||||
<Button
|
||||
@@ -65,10 +66,9 @@ UsernameField.propTypes = {
|
||||
handleSuggestionClick: PropTypes.func.isRequired,
|
||||
handleUsernameSuggestionClose: PropTypes.func.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.string.isRequired,
|
||||
autoComplete: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(UsernameField);
|
||||
export default UsernameField;
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
setUserPipelineDataLoaded,
|
||||
} from '../data/actions';
|
||||
import {
|
||||
FIELDS, FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_SESSION_EXPIRED,
|
||||
FIELDS, FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_AUTHENTICATION_FAILURE, TPA_SESSION_EXPIRED,
|
||||
} from '../data/constants';
|
||||
import RegistrationFailureMessage from '../RegistrationFailure';
|
||||
import RegistrationPage from '../RegistrationPage';
|
||||
@@ -128,6 +128,14 @@ describe('RegistrationPage', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const ssoProvider = {
|
||||
id: 'oa2-apple-id',
|
||||
name: 'Apple',
|
||||
iconClass: null,
|
||||
iconImage: 'https://openedx.devstack.lms/logo.png',
|
||||
loginUrl: '/auth/login/apple-id/?auth_entry=login&next=/dashboard',
|
||||
};
|
||||
|
||||
describe('Test Registration Page', () => {
|
||||
mergeConfig({
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: true,
|
||||
@@ -141,14 +149,6 @@ describe('RegistrationPage', () => {
|
||||
country: 'Select your country or region of residence',
|
||||
};
|
||||
|
||||
const ssoProvider = {
|
||||
id: 'oa2-apple-id',
|
||||
name: 'Apple',
|
||||
iconClass: null,
|
||||
iconImage: 'https://openedx.devstack.lms/logo.png',
|
||||
loginUrl: '/auth/login/apple-id/?auth_entry=login&next=/dashboard',
|
||||
};
|
||||
|
||||
const secondaryProviders = {
|
||||
id: 'saml-test', name: 'Test University', loginUrl: '/dummy-auth', registerUrl: '/dummy_auth',
|
||||
};
|
||||
@@ -351,9 +351,10 @@ describe('RegistrationPage', () => {
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
|
||||
registrationPage.find('input#email').simulate(
|
||||
'change', { target: { value: 'john@gmail.mistake', name: 'email' } },
|
||||
);
|
||||
registrationPage
|
||||
.find('input#email')
|
||||
.simulate('change', { target: { value: 'john@gmail.mistake', name: 'email' } });
|
||||
|
||||
registrationPage.find('input#email').simulate('blur');
|
||||
|
||||
expect(registrationPage.find('.alert-danger').text()).toEqual('Did you mean john@gmail.com?');
|
||||
@@ -480,6 +481,21 @@ describe('RegistrationPage', () => {
|
||||
expect(registrationPage.find('div.alert').first().text()).toEqual(expectedMessage);
|
||||
});
|
||||
|
||||
it('should match tpa authentication failed error message', () => {
|
||||
const expectedMessageSubstring = 'We are sorry, you are not authorized to access';
|
||||
props = {
|
||||
context: {
|
||||
provider: 'Google',
|
||||
},
|
||||
errorCode: TPA_AUTHENTICATION_FAILURE,
|
||||
failureCount: 0,
|
||||
};
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationFailure {...props} />));
|
||||
expect(registrationPage.find('div.alert-heading').length).toEqual(1);
|
||||
expect(registrationPage.find('div.alert').first().text()).toContain(expectedMessageSubstring);
|
||||
});
|
||||
|
||||
// ******** test form buttons and fields ********
|
||||
|
||||
it('should match default button state', () => {
|
||||
@@ -874,6 +890,7 @@ describe('RegistrationPage', () => {
|
||||
},
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
pipelineUserDetails: {
|
||||
@@ -959,6 +976,25 @@ describe('RegistrationPage', () => {
|
||||
expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should clear the registation validation error on change event on field focused', () => {
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
registrationError: {
|
||||
errorCode: 'duplicate-email',
|
||||
email: [{ userMessage: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account` }],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const clearBackendError = jest.fn();
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} {...clearBackendError} />));
|
||||
registrationPage.find('input#email').simulate('change', { target: { value: 'a@gmail.com', name: 'email' } });
|
||||
expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should set country in component state when form is translated using browser translations', () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
|
||||
@@ -975,6 +1011,7 @@ describe('RegistrationPage', () => {
|
||||
describe('Test Configurable Fields', () => {
|
||||
mergeConfig({
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: true,
|
||||
});
|
||||
|
||||
it('should render fields returned by backend', () => {
|
||||
@@ -1107,5 +1144,139 @@ describe('RegistrationPage', () => {
|
||||
registrationPage.find('button.dropdown-item').at(0).simulate('click', { target: { value: 'Pakistan', name: 'countryItem' } });
|
||||
expect(registrationPage.find('div[feedback-for="name"]').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should check TOS and honor code fields if they exist when auto submitting register form', () => {
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: { // setting register to display form for testing TOS and honor code value.
|
||||
...initialState.register,
|
||||
registrationError: {
|
||||
errorCode: 'register-error',
|
||||
},
|
||||
},
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
pipelineUserDetails: {
|
||||
email: 'test@example.com',
|
||||
username: 'test',
|
||||
},
|
||||
autoSubmitRegForm: true,
|
||||
},
|
||||
fieldDescriptions: {
|
||||
terms_of_service: {
|
||||
name: FIELDS.TERMS_OF_SERVICE,
|
||||
error_message: 'You must agree to the Terms and Service agreement of our site',
|
||||
},
|
||||
honor_code: {
|
||||
name: FIELDS.HONOR_CODE,
|
||||
error_message: 'You must agree to the Honor Code agreement of our site',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
|
||||
expect(registrationPage.find('input#tos').props().value).toEqual(true);
|
||||
expect(registrationPage.find('input#honor-code').props().value).toEqual(true);
|
||||
});
|
||||
|
||||
it('should show spinner instead of form while registering if autoSubmitRegForm is true', () => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
backendCountryCode: 'PK',
|
||||
userPipelineDataLoaded: false,
|
||||
},
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
currentProvider: ssoProvider.name,
|
||||
pipelineUserDetails: {
|
||||
name: 'John Doe',
|
||||
username: 'john_doe',
|
||||
email: 'john.doe@example.com',
|
||||
},
|
||||
autoSubmitRegForm: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(registrationPage.find('#tpa-spinner').exists()).toBeTruthy();
|
||||
expect(registrationPage.find('#registration-form').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should set autoSubmitRegisterForm false if third party authentication fails', () => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
backendCountryCode: 'PK',
|
||||
userPipelineDataLoaded: false,
|
||||
},
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
currentProvider: ssoProvider.name,
|
||||
pipelineUserDetails: {},
|
||||
errorMessage: 'An error occured',
|
||||
autoSubmitRegForm: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(registrationPage.find('#tpa-spinner').exists()).toBeFalsy();
|
||||
expect(registrationPage.find('#registration-form').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display errorMessage if third party authentication fails', () => {
|
||||
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
|
||||
getLocale.mockImplementation(() => ('en-us'));
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
register: {
|
||||
...initialState.register,
|
||||
backendCountryCode: 'PK',
|
||||
userPipelineDataLoaded: false,
|
||||
},
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
currentProvider: null,
|
||||
pipelineUserDetails: {},
|
||||
errorMessage: 'An error occured',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
|
||||
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
|
||||
expect(registrationPage.find('div.alert-heading').length).toEqual(1);
|
||||
expect(registrationPage.find('div.alert').first().text()).toContain('An error occured');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -9,23 +9,24 @@ import { FORM_SUBMISSION_ERROR, PASSWORD_RESET, PASSWORD_VALIDATION_ERROR } from
|
||||
import messages from './messages';
|
||||
|
||||
const ResetPasswordFailure = (props) => {
|
||||
const { errorCode, errorMsg, intl } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const { errorCode, errorMsg } = props;
|
||||
|
||||
let errorMessage = null;
|
||||
let heading = intl.formatMessage(messages['reset.password.failure.heading']);
|
||||
let heading = formatMessage(messages['reset.password.failure.heading']);
|
||||
switch (errorCode) {
|
||||
case PASSWORD_RESET.FORBIDDEN_REQUEST:
|
||||
heading = intl.formatMessage(messages['reset.server.rate.limit.error']);
|
||||
errorMessage = intl.formatMessage(messages['rate.limit.error']);
|
||||
heading = formatMessage(messages['reset.server.rate.limit.error']);
|
||||
errorMessage = formatMessage(messages['rate.limit.error']);
|
||||
break;
|
||||
case PASSWORD_RESET.INTERNAL_SERVER_ERROR:
|
||||
errorMessage = intl.formatMessage(messages['internal.server.error']);
|
||||
errorMessage = formatMessage(messages['internal.server.error']);
|
||||
break;
|
||||
case PASSWORD_VALIDATION_ERROR:
|
||||
errorMessage = errorMsg;
|
||||
break;
|
||||
case FORM_SUBMISSION_ERROR:
|
||||
errorMessage = intl.formatMessage(messages['reset.password.form.submission.error']);
|
||||
errorMessage = formatMessage(messages['reset.password.form.submission.error']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -51,7 +52,6 @@ ResetPasswordFailure.defaultProps = {
|
||||
ResetPasswordFailure.propTypes = {
|
||||
errorCode: PropTypes.string,
|
||||
errorMsg: PropTypes.string,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ResetPasswordFailure);
|
||||
export default ResetPasswordFailure;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig, getQueryParameters } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form,
|
||||
Icon,
|
||||
@@ -16,12 +16,12 @@ import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import BaseComponent from '../base-component';
|
||||
import { BaseComponent } from '../base-component';
|
||||
import { PasswordField } from '../common-components';
|
||||
import {
|
||||
LETTER_REGEX, LOGIN_PAGE, NUMBER_REGEX, RESET_PAGE,
|
||||
} from '../data/constants';
|
||||
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||
import { getAllPossibleQueryParams, updatePathWithQueryParams, windowScrollTo } from '../data/utils';
|
||||
import { resetPassword, validateToken } from './data/actions';
|
||||
import {
|
||||
FORM_SUBMISSION_ERROR, PASSWORD_RESET_ERROR, PASSWORD_VALIDATION_ERROR, TOKEN_STATE,
|
||||
@@ -32,7 +32,8 @@ import messages from './messages';
|
||||
import ResetPasswordFailure from './ResetPasswordFailure';
|
||||
|
||||
const ResetPasswordPage = (props) => {
|
||||
const { intl } = props;
|
||||
const { formatMessage } = useIntl();
|
||||
const newPasswordError = formatMessage(messages['password.validation.message']);
|
||||
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
@@ -45,9 +46,9 @@ const ResetPasswordPage = (props) => {
|
||||
setErrorCode(props.status);
|
||||
}
|
||||
if (props.status === PASSWORD_VALIDATION_ERROR) {
|
||||
setFormErrors({ newPassword: intl.formatMessage(messages['password.validation.message']) });
|
||||
setFormErrors({ newPassword: newPasswordError });
|
||||
}
|
||||
}, [props.status, intl]);
|
||||
}, [props.status, newPasswordError]);
|
||||
|
||||
const validatePasswordFromBackend = async (password) => {
|
||||
let errorMessage = '';
|
||||
@@ -67,16 +68,16 @@ const ResetPasswordPage = (props) => {
|
||||
switch (name) {
|
||||
case 'newPassword':
|
||||
if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) {
|
||||
formErrors.newPassword = intl.formatMessage(messages['password.validation.message']);
|
||||
formErrors.newPassword = formatMessage(messages['password.validation.message']);
|
||||
} else {
|
||||
validatePasswordFromBackend(value);
|
||||
}
|
||||
break;
|
||||
case 'confirmPassword':
|
||||
if (!value) {
|
||||
formErrors.confirmPassword = intl.formatMessage(messages['confirm.your.password']);
|
||||
formErrors.confirmPassword = formatMessage(messages['confirm.your.password']);
|
||||
} else if (value !== newPassword) {
|
||||
formErrors.confirmPassword = intl.formatMessage(messages['passwords.do.not.match']);
|
||||
formErrors.confirmPassword = formatMessage(messages['passwords.do.not.match']);
|
||||
} else {
|
||||
formErrors.confirmPassword = '';
|
||||
}
|
||||
@@ -128,7 +129,7 @@ const ResetPasswordPage = (props) => {
|
||||
new_password1: newPassword,
|
||||
new_password2: confirmPassword,
|
||||
};
|
||||
const params = getQueryParameters();
|
||||
const params = getAllPossibleQueryParams();
|
||||
props.resetPassword(formPayload, props.token, params);
|
||||
} else {
|
||||
setErrorCode(FORM_SUBMISSION_ERROR);
|
||||
@@ -139,7 +140,7 @@ const ResetPasswordPage = (props) => {
|
||||
const tabTitle = (
|
||||
<div className="d-inline-flex flex-wrap align-items-center">
|
||||
<Icon src={ChevronLeft} />
|
||||
<span className="ml-2">{intl.formatMessage(messages['sign.in'])}</span>
|
||||
<span className="ml-2">{formatMessage(messages['sign.in'])}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -158,8 +159,8 @@ const ResetPasswordPage = (props) => {
|
||||
<BaseComponent>
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['reset.password.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
<title>
|
||||
{formatMessage(messages['reset.password.page.title'], { siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
<Tabs activeKey="" id="controlled-tab" onSelect={(k) => setKey(k)}>
|
||||
@@ -171,8 +172,8 @@ const ResetPasswordPage = (props) => {
|
||||
<div id="main-content" className="main-content">
|
||||
<div className="mw-xs">
|
||||
<ResetPasswordFailure errorCode={errorCode} errorMsg={props.errorMsg} />
|
||||
<h4>{intl.formatMessage(messages['reset.password'])}</h4>
|
||||
<p className="mb-4">{intl.formatMessage(messages['reset.password.page.instructions'])}</p>
|
||||
<h4>{formatMessage(messages['reset.password'])}</h4>
|
||||
<p className="mb-4">{formatMessage(messages['reset.password.page.instructions'])}</p>
|
||||
<Form id="set-reset-password-form" name="set-reset-password-form">
|
||||
<PasswordField
|
||||
name="newPassword"
|
||||
@@ -181,7 +182,7 @@ const ResetPasswordPage = (props) => {
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={formErrors.newPassword}
|
||||
floatingLabel={intl.formatMessage(messages['new.password.label'])}
|
||||
floatingLabel={formatMessage(messages['new.password.label'])}
|
||||
/>
|
||||
<PasswordField
|
||||
name="confirmPassword"
|
||||
@@ -190,7 +191,7 @@ const ResetPasswordPage = (props) => {
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={formErrors.confirmPassword}
|
||||
showRequirements={false}
|
||||
floatingLabel={intl.formatMessage(messages['confirm.password.label'])}
|
||||
floatingLabel={formatMessage(messages['confirm.password.label'])}
|
||||
/>
|
||||
<StatefulButton
|
||||
id="submit-new-password"
|
||||
@@ -200,7 +201,7 @@ const ResetPasswordPage = (props) => {
|
||||
className="stateful-button-width"
|
||||
state={props.status}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['reset.password']),
|
||||
default: formatMessage(messages['reset.password']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={e => handleSubmit(e)}
|
||||
@@ -224,7 +225,6 @@ ResetPasswordPage.defaultProps = {
|
||||
};
|
||||
|
||||
ResetPasswordPage.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
resetPassword: PropTypes.func.isRequired,
|
||||
validateToken: PropTypes.func.isRequired,
|
||||
token: PropTypes.string,
|
||||
@@ -243,4 +243,4 @@ export default connect(
|
||||
resetPassword,
|
||||
validateToken,
|
||||
},
|
||||
)(injectIntl(ResetPasswordPage));
|
||||
)(ResetPasswordPage);
|
||||
|
||||
@@ -1,26 +1,21 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const ResetPasswordSuccess = (props) => {
|
||||
const { intl } = props;
|
||||
const ResetPasswordSuccess = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
return (
|
||||
<Alert id="reset-password-success" variant="success" className="mb-4">
|
||||
<Alert.Heading>
|
||||
{intl.formatMessage(messages['reset.password.success.heading'])}
|
||||
{formatMessage(messages['reset.password.success.heading'])}
|
||||
</Alert.Heading>
|
||||
<p>{intl.formatMessage(messages['reset.password.success'])}</p>
|
||||
<p>{formatMessage(messages['reset.password.success'])}</p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
ResetPasswordSuccess.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ResetPasswordSuccess);
|
||||
export default ResetPasswordSuccess;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { default } from './ResetPasswordPage';
|
||||
export { default as ResetPasswordPage } from './ResetPasswordPage';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { RESET_PASSWORD } from './data/actions';
|
||||
export { default as saga } from './data/sagas';
|
||||
|
||||
@@ -85,9 +85,9 @@ describe('ResetPasswordPage', () => {
|
||||
await resetPasswordPage.find('button.btn-brand').simulate('click');
|
||||
});
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith(resetPassword(
|
||||
{ new_password1: password, new_password2: password }, props.token, {},
|
||||
));
|
||||
expect(store.dispatch).toHaveBeenCalledWith(
|
||||
resetPassword({ new_password1: password, new_password2: password }, props.token, {}),
|
||||
);
|
||||
resetPasswordPage.unmount();
|
||||
});
|
||||
|
||||
@@ -120,9 +120,10 @@ describe('ResetPasswordPage', () => {
|
||||
},
|
||||
});
|
||||
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
|
||||
resetPasswordPage.find('input#confirmPassword').simulate(
|
||||
'change', { target: { value: 'password-mismatch', name: 'confirmPassword' } },
|
||||
);
|
||||
resetPasswordPage
|
||||
.find('input#confirmPassword')
|
||||
.simulate('change', { target: { value: 'password-mismatch', name: 'confirmPassword' } });
|
||||
|
||||
expect(resetPasswordPage.find('div[feedback-for="confirmPassword"]').text()).toContain('Passwords do not match');
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user