Compare commits
65 Commits
open-relea
...
ahtesham/V
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e634e38a2d | ||
|
|
d5805359a1 | ||
|
|
431f374e20 | ||
|
|
f291efc428 | ||
|
|
1a61ba3cc7 | ||
|
|
3d10cea137 | ||
|
|
6c72e9dad4 | ||
|
|
c5d4f6b94d | ||
|
|
c52d7b6de5 | ||
|
|
6ade0a837f | ||
|
|
81d69c8e72 | ||
|
|
ca333e895f | ||
|
|
8e527efd07 | ||
|
|
c31c03f5a9 | ||
|
|
fe34acf314 | ||
|
|
1f21a874b8 | ||
|
|
ee6a6f0d2d | ||
|
|
8d0181ccca | ||
|
|
e8dba05920 | ||
|
|
6652e2f15c | ||
|
|
578eec8a2d | ||
|
|
8b116d2234 | ||
|
|
b9aa110440 | ||
|
|
9f50bbda79 | ||
|
|
6f2ce69b77 | ||
|
|
8ad2678ce2 | ||
|
|
49c91262fd | ||
|
|
15378682ab | ||
|
|
296861ce3a | ||
|
|
aa59acf0bc | ||
|
|
83e204f3f8 | ||
|
|
497d60e244 | ||
|
|
e8282d6d4a | ||
|
|
72510047d8 | ||
|
|
7dee21eb72 | ||
|
|
d06290642b | ||
|
|
525abe6b88 | ||
|
|
a7704edb9c | ||
|
|
78693f4fc6 | ||
|
|
186484defa | ||
|
|
f8fe704c42 | ||
|
|
82857db236 | ||
|
|
661914b0db | ||
|
|
057299de2b | ||
|
|
93db1a23e2 | ||
|
|
36ec03b24b | ||
|
|
1d6f47c49e | ||
|
|
10dfab7127 | ||
|
|
a8ebe2c096 | ||
|
|
b3dc1c1513 | ||
|
|
7cdae09a94 | ||
|
|
59c2c2fd5d | ||
|
|
70000aab75 | ||
|
|
059d79302d | ||
|
|
ee300466aa | ||
|
|
c4fb3f72e5 | ||
|
|
ed0da96076 | ||
|
|
85fbc54384 | ||
|
|
a6c282520a | ||
|
|
aa4faba4a3 | ||
|
|
5e864c4ff1 | ||
|
|
0c4e612e39 | ||
|
|
92faa846ac | ||
|
|
5bfdd8e1ef | ||
|
|
5af93a57c7 |
23
.env
23
.env
@@ -13,19 +13,24 @@ ORDER_HISTORY_URL=null
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=null
|
||||
USER_INFO_COOKIE_NAME=null
|
||||
AUTHN_MINIMAL_HEADER=true
|
||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||
USER_SURVEY_COOKIE_NAME=null
|
||||
COOKIE_DOMAIN=null
|
||||
WELCOME_PAGE_SUPPORT_LINK=null
|
||||
INFO_EMAIL=''
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
# ***** Cookies *****
|
||||
REGISTER_CONVERSION_COOKIE_NAME=null
|
||||
ENABLE_PROGRESSIVE_PROFILING=''
|
||||
USER_SURVEY_COOKIE_NAME=null
|
||||
# ***** Links *****
|
||||
LOGIN_ISSUE_SUPPORT_LINK=''
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
|
||||
# ***** Features flags *****
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
ENABLE_COOKIE_POLICY_BANNER=''
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
|
||||
ENABLE_PERSONALIZED_RECOMMENDATIONS=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS=''
|
||||
# ***** Zendesk related keys *****
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
# ***** Miscellaneous *****
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -18,21 +18,18 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
INFO_EMAIL='info@example.com'
|
||||
# ***** Cookies *****
|
||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||
SESSION_COOKIE_DOMAIN='localhost'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
AUTHN_MINIMAL_HEADER=true
|
||||
LOGIN_ISSUE_SUPPORT_LINK='/login-issue-support-url'
|
||||
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
||||
# ***** Links *****
|
||||
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
|
||||
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
|
||||
TOS_LINK='http://localhost:18000/tos'
|
||||
PRIVACY_POLICY='http://localhost:18000/privacy'
|
||||
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
||||
COOKIE_DOMAIN='localhost'
|
||||
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
INFO_EMAIL='info@edx.org'
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
# ***** Miscellaneous *****
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
5
.env.private.example
Normal file
5
.env.private.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# Copy these to the .env.private to enable edX specific functionality on local system
|
||||
ENABLE_COOKIE_POLICY_BANNER='true'
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN='true'
|
||||
MARKETING_EMAILS_OPT_IN='true'
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS='true'
|
||||
@@ -16,15 +16,7 @@ ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME='Your Platform Name Here'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
LOGIN_ISSUE_SUPPORT_LINK='https://login-issue-support-url.com'
|
||||
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
|
||||
WELCOME_PAGE_SUPPORT_LINK='http://localhost:1999/welcome'
|
||||
DISABLE_ENTERPRISE_LOGIN=''
|
||||
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
|
||||
MARKETING_EMAILS_OPT_IN=''
|
||||
ENABLE_COPPA_COMPLIANCE=''
|
||||
ZENDESK_KEY=''
|
||||
ZENDESK_LOGO_URL=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
coverage/*
|
||||
dist/
|
||||
docs
|
||||
node_modules/
|
||||
__mocks__/
|
||||
__snapshots__/
|
||||
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
secrets:
|
||||
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
|
||||
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}
|
||||
|
||||
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
20
.github/workflows/add-remove-label-on-comment.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "label: " it tries to apply
|
||||
# the label indicated in rest of comment.
|
||||
# If the comment starts with "remove label: ", it tries
|
||||
# to remove the indicated label.
|
||||
# Note: Labels are allowed to have spaces and this script does
|
||||
# not parse spaces (as often a space is legitimate), so the command
|
||||
# "label: really long lots of words label" will apply the
|
||||
# label "really long lots of words label"
|
||||
|
||||
name: Allows for the adding and removing of labels via comment
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
add_remove_labels:
|
||||
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master
|
||||
|
||||
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
12
.github/workflows/self-assign-issue.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# This workflow runs when a comment is made on the ticket
|
||||
# If the comment starts with "assign me" it assigns the author to the
|
||||
# ticket (case insensitive)
|
||||
|
||||
name: Assign comment author to ticket if they say "assign me"
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
self_assign_by_comment:
|
||||
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ node_modules
|
||||
npm-debug.log
|
||||
coverage
|
||||
module.config.js
|
||||
.env.private
|
||||
|
||||
dist/
|
||||
src/i18n/transifex_input.json
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
# The following users are the owners of all frontend-app-authn files
|
||||
* @edx/vanguards
|
||||
* @openedx/vanguards
|
||||
|
||||
40
README.rst
40
README.rst
@@ -72,8 +72,8 @@ The authentication micro-frontend also requires the following additional variabl
|
||||
- The fully-qualified URL to the password reset support page in the target environment.
|
||||
- ``https://support.example.com``
|
||||
|
||||
* - ``WELCOME_PAGE_SUPPORT_LINK``
|
||||
- The fully-qualified URL to the welcome support page in the target environment.
|
||||
* - ``AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK``
|
||||
- The fully-qualified URL to the progressive profiling support page in the target environment.
|
||||
- ``https://support.example.com``
|
||||
|
||||
* - ``TOS_AND_HONOR_CODE``
|
||||
@@ -96,7 +96,7 @@ The authentication micro-frontend also requires the following additional variabl
|
||||
- Enables support for configurable registration fields on the MFE. This flag must be enabled to show any required registration field besides the default fields (name, email, username, password).
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
* - ``ENABLE_PROGRESSIVE_PROFILING``
|
||||
* - ``ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN``
|
||||
- Enables support for progressive profiling. If enabled, users are redirected to a second page where data for optional registration fields can be collected.
|
||||
- ``true`` | ``''`` (empty strings are falsy)
|
||||
|
||||
@@ -124,11 +124,45 @@ Furthermore, there are several edX-specific environment variables that enable in
|
||||
For more information see the document: `Micro-frontend applications in Open
|
||||
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
|
||||
|
||||
How To Contribute
|
||||
------------
|
||||
Contributions are very welcome, and strongly encouraged! We've
|
||||
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
|
||||
|
||||
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.
|
||||
|
||||
PR description template should be automatically applied if you are sending PR from github interface; otherwise you
|
||||
can find it it at `PULL_REQUEST_TEMPLATE.md <https://github.com/openedx/frontend-app-authn/blob/master/.github/pull_request_template.md>`_
|
||||
|
||||
This project is currently accepting all types of contributions, bug fixes and security fixes.
|
||||
|
||||
Open edX Code of Conduct
|
||||
------------------------
|
||||
All community members are expected to follow the `Open edX Code of Conduct <https://openedx.org/code-of-conduct/>`_.
|
||||
|
||||
People
|
||||
------
|
||||
The assigned maintainers for this component and other project details may be
|
||||
found in `Backstage <https://backstage.openedx.org/catalog/default/group/vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml``
|
||||
file in this repo.
|
||||
|
||||
Reporting Security Issues
|
||||
-------------------------
|
||||
|
||||
Please do not report security issues in public. Please email security@edx.org.
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
|
||||
None
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
The code in this repository is licensed under the GNU Affero General Public License v3.0, unless
|
||||
otherwise noted.
|
||||
|
||||
Please see `LICENSE <https://github.com/openedx/frontend-app-authn/blob/master/LICENSE>`_ for details.
|
||||
|
||||
==============================
|
||||
|
||||
|
||||
18
catalog-info.yaml
Normal file
18
catalog-info.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
# This file records information about this repo. Its use is described in OEP-55:
|
||||
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
|
||||
|
||||
apiVersion: backstage.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: 'frontend-app-authn'
|
||||
description: "Micro-frontend for authentication service. It contains views for login, registration and password reset functionality."
|
||||
links:
|
||||
- url: 'https://github.com/openedx/frontend-app-authn/blob/master/README.rst'
|
||||
title: 'Documentation'
|
||||
icon: 'Article'
|
||||
annotations:
|
||||
openedx.org/arch-interest-groups: ""
|
||||
spec:
|
||||
owner: group:vanguards
|
||||
type: 'service'
|
||||
lifecycle: 'production'
|
||||
@@ -91,7 +91,7 @@ In the data sub-directory, the file names describe what each piece of code does.
|
||||
/ProfilePhotoUploader.jsx // supporting view
|
||||
/data // Note: most files here are named with a plural, as they contain many of the things in question.
|
||||
/actions.js
|
||||
/constants.js
|
||||
/mockedData.js
|
||||
/reducers.js
|
||||
/sagas.js
|
||||
/selectors.js
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
|
||||
nick: Authn MFE
|
||||
oeps: {}
|
||||
owner: edx/vanguards
|
||||
owner: openedx/vanguards
|
||||
openedx-release:
|
||||
ref: master
|
||||
|
||||
721
package-lock.json
generated
721
package-lock.json
generated
@@ -11,14 +11,15 @@
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-cookie-policy-banner": "2.2.0",
|
||||
"@edx/frontend-platform": "3.1.1",
|
||||
"@edx/paragon": "20.20.0",
|
||||
"@edx/frontend-platform": "3.2.0",
|
||||
"@edx/paragon": "20.28.4",
|
||||
"@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",
|
||||
"algoliasearch": "^4.14.3",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.26.1",
|
||||
"extract-react-intl-messages": "4.1.1",
|
||||
@@ -40,11 +41,11 @@
|
||||
"redux": "4.2.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"redux-saga": "1.2.1",
|
||||
"redux-saga": "1.2.2",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"reselect": "4.1.7",
|
||||
"sanitize-html": "2.7.3",
|
||||
"sanitize-html": "2.8.0",
|
||||
"semver-regex": "3.1.4",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
@@ -52,7 +53,7 @@
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "11.0.2",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"babel-plugin-formatjs": "10.3.31",
|
||||
"babel-plugin-formatjs": "10.3.35",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.7",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
@@ -63,6 +64,121 @@
|
||||
"react-test-renderer": "16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/cache-browser-local-storage": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.3.tgz",
|
||||
"integrity": "sha512-hWH1yCxgG3+R/xZIscmUrWAIBnmBFHH5j30fY/+aPkEZWt90wYILfAHIOZ1/Wxhho5SkPfwFmT7ooX2d9JeQBw==",
|
||||
"dependencies": {
|
||||
"@algolia/cache-common": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/cache-common": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.14.3.tgz",
|
||||
"integrity": "sha512-oZJofOoD9FQOwiGTzyRnmzvh3ZP8WVTNPBLH5xU5JNF7drDbRT0ocVT0h/xB2rPHYzOeXRrLaQQBwRT/CKom0Q=="
|
||||
},
|
||||
"node_modules/@algolia/cache-in-memory": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.14.3.tgz",
|
||||
"integrity": "sha512-ES0hHQnzWjeioLQf5Nq+x1AWdZJ50znNPSH3puB/Y4Xsg4Av1bvLmTJe7SY2uqONaeMTvL0OaVcoVtQgJVw0vg==",
|
||||
"dependencies": {
|
||||
"@algolia/cache-common": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-account": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.14.3.tgz",
|
||||
"integrity": "sha512-PBcPb0+f5Xbh5UfLZNx2Ow589OdP8WYjB4CnvupfYBrl9JyC1sdH4jcq/ri8osO/mCZYjZrQsKAPIqW/gQmizQ==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "4.14.3",
|
||||
"@algolia/client-search": "4.14.3",
|
||||
"@algolia/transporter": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-analytics": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.14.3.tgz",
|
||||
"integrity": "sha512-eAwQq0Hb/aauv9NhCH5Dp3Nm29oFx28sayFN2fdOWemwSeJHIl7TmcsxVlRsO50fsD8CtPcDhtGeD3AIFLNvqw==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "4.14.3",
|
||||
"@algolia/client-search": "4.14.3",
|
||||
"@algolia/requester-common": "4.14.3",
|
||||
"@algolia/transporter": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-common": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.14.3.tgz",
|
||||
"integrity": "sha512-jkPPDZdi63IK64Yg4WccdCsAP4pHxSkr4usplkUZM5C1l1oEpZXsy2c579LQ0rvwCs5JFmwfNG4ahOszidfWPw==",
|
||||
"dependencies": {
|
||||
"@algolia/requester-common": "4.14.3",
|
||||
"@algolia/transporter": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-personalization": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.14.3.tgz",
|
||||
"integrity": "sha512-UCX1MtkVNgaOL9f0e22x6tC9e2H3unZQlSUdnVaSKpZ+hdSChXGaRjp2UIT7pxmPqNCyv51F597KEX5WT60jNg==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "4.14.3",
|
||||
"@algolia/requester-common": "4.14.3",
|
||||
"@algolia/transporter": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-search": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.14.3.tgz",
|
||||
"integrity": "sha512-I2U7xBx5OPFdPLA8AXKUPPxGY3HDxZ4r7+mlZ8ZpLbI8/ri6fnu6B4z3wcL7sgHhDYMwnAE8Xr0AB0h3Hnkp4A==",
|
||||
"dependencies": {
|
||||
"@algolia/client-common": "4.14.3",
|
||||
"@algolia/requester-common": "4.14.3",
|
||||
"@algolia/transporter": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/logger-common": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.14.3.tgz",
|
||||
"integrity": "sha512-kUEAZaBt/J3RjYi8MEBT2QEexJR2kAE2mtLmezsmqMQZTV502TkHCxYzTwY2dE7OKcUTxi4OFlMuS4GId9CWPw=="
|
||||
},
|
||||
"node_modules/@algolia/logger-console": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.14.3.tgz",
|
||||
"integrity": "sha512-ZWqAlUITktiMN2EiFpQIFCJS10N96A++yrexqC2Z+3hgF/JcKrOxOdT4nSCQoEPvU4Ki9QKbpzbebRDemZt/hw==",
|
||||
"dependencies": {
|
||||
"@algolia/logger-common": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/requester-browser-xhr": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.3.tgz",
|
||||
"integrity": "sha512-AZeg2T08WLUPvDncl2XLX2O67W5wIO8MNaT7z5ii5LgBTuk/rU4CikTjCe2xsUleIZeFl++QrPAi4Bdxws6r/Q==",
|
||||
"dependencies": {
|
||||
"@algolia/requester-common": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/requester-common": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.14.3.tgz",
|
||||
"integrity": "sha512-RrRzqNyKFDP7IkTuV3XvYGF9cDPn9h6qEDl595lXva3YUk9YSS8+MGZnnkOMHvjkrSCKfoLeLbm/T4tmoIeclw=="
|
||||
},
|
||||
"node_modules/@algolia/requester-node-http": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.14.3.tgz",
|
||||
"integrity": "sha512-O5wnPxtDRPuW2U0EaOz9rMMWdlhwP0J0eSL1Z7TtXF8xnUeeUyNJrdhV5uy2CAp6RbhM1VuC3sOJcIR6Av+vbA==",
|
||||
"dependencies": {
|
||||
"@algolia/requester-common": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/transporter": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.14.3.tgz",
|
||||
"integrity": "sha512-2qlKlKsnGJ008exFRb5RTeTOqhLZj0bkMCMVskxoqWejs2Q2QtWmsiH98hDfpw0fmnyhzHEt0Z7lqxBYp8bW2w==",
|
||||
"dependencies": {
|
||||
"@algolia/cache-common": "4.14.3",
|
||||
"@algolia/logger-common": "4.14.3",
|
||||
"@algolia/requester-common": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/cli": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.0.tgz",
|
||||
@@ -3524,9 +3640,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-platform": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-3.1.1.tgz",
|
||||
"integrity": "sha512-vXpuOISGuTpzN7PAGCmvZ1XFxgnsVyc9/WA/IcAFPZaNKsjbTOtDn5oiFI9avjWKSnlFqsvJTYUK9JfPOSNW5A==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-3.2.0.tgz",
|
||||
"integrity": "sha512-fEAd9RuaN22v8f9X+g52JOaMQklfvDthiENS7X2woV/MO/m3CdN4sPsV7o8gab0cDWgLmLA1Kz0zPhvkTgqQyQ==",
|
||||
"dependencies": {
|
||||
"@cospired/i18n-iso-languages": "2.2.0",
|
||||
"@formatjs/intl-pluralrules": "4.3.3",
|
||||
@@ -3611,9 +3727,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon": {
|
||||
"version": "20.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.20.0.tgz",
|
||||
"integrity": "sha512-spoEuQgRA0pf4Lg3O9a5qjePbf9dWlg206+l5WPNZtDVdBzqoxFdn9vL47GPNJRP4ksHfpTDLdmIkgX7t1TngQ==",
|
||||
"version": "20.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.28.4.tgz",
|
||||
"integrity": "sha512-JiEAUaEuOnHB/zC8h9d5f3AwchREiHFKbXFm7JpXNcvXpkTj0TTKTCS6zcfwZeDl77q/+Rx6Js7SpSE2EzAtDg==",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
@@ -3637,7 +3753,8 @@
|
||||
"react-table": "^7.7.0",
|
||||
"react-transition-group": "^4.4.2",
|
||||
"tabbable": "^5.3.3",
|
||||
"uncontrollable": "^7.2.1"
|
||||
"uncontrollable": "^7.2.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.6 || ^17.0.0",
|
||||
@@ -3666,9 +3783,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon/node_modules/glob": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
|
||||
"integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
|
||||
"dependencies": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
@@ -3698,9 +3815,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon/node_modules/minimatch": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
|
||||
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
@@ -3726,9 +3843,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/paragon/node_modules/react-loading-skeleton": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.1.0.tgz",
|
||||
"integrity": "sha512-j1U1CWWs68nBPOg7tkQqnlFcAMFF6oEK6MgqAo15f8A5p7mjH6xyKn2gHbkcimpwfO0VQXqxAswnSYVr8lWzjw==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.1.1.tgz",
|
||||
"integrity": "sha512-gQeNzzKXlevvquRDOhN5kas5VE7toTKrpmE9BpUdf0iip9c5ae0LcVbulh5ebO2CFl4RrYsyLuDtYGyrh1h1iA==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
@@ -3746,6 +3863,14 @@
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
|
||||
"integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA=="
|
||||
},
|
||||
"node_modules/@edx/paragon/node_modules/uuid": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/reactifex": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/reactifex/-/reactifex-1.1.0.tgz",
|
||||
@@ -3849,13 +3974,13 @@
|
||||
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
|
||||
},
|
||||
"node_modules/@formatjs/ecma402-abstract": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.13.0.tgz",
|
||||
"integrity": "sha512-CQ8Ykd51jYD1n05dtoX6ns6B9n/+6ZAxnWUAonvHC4kkuAemROYBhHkEB4tm1uVrRlE7gLDqXkAnY51Y0pRCWQ==",
|
||||
"version": "1.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz",
|
||||
"integrity": "sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@formatjs/intl-localematcher": "0.2.31",
|
||||
"tslib": "2.4.0"
|
||||
"@formatjs/intl-localematcher": "0.2.32",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/fast-memoize": {
|
||||
@@ -3867,24 +3992,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-messageformat-parser": {
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.10.tgz",
|
||||
"integrity": "sha512-KkRMxhifWkRC45dhM9tqm0GXbb6NPYTGVYY3xx891IKc6p++DQrZTnmkVSNNO47OEERLfuP2KkPFPJBuu8z/wg==",
|
||||
"version": "2.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.14.tgz",
|
||||
"integrity": "sha512-0KqeVOb72losEhUW+59vhZGGd14s1f35uThfEMVKZHKLEObvJdFTiI3ZQwvTMUCzLEMxnS6mtnYPmG4mTvwd3Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.13.0",
|
||||
"@formatjs/icu-skeleton-parser": "1.3.14",
|
||||
"tslib": "2.4.0"
|
||||
"@formatjs/ecma402-abstract": "1.14.3",
|
||||
"@formatjs/icu-skeleton-parser": "1.3.18",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/icu-skeleton-parser": {
|
||||
"version": "1.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.14.tgz",
|
||||
"integrity": "sha512-7bv60HQQcBb3+TSj+45tOb/CHV5z1hOpwdtS50jsSBXfB+YpGhnoRsZxSRksXeCxMy6xn6tA6VY2601BrrK+OA==",
|
||||
"version": "1.3.18",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz",
|
||||
"integrity": "sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@formatjs/ecma402-abstract": "1.13.0",
|
||||
"tslib": "2.4.0"
|
||||
"@formatjs/ecma402-abstract": "1.14.3",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl": {
|
||||
@@ -3964,12 +4089,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-localematcher": {
|
||||
"version": "0.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.31.tgz",
|
||||
"integrity": "sha512-9QTjdSBpQ7wHShZgsNzNig5qT3rCPvmZogS/wXZzKotns5skbXgs0I7J8cuN0PPqXyynvNVuN+iOKhNS2eb+ZA==",
|
||||
"version": "0.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
|
||||
"integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "2.4.0"
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@formatjs/intl-numberformat": {
|
||||
@@ -6019,9 +6144,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@redux-saga/core": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.1.tgz",
|
||||
"integrity": "sha512-ABCxsZy9DwmNoYNo54ZlfuTvh77RXx8ODKpxOHeWam2dOaLGQ7vAktpfOtqSeTdYrKEORtTeWnxkGJMmPOoukg==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.2.tgz",
|
||||
"integrity": "sha512-0qr5oleOAmI5WoZLRA6FEa30M4qKZcvx+ZQOQw+RqFeH8t20bvhE329XSPsNfTVP8C6qyDsXOSjuoV+g3+8zkg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.6.3",
|
||||
"@redux-saga/deferred": "^1.2.1",
|
||||
@@ -7269,6 +7394,27 @@
|
||||
"ajv": "^6.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/algoliasearch": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.14.3.tgz",
|
||||
"integrity": "sha512-GZTEuxzfWbP/vr7ZJfGzIl8fOsoxN916Z6FY2Egc9q2TmZ6hvq5KfAxY89pPW01oW/2HDEKA8d30f9iAH9eXYg==",
|
||||
"dependencies": {
|
||||
"@algolia/cache-browser-local-storage": "4.14.3",
|
||||
"@algolia/cache-common": "4.14.3",
|
||||
"@algolia/cache-in-memory": "4.14.3",
|
||||
"@algolia/client-account": "4.14.3",
|
||||
"@algolia/client-analytics": "4.14.3",
|
||||
"@algolia/client-common": "4.14.3",
|
||||
"@algolia/client-personalization": "4.14.3",
|
||||
"@algolia/client-search": "4.14.3",
|
||||
"@algolia/logger-common": "4.14.3",
|
||||
"@algolia/logger-console": "4.14.3",
|
||||
"@algolia/requester-browser-xhr": "4.14.3",
|
||||
"@algolia/requester-common": "4.14.3",
|
||||
"@algolia/requester-node-http": "4.14.3",
|
||||
"@algolia/transporter": "4.14.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
@@ -7855,9 +8001,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-loader/node_modules/json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
@@ -7940,9 +8086,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-formatjs": {
|
||||
"version": "10.3.31",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-formatjs/-/babel-plugin-formatjs-10.3.31.tgz",
|
||||
"integrity": "sha512-zdtWGbHHPlLB2305Uea3etfOwt3jAp5/JxcgDchF+XgWZCjjZ5VT5aXsm3mxxO4kUTZF5nuUG7E811Cm0FJRkQ==",
|
||||
"version": "10.3.35",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-formatjs/-/babel-plugin-formatjs-10.3.35.tgz",
|
||||
"integrity": "sha512-AGPsA9jOH68CBk2kKRlC8pFkjR9F1fFQ5+BEYc3y2+8E4j188sSlooYy8l//sO5mLISfIVuo7C0PZT61MlifKA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.10.4",
|
||||
@@ -7950,26 +8096,26 @@
|
||||
"@babel/plugin-syntax-jsx": "7",
|
||||
"@babel/traverse": "7",
|
||||
"@babel/types": "^7.12.11",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.10",
|
||||
"@formatjs/ts-transformer": "3.11.1",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.14",
|
||||
"@formatjs/ts-transformer": "3.11.5",
|
||||
"@types/babel__core": "^7.1.7",
|
||||
"@types/babel__helper-plugin-utils": "^7.10.0",
|
||||
"@types/babel__traverse": "^7.1.7",
|
||||
"tslib": "2.4.0"
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-plugin-formatjs/node_modules/@formatjs/ts-transformer": {
|
||||
"version": "3.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-3.11.1.tgz",
|
||||
"integrity": "sha512-mZw3cgok/4zdqR3KAKayncy7IvTypwzJeENq6N+Tcx7rc2NtyihfPFVDn2W2AGRjVH9GRFibkNiu4nVI9xF0Og==",
|
||||
"version": "3.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-3.11.5.tgz",
|
||||
"integrity": "sha512-cAmvKzgPqdetAr/RsxoXYhfNhMf5tERlXzJTsQw+j6tddPwIAbihACQx6KaajyJJ4aNssiziWNmcaHtjTqrrVw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@formatjs/icu-messageformat-parser": "2.1.10",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.14",
|
||||
"@types/json-stable-stringify": "^1.0.32",
|
||||
"@types/node": "14 || 16 || 17",
|
||||
"chalk": "^4.0.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"tslib": "2.4.0",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -11126,9 +11272,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
|
||||
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==",
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -14296,6 +14442,7 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
||||
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
@@ -14314,6 +14461,7 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
@@ -19690,12 +19838,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
@@ -20413,7 +20558,8 @@
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/minimist-options": {
|
||||
"version": "4.1.0",
|
||||
@@ -23683,11 +23829,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/redux-saga": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.1.tgz",
|
||||
"integrity": "sha512-fVCicLlf4hLP+KB6H7RHfZlZ8LdYckhaemXBB3wh//a2ESyz/z/l8ygxlm0OqPjS/PARdsQ2hIdAltxEB+NgvA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.2.tgz",
|
||||
"integrity": "sha512-6xAHWgOqRP75MFuLq88waKK9/+6dCdMQjii2TohDMARVHeQ6HZrZoJ9HZ3dLqMWCZ9kj4iuS6CDsujgnovn11A==",
|
||||
"dependencies": {
|
||||
"@redux-saga/core": "^1.2.1"
|
||||
"@redux-saga/core": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
@@ -24353,18 +24499,69 @@
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.3.tgz",
|
||||
"integrity": "sha512-jMaHG29ak4miiJ8wgqA1849iInqORgNv7SLfSw9LtfOhEUQ1C0YHKH73R+hgyufBW9ZFeJrb057k9hjlfBCVlw==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.8.0.tgz",
|
||||
"integrity": "sha512-ZsGyc6avnqgvEm3eMKrcy8xa7WM1MrGrfkGsUgQee2CU+vg3PCfNCexXwBDF/6dEPvaQ4k/QqRjnYKHL8xgNjg==",
|
||||
"dependencies": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"htmlparser2": "^6.0.0",
|
||||
"htmlparser2": "^8.0.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"parse-srcset": "^1.0.2",
|
||||
"postcss": "^8.3.11"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html/node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html/node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html/node_modules/domutils": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz",
|
||||
"integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html/node_modules/entities": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
|
||||
"integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA==",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html/node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
@@ -24376,6 +24573,24 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/sanitize-html/node_modules/htmlparser2": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz",
|
||||
"integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.49.9",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz",
|
||||
@@ -25143,9 +25358,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-loader/node_modules/json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
@@ -26280,9 +26495,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths/node_modules/json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
@@ -27916,6 +28131,121 @@
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@algolia/cache-browser-local-storage": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.3.tgz",
|
||||
"integrity": "sha512-hWH1yCxgG3+R/xZIscmUrWAIBnmBFHH5j30fY/+aPkEZWt90wYILfAHIOZ1/Wxhho5SkPfwFmT7ooX2d9JeQBw==",
|
||||
"requires": {
|
||||
"@algolia/cache-common": "4.14.3"
|
||||
}
|
||||
},
|
||||
"@algolia/cache-common": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.14.3.tgz",
|
||||
"integrity": "sha512-oZJofOoD9FQOwiGTzyRnmzvh3ZP8WVTNPBLH5xU5JNF7drDbRT0ocVT0h/xB2rPHYzOeXRrLaQQBwRT/CKom0Q=="
|
||||
},
|
||||
"@algolia/cache-in-memory": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.14.3.tgz",
|
||||
"integrity": "sha512-ES0hHQnzWjeioLQf5Nq+x1AWdZJ50znNPSH3puB/Y4Xsg4Av1bvLmTJe7SY2uqONaeMTvL0OaVcoVtQgJVw0vg==",
|
||||
"requires": {
|
||||
"@algolia/cache-common": "4.14.3"
|
||||
}
|
||||
},
|
||||
"@algolia/client-account": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.14.3.tgz",
|
||||
"integrity": "sha512-PBcPb0+f5Xbh5UfLZNx2Ow589OdP8WYjB4CnvupfYBrl9JyC1sdH4jcq/ri8osO/mCZYjZrQsKAPIqW/gQmizQ==",
|
||||
"requires": {
|
||||
"@algolia/client-common": "4.14.3",
|
||||
"@algolia/client-search": "4.14.3",
|
||||
"@algolia/transporter": "4.14.3"
|
||||
}
|
||||
},
|
||||
"@algolia/client-analytics": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.14.3.tgz",
|
||||
"integrity": "sha512-eAwQq0Hb/aauv9NhCH5Dp3Nm29oFx28sayFN2fdOWemwSeJHIl7TmcsxVlRsO50fsD8CtPcDhtGeD3AIFLNvqw==",
|
||||
"requires": {
|
||||
"@algolia/client-common": "4.14.3",
|
||||
"@algolia/client-search": "4.14.3",
|
||||
"@algolia/requester-common": "4.14.3",
|
||||
"@algolia/transporter": "4.14.3"
|
||||
}
|
||||
},
|
||||
"@algolia/client-common": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.14.3.tgz",
|
||||
"integrity": "sha512-jkPPDZdi63IK64Yg4WccdCsAP4pHxSkr4usplkUZM5C1l1oEpZXsy2c579LQ0rvwCs5JFmwfNG4ahOszidfWPw==",
|
||||
"requires": {
|
||||
"@algolia/requester-common": "4.14.3",
|
||||
"@algolia/transporter": "4.14.3"
|
||||
}
|
||||
},
|
||||
"@algolia/client-personalization": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.14.3.tgz",
|
||||
"integrity": "sha512-UCX1MtkVNgaOL9f0e22x6tC9e2H3unZQlSUdnVaSKpZ+hdSChXGaRjp2UIT7pxmPqNCyv51F597KEX5WT60jNg==",
|
||||
"requires": {
|
||||
"@algolia/client-common": "4.14.3",
|
||||
"@algolia/requester-common": "4.14.3",
|
||||
"@algolia/transporter": "4.14.3"
|
||||
}
|
||||
},
|
||||
"@algolia/client-search": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.14.3.tgz",
|
||||
"integrity": "sha512-I2U7xBx5OPFdPLA8AXKUPPxGY3HDxZ4r7+mlZ8ZpLbI8/ri6fnu6B4z3wcL7sgHhDYMwnAE8Xr0AB0h3Hnkp4A==",
|
||||
"requires": {
|
||||
"@algolia/client-common": "4.14.3",
|
||||
"@algolia/requester-common": "4.14.3",
|
||||
"@algolia/transporter": "4.14.3"
|
||||
}
|
||||
},
|
||||
"@algolia/logger-common": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.14.3.tgz",
|
||||
"integrity": "sha512-kUEAZaBt/J3RjYi8MEBT2QEexJR2kAE2mtLmezsmqMQZTV502TkHCxYzTwY2dE7OKcUTxi4OFlMuS4GId9CWPw=="
|
||||
},
|
||||
"@algolia/logger-console": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.14.3.tgz",
|
||||
"integrity": "sha512-ZWqAlUITktiMN2EiFpQIFCJS10N96A++yrexqC2Z+3hgF/JcKrOxOdT4nSCQoEPvU4Ki9QKbpzbebRDemZt/hw==",
|
||||
"requires": {
|
||||
"@algolia/logger-common": "4.14.3"
|
||||
}
|
||||
},
|
||||
"@algolia/requester-browser-xhr": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.3.tgz",
|
||||
"integrity": "sha512-AZeg2T08WLUPvDncl2XLX2O67W5wIO8MNaT7z5ii5LgBTuk/rU4CikTjCe2xsUleIZeFl++QrPAi4Bdxws6r/Q==",
|
||||
"requires": {
|
||||
"@algolia/requester-common": "4.14.3"
|
||||
}
|
||||
},
|
||||
"@algolia/requester-common": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.14.3.tgz",
|
||||
"integrity": "sha512-RrRzqNyKFDP7IkTuV3XvYGF9cDPn9h6qEDl595lXva3YUk9YSS8+MGZnnkOMHvjkrSCKfoLeLbm/T4tmoIeclw=="
|
||||
},
|
||||
"@algolia/requester-node-http": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.14.3.tgz",
|
||||
"integrity": "sha512-O5wnPxtDRPuW2U0EaOz9rMMWdlhwP0J0eSL1Z7TtXF8xnUeeUyNJrdhV5uy2CAp6RbhM1VuC3sOJcIR6Av+vbA==",
|
||||
"requires": {
|
||||
"@algolia/requester-common": "4.14.3"
|
||||
}
|
||||
},
|
||||
"@algolia/transporter": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.14.3.tgz",
|
||||
"integrity": "sha512-2qlKlKsnGJ008exFRb5RTeTOqhLZj0bkMCMVskxoqWejs2Q2QtWmsiH98hDfpw0fmnyhzHEt0Z7lqxBYp8bW2w==",
|
||||
"requires": {
|
||||
"@algolia/cache-common": "4.14.3",
|
||||
"@algolia/logger-common": "4.14.3",
|
||||
"@algolia/requester-common": "4.14.3"
|
||||
}
|
||||
},
|
||||
"@babel/cli": {
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.16.0.tgz",
|
||||
@@ -30453,9 +30783,9 @@
|
||||
}
|
||||
},
|
||||
"@edx/frontend-platform": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-3.1.1.tgz",
|
||||
"integrity": "sha512-vXpuOISGuTpzN7PAGCmvZ1XFxgnsVyc9/WA/IcAFPZaNKsjbTOtDn5oiFI9avjWKSnlFqsvJTYUK9JfPOSNW5A==",
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-3.2.0.tgz",
|
||||
"integrity": "sha512-fEAd9RuaN22v8f9X+g52JOaMQklfvDthiENS7X2woV/MO/m3CdN4sPsV7o8gab0cDWgLmLA1Kz0zPhvkTgqQyQ==",
|
||||
"requires": {
|
||||
"@cospired/i18n-iso-languages": "2.2.0",
|
||||
"@formatjs/intl-pluralrules": "4.3.3",
|
||||
@@ -30527,9 +30857,9 @@
|
||||
}
|
||||
},
|
||||
"@edx/paragon": {
|
||||
"version": "20.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.20.0.tgz",
|
||||
"integrity": "sha512-spoEuQgRA0pf4Lg3O9a5qjePbf9dWlg206+l5WPNZtDVdBzqoxFdn9vL47GPNJRP4ksHfpTDLdmIkgX7t1TngQ==",
|
||||
"version": "20.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@edx/paragon/-/paragon-20.28.4.tgz",
|
||||
"integrity": "sha512-JiEAUaEuOnHB/zC8h9d5f3AwchREiHFKbXFm7JpXNcvXpkTj0TTKTCS6zcfwZeDl77q/+Rx6Js7SpSE2EzAtDg==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
@@ -30553,7 +30883,8 @@
|
||||
"react-table": "^7.7.0",
|
||||
"react-transition-group": "^4.4.2",
|
||||
"tabbable": "^5.3.3",
|
||||
"uncontrollable": "^7.2.1"
|
||||
"uncontrollable": "^7.2.1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/react-fontawesome": {
|
||||
@@ -30573,9 +30904,9 @@
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "8.0.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz",
|
||||
"integrity": "sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ==",
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||
"integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
@@ -30596,9 +30927,9 @@
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz",
|
||||
"integrity": "sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg==",
|
||||
"version": "5.1.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||
"requires": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
}
|
||||
@@ -30615,9 +30946,9 @@
|
||||
}
|
||||
},
|
||||
"react-loading-skeleton": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.1.0.tgz",
|
||||
"integrity": "sha512-j1U1CWWs68nBPOg7tkQqnlFcAMFF6oEK6MgqAo15f8A5p7mjH6xyKn2gHbkcimpwfO0VQXqxAswnSYVr8lWzjw==",
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.1.1.tgz",
|
||||
"integrity": "sha512-gQeNzzKXlevvquRDOhN5kas5VE7toTKrpmE9BpUdf0iip9c5ae0LcVbulh5ebO2CFl4RrYsyLuDtYGyrh1h1iA==",
|
||||
"requires": {}
|
||||
},
|
||||
"strict-uri-encode": {
|
||||
@@ -30629,6 +30960,11 @@
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-5.3.3.tgz",
|
||||
"integrity": "sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA=="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "9.0.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz",
|
||||
"integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -30731,13 +31067,13 @@
|
||||
"integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA=="
|
||||
},
|
||||
"@formatjs/ecma402-abstract": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.13.0.tgz",
|
||||
"integrity": "sha512-CQ8Ykd51jYD1n05dtoX6ns6B9n/+6ZAxnWUAonvHC4kkuAemROYBhHkEB4tm1uVrRlE7gLDqXkAnY51Y0pRCWQ==",
|
||||
"version": "1.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz",
|
||||
"integrity": "sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@formatjs/intl-localematcher": "0.2.31",
|
||||
"tslib": "2.4.0"
|
||||
"@formatjs/intl-localematcher": "0.2.32",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@formatjs/fast-memoize": {
|
||||
@@ -30749,24 +31085,24 @@
|
||||
}
|
||||
},
|
||||
"@formatjs/icu-messageformat-parser": {
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.10.tgz",
|
||||
"integrity": "sha512-KkRMxhifWkRC45dhM9tqm0GXbb6NPYTGVYY3xx891IKc6p++DQrZTnmkVSNNO47OEERLfuP2KkPFPJBuu8z/wg==",
|
||||
"version": "2.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.14.tgz",
|
||||
"integrity": "sha512-0KqeVOb72losEhUW+59vhZGGd14s1f35uThfEMVKZHKLEObvJdFTiI3ZQwvTMUCzLEMxnS6mtnYPmG4mTvwd3Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.13.0",
|
||||
"@formatjs/icu-skeleton-parser": "1.3.14",
|
||||
"tslib": "2.4.0"
|
||||
"@formatjs/ecma402-abstract": "1.14.3",
|
||||
"@formatjs/icu-skeleton-parser": "1.3.18",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@formatjs/icu-skeleton-parser": {
|
||||
"version": "1.3.14",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.14.tgz",
|
||||
"integrity": "sha512-7bv60HQQcBb3+TSj+45tOb/CHV5z1hOpwdtS50jsSBXfB+YpGhnoRsZxSRksXeCxMy6xn6tA6VY2601BrrK+OA==",
|
||||
"version": "1.3.18",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz",
|
||||
"integrity": "sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@formatjs/ecma402-abstract": "1.13.0",
|
||||
"tslib": "2.4.0"
|
||||
"@formatjs/ecma402-abstract": "1.14.3",
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@formatjs/intl": {
|
||||
@@ -30880,12 +31216,12 @@
|
||||
}
|
||||
},
|
||||
"@formatjs/intl-localematcher": {
|
||||
"version": "0.2.31",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.31.tgz",
|
||||
"integrity": "sha512-9QTjdSBpQ7wHShZgsNzNig5qT3rCPvmZogS/wXZzKotns5skbXgs0I7J8cuN0PPqXyynvNVuN+iOKhNS2eb+ZA==",
|
||||
"version": "0.2.32",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz",
|
||||
"integrity": "sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "2.4.0"
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"@formatjs/intl-numberformat": {
|
||||
@@ -32435,9 +32771,9 @@
|
||||
}
|
||||
},
|
||||
"@redux-saga/core": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.1.tgz",
|
||||
"integrity": "sha512-ABCxsZy9DwmNoYNo54ZlfuTvh77RXx8ODKpxOHeWam2dOaLGQ7vAktpfOtqSeTdYrKEORtTeWnxkGJMmPOoukg==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.2.2.tgz",
|
||||
"integrity": "sha512-0qr5oleOAmI5WoZLRA6FEa30M4qKZcvx+ZQOQw+RqFeH8t20bvhE329XSPsNfTVP8C6qyDsXOSjuoV+g3+8zkg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.6.3",
|
||||
"@redux-saga/deferred": "^1.2.1",
|
||||
@@ -33473,6 +33809,27 @@
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"algoliasearch": {
|
||||
"version": "4.14.3",
|
||||
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.14.3.tgz",
|
||||
"integrity": "sha512-GZTEuxzfWbP/vr7ZJfGzIl8fOsoxN916Z6FY2Egc9q2TmZ6hvq5KfAxY89pPW01oW/2HDEKA8d30f9iAH9eXYg==",
|
||||
"requires": {
|
||||
"@algolia/cache-browser-local-storage": "4.14.3",
|
||||
"@algolia/cache-common": "4.14.3",
|
||||
"@algolia/cache-in-memory": "4.14.3",
|
||||
"@algolia/client-account": "4.14.3",
|
||||
"@algolia/client-analytics": "4.14.3",
|
||||
"@algolia/client-common": "4.14.3",
|
||||
"@algolia/client-personalization": "4.14.3",
|
||||
"@algolia/client-search": "4.14.3",
|
||||
"@algolia/logger-common": "4.14.3",
|
||||
"@algolia/logger-console": "4.14.3",
|
||||
"@algolia/requester-browser-xhr": "4.14.3",
|
||||
"@algolia/requester-common": "4.14.3",
|
||||
"@algolia/requester-node-http": "4.14.3",
|
||||
"@algolia/transporter": "4.14.3"
|
||||
}
|
||||
},
|
||||
"ansi-escapes": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||
@@ -33903,9 +34260,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.0"
|
||||
@@ -33971,9 +34328,9 @@
|
||||
}
|
||||
},
|
||||
"babel-plugin-formatjs": {
|
||||
"version": "10.3.31",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-formatjs/-/babel-plugin-formatjs-10.3.31.tgz",
|
||||
"integrity": "sha512-zdtWGbHHPlLB2305Uea3etfOwt3jAp5/JxcgDchF+XgWZCjjZ5VT5aXsm3mxxO4kUTZF5nuUG7E811Cm0FJRkQ==",
|
||||
"version": "10.3.35",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-formatjs/-/babel-plugin-formatjs-10.3.35.tgz",
|
||||
"integrity": "sha512-AGPsA9jOH68CBk2kKRlC8pFkjR9F1fFQ5+BEYc3y2+8E4j188sSlooYy8l//sO5mLISfIVuo7C0PZT61MlifKA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/core": "^7.10.4",
|
||||
@@ -33981,26 +34338,26 @@
|
||||
"@babel/plugin-syntax-jsx": "7",
|
||||
"@babel/traverse": "7",
|
||||
"@babel/types": "^7.12.11",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.10",
|
||||
"@formatjs/ts-transformer": "3.11.1",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.14",
|
||||
"@formatjs/ts-transformer": "3.11.5",
|
||||
"@types/babel__core": "^7.1.7",
|
||||
"@types/babel__helper-plugin-utils": "^7.10.0",
|
||||
"@types/babel__traverse": "^7.1.7",
|
||||
"tslib": "2.4.0"
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formatjs/ts-transformer": {
|
||||
"version": "3.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-3.11.1.tgz",
|
||||
"integrity": "sha512-mZw3cgok/4zdqR3KAKayncy7IvTypwzJeENq6N+Tcx7rc2NtyihfPFVDn2W2AGRjVH9GRFibkNiu4nVI9xF0Og==",
|
||||
"version": "3.11.5",
|
||||
"resolved": "https://registry.npmjs.org/@formatjs/ts-transformer/-/ts-transformer-3.11.5.tgz",
|
||||
"integrity": "sha512-cAmvKzgPqdetAr/RsxoXYhfNhMf5tERlXzJTsQw+j6tddPwIAbihACQx6KaajyJJ4aNssiziWNmcaHtjTqrrVw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@formatjs/icu-messageformat-parser": "2.1.10",
|
||||
"@formatjs/icu-messageformat-parser": "2.1.14",
|
||||
"@types/json-stable-stringify": "^1.0.32",
|
||||
"@types/node": "14 || 16 || 17",
|
||||
"chalk": "^4.0.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"tslib": "2.4.0",
|
||||
"tslib": "^2.4.0",
|
||||
"typescript": "^4.7"
|
||||
}
|
||||
},
|
||||
@@ -36525,9 +36882,9 @@
|
||||
}
|
||||
},
|
||||
"domelementtype": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz",
|
||||
"integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A=="
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="
|
||||
},
|
||||
"domexception": {
|
||||
"version": "2.0.1",
|
||||
@@ -38934,6 +39291,7 @@
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz",
|
||||
"integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^4.0.0",
|
||||
@@ -38944,7 +39302,8 @@
|
||||
"entities": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -43018,12 +43377,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "6.1.0",
|
||||
@@ -43582,7 +43938,8 @@
|
||||
"minimist": {
|
||||
"version": "1.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz",
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
|
||||
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==",
|
||||
"dev": true
|
||||
},
|
||||
"minimist-options": {
|
||||
"version": "4.1.0",
|
||||
@@ -45988,11 +46345,11 @@
|
||||
}
|
||||
},
|
||||
"redux-saga": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.1.tgz",
|
||||
"integrity": "sha512-fVCicLlf4hLP+KB6H7RHfZlZ8LdYckhaemXBB3wh//a2ESyz/z/l8ygxlm0OqPjS/PARdsQ2hIdAltxEB+NgvA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.2.2.tgz",
|
||||
"integrity": "sha512-6xAHWgOqRP75MFuLq88waKK9/+6dCdMQjii2TohDMARVHeQ6HZrZoJ9HZ3dLqMWCZ9kj4iuS6CDsujgnovn11A==",
|
||||
"requires": {
|
||||
"@redux-saga/core": "^1.2.1"
|
||||
"@redux-saga/core": "^1.2.2"
|
||||
}
|
||||
},
|
||||
"redux-thunk": {
|
||||
@@ -46525,22 +46882,66 @@
|
||||
}
|
||||
},
|
||||
"sanitize-html": {
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.3.tgz",
|
||||
"integrity": "sha512-jMaHG29ak4miiJ8wgqA1849iInqORgNv7SLfSw9LtfOhEUQ1C0YHKH73R+hgyufBW9ZFeJrb057k9hjlfBCVlw==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.8.0.tgz",
|
||||
"integrity": "sha512-ZsGyc6avnqgvEm3eMKrcy8xa7WM1MrGrfkGsUgQee2CU+vg3PCfNCexXwBDF/6dEPvaQ4k/QqRjnYKHL8xgNjg==",
|
||||
"requires": {
|
||||
"deepmerge": "^4.2.2",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"htmlparser2": "^6.0.0",
|
||||
"htmlparser2": "^8.0.0",
|
||||
"is-plain-object": "^5.0.0",
|
||||
"parse-srcset": "^1.0.2",
|
||||
"postcss": "^8.3.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"requires": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
}
|
||||
},
|
||||
"domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"requires": {
|
||||
"domelementtype": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"domutils": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.0.1.tgz",
|
||||
"integrity": "sha512-z08c1l761iKhDFtfXO04C7kTdPBLi41zwOZl00WS8b5eiaebNpY00HKbztwBq+e3vyqWNwWF3mP9YLUeqIrF+Q==",
|
||||
"requires": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.4.0.tgz",
|
||||
"integrity": "sha512-oYp7156SP8LkeGD0GF85ad1X9Ai79WtRsZ2gxJqtBuzH+98YUV6jkHEKlZkMbcrjJjIVJNIDP/3WL9wQkoPbWA=="
|
||||
},
|
||||
"escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
|
||||
},
|
||||
"htmlparser2": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.1.tgz",
|
||||
"integrity": "sha512-4lVbmc1diZC7GUJQtRQ5yBAeUCL1exyMwmForWkRLnwyzWBFxN633SALPMGYaWZvKe9j1pRZJpauvmxENSp/EA==",
|
||||
"requires": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -47159,9 +47560,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.0"
|
||||
@@ -48065,9 +48466,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.0"
|
||||
|
||||
11
package.json
11
package.json
@@ -34,14 +34,15 @@
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-cookie-policy-banner": "2.2.0",
|
||||
"@edx/frontend-platform": "3.1.1",
|
||||
"@edx/paragon": "20.20.0",
|
||||
"@edx/frontend-platform": "3.2.0",
|
||||
"@edx/paragon": "20.28.4",
|
||||
"@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",
|
||||
"algoliasearch": "^4.14.3",
|
||||
"classnames": "2.3.2",
|
||||
"core-js": "3.26.1",
|
||||
"extract-react-intl-messages": "4.1.1",
|
||||
@@ -63,11 +64,11 @@
|
||||
"redux": "4.2.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-mock-store": "1.5.4",
|
||||
"redux-saga": "1.2.1",
|
||||
"redux-saga": "1.2.2",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "0.13.11",
|
||||
"reselect": "4.1.7",
|
||||
"sanitize-html": "2.7.3",
|
||||
"sanitize-html": "2.8.0",
|
||||
"semver-regex": "3.1.4",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
@@ -75,7 +76,7 @@
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "11.0.2",
|
||||
"@edx/reactifex": "1.1.0",
|
||||
"babel-plugin-formatjs": "10.3.31",
|
||||
"babel-plugin-formatjs": "10.3.35",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.7",
|
||||
"eslint-plugin-import": "2.26.0",
|
||||
|
||||
@@ -28,6 +28,9 @@
|
||||
},
|
||||
chat: {
|
||||
suppress: false,
|
||||
departments: {
|
||||
enabled: ['account settings', 'billing and payments', 'certificates', 'deadlines', 'errors and technical issues', 'other', 'proctoring']
|
||||
}
|
||||
},
|
||||
contactForm: {
|
||||
ticketForms: [
|
||||
|
||||
@@ -10,12 +10,19 @@ import {
|
||||
} from './common-components';
|
||||
import configureStore from './data/configureStore';
|
||||
import {
|
||||
LOGIN_PAGE, PAGE_NOT_FOUND, PASSWORD_RESET_CONFIRM, REGISTER_PAGE, RESET_PAGE, WELCOME_PAGE,
|
||||
AUTHN_PROGRESSIVE_PROFILING,
|
||||
LOGIN_PAGE,
|
||||
PAGE_NOT_FOUND,
|
||||
PASSWORD_RESET_CONFIRM,
|
||||
RECOMMENDATIONS,
|
||||
REGISTER_PAGE,
|
||||
RESET_PAGE,
|
||||
} from './data/constants';
|
||||
import { updatePathWithQueryParams } from './data/utils';
|
||||
import ForgotPasswordPage from './forgot-password';
|
||||
import { ProgressiveProfiling } from './progressive-profiling';
|
||||
import RecommendationsPage from './recommendations';
|
||||
import ResetPasswordPage from './reset-password';
|
||||
import { ProgressiveProfiling } from './welcome';
|
||||
import './index.scss';
|
||||
|
||||
registerIcons();
|
||||
@@ -33,7 +40,8 @@ const MainApp = () => (
|
||||
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={Logistration} />
|
||||
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
|
||||
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
|
||||
<Route exact path={WELCOME_PAGE} component={ProgressiveProfiling} />
|
||||
<Route exact path={AUTHN_PROGRESSIVE_PROFILING} component={ProgressiveProfiling} />
|
||||
<Route exact path={RECOMMENDATIONS} component={RecommendationsPage} />
|
||||
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
|
||||
<Route path="*">
|
||||
<Redirect to={PAGE_NOT_FOUND} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -39,7 +39,7 @@ const AuthLargeLayout = ({ intl, username }) => (
|
||||
);
|
||||
|
||||
AuthLargeLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -42,7 +42,7 @@ const AuthMediumLayout = ({ intl, username }) => (
|
||||
);
|
||||
|
||||
AuthMediumLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -31,7 +31,7 @@ const AuthSmallLayout = ({ intl, username }) => (
|
||||
);
|
||||
|
||||
AuthSmallLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { getLocale } from '@edx/frontend-platform/i18n';
|
||||
import { breakpoints } from '@edx/paragon';
|
||||
@@ -21,7 +22,7 @@ const BaseComponent = ({ children, showWelcomeBanner }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<CookiePolicyBanner languageCode={getLocale()} />
|
||||
{getConfig().ENABLE_COOKIE_POLICY_BANNER ? <CookiePolicyBanner languageCode={getLocale()} /> : null}
|
||||
<div className="col-md-12 extra-large-screen-top-stripe" />
|
||||
<div className="layout">
|
||||
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -39,7 +40,7 @@ const LargeLayout = ({ intl }) => (
|
||||
);
|
||||
|
||||
LargeLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LargeLayout);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -44,7 +45,7 @@ const MediumLayout = ({ intl }) => (
|
||||
);
|
||||
|
||||
MediumLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(MediumLayout);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Image } from '@edx/paragon';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -33,7 +34,7 @@ const SmallLayout = ({ intl }) => (
|
||||
);
|
||||
|
||||
SmallLayout.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SmallLayout);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Form,
|
||||
} from '@edx/paragon';
|
||||
@@ -12,6 +12,9 @@ import PropTypes from 'prop-types';
|
||||
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* This component renders the Single sign-on (SSO) button only for the tpa provider passed
|
||||
* */
|
||||
const EnterpriseSSO = (props) => {
|
||||
const { intl } = props;
|
||||
const tpaProvider = props.provider;
|
||||
@@ -97,7 +100,7 @@ EnterpriseSSO.propTypes = {
|
||||
loginUrl: PropTypes.string,
|
||||
registerUrl: PropTypes.string,
|
||||
}),
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(EnterpriseSSO);
|
||||
|
||||
@@ -37,7 +37,6 @@ const FormGroup = (props) => {
|
||||
onClick={handleClick}
|
||||
onChange={props.handleChange}
|
||||
controlClassName={props.borderClass}
|
||||
|
||||
trailingElement={props.trailingElement}
|
||||
floatingLabel={props.floatingLabel}
|
||||
>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Hyperlink, Icon } from '@edx/paragon';
|
||||
import { Institution } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* This component renders the Institution login button
|
||||
* */
|
||||
export const RenderInstitutionButton = props => {
|
||||
const { onSubmitHandler, buttonTitle } = props;
|
||||
|
||||
@@ -24,6 +27,9 @@ export const RenderInstitutionButton = props => {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* This component renders the page list of available institutions for login
|
||||
* */
|
||||
const InstitutionLogistration = props => {
|
||||
const lmsBaseUrl = getConfig().LMS_BASE_URL;
|
||||
const {
|
||||
@@ -89,7 +95,7 @@ RenderInstitutionButton.defaultProps = {
|
||||
|
||||
InstitutionLogistration.propTypes = {
|
||||
...LogistrationProps,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
headingTitle: PropTypes.string,
|
||||
};
|
||||
InstitutionLogistration.defaultProps = {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { getAuthService } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Icon,
|
||||
Tab,
|
||||
@@ -15,14 +16,21 @@ import { Redirect } from 'react-router-dom';
|
||||
|
||||
import BaseComponent from '../base-component';
|
||||
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
|
||||
import { getTpaHint, updatePathWithQueryParams } from '../data/utils';
|
||||
import { getTpaHint, getTpaProvider, updatePathWithQueryParams } from '../data/utils';
|
||||
import { LoginPage } from '../login';
|
||||
import { RegistrationPage } from '../register';
|
||||
import { backupRegistrationForm } from '../register/data/actions';
|
||||
import {
|
||||
tpaProvidersSelector,
|
||||
} from './data/selectors';
|
||||
import messages from './messages';
|
||||
|
||||
const Logistration = (props) => {
|
||||
const { intl, selectedPage } = props;
|
||||
const tpa = getTpaHint();
|
||||
const { intl, selectedPage, tpaProviders } = props;
|
||||
const tpaHint = getTpaHint();
|
||||
const {
|
||||
providers, secondaryProviders,
|
||||
} = tpaProviders;
|
||||
const [institutionLogin, setInstitutionLogin] = useState(false);
|
||||
const [key, setKey] = useState('');
|
||||
|
||||
@@ -46,6 +54,9 @@ const Logistration = (props) => {
|
||||
|
||||
const handleOnSelect = (tabKey) => {
|
||||
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
|
||||
if (tabKey === LOGIN_PAGE) {
|
||||
props.backupRegistrationForm();
|
||||
}
|
||||
setKey(tabKey);
|
||||
};
|
||||
|
||||
@@ -60,6 +71,11 @@ const Logistration = (props) => {
|
||||
</div>
|
||||
);
|
||||
|
||||
const isValidTpaHint = () => {
|
||||
const { provider } = getTpaProvider(tpaHint, providers, secondaryProviders);
|
||||
return !!provider;
|
||||
};
|
||||
|
||||
return (
|
||||
<BaseComponent>
|
||||
<div>
|
||||
@@ -69,16 +85,14 @@ const Logistration = (props) => {
|
||||
<Tab title={tabTitle} eventKey={selectedPage === LOGIN_PAGE ? LOGIN_PAGE : REGISTER_PAGE} />
|
||||
</Tabs>
|
||||
)
|
||||
: (
|
||||
: (!isValidTpaHint() && (
|
||||
<>
|
||||
{!tpa && (
|
||||
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={handleOnSelect}>
|
||||
<Tab title={intl.formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
|
||||
<Tab title={intl.formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
|
||||
</Tabs>
|
||||
)}
|
||||
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={handleOnSelect}>
|
||||
<Tab title={intl.formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
|
||||
<Tab title={intl.formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
|
||||
</Tabs>
|
||||
</>
|
||||
)}
|
||||
))}
|
||||
{ key && (
|
||||
<Redirect to={updatePathWithQueryParams(key)} />
|
||||
)}
|
||||
@@ -93,12 +107,33 @@ const Logistration = (props) => {
|
||||
};
|
||||
|
||||
Logistration.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
selectedPage: PropTypes.string,
|
||||
backupRegistrationForm: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.shape({
|
||||
providers: PropTypes.array,
|
||||
secondaryProviders: PropTypes.array,
|
||||
}),
|
||||
};
|
||||
|
||||
Logistration.defaultProps = {
|
||||
tpaProviders: {
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
},
|
||||
};
|
||||
|
||||
Logistration.defaultProps = {
|
||||
selectedPage: REGISTER_PAGE,
|
||||
};
|
||||
|
||||
export default injectIntl(Logistration);
|
||||
const mapStateToProps = state => ({
|
||||
tpaProviders: tpaProvidersSelector(state),
|
||||
});
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
backupRegistrationForm,
|
||||
},
|
||||
)(injectIntl(Logistration));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
|
||||
} from '@edx/paragon';
|
||||
@@ -30,11 +30,11 @@ const PasswordField = (props) => {
|
||||
};
|
||||
|
||||
const HideButton = (
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
|
||||
);
|
||||
|
||||
const ShowButton = (
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
|
||||
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
|
||||
);
|
||||
const placement = window.innerWidth < 768 ? 'top' : 'left';
|
||||
const tooltip = (
|
||||
@@ -100,7 +100,7 @@ PasswordField.propTypes = {
|
||||
handleBlur: PropTypes.func,
|
||||
handleFocus: PropTypes.func,
|
||||
handleChange: PropTypes.func,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
showRequirements: PropTypes.bool,
|
||||
value: PropTypes.string.isRequired,
|
||||
|
||||
@@ -4,12 +4,19 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
|
||||
import { WELCOME_PAGE } from '../data/constants';
|
||||
import { AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS } from '../data/constants';
|
||||
import { setCookie } from '../data/utils';
|
||||
|
||||
function RedirectLogistration(props) {
|
||||
const {
|
||||
finishAuthUrl, redirectUrl, redirectToWelcomePage, success, optionalFields,
|
||||
finishAuthUrl,
|
||||
redirectUrl,
|
||||
redirectToProgressiveProfilingPage,
|
||||
success,
|
||||
optionalFields,
|
||||
redirectToRecommendationsPage,
|
||||
educationLevel,
|
||||
userId,
|
||||
} = props;
|
||||
let finalRedirectUrl = '';
|
||||
|
||||
@@ -24,14 +31,14 @@ function RedirectLogistration(props) {
|
||||
finalRedirectUrl = redirectUrl;
|
||||
}
|
||||
|
||||
if (redirectToWelcomePage) {
|
||||
// Redirect to Progressive Profiling after successful registration
|
||||
if (redirectToProgressiveProfilingPage) {
|
||||
// TODO: Do we still need this cookie?
|
||||
setCookie('van-504-returning-user', true);
|
||||
// use this component to redirect WelcomePage after successful registration
|
||||
// return <Redirect to={WELCOME_PAGE} />;
|
||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||
return (
|
||||
<Redirect to={{
|
||||
pathname: WELCOME_PAGE,
|
||||
pathname: AUTHN_PROGRESSIVE_PROFILING,
|
||||
state: {
|
||||
registrationResult,
|
||||
optionalFields,
|
||||
@@ -41,25 +48,47 @@ function RedirectLogistration(props) {
|
||||
);
|
||||
}
|
||||
|
||||
// Redirect to Recommendation page
|
||||
if (redirectToRecommendationsPage) {
|
||||
const registrationResult = { redirectUrl: finalRedirectUrl, success };
|
||||
return (
|
||||
<Redirect to={{
|
||||
pathname: RECOMMENDATIONS,
|
||||
state: {
|
||||
registrationResult,
|
||||
educationLevel,
|
||||
userId,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
window.location.href = finalRedirectUrl;
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
|
||||
RedirectLogistration.defaultProps = {
|
||||
educationLevel: null,
|
||||
finishAuthUrl: null,
|
||||
success: false,
|
||||
redirectUrl: '',
|
||||
redirectToWelcomePage: false,
|
||||
redirectToProgressiveProfilingPage: false,
|
||||
optionalFields: {},
|
||||
redirectToRecommendationsPage: false,
|
||||
userId: null,
|
||||
};
|
||||
|
||||
RedirectLogistration.propTypes = {
|
||||
educationLevel: PropTypes.string,
|
||||
finishAuthUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
redirectUrl: PropTypes.string,
|
||||
redirectToWelcomePage: PropTypes.bool,
|
||||
redirectToProgressiveProfilingPage: PropTypes.bool,
|
||||
optionalFields: PropTypes.shape({}),
|
||||
redirectToRecommendationsPage: PropTypes.bool,
|
||||
userId: PropTypes.number,
|
||||
};
|
||||
|
||||
export default RedirectLogistration;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -60,7 +60,7 @@ SocialAuthProviders.defaultProps = {
|
||||
};
|
||||
|
||||
SocialAuthProviders.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
referrer: PropTypes.string,
|
||||
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { TransitionReplace } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const onChildExit = (htmlNode) => {
|
||||
// If the leaving child has focus, take control and redirect it
|
||||
if (htmlNode.contains(document.activeElement)) {
|
||||
// Get the newly entering sibling.
|
||||
// It's the previousSibling, but not for any explicit reason. So checking for both.
|
||||
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
|
||||
|
||||
// There's no replacement, do nothing.
|
||||
if (!enteringChild) return; // eslint-disable-line curly
|
||||
|
||||
// Get all the focusable elements in the entering child and focus the first one
|
||||
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (focusableElements.length) {
|
||||
focusableElements[0].focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function SwitchContent({ expression, cases, className }) {
|
||||
const getContent = (caseKey) => {
|
||||
if (cases[caseKey]) {
|
||||
if (typeof cases[caseKey] === 'string') {
|
||||
return getContent(cases[caseKey]);
|
||||
}
|
||||
return React.cloneElement(cases[caseKey], { key: caseKey });
|
||||
} else if (cases.default) { // eslint-disable-line no-else-return
|
||||
if (typeof cases.default === 'string') {
|
||||
return getContent(cases.default);
|
||||
}
|
||||
React.cloneElement(cases.default, { key: 'default' });
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<TransitionReplace
|
||||
className={className}
|
||||
onChildExit={onChildExit}
|
||||
>
|
||||
{getContent(expression)}
|
||||
</TransitionReplace>
|
||||
);
|
||||
}
|
||||
|
||||
SwitchContent.propTypes = {
|
||||
expression: PropTypes.string,
|
||||
cases: PropTypes.objectOf(PropTypes.node).isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
SwitchContent.defaultProps = {
|
||||
expression: null,
|
||||
className: null,
|
||||
};
|
||||
|
||||
export default SwitchContent;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
@@ -19,23 +19,33 @@ const ThirdPartyAuthAlert = (props) => {
|
||||
message = intl.formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
|
||||
}
|
||||
|
||||
if (!currentProvider) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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' : 'alert-warning mt-n2'}>
|
||||
{referrer === REGISTER_PAGE ? (
|
||||
<Alert.Heading>{intl.formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
) : null}
|
||||
<p>{ message }</p>
|
||||
</Alert>
|
||||
{referrer === REGISTER_PAGE ? (
|
||||
<Alert.Heading>{intl.formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
|
||||
<h4 className="mt-4 mb-4">{intl.formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
||||
) : null}
|
||||
<p>{ message }</p>
|
||||
</Alert>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ThirdPartyAuthAlert.defaultProps = {
|
||||
currentProvider: '',
|
||||
referrer: LOGIN_PAGE,
|
||||
};
|
||||
|
||||
ThirdPartyAuthAlert.propTypes = {
|
||||
currentProvider: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
currentProvider: PropTypes.string,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
referrer: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@@ -2,10 +2,17 @@ import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
|
||||
import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
|
||||
|
||||
export const defaultState = {
|
||||
extendedProfile: [],
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
},
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
@@ -15,15 +22,15 @@ const reducer = (state = defaultState, action) => {
|
||||
...state,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
};
|
||||
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
|
||||
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
extendedProfile: action.payload.fieldDescriptions.extendedProfile,
|
||||
fieldDescriptions: action.payload.fieldDescriptions.fields,
|
||||
optionalFields: action.payload.optionalFields,
|
||||
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
|
||||
thirdPartyAuthApiStatus: COMPLETE_STATE,
|
||||
};
|
||||
}
|
||||
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
|
||||
@@ -14,12 +14,15 @@ export const fieldDescriptionSelector = createSelector(
|
||||
commonComponents => commonComponents.fieldDescriptions,
|
||||
);
|
||||
|
||||
export const extendedProfileSelector = createSelector(
|
||||
commonComponentsSelector,
|
||||
commonComponents => commonComponents.extendedProfile,
|
||||
);
|
||||
|
||||
export const optionalFieldsSelector = createSelector(
|
||||
commonComponentsSelector,
|
||||
commonComponents => commonComponents.optionalFields,
|
||||
);
|
||||
|
||||
export const tpaProvidersSelector = createSelector(
|
||||
commonComponentsSelector,
|
||||
commonComponents => ({
|
||||
providers: commonComponents.thirdPartyAuthContext.providers,
|
||||
secondaryProviders: commonComponents.thirdPartyAuthContext.secondaryProviders,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -20,10 +20,8 @@ export async function getThirdPartyAuthContext(urlParams) {
|
||||
return {
|
||||
fieldDescriptions: data.registration_fields || {},
|
||||
optionalFields: data.optional_fields || {},
|
||||
// For backward compatibility with the API, once https://github.com/openedx/edx-platform/pull/30198 is merged
|
||||
// and deployed update it to use data.context_data
|
||||
thirdPartyAuthContext: camelCaseObject(
|
||||
convertKeyNames(data.context_data || data, { fullname: 'name' }),
|
||||
convertKeyNames(data.context_data, { fullname: 'name' }),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
46
src/common-components/data/tests/reducer.test.js
Normal file
46
src/common-components/data/tests/reducer.test.js
Normal file
@@ -0,0 +1,46 @@
|
||||
import { THIRD_PARTY_AUTH_CONTEXT } from '../actions';
|
||||
import reducer from '../reducers';
|
||||
|
||||
describe('common components reducer', () => {
|
||||
it('test mfe context response', () => {
|
||||
const state = {
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
},
|
||||
};
|
||||
const fieldDescriptions = {
|
||||
fields: [],
|
||||
};
|
||||
const optionalFields = {
|
||||
fields: [],
|
||||
extended_profile: {},
|
||||
};
|
||||
const thirdPartyAuthContext = { ...state.thirdPartyAuthContext };
|
||||
const action = {
|
||||
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
|
||||
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
|
||||
};
|
||||
|
||||
expect(
|
||||
reducer(state, action),
|
||||
).toEqual(
|
||||
{
|
||||
...state,
|
||||
fieldDescriptions: [],
|
||||
optionalFields: {
|
||||
fields: [],
|
||||
extended_profile: {},
|
||||
},
|
||||
thirdPartyAuthApiStatus: 'complete',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,29 +1,13 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
// institution login strings
|
||||
'institution.login.page.sub.heading': {
|
||||
id: 'institution.login.page.sub.heading',
|
||||
defaultMessage: 'Choose your institution from the list below',
|
||||
description: 'Heading of the institutions list',
|
||||
},
|
||||
// Confirmation Alert Message
|
||||
'forgot.password.confirmation.title': {
|
||||
id: 'forgot.password.confirmation.title',
|
||||
defaultMessage: 'Check your email',
|
||||
description: 'Forgot password confirmation message title',
|
||||
},
|
||||
'forgot.password.confirmation.support.link': {
|
||||
id: 'forgot.password.confirmation.support.link',
|
||||
defaultMessage: 'contact technical support',
|
||||
description: 'Technical support link text',
|
||||
},
|
||||
'forgot.password.confirmation.info': {
|
||||
id: 'forgot.password.confirmation.info',
|
||||
defaultMessage: 'If you do not receive a password reset message after 1 minute, verify that you entered the correct '
|
||||
+ 'email address, or check your spam folder.',
|
||||
description: 'Part of message that appears after user requests password change',
|
||||
},
|
||||
// Logistration strinsg
|
||||
// logistration strings
|
||||
'logistration.sign.in': {
|
||||
id: 'logistration.sign.in',
|
||||
defaultMessage: 'Sign in',
|
||||
@@ -34,27 +18,12 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Register',
|
||||
description: 'Text that appears on the tab to switch between login and register',
|
||||
},
|
||||
'internal.server.error.message': {
|
||||
id: 'internal.server.error.message',
|
||||
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
|
||||
description: 'Error message that appears when server responds with 500 error code',
|
||||
},
|
||||
'server.ratelimit.error.message': {
|
||||
id: 'server.ratelimit.error.message',
|
||||
defaultMessage: 'An error has occurred because of too many requests. Please try again after some time.',
|
||||
description: 'Error message that appears when server responds with 429 error code',
|
||||
},
|
||||
// enterprise sso strings
|
||||
'enterprisetpa.title.heading': {
|
||||
id: 'enterprisetpa.title.heading',
|
||||
defaultMessage: 'Would you like to sign in using your {providerName} credentials?',
|
||||
description: 'Header text used in enterprise third party authentication',
|
||||
},
|
||||
'enterprisetpa.sso.button.title': {
|
||||
id: 'enterprisetpa.sso.button.title',
|
||||
defaultMessage: 'Sign in using {providerName}',
|
||||
description: 'Text for third party auth provider buttons',
|
||||
},
|
||||
'enterprisetpa.login.button.text': {
|
||||
id: 'enterprisetpa.login.button.text',
|
||||
defaultMessage: 'Show me other ways to sign in or register',
|
||||
@@ -123,6 +92,11 @@ const messages = defineMessages({
|
||||
description: 'Message that appears on register page if user has successfully authenticated with TPA '
|
||||
+ 'but no associated platform account exists',
|
||||
},
|
||||
'registration.using.tpa.form.heading': {
|
||||
id: 'registration.using.tpa.form.heading',
|
||||
defaultMessage: 'Finish creating your account',
|
||||
description: 'Heading that appears above form when user is trying to create account using social auth',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { MemoryRouter } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { COMPLETE_STATE, LOGIN_PAGE } from '../../data/constants';
|
||||
import { backupRegistrationForm } from '../../register/data/actions';
|
||||
import { RenderInstitutionButton } from '../InstitutionLogistration';
|
||||
import Logistration from '../Logistration';
|
||||
|
||||
@@ -39,9 +40,7 @@ describe('Logistration', () => {
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'edX' }));
|
||||
});
|
||||
it('should render registration page', () => {
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'test-user' }));
|
||||
configure({
|
||||
loggingService: { logError: jest.fn() },
|
||||
config: {
|
||||
@@ -50,14 +49,19 @@ describe('Logistration', () => {
|
||||
},
|
||||
messages: { 'es-419': {}, de: {}, 'en-us': {} },
|
||||
});
|
||||
});
|
||||
|
||||
it('should render registration page', () => {
|
||||
store = mockStore({
|
||||
register: {
|
||||
registrationResult: { success: false, redirectUrl: '' },
|
||||
registrationError: {},
|
||||
},
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
const logistration = mount(reduxWrapper(<IntlLogistration />));
|
||||
@@ -71,7 +75,10 @@ describe('Logistration', () => {
|
||||
loginResult: { success: false, redirectUrl: '' },
|
||||
},
|
||||
commonComponents: {
|
||||
thirdPartyAuthApiStatus: null,
|
||||
thirdPartyAuthContext: {
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -178,4 +185,27 @@ describe('Logistration', () => {
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fire action to backup registration form on 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(backupRegistrationForm());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,26 +21,33 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
|
||||
`;
|
||||
|
||||
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
|
||||
<div
|
||||
className="fade alert-content alert-success mt-n2 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
>
|
||||
Array [
|
||||
<div
|
||||
className="pgn__alert-message-wrapper"
|
||||
className="fade alert-content alert-success mt-n2 alert show"
|
||||
id="tpa-alert"
|
||||
role="alert"
|
||||
>
|
||||
<div
|
||||
className="alert-message-content"
|
||||
className="pgn__alert-message-wrapper"
|
||||
>
|
||||
<div
|
||||
className="alert-heading h4"
|
||||
className="alert-message-content"
|
||||
>
|
||||
Almost done!
|
||||
<div
|
||||
className="alert-heading h4"
|
||||
>
|
||||
Almost done!
|
||||
</div>
|
||||
<p>
|
||||
You've successfully signed into Google! We just need a little more information before you start learning with Your Platform Name Here.
|
||||
</p>
|
||||
</div>
|
||||
<p>
|
||||
You've successfully signed into Google! We just need a little more information before you start learning with Your Platform Name Here.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
<h4
|
||||
className="mt-4 mb-4"
|
||||
>
|
||||
Finish creating your account
|
||||
</h4>,
|
||||
]
|
||||
`;
|
||||
|
||||
27
src/config/index.js
Normal file
27
src/config/index.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const configuration = {
|
||||
// Cookies related configs
|
||||
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN,
|
||||
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null,
|
||||
USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME || null,
|
||||
// Features
|
||||
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
|
||||
ENABLE_COOKIE_POLICY_BANNER: process.env.ENABLE_COOKIE_POLICY_BANNER || false,
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
|
||||
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
|
||||
ENABLE_PERSONALIZED_RECOMMENDATIONS: process.env.ENABLE_PERSONALIZED_RECOMMENDATIONS || false,
|
||||
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
|
||||
SHOW_CONFIGURABLE_EDX_FIELDS: process.env.SHOW_CONFIGURABLE_EDX_FIELDS || false,
|
||||
// Links
|
||||
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: process.env.AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK || null,
|
||||
LOGIN_ISSUE_SUPPORT_LINK: process.env.LOGIN_ISSUE_SUPPORT_LINK || null,
|
||||
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || null,
|
||||
PRIVACY_POLICY: process.env.PRIVACY_POLICY || null,
|
||||
TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null,
|
||||
TOS_LINK: process.env.TOS_LINK || null,
|
||||
// Miscellaneous
|
||||
GENERAL_RECOMMENDATIONS: process.env.GENERAL_RECOMMENDATIONS || '[]',
|
||||
INFO_EMAIL: process.env.INFO_EMAIL || '',
|
||||
};
|
||||
|
||||
export default configuration;
|
||||
@@ -2,8 +2,9 @@
|
||||
export const LOGIN_PAGE = '/login';
|
||||
export const REGISTER_PAGE = '/register';
|
||||
export const RESET_PAGE = '/reset';
|
||||
export const WELCOME_PAGE = '/welcome';
|
||||
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
|
||||
export const DEFAULT_REDIRECT_URL = '/dashboard';
|
||||
export const RECOMMENDATIONS = '/recommendations';
|
||||
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
|
||||
export const PAGE_NOT_FOUND = '/notfound';
|
||||
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
|
||||
@@ -30,7 +31,7 @@ export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+
|
||||
+ '|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$';
|
||||
export const LETTER_REGEX = /[a-zA-Z]/;
|
||||
export const NUMBER_REGEX = /\d/;
|
||||
export const VALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
|
||||
export const INVALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/gi; // eslint-disable-line no-useless-escape
|
||||
|
||||
// Query string parameters that can be passed to LMS to manage
|
||||
// things like auto-enrollment upon login and registration.
|
||||
|
||||
@@ -12,6 +12,10 @@ import {
|
||||
reducer as loginReducer,
|
||||
storeName as loginStoreName,
|
||||
} from '../login';
|
||||
import {
|
||||
reducer as authnProgressiveProfilingReducers,
|
||||
storeName as authnProgressiveProfilingStoreName,
|
||||
} from '../progressive-profiling';
|
||||
import {
|
||||
reducer as registerReducer,
|
||||
storeName as registerStoreName,
|
||||
@@ -20,10 +24,6 @@ import {
|
||||
reducer as resetPasswordReducer,
|
||||
storeName as resetPasswordStoreName,
|
||||
} from '../reset-password';
|
||||
import {
|
||||
reducer as welcomePageReducers,
|
||||
storeName as welcomePageStoreName,
|
||||
} from '../welcome';
|
||||
|
||||
const createRootReducer = () => combineReducers({
|
||||
[loginStoreName]: loginReducer,
|
||||
@@ -31,6 +31,6 @@ const createRootReducer = () => combineReducers({
|
||||
[commonComponentsStoreName]: commonComponentsReducer,
|
||||
[forgotPasswordStoreName]: forgotPasswordReducer,
|
||||
[resetPasswordStoreName]: resetPasswordReducer,
|
||||
[welcomePageStoreName]: welcomePageReducers,
|
||||
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
|
||||
});
|
||||
export default createRootReducer;
|
||||
|
||||
@@ -3,9 +3,9 @@ import { all } from 'redux-saga/effects';
|
||||
import { saga as commonComponentsSaga } from '../common-components';
|
||||
import { saga as forgotPasswordSaga } from '../forgot-password';
|
||||
import { saga as loginSaga } from '../login';
|
||||
import { saga as authnProgressiveProfilingSaga } from '../progressive-profiling';
|
||||
import { saga as registrationSaga } from '../register';
|
||||
import { saga as resetPasswordSaga } from '../reset-password';
|
||||
import { saga as welcomePageSaga } from '../welcome';
|
||||
|
||||
export default function* rootSaga() {
|
||||
yield all([
|
||||
@@ -14,6 +14,6 @@ export default function* rootSaga() {
|
||||
commonComponentsSaga(),
|
||||
forgotPasswordSaga(),
|
||||
resetPasswordSaga(),
|
||||
welcomePageSaga(),
|
||||
authnProgressiveProfilingSaga(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Cookies from 'universal-cookie';
|
||||
|
||||
export function setCookie(cookieName, cookieValue, cookieExpiry) {
|
||||
const cookies = new Cookies();
|
||||
const options = { domain: getConfig().COOKIE_DOMAIN, path: '/' };
|
||||
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
|
||||
if (cookieExpiry) {
|
||||
options.expires = cookieExpiry;
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ export const updatePathWithQueryParams = (path) => {
|
||||
return `${path}${queryParams}`;
|
||||
};
|
||||
|
||||
export const getAllPossibleQueryParam = () => {
|
||||
const urlParams = QueryString.parse(window.location.search);
|
||||
export const getAllPossibleQueryParams = (locationURl = null) => {
|
||||
const urlParams = locationURl ? QueryString.parseUrl(locationURl).query : QueryString.parse(window.location.search);
|
||||
const params = {};
|
||||
Object.entries(urlParams).forEach(([key, value]) => {
|
||||
if (AUTH_PARAMS.indexOf(key) > -1) {
|
||||
|
||||
@@ -2,7 +2,7 @@ export {
|
||||
getTpaProvider,
|
||||
getTpaHint,
|
||||
updatePathWithQueryParams,
|
||||
getAllPossibleQueryParam,
|
||||
getAllPossibleQueryParams,
|
||||
getActivationStatus,
|
||||
windowScrollTo,
|
||||
} from './dataUtils';
|
||||
|
||||
@@ -7,7 +7,7 @@ import PropTypes from 'prop-types';
|
||||
const FormFieldRenderer = (props) => {
|
||||
let formField = null;
|
||||
const {
|
||||
errorMessage, fieldData, onChangeHandler, isRequired, value,
|
||||
className, errorMessage, fieldData, onChangeHandler, isRequired, value,
|
||||
} = props;
|
||||
|
||||
const handleFocus = (e) => {
|
||||
@@ -26,6 +26,7 @@ const FormFieldRenderer = (props) => {
|
||||
formField = (
|
||||
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
||||
<Form.Control
|
||||
className={className}
|
||||
as="select"
|
||||
name={fieldData.name}
|
||||
value={value}
|
||||
@@ -54,6 +55,7 @@ const FormFieldRenderer = (props) => {
|
||||
formField = (
|
||||
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
||||
<Form.Control
|
||||
className={className}
|
||||
as="textarea"
|
||||
name={fieldData.name}
|
||||
value={value}
|
||||
@@ -76,6 +78,7 @@ const FormFieldRenderer = (props) => {
|
||||
formField = (
|
||||
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
|
||||
<Form.Control
|
||||
className={className}
|
||||
name={fieldData.name}
|
||||
value={value}
|
||||
aria-invalid={isRequired && Boolean(errorMessage)}
|
||||
@@ -97,6 +100,7 @@ const FormFieldRenderer = (props) => {
|
||||
formField = (
|
||||
<Form.Group isInvalid={!!(isRequired && errorMessage)}>
|
||||
<Form.Checkbox
|
||||
className={className}
|
||||
id={fieldData.name}
|
||||
checked={!!value}
|
||||
name={fieldData.name}
|
||||
@@ -124,6 +128,7 @@ const FormFieldRenderer = (props) => {
|
||||
return formField;
|
||||
};
|
||||
FormFieldRenderer.defaultProps = {
|
||||
className: '',
|
||||
value: '',
|
||||
handleBlur: null,
|
||||
handleFocus: null,
|
||||
@@ -132,6 +137,7 @@ FormFieldRenderer.defaultProps = {
|
||||
};
|
||||
|
||||
FormFieldRenderer.propTypes = {
|
||||
className: PropTypes.string,
|
||||
fieldData: PropTypes.shape({
|
||||
type: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
@@ -142,7 +148,10 @@ FormFieldRenderer.propTypes = {
|
||||
handleFocus: PropTypes.func,
|
||||
errorMessage: PropTypes.string,
|
||||
isRequired: PropTypes.bool,
|
||||
value: PropTypes.string,
|
||||
value: PropTypes.oneOfType([
|
||||
PropTypes.string,
|
||||
PropTypes.bool,
|
||||
]),
|
||||
};
|
||||
|
||||
export default FormFieldRenderer;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import FieldRenderer from '../FieldRenderer';
|
||||
@@ -82,7 +83,7 @@ describe('FieldRendererTests', () => {
|
||||
it('should render checkbox field', () => {
|
||||
const fieldData = {
|
||||
type: 'checkbox',
|
||||
label: 'I agree that edX may send me marketing messages.',
|
||||
label: `I agree that ${getConfig().SITE_NAME} may send me marketing messages.`,
|
||||
name: 'marketing-emails-opt-in-field',
|
||||
};
|
||||
|
||||
@@ -91,7 +92,7 @@ describe('FieldRendererTests', () => {
|
||||
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
|
||||
|
||||
expect(field.prop('type')).toEqual('checkbox');
|
||||
expect(fieldRenderer.find('label').text()).toEqual('I agree that edX may send me marketing messages.');
|
||||
expect(fieldRenderer.find('label').text()).toEqual(fieldData.label);
|
||||
expect(value).toEqual(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { CheckCircle, Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -93,7 +93,7 @@ ForgotPasswordAlert.defaultProps = {
|
||||
ForgotPasswordAlert.propTypes = {
|
||||
status: PropTypes.string.isRequired,
|
||||
email: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
emailError: PropTypes.string,
|
||||
};
|
||||
|
||||
|
||||
@@ -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, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form,
|
||||
Hyperlink,
|
||||
@@ -141,16 +141,18 @@ const ForgotPasswordPage = (props) => {
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
<Hyperlink
|
||||
id="forgot-password"
|
||||
name="forgot-password"
|
||||
className="ml-4 font-weight-500 text-body"
|
||||
destination={getConfig().LOGIN_ISSUE_SUPPORT_LINK}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{intl.formatMessage(messages['need.help.sign.in.text'])}
|
||||
</Hyperlink>
|
||||
{(getConfig().LOGIN_ISSUE_SUPPORT_LINK) && (
|
||||
<Hyperlink
|
||||
id="forgot-password"
|
||||
name="forgot-password"
|
||||
className="ml-4 font-weight-500 text-body"
|
||||
destination={getConfig().LOGIN_ISSUE_SUPPORT_LINK}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
{intl.formatMessage(messages['need.help.sign.in.text'])}
|
||||
</Hyperlink>
|
||||
)}
|
||||
<p className="mt-5.5 small text-gray-700">
|
||||
{intl.formatMessage(messages['additional.help.text'], { platformName })}
|
||||
<span>
|
||||
@@ -168,7 +170,7 @@ ForgotPasswordPage.propTypes = {
|
||||
email: PropTypes.string,
|
||||
emailValidationError: PropTypes.string,
|
||||
forgotPassword: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
setForgotPasswordFormData: PropTypes.func.isRequired,
|
||||
status: PropTypes.string,
|
||||
submitState: PropTypes.string,
|
||||
|
||||
@@ -51,11 +51,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Enter your email',
|
||||
description: 'Error message that appears when user tries to submit empty email field',
|
||||
},
|
||||
'forgot.password.invalid.email.message': {
|
||||
id: 'forgot.password.invalid.email.message',
|
||||
defaultMessage: "The email address you've provided isn't formatted correctly.",
|
||||
description: 'message for invalid email',
|
||||
},
|
||||
'forgot.password.email.help.text': {
|
||||
id: 'forgot.password.email.help.text',
|
||||
defaultMessage: 'The email address you used to register with {platformName}',
|
||||
@@ -129,10 +124,5 @@ const messages = defineMessages({
|
||||
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
|
||||
description: 'Error message that appears when server responds with 500 error code',
|
||||
},
|
||||
'rate.limit.error': {
|
||||
id: 'rate.limit.error',
|
||||
defaultMessage: 'An error has occurred because of too many requests. Please try again after some time.',
|
||||
description: 'Error message that appears when server responds with 429 error code',
|
||||
},
|
||||
});
|
||||
export default messages;
|
||||
|
||||
@@ -51,7 +51,7 @@ describe('ForgotPasswordPage', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore(initialState);
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'edX' }));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'test-user' }));
|
||||
configure({
|
||||
loggingService: { logError: jest.fn() },
|
||||
config: {
|
||||
@@ -66,7 +66,15 @@ describe('ForgotPasswordPage', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('not should display need other help signing in button', () => {
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
expect(wrapper.find('#forgot-password').exists()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should display need other help signing in button', () => {
|
||||
mergeConfig({
|
||||
LOGIN_ISSUE_SUPPORT_LINK: '/support',
|
||||
});
|
||||
const wrapper = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
|
||||
expect(wrapper.find('#forgot-password').first().text()).toEqual('Need help signing in?');
|
||||
});
|
||||
|
||||
@@ -5,15 +5,9 @@
|
||||
"complete.your.profile.2": "ملفك الشخصي",
|
||||
"welcome.to.platform": "أهلا بك {username} في {siteName}",
|
||||
"institution.login.page.sub.heading": "اختر مؤسستك من القائمة أدناه",
|
||||
"forgot.password.confirmation.title": "تفقّد بريدك الإلكتروني",
|
||||
"forgot.password.confirmation.support.link": "اتصل بالدعم الفني",
|
||||
"forgot.password.confirmation.info": "إن لم تستلم رسالة إعادة تعيين كلمة المرور بعد دقيقة واحدة، فتحقق من أنك أدخلت عنوان البريد الإلكتروني الصحيح، أو تفقّد مجلد الرسائل غير المرغوب فيها في بريدك الإلكتروني.",
|
||||
"logistration.sign.in": "تسجيل الدخول",
|
||||
"logistration.register": "التسجيل",
|
||||
"internal.server.error.message": "حدث خطأ ما. جرّب تحديث الصفحة أو تحقق من اتصالك بالانترنت.",
|
||||
"server.ratelimit.error.message": "حدث خطأ بسبب كثرة الطلبات. رجاءً حاول مرة أخرى بعد مضي بعض الوقت.",
|
||||
"enterprisetpa.title.heading": "هل ترغب في تسجيل الدخول باستخدام بيانات {providerName} الخاصة بك؟",
|
||||
"enterprisetpa.sso.button.title": "تسجيل الدخول باستخدام {providerName}",
|
||||
"enterprisetpa.login.button.text": "أرِني وسائل أخرى لتسجيل الدخول أو للتسجيل",
|
||||
"sso.sign.in.with": "تسجيل الدخول باستخدام {providerName}",
|
||||
"sso.create.account.using": "إنشاء حساب باستخدام {providerName}",
|
||||
@@ -26,6 +20,7 @@
|
||||
"tpa.alert.heading": "انتهينا تقريبا!",
|
||||
"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": "إتمام إنشاء حسابك",
|
||||
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجددًا.",
|
||||
"forgot.password.confirmation.message": "لقد أرسلنا بريدًا إلكترونيًا إلى {email} به إرشادات لإعادة ضبط كلمة المرور الخاصة بك. إن لم تستلم رسالة إعادة ضبط كلمة المرور بعد دقيقة واحدة، فتحقق من إدخال عنوان البريد الإلكتروني الصحيح، أو تفقد مجلد الرسائل غير المرغوب فيها. إن احتجت مزيدًا من المساعدة، {supportLink}.",
|
||||
"forgot.password.page.title": "نسيت كلمة المرور | {siteName}",
|
||||
@@ -38,7 +33,6 @@
|
||||
"forgot.password.error.message.title": "حدث خطأ ما.",
|
||||
"forgot.password.request.in.progress.message": "طلبك السابق قيد التنفيذ، يرجى المحاولة مرة أخرى بعد لحظات قليلة.",
|
||||
"forgot.password.empty.email.field.error": "أدخل بريدك الإلكتروني",
|
||||
"forgot.password.invalid.email.message": "صيغة عنوان البريد الإلكتروني الذي أدخلته غير صحيحة",
|
||||
"forgot.password.email.help.text": "عنوان البريد الإلكتروني الذي استخدمته للتسجيل في {platformName}",
|
||||
"confirmation.message.title": "تفقّد بريدك الإلكتروني",
|
||||
"confirmation.support.link": "اتصل بالدعم الفني",
|
||||
@@ -53,7 +47,6 @@
|
||||
"token.validation.internal.sever.error.heading": "فشل في التحقق من صحة الشارة",
|
||||
"token.validation.internal.sever.error": "حدث خطأ ما. جرب تحديث الصفحة أو تحقق من اتصالك بالإنترنت.",
|
||||
"internal.server.error": "حدث خطأ ما. جرب تحديث الصفحة أو تحقق من اتصالك بالإنترنت.",
|
||||
"rate.limit.error": "حدث خطأ بسبب كثرة الطلبات. رجاءً حاول مرة أخرى بعد مضي بعض الوقت.",
|
||||
"account.activation.error.message": "شي ما لم يسر على ما يرام، يرجى {supportLink} لحل هذه المشكلة.",
|
||||
"login.inactive.user.error": "أنت بحاجة لتفعيل حسابك من أجل تسجيل الدخول{lineBreak}\n{lineBreak}لقد أرسلنا للتو رابطًا للتفعيل إلى {email}. إن لم تتلقّ بريدًا إلكترونيا، تفقّد مجلدات الرسائل غير المرغوب فيها أو {supportLink}.",
|
||||
"allowed.domain.login.error": "كونك مستخدمًا على {allowedDomain}، فإن عليك تسجيل الدخول باستخدام {tpaLink} الخاص بـ {allowedDomain} .",
|
||||
@@ -65,45 +58,32 @@
|
||||
"login.user.identity.label": "اسم المستخدم أو البريد الإلكتروني",
|
||||
"login.password.label": "كلمة المرور",
|
||||
"sign.in.button": "تسجيل الدخول",
|
||||
"sign.in.btn.pending.state": "التحميل جارٍ",
|
||||
"need.help.signing.in.collapsible.menu": "هل تحتاج مساعدة في تسجيل الدخول؟",
|
||||
"forgot.password.link": "نسيت كلمة مروري",
|
||||
"forgot.password": "نسيت كلمة المرور",
|
||||
"other.sign.in.issues": "مشاكل أخرى عند تسجيل الدخول",
|
||||
"need.other.help.signing.in.collapsible.menu": "هل تحتاج مساعدة أخرى في تسجيل الدخول؟",
|
||||
"institution.login.button": "بيانات المؤسسة / الجامعة",
|
||||
"institution.login.page.title": "تسجيل الدخول باستخدام بيانات المؤسسة / الجامعة",
|
||||
"institution.login.page.back.button": "العودة إلى تسجيل الدخول",
|
||||
"create.an.account": " أنشئ حساباً جديداً.",
|
||||
"login.other.options.heading": "أو قم بتسجيل الدخول باستخدام:",
|
||||
"non.compliant.password.title": "لقد غيرنا متطلبات أمان كلمة المرور مؤخرًا",
|
||||
"non.compliant.password.message": "كلمة مرورك الحالية لا تستسجيب لمتطلبات الأمان الجديدة. لقد أرسلنا للتو رسالة لإعادة ضبط كلمة المرور إلى عنوان البريد الإلكتروني المرتبط بهذا الحساب. شكرًا لك على مساعدتنا في الحفاظ على سلامة بياناتك.",
|
||||
"account.locked.out.message.1": "لحماية حسابك، تم إقفاله مؤقتًا. حاول مرة أخرى بعد 30 دقيقة.",
|
||||
"first.time.here": "أول مرة لك هنا؟",
|
||||
"email.help.message": "عنوان البريد الإلكتروني الذي استخدمته للتسجيل في edX.",
|
||||
"enterprise.login.btn.text": "بيانات الشركة أو المدرسة",
|
||||
"email.format.validation.message": "صيغة عنوان البريد الإلكتروني الذي أدخلته غير صحيحة",
|
||||
"username.or.email.format.validation.less.chars.message": "يجب أن يحتوي اسم المستخدم أو البريد الإلكتروني على 3 أحرف على الأقل.",
|
||||
"email.validation.message": "أدخل اسم المستخدم أو البريد الإلكتروني الخاص بك",
|
||||
"password.validation.message": "لم يتم استيفاء معايير كلمة المرور",
|
||||
"register.link": " أنشئ حساباً",
|
||||
"sign.in.heading": "تسجيل الدخول",
|
||||
"account.activation.success.message.title": "نجح الأمر! لقد قمت بتفعيل حسابك.",
|
||||
"account.activation.success.message": "ستصلك الآن تحديثات وتنبيهات عبر البريد الإلكتروني منا تتعلق بالمساقات التي قمت بالتسجيل فيها. قم بتسجيل الدخول للمتابعة.",
|
||||
"account.activation.info.message": "هذا الحساب مفعَّل من قبل.",
|
||||
"account.activation.error.message.title": "لا يمكن تفعيل حسابك",
|
||||
"account.activation.support.link": "الاتصال بالدعم",
|
||||
"tpa.account.link": "حساب {provider}",
|
||||
"account.confirmation.success.message.title": "نجحت العملية! لقد أكدت بريدك الإلكتروني.",
|
||||
"account.confirmation.success.message": "سجل دخولك للمتابعة.",
|
||||
"account.confirmation.info.message": "هذا البريد الإلكتروني مؤكد من قبل.",
|
||||
"account.confirmation.error.message.title": "لا يمكن تأكيد بريدك الإلكتروني",
|
||||
"tpa.account.link": "حساب {provider}",
|
||||
"internal.server.error.message": "حدث خطأ ما. جرّب تحديث الصفحة أو تحقق من اتصالك بالانترنت.",
|
||||
"login.rate.limit.reached.message": "كثرت محاولات تسجيل الدخول الفاشلة. رجاءً أعد المحاولة لاحقًا.",
|
||||
"login.failure.header.title": "لم نتمكّن من تسجيل دخولك.",
|
||||
"contact.support.link": "اتصل بدعم {platformName}",
|
||||
"login.incorrect.credentials.error": "اسم المستخدم أو البريد الإلكتروني أو كلمة المرور التي أدخلتها غير صحيحة. حاول مرة اخرى.",
|
||||
"login.failed.attempt.error": "لديك {remainingAttempts, plural,\none {محاولة واحدة}\ntwo {محاولتان}\nfew {# محاولات}\nmany {# محاولة}\nother {# محاولة}\n} أخرى لتسجيل الدخول قبل أن يتم إقفال حسابك مؤقتًا.",
|
||||
"login.locked.out.error.message": "لحماية حسابك، تم إقفاله مؤقتًا. حاول مرة أخرى بعد {lockedOutPeriod, plural,\n one {دقيقة واحدة}\n two {دقيقتين}\n few {# دقائق}\n many {# دقيقة}\n other {# دقيقة}\n}.",
|
||||
"login.form.invalid.error.message": "رجاءً املأ الحقول أدناه.",
|
||||
"login.incorrect.credentials.error.reset.link.text": "إعادة ضبط كلمه المرور",
|
||||
"login.incorrect.credentials.error.before.account.blocked.text": "انقر هنا لإعادة ضبطها.",
|
||||
@@ -113,8 +93,20 @@
|
||||
"password.security.block.body": "اكتشف نظامنا أن كلمة مرورك صعيفة. غيّر كلمة مرورك حتى يظل حسابك آمنًا.",
|
||||
"password.security.close.button": "إغلاق",
|
||||
"password.security.redirect.to.reset.password.button": "إعادة ضبط كلمة المرور",
|
||||
"register.page.terms.of.service.and.honor.code": "بإنشاءك حسابًا، فإنك توافق على {tosAndHonorCode} و تقر بأن {platformName} و كل عضو يعالج بياناتك الشخصية وفقًا لـ{privacyPolicy}.",
|
||||
"register.page.honor.code": "أوافق على {tosAndHonorCode} الخاصة بـ{platformName}",
|
||||
"progressive.profiling.page.title": "الحقول الاختيارية | {siteName}",
|
||||
"progressive.profiling.page.heading": "بعض الأسئلة الموجهة لك ستساعدنا كي نزداد ذكاءً.",
|
||||
"optional.fields.information.link": "معرفة المزيد عن كيفية استخدامنا لهذه المعلومات.",
|
||||
"optional.fields.submit.button": "إرسال",
|
||||
"optional.fields.skip.button": "التخطي مؤقتا",
|
||||
"optional.fields.next.button": "Next",
|
||||
"continue.to.platform": "المواصلة إلى {platformName}",
|
||||
"modal.title": "شكرا لإعلامنا.",
|
||||
"modal.description": "إن غيرت رأيك، قيمكنك إكمال ملفك الشخصي ضمن الإعدادات في أي وقت.",
|
||||
"welcome.page.error.heading": "لم نتمكن من تحديث ملفك الشخصي",
|
||||
"welcome.page.error.message": "حدث خطأ ما. يمكنك إكمال ملفك الشخصي ضمن الإعدادات في أي وقت.",
|
||||
"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}",
|
||||
"registration.fullname.label": "الاسم الكامل",
|
||||
"registration.email.label": "البريد الإلكتروني",
|
||||
@@ -127,22 +119,19 @@
|
||||
"help.text.username.2": "لا يمكن تغيير هذا لاحقًا.",
|
||||
"help.text.email": "لتفعيل الحساب و التحديثات الهامة",
|
||||
"create.account.for.free.button": "إنشاء حساب مجانا",
|
||||
"create.an.account.btn.pending.state": "التحميل جارٍ",
|
||||
"registration.other.options.heading": "أو سجل باستخدام:",
|
||||
"register.institution.login.button": "بيانات المؤسسة / الجامعة",
|
||||
"register.institution.login.page.title": "التسجيل باستخدام بيانات المؤسسة / الجامعة",
|
||||
"empty.name.field.error": "أدخل اسمك الكامل",
|
||||
"empty.email.field.error": "أدخل بريدك الإلكتروني",
|
||||
"email.do.not.match": "عناوين البريد الإلكتروني غير متطابقة.",
|
||||
"empty.username.field.error": "يجب أن يتكون اسم المستخدم من 2 إلى 30 حرفًا",
|
||||
"empty.password.field.error": "لم يتم استيفاء معايير كلمة المرور",
|
||||
"empty.country.field.error": "حدد بلدك أو منطقة إقامتك",
|
||||
"email.do.not.match": "عناوين البريد الإلكتروني غير متطابقة.",
|
||||
"email.invalid.format.error": "أدخل بريدا إلكترونيا صحيحا",
|
||||
"email.ratelimit.less.chars.validation.message": "يجب أن يتكون البريد الإلكتروني من 3 أحرف على الأقل.",
|
||||
"username.validation.message": "يجب أن يتكون اسم المستخدم من 2 إلى 30 حرفًا",
|
||||
"name.validation.message": "أدخل اسمًا صحيحا",
|
||||
"username.format.validation.message": "يمكن أن تحتوي أسماء المستخدمين فقط على أحرف (A-Z، a-z)، و أرقام (0-9)، و أسطر سفلية (_)، و واصلات (-). لا يمكن أن تحتوي أسماء المستخدمين على مسافات",
|
||||
"support.education.research": "دعم الأبحاث التربوية من خلال توفير معلومات إضافية. (اختياري)",
|
||||
"registration.request.failure.header": "لم نتمكّن من إنشاء حسابك.",
|
||||
"registration.empty.form.submission.error": "رجاءً تحقّق من أجوبتك و حاول مجددا.",
|
||||
"registration.request.server.error": "حدث خطأ ما. جرب تحديث الصفحة أو تحقق من اتصالك بالإنترنت.",
|
||||
@@ -152,26 +141,11 @@
|
||||
"privacy.policy": "سياسة الخصوصية",
|
||||
"honor.code": "ميثاق الشرف الأكاديمي",
|
||||
"terms.of.service": "شروط الخدمة",
|
||||
"registration.year.of.birth.label": "سنة الميلاد (اختياري)",
|
||||
"registration.field.gender.options.label": "الجنس (اختياري)",
|
||||
"registration.goals.label": "أخبرنا عن سبب اهتمامك بـ edX (اختياري)",
|
||||
"registration.field.gender.options.f": "أنثى",
|
||||
"registration.field.gender.options.m": "ذكر",
|
||||
"registration.field.gender.options.o": "آخر / أفضل عدم التصريح",
|
||||
"registration.field.education.levels.label": "أعلى مستوى تعليمي مكتمل (اختياري)",
|
||||
"registration.field.education.levels.p": "دكتوراه",
|
||||
"registration.field.education.levels.m": "ماجستير / ماستر أو شهادة مهنيّة",
|
||||
"registration.field.education.levels.b": "بكالوريوس / ليسانس",
|
||||
"registration.field.education.levels.a": "درجة الزمالة / دبلوم الدراسات الجامعية",
|
||||
"registration.field.education.levels.hs": "الثانوية العامة / البكالوريا",
|
||||
"registration.field.education.levels.jhs": "المدرسة الإعدادية / المتوسطة",
|
||||
"registration.field.education.levels.el": "المدرسة الابتدائية / الأساسية",
|
||||
"registration.field.education.levels.none": "دون تعليم رسمي",
|
||||
"registration.field.education.levels.other": "نوع آخر من التعليم",
|
||||
"registration.username.suggestion.label": "مقترح:",
|
||||
"registration.using.tpa.form.heading": "إتمام إنشاء حسابك",
|
||||
"did.you.mean.alert.text": "هل تقصد",
|
||||
"register.page.terms.of.service": "أوافق على {termsOfService} الخاصة بـ{platformName} ",
|
||||
"register.page.terms.of.service.and.honor.code": "بإنشاءك حسابًا، فإنك توافق على {tosAndHonorCode} و تقر بأن {platformName} و كل عضو يعالج بياناتك الشخصية وفقًا لـ{privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "تسجيل الدخول",
|
||||
"reset.password.page.title": "إعادة ضبط كلمة المرور | {siteName}",
|
||||
"reset.password": "إعادة ضبط كلمة المرور",
|
||||
@@ -180,40 +154,10 @@
|
||||
"confirm.password.label": "تأكيد كلمة المرور",
|
||||
"passwords.do.not.match": "كلمتا المرور غير متطابقتين",
|
||||
"confirm.your.password": "تأكيد كلمة مرورك",
|
||||
"forgot.password.confirmation.sign.in.link": "تسجيل الدخول",
|
||||
"reset.password.request.forgot.password.text": "نسيت كلمة المرور",
|
||||
"reset.password.request.invalid.token.header": "رابط إعادة ضبط كلمة المرور غير صالح",
|
||||
"reset.password.empty.new.password.field.error": "رجاءً أدخل كلمة مرورك الجديدة.",
|
||||
"reset.password.failure.heading": "لم نتمكن من إعادة ضبط كلمة مرورك.",
|
||||
"reset.password.form.submission.error": "رجاءً تحقق من أجوبتك وحاول مجددًا.",
|
||||
"reset.password.request.server.error": "فشلت إعادة ضبط كلمة المرور",
|
||||
"reset.password.token.validation.sever.error": "فشل التحقق من صحة الشارة",
|
||||
"reset.server.rate.limit.error": "طلبات أكثر مما ينبغي.",
|
||||
"reset.password.success.heading": "تمت إعادة ضبط كلمة المرور.",
|
||||
"reset.password.success": "تمت إعادة ضبط كلمة مرورك. سجل الدخول إلى حسابك.",
|
||||
"progressive.profiling.page.title": "الحقول الاختيارية | {siteName}",
|
||||
"progressive.profiling.page.heading": "بعض الأسئلة الموجهة لك ستساعدنا كي نزداد ذكاءً.",
|
||||
"gender.options.label": "الجنس (اختياري)",
|
||||
"gender.options.f": "أنثى",
|
||||
"gender.options.m": "ذكر",
|
||||
"gender.options.o": "آخر / أفضل عدم التصريح",
|
||||
"education.levels.label": "أعلى مستوى تعليمي مكتمل (اختياري)",
|
||||
"education.levels.p": "دكتوراه",
|
||||
"education.levels.m": "ماجستير / ماستر أو شهادة مهنيّة",
|
||||
"education.levels.b": "بكالوريوس / ليسانس",
|
||||
"education.levels.a": "درجة الزمالة / دبلوم الدراسات الجامعية",
|
||||
"education.levels.hs": "الثانوية العامة / البكالوريا",
|
||||
"education.levels.jhs": "المدرسة الإعدادية / المتوسطة",
|
||||
"education.levels.el": "المدرسة الابتدائية / الأساسية",
|
||||
"education.levels.none": "دون تعليم رسمي",
|
||||
"education.levels.other": "نوع آخر من التعليم",
|
||||
"year.of.birth.label": "سنة الميلاد (اختياري)",
|
||||
"optional.fields.information.link": "معرفة المزيد عن كيفية استخدامنا لهذه المعلومات.",
|
||||
"optional.fields.submit.button": "إرسال",
|
||||
"optional.fields.skip.button": "التخطي مؤقتا",
|
||||
"continue.to.platform": "المواصلة إلى {platformName}",
|
||||
"modal.title": "شكرا لإعلامنا.",
|
||||
"modal.description": "إن غيرت رأيك، قيمكنك إكمال ملفك الشخصي ضمن الإعدادات في أي وقت.",
|
||||
"welcome.page.error.heading": "لم نتمكن من تحديث ملفك الشخصي",
|
||||
"welcome.page.error.message": "حدث خطأ ما. يمكنك إكمال ملفك الشخصي ضمن الإعدادات في أي وقت."
|
||||
"rate.limit.error": "حدث خطأ بسبب كثرة الطلبات. رجاءً حاول مرة أخرى بعد مضي بعض الوقت."
|
||||
}
|
||||
@@ -1,219 +1,163 @@
|
||||
{
|
||||
"start.learning": "Start learning",
|
||||
"with.site.name": "with {siteName}",
|
||||
"complete.your.profile.1": "Complete",
|
||||
"complete.your.profile.2": "your profile",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
||||
"forgot.password.confirmation.title": "Check your email",
|
||||
"forgot.password.confirmation.support.link": "contact technical support",
|
||||
"forgot.password.confirmation.info": "If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.",
|
||||
"logistration.sign.in": "Sign in",
|
||||
"logistration.register": "Register",
|
||||
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"server.ratelimit.error.message": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
|
||||
"enterprisetpa.sso.button.title": "Sign in using {providerName}",
|
||||
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
|
||||
"sso.sign.in.with": "Sign in with {providerName}",
|
||||
"sso.create.account.using": "Create account using {providerName}",
|
||||
"show.password": "Show password",
|
||||
"hide.password": "Hide password",
|
||||
"one.letter": "1 letter",
|
||||
"one.number": "1 number",
|
||||
"eight.characters": "8 characters",
|
||||
"password.sr.only.helping.text": "Password must contain at least 8 characters, at least one letter, and at least one number",
|
||||
"tpa.alert.heading": "Almost done!",
|
||||
"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}.",
|
||||
"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}",
|
||||
"forgot.password.page.heading": "Reset password",
|
||||
"forgot.password.page.instructions": "Please enter your email address below and we will send you an email with instructions on how to reset your password.",
|
||||
"forgot.password.page.invalid.email.message": "Enter a valid email address",
|
||||
"forgot.password.page.email.field.label": "Email",
|
||||
"forgot.password.page.submit.button": "Submit",
|
||||
"forgot.password.error.alert.title.": "We were unable to contact you.",
|
||||
"forgot.password.error.message.title": "An error occurred.",
|
||||
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
|
||||
"forgot.password.empty.email.field.error": "Enter your email",
|
||||
"forgot.password.invalid.email.message": "The email address you've provided isn't formatted correctly.",
|
||||
"forgot.password.email.help.text": "The email address you used to register with {platformName}",
|
||||
"confirmation.message.title": "Check your email",
|
||||
"confirmation.support.link": "contact technical support",
|
||||
"need.help.sign.in.text": "Need help signing in?",
|
||||
"additional.help.text": "For additional help, contact {platformName} support at ",
|
||||
"sign.in.text": "Sign in",
|
||||
"extend.field.errors": "{emailError} below.",
|
||||
"invalid.token.heading": "Invalid password reset link",
|
||||
"invalid.token.error.message": "This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.",
|
||||
"token.validation.rate.limit.error.heading": "Too many requests",
|
||||
"token.validation.rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"token.validation.internal.sever.error.heading": "Token validation failure",
|
||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "The username, email or password you entered is incorrect. You have {remainingAttempts} more sign in\n attempts before your account is temporarily locked.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "If you've forgotten your password, {resetLink}",
|
||||
"account.locked.out.message.2": "To be on the safe side, you can {resetLink} before trying again.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "The username, email, or password you entered is incorrect. Please try again or {resetLink}.",
|
||||
"login.page.title": "Login | {siteName}",
|
||||
"login.user.identity.label": "Username or email",
|
||||
"login.password.label": "Password",
|
||||
"sign.in.button": "Sign in",
|
||||
"sign.in.btn.pending.state": "Loading",
|
||||
"need.help.signing.in.collapsible.menu": "Need help signing in?",
|
||||
"forgot.password.link": "Forgot my password",
|
||||
"forgot.password": "Forgot password",
|
||||
"other.sign.in.issues": "Other sign in issues",
|
||||
"need.other.help.signing.in.collapsible.menu": "Need other help signing in?",
|
||||
"institution.login.button": "Institution/campus credentials",
|
||||
"institution.login.page.title": "Sign in with institution/campus credentials",
|
||||
"institution.login.page.back.button": "Back to sign in",
|
||||
"create.an.account": "Create an account",
|
||||
"login.other.options.heading": "Or sign in with:",
|
||||
"non.compliant.password.title": "We recently changed our password requirements",
|
||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
||||
"first.time.here": "First time here?",
|
||||
"email.help.message": "The email address you used to register with edX.",
|
||||
"enterprise.login.btn.text": "Company or school credentials",
|
||||
"email.format.validation.message": "The email address you've provided isn't formatted correctly.",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
||||
"email.validation.message": "Enter your username or email",
|
||||
"password.validation.message": "Password criteria has not been met",
|
||||
"register.link": "Create an account",
|
||||
"sign.in.heading": "Sign in",
|
||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
||||
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
|
||||
"account.activation.info.message": "This account has already been activated.",
|
||||
"account.activation.error.message.title": "Your account could not be activated",
|
||||
"account.activation.support.link": "contact support",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
|
||||
"account.confirmation.success.message": "Sign in to continue.",
|
||||
"account.confirmation.info.message": "This email has already been confirmed.",
|
||||
"account.confirmation.error.message.title": "Your email could not be confirmed",
|
||||
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
|
||||
"login.failure.header.title": "We couldn't sign you in.",
|
||||
"contact.support.link": "contact {platformName} support",
|
||||
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
|
||||
"login.failed.attempt.error": "You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.locked.out.error.message": "To protect your account, it’s been temporarily locked. Try again in {lockedOutPeriod} minutes.",
|
||||
"login.form.invalid.error.message": "Please fill in the fields below.",
|
||||
"login.incorrect.credentials.error.reset.link.text": "reset your password",
|
||||
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
|
||||
"password.security.nudge.title": "Password security",
|
||||
"password.security.block.title": "Password change required",
|
||||
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
|
||||
"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",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.title": "Register | {siteName}",
|
||||
"registration.fullname.label": "Full name",
|
||||
"registration.email.label": "Email",
|
||||
"registration.username.label": "Public username",
|
||||
"registration.password.label": "Password",
|
||||
"registration.country.label": "Country/Region",
|
||||
"registration.opt.in.label": "I agree that {siteName} may send me marketing messages.",
|
||||
"help.text.name": "This name will be used by any certificates that you earn.",
|
||||
"help.text.username.1": "The name that will identify you in your courses.",
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
"register.institution.login.button": "Institution/campus credentials",
|
||||
"register.institution.login.page.title": "Register with institution/campus credentials",
|
||||
"empty.name.field.error": "Enter your full name",
|
||||
"empty.email.field.error": "Enter your email",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"empty.username.field.error": "Username must be between 2 and 30 characters",
|
||||
"empty.password.field.error": "Password criteria has not been met",
|
||||
"empty.country.field.error": "Select your country or region of residence",
|
||||
"email.invalid.format.error": "Enter a valid email address",
|
||||
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
|
||||
"username.validation.message": "Username must be between 2 and 30 characters",
|
||||
"name.validation.message": "Enter a valid name",
|
||||
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces",
|
||||
"support.education.research": "Support education research by providing additional information. (Optional)",
|
||||
"registration.request.failure.header": "We couldn't create your account.",
|
||||
"registration.empty.form.submission.error": "Please check your responses and try again.",
|
||||
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"registration.rate.limit.error": "Too many failed registration attempts. Try again later.",
|
||||
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
|
||||
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
|
||||
"privacy.policy": "Privacy Policy",
|
||||
"honor.code": "Honor Code",
|
||||
"terms.of.service": "Terms of Service",
|
||||
"registration.year.of.birth.label": "Year of birth (optional)",
|
||||
"registration.field.gender.options.label": "Gender (optional)",
|
||||
"registration.goals.label": "Tell us why you're interested in edX (optional)",
|
||||
"registration.field.gender.options.f": "Female",
|
||||
"registration.field.gender.options.m": "Male",
|
||||
"registration.field.gender.options.o": "Other/Prefer not to say",
|
||||
"registration.field.education.levels.label": "Highest level of education completed (optional)",
|
||||
"registration.field.education.levels.p": "Doctorate",
|
||||
"registration.field.education.levels.m": "Master's or professional degree",
|
||||
"registration.field.education.levels.b": "Bachelor's degree",
|
||||
"registration.field.education.levels.a": "Associate's degree",
|
||||
"registration.field.education.levels.hs": "Secondary/high school",
|
||||
"registration.field.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"registration.field.education.levels.el": "Elementary/primary school",
|
||||
"registration.field.education.levels.none": "No formal education",
|
||||
"registration.field.education.levels.other": "Other education",
|
||||
"registration.username.suggestion.label": "Suggested:",
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"did.you.mean.alert.text": "Did you mean",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Sign in",
|
||||
"reset.password.page.title": "Reset Password | {siteName}",
|
||||
"reset.password": "Reset password",
|
||||
"reset.password.page.instructions": "Enter and confirm your new password.",
|
||||
"new.password.label": "New password",
|
||||
"confirm.password.label": "Confirm password",
|
||||
"passwords.do.not.match": "Passwords do not match",
|
||||
"confirm.your.password": "Confirm your password",
|
||||
"forgot.password.confirmation.sign.in.link": "sign in",
|
||||
"reset.password.request.forgot.password.text": "Forgot password",
|
||||
"reset.password.request.invalid.token.header": "Invalid password reset link",
|
||||
"reset.password.empty.new.password.field.error": "Please enter your new password.",
|
||||
"reset.password.failure.heading": "We couldn't reset your password.",
|
||||
"reset.password.form.submission.error": "Please check your responses and try again.",
|
||||
"reset.password.request.server.error": "Failed to reset password",
|
||||
"reset.password.token.validation.sever.error": "Token validation failure",
|
||||
"reset.server.rate.limit.error": "Too many requests.",
|
||||
"reset.password.success.heading": "Password reset complete.",
|
||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
||||
"progressive.profiling.page.title": "Optional Fields | {siteName}",
|
||||
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
|
||||
"gender.options.label": "Gender (optional)",
|
||||
"gender.options.f": "Female",
|
||||
"gender.options.m": "Male",
|
||||
"gender.options.o": "Other/Prefer not to say",
|
||||
"education.levels.label": "Highest level of education completed (optional)",
|
||||
"education.levels.p": "Doctorate",
|
||||
"education.levels.m": "Master's or professional degree",
|
||||
"education.levels.b": "Bachelor's degree",
|
||||
"education.levels.a": "Associate's degree",
|
||||
"education.levels.hs": "Secondary/high school",
|
||||
"education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"education.levels.el": "Elementary/primary school",
|
||||
"education.levels.none": "No formal education",
|
||||
"education.levels.other": "Other education",
|
||||
"year.of.birth.label": "Year of birth (optional)",
|
||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
||||
"optional.fields.submit.button": "Submit",
|
||||
"optional.fields.skip.button": "Skip for now",
|
||||
"continue.to.platform": "Continue to {platformName}",
|
||||
"modal.title": "Thanks for letting us know.",
|
||||
"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."
|
||||
"start.learning": "Beginne zu lernen",
|
||||
"with.site.name": "mit {siteName}",
|
||||
"complete.your.profile.1": "Vervollständige",
|
||||
"complete.your.profile.2": "dein Profil",
|
||||
"welcome.to.platform": "Willkommen bei {siteName}, {username}!",
|
||||
"institution.login.page.sub.heading": "Wählen Sie Ihre Institution aus der folgenden Liste aus",
|
||||
"logistration.sign.in": "Anmelden",
|
||||
"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",
|
||||
"sso.sign.in.with": "Melden Sie sich mit {providerName} an",
|
||||
"sso.create.account.using": "Erstellen Sie ein Konto mit {providerName}",
|
||||
"show.password": "Passwort anzeigen",
|
||||
"hide.password": "Passwort verbergen",
|
||||
"one.letter": "1 Buchstabe",
|
||||
"one.number": "1 Nummer",
|
||||
"eight.characters": "8 Charaktere",
|
||||
"password.sr.only.helping.text": "Das Passwort muss mindestens 8 Zeichen, mindestens einen Buchstaben und mindestens eine Zahl enthalten",
|
||||
"tpa.alert.heading": "Fast fertig!",
|
||||
"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",
|
||||
"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}",
|
||||
"forgot.password.page.heading": "Passwort zurücksetzen",
|
||||
"forgot.password.page.instructions": "Bitte geben Sie unten Ihre E-Mail-Adresse ein und wir senden Ihnen eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts.",
|
||||
"forgot.password.page.invalid.email.message": "Geben sie eine gültige E-Mail-Adresse an",
|
||||
"forgot.password.page.email.field.label": "E-Mail Adresse",
|
||||
"forgot.password.page.submit.button": "Einreichen",
|
||||
"forgot.password.error.alert.title.": "Wir konnten Sie nicht kontaktieren.",
|
||||
"forgot.password.error.message.title": "Ein Fehler ist aufgetreten.",
|
||||
"forgot.password.request.in.progress.message": "Ihre vorherige Anfrage ist in Bearbeitung, bitte versuchen Sie es in wenigen Augenblicken erneut.",
|
||||
"forgot.password.empty.email.field.error": "Geben sie ihre E-Mail Adresse ein",
|
||||
"forgot.password.email.help.text": "Die E-Mail-Adresse, mit der Sie sich bei {platformName} registriert haben",
|
||||
"confirmation.message.title": "Prüfen Sie Ihr E-Mail-Postfach",
|
||||
"confirmation.support.link": "wenden Sie sich an den technischen Support",
|
||||
"need.help.sign.in.text": "Brauchen Sie Hilfe bei der Anmeldung?",
|
||||
"additional.help.text": "Wenden Sie sich für weitere Hilfe an den {platformName}-Support unter",
|
||||
"sign.in.text": "Anmelden",
|
||||
"extend.field.errors": "{emailError} unten.",
|
||||
"invalid.token.heading": "Ungültiger Link zum Zurücksetzen des Passworts",
|
||||
"invalid.token.error.message": "Dieser Link zum Zurücksetzen des Passwortes ist ungültig. Möglicherweise wurde es bereits verwendet. Geben Sie unten Ihre E-Mail-Adresse ein, um einen neuen Link zu erhalten.",
|
||||
"token.validation.rate.limit.error.heading": "Zu viele Anfragen",
|
||||
"token.validation.rate.limit.error": "Aufgrund von zu vieler Anfragen ist ein Fehler aufgetreten. Bitte versuchen Sie es nach einiger Zeit erneut.",
|
||||
"token.validation.internal.sever.error.heading": "Token-Validierungsfehler",
|
||||
"token.validation.internal.sever.error": "Ein Fehler ist aufgetreten. Versuchen Sie, die Seite zu aktualisieren, oder überprüfen Sie Ihre Internetverbindung.",
|
||||
"internal.server.error": "Ein Fehler ist aufgetreten. Versuchen Sie, die Seite zu aktualisieren, oder überprüfen Sie Ihre Internetverbindung.",
|
||||
"account.activation.error.message": "Etwas ist schief gelaufen, bitte {supportLink} um dieses Problem zu lösen.",
|
||||
"login.inactive.user.error": "Um sich anzumelden, müssen Sie Ihr Konto aktivieren.{lineBreak} {lineBreak}Wir haben gerade einen Aktivierungslink an {email} gesendet. Wenn Sie keine E-Mail erhalten, überprüfen Sie Ihre Spam-Ordner oder {supportLink}.",
|
||||
"allowed.domain.login.error": "Als {allowedDomain}-Benutzer müssen Sie sich mit Ihrem {allowedDomain} {tpaLink} anmelden.",
|
||||
"login.incorrect.credentials.error.attempts.text.1": "Der eingegebene Benutzername, die E-Mail oder das Passwort ist falsch. Sie haben {remainingAttempts} weitere Anmeldeversuche, bevor Ihr Konto vorübergehend gesperrt wird.",
|
||||
"login.incorrect.credentials.error.attempts.text.2": "Wenn Sie Ihr Passwort vergessen haben, {resetLink}",
|
||||
"account.locked.out.message.2": "Um auf der sicheren Seite zu sein, können Sie {resetLink} tun, bevor Sie es erneut versuchen.",
|
||||
"login.incorrect.credentials.error.with.reset.link": "Der eingegebene Benutzername, die E-Mail-Adresse oder das Passwort ist falsch. Bitte versuchen Sie es erneut oder {resetLink}.",
|
||||
"login.page.title": "Anmelden | {siteName}",
|
||||
"login.user.identity.label": "Benutzername oder E-Mail-Adresse",
|
||||
"login.password.label": "Passwort",
|
||||
"sign.in.button": "Anmelden",
|
||||
"forgot.password": "Passwort vergessen",
|
||||
"institution.login.button": "Zeugnisse der Institution/des Campus",
|
||||
"institution.login.page.title": "Melden Sie sich mit Institutions-/Campus-Anmeldeinformationen an",
|
||||
"login.other.options.heading": "Oder melden Sie sich an mit:",
|
||||
"non.compliant.password.title": "Wir haben kürzlich unsere Passwortanforderungen geändert",
|
||||
"non.compliant.password.message": "Ihr aktuelles Passwort entspricht nicht den neuen Sicherheitsanforderungen. Wir haben gerade eine Nachricht zum Zurücksetzen des Passworts an die mit diesem Konto verknüpfte E-Mail-Adresse gesendet. Vielen Dank, dass Sie uns helfen, Ihre Daten zu schützen.",
|
||||
"account.locked.out.message.1": "Um Ihr Konto zu schützen, wurde es vorübergehend gesperrt. Versuchen Sie es in 30 Minuten erneut.",
|
||||
"enterprise.login.btn.text": "Arbeits- oder Schulzeugnisse",
|
||||
"username.or.email.format.validation.less.chars.message": "Benutzername oder E-Mail müssen mindestens 3 Zeichen lang sein.",
|
||||
"email.validation.message": "Geben Sie Ihren Benutzernamen oder Ihre E-Mail-Adresse ein",
|
||||
"password.validation.message": "Die Passwortkriterien wurden nicht erfüllt",
|
||||
"account.activation.success.message.title": "Super! Sie haben Ihr Konto aktiviert.",
|
||||
"account.activation.success.message": "Sie erhalten jetzt E-Mail-Updates und Benachrichtigungen von uns in Bezug auf die Kurse, für die Sie eingeschrieben sind. Melden Sie sich an, um fortzufahren.",
|
||||
"account.activation.info.message": "Dieses Konto wurde bereits aktiviert.",
|
||||
"account.activation.error.message.title": "Ihr Konto konnte nicht aktiviert werden",
|
||||
"account.activation.support.link": "kontaktieren Sie den Support",
|
||||
"account.confirmation.success.message.title": "Super! Sie haben Ihre E-Mail bestätigt.",
|
||||
"account.confirmation.success.message": "Melden Sie sich an, um fortzufahren.",
|
||||
"account.confirmation.info.message": "Diese E-Mail-Adresse wurde bereits bestätigt.",
|
||||
"account.confirmation.error.message.title": "Ihre E-Mail-Adresse konnte nicht bestätigt werden",
|
||||
"tpa.account.link": "{provider}-Konto",
|
||||
"internal.server.error.message": "Ein Fehler ist aufgetreten. Versuchen Sie, die Seite zu aktualisieren, oder überprüfen Sie Ihre Internetverbindung.",
|
||||
"login.rate.limit.reached.message": "Zu viele fehlgeschlagene Anmeldeversuche. Bitte versuche es später noch einmal.",
|
||||
"login.failure.header.title": "Wir konnten Sie leider nicht einloggen.",
|
||||
"contact.support.link": "Wenden Sie sich an den Support der {platformName}",
|
||||
"login.incorrect.credentials.error": "Der eingegebene Benutzername, die E-Mail-Adresse oder das Passwort ist falsch. Bitte versuche es erneut.",
|
||||
"login.form.invalid.error.message": "Bitte füllen Sie die unten stehenden Felder aus.",
|
||||
"login.incorrect.credentials.error.reset.link.text": "Setzen Sie Ihr Passwort zurück",
|
||||
"login.incorrect.credentials.error.before.account.blocked.text": "Klicken Sie hier, um es zurückzusetzen.",
|
||||
"password.security.nudge.title": "Passwortsicherheit",
|
||||
"password.security.block.title": "Passwortänderung erforderlich",
|
||||
"password.security.nudge.body": "Unser System hat festgestellt, dass Ihr Passwort angreifbar ist. Wir empfehlen Ihnen, es zu ändern, damit Ihr Konto sicher bleibt.",
|
||||
"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.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",
|
||||
"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",
|
||||
"register.page.title": "Registrieren | {siteName}",
|
||||
"registration.fullname.label": "Vollständiger Name",
|
||||
"registration.email.label": "E-Mail-Adresse",
|
||||
"registration.username.label": "Öffentlicher Benutzername",
|
||||
"registration.password.label": "Passwort",
|
||||
"registration.country.label": "Land/Region",
|
||||
"registration.opt.in.label": "Ich stimme zu, dass {siteName} mir Marketingmitteilungen senden darf.",
|
||||
"help.text.name": "Dieser Name wird von allen Zertifikaten verwendet, die Sie erwerben.",
|
||||
"help.text.username.1": "Der Name, der Sie in Ihren Kursen identifiziert.",
|
||||
"help.text.username.2": "Dies kann später nicht mehr geändert werden.",
|
||||
"help.text.email": "Für die Kontoaktivierung und wichtige Updates",
|
||||
"create.account.for.free.button": "Erstellen Sie kostenlos ein Benutzerkonto",
|
||||
"registration.other.options.heading": "Oder registrieren Sie sich bei:",
|
||||
"register.institution.login.button": "Zeugnisse der Institution/des Campus",
|
||||
"register.institution.login.page.title": "Registrieren Sie sich mit Institutions-/Campus-Anmeldeinformationen",
|
||||
"empty.name.field.error": "Geben Sie Ihren vollständigen Namen ein",
|
||||
"empty.email.field.error": "Geben Sie Ihre E-Mail-Adresse ein",
|
||||
"empty.username.field.error": "Der Benutzername muss zwischen 2 und 30 Zeichen lang sein",
|
||||
"empty.password.field.error": "Kennwortkriterien wurden nicht erfüllt",
|
||||
"empty.country.field.error": "Wählen Sie das Land oder die Region Ihres Wohnsitzes aus",
|
||||
"email.do.not.match": "Die E-Mail-Adressen stimmen nicht überein.",
|
||||
"email.invalid.format.error": "Geben sie eine gültige E-Mail-Adresse an",
|
||||
"username.validation.message": "Der Benutzername muss zwischen 2 und 30 Zeichen lang sein",
|
||||
"name.validation.message": "Geben Sie einen gültigen Namen ein",
|
||||
"username.format.validation.message": "Benutzernamen dürfen nur Buchstaben (AZ, az), Ziffern (0-9), Unterstriche (_) und Bindestriche (-) enthalten. Benutzernamen dürfen keine Leerzeichen enthalten",
|
||||
"registration.request.failure.header": "Wir konnten Ihr Konto leider nicht erstellen.",
|
||||
"registration.empty.form.submission.error": "Bitte überprüfen Sie Ihre Antworten und versuchen Sie es erneut.",
|
||||
"registration.request.server.error": "Ein Fehler ist aufgetreten. Versuchen Sie, die Seite zu aktualisieren, oder überprüfen Sie Ihre Internetverbindung.",
|
||||
"registration.rate.limit.error": "Zu viele fehlgeschlagene Registrierungsversuche. Versuchen Sie es später noch einmal.",
|
||||
"registration.tpa.session.expired": "Die Registrierung mit {provider} ist abgelaufen.",
|
||||
"terms.of.service.and.honor.code": "Nutzungsbedingungen und Verhaltenskodex",
|
||||
"privacy.policy": "Datenschutzbestimmungen",
|
||||
"honor.code": "Verhaltenskodex",
|
||||
"terms.of.service": "Nutzungsbedingungen",
|
||||
"registration.username.suggestion.label": "Empfohlen:",
|
||||
"did.you.mean.alert.text": "Meinten Sie",
|
||||
"register.page.terms.of.service.and.honor.code": "Wenn Sie ein Konto erstellen, stimmen Sie den {tosAndHonorCode} zu und erkennen an, dass {platformName} und jedes \nMitglied Ihre personenbezogenen Daten in Übereinstimmung mit den {privacyPolicy} verarbeitet.",
|
||||
"register.page.honor.code": "Ich stimme den {platformName} {tosAndHonorCode} zu",
|
||||
"register.page.terms.of.service": "Ich stimme den {platformName} {termsOfService} zu",
|
||||
"sign.in": "Anmelden",
|
||||
"reset.password.page.title": "Passwort zurücksetzen | {siteName}",
|
||||
"reset.password": "Passwort zurücksetzen",
|
||||
"reset.password.page.instructions": "Neues Passwort eingeben und bestätigen",
|
||||
"new.password.label": "Neues Passwort",
|
||||
"confirm.password.label": "Kennwort bestätigen",
|
||||
"passwords.do.not.match": "Passwörter stimmen nicht überein",
|
||||
"confirm.your.password": "Bestätigen Sie Ihr Passwort",
|
||||
"reset.password.failure.heading": "Wir konnten Ihr Passwort nicht zurücksetzen.",
|
||||
"reset.password.form.submission.error": "Bitte überprüfen Sie Ihre Antworten und versuchen Sie es erneut.",
|
||||
"reset.server.rate.limit.error": "Zu viele Anfragen.",
|
||||
"reset.password.success.heading": "Zurücksetzen des Passworts abgeschlossen.",
|
||||
"reset.password.success": "Ihr Passwort wurde zurückgesetzt. Melden Sie sich bei Ihrem Konto an.",
|
||||
"rate.limit.error": "Aufgrund zu vieler Anfragen ist ein Fehler aufgetreten. Bitte versuchen Sie es nach einiger Zeit erneut."
|
||||
}
|
||||
@@ -5,15 +5,9 @@
|
||||
"complete.your.profile.2": "tu perfil ",
|
||||
"welcome.to.platform": "¡Bienvenido a {siteName}, {username}!",
|
||||
"institution.login.page.sub.heading": "Selecciona tu institución de la lista siguiente",
|
||||
"forgot.password.confirmation.title": "Verifica tu correo electrónico",
|
||||
"forgot.password.confirmation.support.link": "contacta con el equipo de soporte técnico",
|
||||
"forgot.password.confirmation.info": "Si no recibes un mensaje de recuperación de tu contraseña en un minuto, verifica que introduciste la dirección de correo electrónico correcta, o verifica tu carpeta de correo no deseado.",
|
||||
"logistration.sign.in": "Iniciar sesión",
|
||||
"logistration.register": "Registrarse",
|
||||
"internal.server.error.message": "Se ha producido un error. Intenta actualizar la página o comprueba tu conexión a Internet.",
|
||||
"server.ratelimit.error.message": "Se ha producido un error debido a demasiadas solicitudes. Por favor, inténtalo de nuevo después de algún tiempo.",
|
||||
"enterprisetpa.title.heading": "¿Deseas iniciar sesión con tus credenciales de {providerName}?",
|
||||
"enterprisetpa.sso.button.title": "Inicio de sesión con {providerName}",
|
||||
"enterprisetpa.login.button.text": "Mostrar otras formas de iniciar sesión o de registrarme",
|
||||
"sso.sign.in.with": "Inicio de sesión con {providerName}",
|
||||
"sso.create.account.using": "Crear una cuenta con {providerName}",
|
||||
@@ -26,6 +20,7 @@
|
||||
"tpa.alert.heading": "¡Ya casi has terminado!",
|
||||
"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",
|
||||
"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}",
|
||||
@@ -38,7 +33,6 @@
|
||||
"forgot.password.error.message.title": "Ha ocurrido un error.",
|
||||
"forgot.password.request.in.progress.message": "Su solicitud anterior está en progreso, por favor inténtalo de nuevo en unos minutos.",
|
||||
"forgot.password.empty.email.field.error": "Introduce tu email",
|
||||
"forgot.password.invalid.email.message": "La dirección de correo que has ingresado no está en el formato correcto.",
|
||||
"forgot.password.email.help.text": "El correo electrónico que utilizaste para registrarte en {platformName}",
|
||||
"confirmation.message.title": "Verifica tu correo electrónico",
|
||||
"confirmation.support.link": "entra en contacto con el equipo de soporte técnico",
|
||||
@@ -53,7 +47,6 @@
|
||||
"token.validation.internal.sever.error.heading": "Fallo de validación del token",
|
||||
"token.validation.internal.sever.error": "Se ha producido un error. Intenta actualizar la página o verifica tu conexión a Internet.",
|
||||
"internal.server.error": "Se ha producido un error. Intenta actualizar la página o verifica tu conexión a Internet.",
|
||||
"rate.limit.error": "Se ha producido un error debido a demasiadas solicitudes. Por favor, inténtalo de nuevo después de algún tiempo.",
|
||||
"account.activation.error.message": "Algo no funcionó correctamente, por favor {supportLink} para resolver este problema.",
|
||||
"login.inactive.user.error": "Para iniciar sesión, debes activar tu cuenta..{lineBreak}\n {lineBreak} Acabamos de enviar un enlace de activación a {email}. Si no recibes un correo electrónico,\n revisa tus carpetas de spam o {supportLink}.",
|
||||
"allowed.domain.login.error": "Como usuario {allowedDomain}, debe iniciar sesión con su {allowedDomain} {tpaLink}.",
|
||||
@@ -65,45 +58,32 @@
|
||||
"login.user.identity.label": "Nombre de usuario o correo electrónico",
|
||||
"login.password.label": "Contraseña",
|
||||
"sign.in.button": "Iniciar sesión",
|
||||
"sign.in.btn.pending.state": "Cargando",
|
||||
"need.help.signing.in.collapsible.menu": "¿Necesitas ayuda para iniciar sesión?",
|
||||
"forgot.password.link": "Olvidé mi contraseña",
|
||||
"forgot.password": "Olvidé mi contraseña",
|
||||
"other.sign.in.issues": "Otros problemas de inicio de sesión ",
|
||||
"need.other.help.signing.in.collapsible.menu": "¿Necesitas más ayuda para iniciar sesión?",
|
||||
"institution.login.button": "Credenciales de la institución/campus",
|
||||
"institution.login.page.title": "Iniciar sesión con las credenciales de la institución/campus",
|
||||
"institution.login.page.back.button": "Volver al inicio",
|
||||
"create.an.account": "Crear una cuenta",
|
||||
"login.other.options.heading": "O bien, inicia sesión con:",
|
||||
"non.compliant.password.title": "Recientemente hemos cambiado los requisitos de las contraseñas",
|
||||
"non.compliant.password.message": "Tu contraseña actual no cumple con los nuevos requisitos de seguridad. Acabamos de enviar un mensaje de restablecimiento de contraseña a la dirección de correo electrónico asociada a esta cuenta. Gracias por ayudarnos a mantener tus datos seguros.",
|
||||
"account.locked.out.message.1": "Para proteger tu cuenta, se ha bloqueado temporalmente. Inténtalo de nuevo en 30 minutos.",
|
||||
"first.time.here": "Primera vez aquí?",
|
||||
"email.help.message": "La dirección de correo electrónico que usaste para registrarte en edX.",
|
||||
"enterprise.login.btn.text": "Credenciales de la empresa o de la institución ",
|
||||
"email.format.validation.message": "La dirección de correo que has ingresado no está en el formato correcto.",
|
||||
"username.or.email.format.validation.less.chars.message": "El nombre de usuario o el correo electrónico deben tener al menos 3 caracteres.",
|
||||
"email.validation.message": "Introduce tu nombre de usuario o correo electrónico",
|
||||
"password.validation.message": "No se han cumplido los criterios de la contraseña",
|
||||
"register.link": "Crear una cuenta",
|
||||
"sign.in.heading": "Iniciar sesión",
|
||||
"account.activation.success.message.title": "Ha sido un éxito. Has activado tu cuenta.",
|
||||
"account.activation.success.message": "Ahora recibirás por correo electrónico actualizaciones y alertas relacionadas con los cursos en los que estás inscrito. Inicia sesión para continuar.",
|
||||
"account.activation.info.message": "La cuenta ya ha sido activada.",
|
||||
"account.activation.error.message.title": "Tu cuenta no ha podido ser activada",
|
||||
"account.activation.support.link": "contacta al equipo de soporte de edX",
|
||||
"tpa.account.link": "{provider} cuenta",
|
||||
"account.confirmation.success.message.title": "¡Éxito! Has confirmado tu correo electrónico.",
|
||||
"account.confirmation.success.message": "Inicia sesión para continuar.",
|
||||
"account.confirmation.info.message": "Este correo electrónico ya ha sido confirmado.",
|
||||
"account.confirmation.error.message.title": "Tu correo electrónico no pudo ser confirmado",
|
||||
"tpa.account.link": "{provider} cuenta",
|
||||
"internal.server.error.message": "Se ha producido un error. Intenta actualizar la página o comprueba tu conexión a Internet.",
|
||||
"login.rate.limit.reached.message": "Demasiados intentos fallidos de inicio de sesión. Inténtelo de nuevo más tarde.",
|
||||
"login.failure.header.title": "No se ha podido iniciar tu sesión.",
|
||||
"contact.support.link": "entrar en contacto con el soporte de {platformName}",
|
||||
"login.incorrect.credentials.error": "El nombre de usuario, el correo electrónico o la contraseña que has introducido son incorrectos. Por favor, inténtalo de nuevo.",
|
||||
"login.failed.attempt.error": "Tienes {remainAttempts} más intentos de inicio de sesión antes de que tu cuenta se bloquee temporalmente.",
|
||||
"login.locked.out.error.message": "Para proteger tu cuenta, se ha bloqueado temporalmente. Inténtalo de nuevo en {lockedOutPeriod} minutos.",
|
||||
"login.form.invalid.error.message": "Por favor, complete los siguientes campos.",
|
||||
"login.incorrect.credentials.error.reset.link.text": "restablecer la contraseña",
|
||||
"login.incorrect.credentials.error.before.account.blocked.text": "Pulse aquí para restablecerla.",
|
||||
@@ -113,8 +93,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",
|
||||
"register.page.terms.of.service.and.honor.code": "Al crear una cuenta, aceptas el {tosAndHonorCode} y reconoces que {platformName} y cada\n Miembro procesa tus datos personales de acuerdo con la {privacyPolicy}.",
|
||||
"register.page.honor.code": "Acepto las {platformName} {tosAndHonorCode}",
|
||||
"progressive.profiling.page.title": "Campos opcionales | {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",
|
||||
"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",
|
||||
"register.page.title": "Register | {siteName}",
|
||||
"registration.fullname.label": "Nombre completo",
|
||||
"registration.email.label": "Correo electrónico",
|
||||
@@ -127,22 +119,19 @@
|
||||
"help.text.username.2": "Esto no puede modificarse posteriormente.",
|
||||
"help.text.email": "Para la activación de la cuenta y las actualizaciones importantes",
|
||||
"create.account.for.free.button": "Crea una cuenta gratis",
|
||||
"create.an.account.btn.pending.state": "Cargando",
|
||||
"registration.other.options.heading": "O regístrese con:",
|
||||
"register.institution.login.button": "Credenciales de la institución/campus",
|
||||
"register.institution.login.page.title": "Registro con credenciales de la institución/campus",
|
||||
"empty.name.field.error": "Introduce tu nombre completo",
|
||||
"empty.email.field.error": "Introduce tu email",
|
||||
"email.do.not.match": "Los correos electrónicos no son iguales.",
|
||||
"empty.username.field.error": "El nombre de usuario debe tener entre 2 y 30 caracteres",
|
||||
"empty.password.field.error": "No se han cumplido los criterios de la contraseña",
|
||||
"empty.country.field.error": "Selecciona tu país o región de residencia",
|
||||
"email.do.not.match": "Los correos electrónicos no son iguales.",
|
||||
"email.invalid.format.error": "Introduce una dirección de correo electrónico válida",
|
||||
"email.ratelimit.less.chars.validation.message": "El correo electrónico debe tener 3 caracteres.",
|
||||
"username.validation.message": "El nombre de usuario debe tener entre 2 y 30 caracteres",
|
||||
"name.validation.message": "Introduce un nombre válido",
|
||||
"username.format.validation.message": "Los nombres de usuario solo pueden contener letras (A-Z, a-z), números (0-9), guiones bajos (_) y guiones (-). Los nombres de usuario no pueden contener espacios",
|
||||
"support.education.research": "Apoya la investigación sobre educación proporcionando información adicional. (Opcional)",
|
||||
"registration.request.failure.header": "No pudimos crear tu cuenta.",
|
||||
"registration.empty.form.submission.error": "Por favor, verifica tus respuestas y vuelve a intentarlo.",
|
||||
"registration.request.server.error": "Se ha producido un error. Intenta actualizar la página o comprueba tu conexión a Internet.",
|
||||
@@ -152,26 +141,11 @@
|
||||
"privacy.policy": "Política de privacidad ",
|
||||
"honor.code": "Código de Honor",
|
||||
"terms.of.service": "Términos de servicio",
|
||||
"registration.year.of.birth.label": "Año de nacimiento (opcional)",
|
||||
"registration.field.gender.options.label": "Género (opcional)",
|
||||
"registration.goals.label": "Díganos por qué estás interesado en edX (opcional)",
|
||||
"registration.field.gender.options.f": "Femenino ",
|
||||
"registration.field.gender.options.m": "Masculino",
|
||||
"registration.field.gender.options.o": "Otro/Prefiero no decir",
|
||||
"registration.field.education.levels.label": "Nivel más alto de educación completado (opcional)",
|
||||
"registration.field.education.levels.p": "Doctorado",
|
||||
"registration.field.education.levels.m": "Maestría o magíster",
|
||||
"registration.field.education.levels.b": "Pregrado o Licenciatura",
|
||||
"registration.field.education.levels.a": "Grado técnico - tecnológico",
|
||||
"registration.field.education.levels.hs": "Enseñanza secundaria",
|
||||
"registration.field.education.levels.jhs": "Formación media",
|
||||
"registration.field.education.levels.el": "Enseñanza primaria",
|
||||
"registration.field.education.levels.none": "Ninguna educación formal",
|
||||
"registration.field.education.levels.other": "Otra educación",
|
||||
"registration.username.suggestion.label": "Se recomienda:",
|
||||
"registration.using.tpa.form.heading": "Termina de crear tu cuenta",
|
||||
"did.you.mean.alert.text": "¿Quieres decir",
|
||||
"register.page.terms.of.service": "Acepto lo siguiente: {platformName} {termsOfService}",
|
||||
"register.page.terms.of.service.and.honor.code": "Al crear una cuenta, aceptas el {tosAndHonorCode} y reconoces que {platformName} y cada\n Miembro procesa tus datos personales de acuerdo con la {privacyPolicy}.",
|
||||
"register.page.honor.code": "Acepto las {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "Acepto las {platformName} {termsOfService}",
|
||||
"sign.in": "Iniciar sesión",
|
||||
"reset.password.page.title": "Restablecer contraseña | {siteName}",
|
||||
"reset.password": "Restablecer mi contraseña",
|
||||
@@ -180,40 +154,10 @@
|
||||
"confirm.password.label": "Confirmar contraseña",
|
||||
"passwords.do.not.match": "Las contraseñas no coinciden",
|
||||
"confirm.your.password": "Confirma tu contraseña",
|
||||
"forgot.password.confirmation.sign.in.link": "iniciar sesión",
|
||||
"reset.password.request.forgot.password.text": "Olvidé mi contraseña",
|
||||
"reset.password.request.invalid.token.header": "Enlace de restablecimiento de contraseña inválido",
|
||||
"reset.password.empty.new.password.field.error": "Por favor, introduce tu nueva contraseña.",
|
||||
"reset.password.failure.heading": "No hemos podido restablecer tu contraseña.",
|
||||
"reset.password.form.submission.error": "Por favor, verifica tus respuestas y vuelve a intentarlo.",
|
||||
"reset.password.request.server.error": "No se ha podido restablecer la contraseña",
|
||||
"reset.password.token.validation.sever.error": "Fallo de validación del token",
|
||||
"reset.server.rate.limit.error": "Demasiadas solicitudes.",
|
||||
"reset.password.success.heading": "Restablecimiento de la contraseña completado.",
|
||||
"reset.password.success": "Tu contraseña ha sido restablecida. Acceda a tu cuenta.",
|
||||
"progressive.profiling.page.title": "Campos opcionales | {siteName}",
|
||||
"progressive.profiling.page.heading": "Unas cuantas preguntas para ti nos ayudarán a mejorar.",
|
||||
"gender.options.label": "Género (opcional)",
|
||||
"gender.options.f": "Femenino ",
|
||||
"gender.options.m": "Masculino",
|
||||
"gender.options.o": "Otro/Prefiero no decir",
|
||||
"education.levels.label": "Nivel más alto de educación completado (opcional)",
|
||||
"education.levels.p": "Doctorado",
|
||||
"education.levels.m": "Maestría o magíster",
|
||||
"education.levels.b": "Pregrado o Licenciatura",
|
||||
"education.levels.a": "Grado técnico - tecnológico",
|
||||
"education.levels.hs": "Enseñanza secundaria",
|
||||
"education.levels.jhs": "Formación media",
|
||||
"education.levels.el": "Enseñanza primaria",
|
||||
"education.levels.none": "Ninguna educación formal",
|
||||
"education.levels.other": "Otra educación",
|
||||
"year.of.birth.label": "Año de nacimiento (opcional)",
|
||||
"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 ",
|
||||
"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."
|
||||
"rate.limit.error": "Se ha producido un error debido a demasiadas solicitudes. Por favor, inténtalo de nuevo después de algún tiempo."
|
||||
}
|
||||
@@ -5,15 +5,9 @@
|
||||
"complete.your.profile.2": "votre profil",
|
||||
"welcome.to.platform": "Bienvenue sur {siteName}, {username}!",
|
||||
"institution.login.page.sub.heading": "Sélectionner votre institution dans la liste ci-dessous",
|
||||
"forgot.password.confirmation.title": "Vérifiez votre email",
|
||||
"forgot.password.confirmation.support.link": "contacter le support technique",
|
||||
"forgot.password.confirmation.info": "Si vous ne recevez pas de message de réinitialisation de mot de passe après 1 minute, vérifiez que vous avez entré la bonne adresse courriel ou vérifiez votre dossier de pourriels.",
|
||||
"logistration.sign.in": "Connectez-vous",
|
||||
"logistration.register": "S'inscrire",
|
||||
"internal.server.error.message": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
|
||||
"server.ratelimit.error.message": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps.",
|
||||
"enterprisetpa.title.heading": "Souhaitez-vous vous connecter à l'aide de vos identifiants {providerName} ?",
|
||||
"enterprisetpa.sso.button.title": "Connectez-vous avec {providerName}",
|
||||
"enterprisetpa.login.button.text": "Montrez-moi d'autres méthodes pour me connecter ou m'inscrire",
|
||||
"sso.sign.in.with": "Connectez-vous avec {providerName}",
|
||||
"sso.create.account.using": "Créer un compte avec {providerName}",
|
||||
@@ -26,6 +20,7 @@
|
||||
"tpa.alert.heading": "Presque fini !",
|
||||
"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",
|
||||
"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}",
|
||||
@@ -38,7 +33,6 @@
|
||||
"forgot.password.error.message.title": "Une erreur est survenue.",
|
||||
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
|
||||
"forgot.password.empty.email.field.error": "Saisissez votre courriel",
|
||||
"forgot.password.invalid.email.message": "L'adresse email que vous avez fournie est incorrecte.",
|
||||
"forgot.password.email.help.text": "L'adresse courriel que vous avez utilisée pour vous inscrire sur {platformName}",
|
||||
"confirmation.message.title": "Vérifiez votre email",
|
||||
"confirmation.support.link": "contacter le support technique",
|
||||
@@ -53,7 +47,6 @@
|
||||
"token.validation.internal.sever.error.heading": "Échec de la validation du jeton",
|
||||
"token.validation.internal.sever.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
|
||||
"internal.server.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
|
||||
"rate.limit.error": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps.",
|
||||
"account.activation.error.message": "Une erreur s'est produite, veuillez {supportLink} pour résoudre ce problème.",
|
||||
"login.inactive.user.error": "Pour vous connecter, vous devez activer votre compte.{lineBreak}\n {lineBreak}Nous venons d'envoyer un lien d'activation à {email}. Si vous ne recevez pas de courriel,\n vérifiez vos dossiers de spam ou {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
@@ -65,45 +58,32 @@
|
||||
"login.user.identity.label": "Nom d'utilisateur ou courriel",
|
||||
"login.password.label": "Mot de passe",
|
||||
"sign.in.button": "Connectez-vous",
|
||||
"sign.in.btn.pending.state": "Chargement en cours",
|
||||
"need.help.signing.in.collapsible.menu": "Besoin d'aide pour vous enregistrer?",
|
||||
"forgot.password.link": "J'ai oublié mon mot de passe",
|
||||
"forgot.password": "Mot de passe oublié",
|
||||
"other.sign.in.issues": "Autres problèmes de connexion",
|
||||
"need.other.help.signing.in.collapsible.menu": "Encore besoin d'aide pour vous enregistrer?",
|
||||
"institution.login.button": "Identifiants de l'établissement/du campus",
|
||||
"institution.login.page.title": "Connectez vous avec les crédentiels d'institution ou de campus",
|
||||
"institution.login.page.back.button": "Retour à la connexion",
|
||||
"create.an.account": "Créer un compte",
|
||||
"login.other.options.heading": "Ou se connecter avec :",
|
||||
"non.compliant.password.title": "Nous avons récemment modifié nos exigences en matière de mot de passe",
|
||||
"non.compliant.password.message": "Votre mot de passe actuel ne répond pas aux nouvelles exigences de sécurité. Nous venons d'envoyer un message de réinitialisation de mot de passe à l'adresse courriel associée à ce compte. Merci de nous aider à protéger vos données.",
|
||||
"account.locked.out.message.1": "Pour protéger votre compte, il a été temporairement verrouillé. Réessayez dans 30 minutes.",
|
||||
"first.time.here": "C'est votre première visite ?",
|
||||
"email.help.message": "L'adresse électronique que vous avez utilisée pour vous inscrire à edX.",
|
||||
"enterprise.login.btn.text": "Identifiants de la compagnie ou de l'école",
|
||||
"email.format.validation.message": "L'adresse email que vous avez fournie est incorrecte.",
|
||||
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 3 caractères.",
|
||||
"email.validation.message": "Entrez votre nom d'utilisateur ou votre adresse courriel",
|
||||
"password.validation.message": "Les critères de mot de passe n'ont pas été remplis",
|
||||
"register.link": "Créer un compte",
|
||||
"sign.in.heading": "Connectez-vous",
|
||||
"account.activation.success.message.title": "Succès! Vous avez activé votre compte.",
|
||||
"account.activation.success.message": "Vous recevrez maintenant des mises à jour par courriel et des alertes de notre part concernant les cours auxquels vous êtes inscrit. Connectez-vous pour continuer.",
|
||||
"account.activation.info.message": "Ce compte a déjà été activé.",
|
||||
"account.activation.error.message.title": "Votre compte n'a pas pu être activé",
|
||||
"account.activation.support.link": "contacter le support",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"account.confirmation.success.message.title": "Succès ! Vous avez confirmé votre courriel.",
|
||||
"account.confirmation.success.message": "Se connecter pour continuer.",
|
||||
"account.confirmation.info.message": "Ce courriel a déjà été confirmé.",
|
||||
"account.confirmation.error.message.title": "Votre courriel ne peut pas être confirmé.",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"internal.server.error.message": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
|
||||
"login.rate.limit.reached.message": "Trop de tentatives de connexion échouées. Réessayez plus tard.",
|
||||
"login.failure.header.title": "Nous n'avons pas pu vous connecter.",
|
||||
"contact.support.link": "veuillez contacter le support {platformName}",
|
||||
"login.incorrect.credentials.error": "Le nom d'utilisateur, l'adresse courriel ou le mot de passe que vous avez saisis sont incorrects. Veuillez réessayer.",
|
||||
"login.failed.attempt.error": "Il vous reste {remainingAttempts} tentatives de connexion supplémentaires avant que votre compte ne soit temporairement verrouillé.",
|
||||
"login.locked.out.error.message": "Pour protéger votre compte, il a été temporairement verrouillé. Réessayez dans {lockedOutPeriod} minutes.",
|
||||
"login.form.invalid.error.message": "Veuillez remplir les champs ci-dessous.",
|
||||
"login.incorrect.credentials.error.reset.link.text": "réinitialiser votre mot de passe",
|
||||
"login.incorrect.credentials.error.before.account.blocked.text": "cliquez ici pour le réinitialiser.",
|
||||
@@ -113,8 +93,20 @@
|
||||
"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",
|
||||
"register.page.terms.of.service.and.honor.code": "En créant un compte, vous acceptez le {tosAndHonorCode} et vous reconnaissez que {platformName} et chaque\n membre peut traiter vos données personnelles conformément à la {privacyPolicy}.",
|
||||
"register.page.honor.code": "J'accepte le {tosAndHonorCode} {platformName}",
|
||||
"progressive.profiling.page.title": "Champs optionnels | {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",
|
||||
"optional.fields.skip.button": "Ignorer pour l'instant",
|
||||
"optional.fields.next.button": "Next",
|
||||
"continue.to.platform": "Continuer vers {platformName}",
|
||||
"modal.title": "Merci de nous en informer.",
|
||||
"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.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "S'inscrire | {siteName}",
|
||||
"registration.fullname.label": "Nom complet",
|
||||
"registration.email.label": "Email",
|
||||
@@ -127,22 +119,19 @@
|
||||
"help.text.username.2": "Cela ne peut pas être modifié ultérieurement.",
|
||||
"help.text.email": "Pour l'activation du compte et les mises à jour importantes",
|
||||
"create.account.for.free.button": "Créer un compte gratuitement",
|
||||
"create.an.account.btn.pending.state": "Chargement en cours",
|
||||
"registration.other.options.heading": "Ou inscrivez-vous avec :",
|
||||
"register.institution.login.button": "Identifiants de l'établissement/du campus",
|
||||
"register.institution.login.page.title": "Inscription avec les crédentiels d'institution ou de campus",
|
||||
"empty.name.field.error": "Saisissez votre nom complet",
|
||||
"empty.email.field.error": "Saisissez votre courriel",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"empty.username.field.error": "Le nom d'utilisateur doit comporter entre 2 et 30 caractères",
|
||||
"empty.password.field.error": "Les critères de mot de passe n'ont pas été remplis",
|
||||
"empty.country.field.error": "Sélectionnez votre pays ou région de résidence",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"email.invalid.format.error": "Enter a valid email address",
|
||||
"email.ratelimit.less.chars.validation.message": "Le courriel doit comporter 3 caractères.",
|
||||
"username.validation.message": "Le nom d'utilisateur doit comporter entre 2 et 30 caractères",
|
||||
"name.validation.message": "Enter a valid name",
|
||||
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces",
|
||||
"support.education.research": "Soutenez la recherche en éducation en fournissant des informations additionnelles. (Optionel)",
|
||||
"registration.request.failure.header": "Nous n'avons pas pu créer votre compte.",
|
||||
"registration.empty.form.submission.error": "Veuillez vérifier vos réponses et réessayer.",
|
||||
"registration.request.server.error": "Une erreur est survenue. Essayer de rafraîchir la page, ou vérifier votre connexion Internet.",
|
||||
@@ -152,26 +141,11 @@
|
||||
"privacy.policy": "Politique de confidentialité",
|
||||
"honor.code": "Code d'honneur",
|
||||
"terms.of.service": " Conditions d'utilisation",
|
||||
"registration.year.of.birth.label": "Année de naissance (facultatif)",
|
||||
"registration.field.gender.options.label": "Sexe (facultatif)",
|
||||
"registration.goals.label": "Dites-nous pourquoi vous êtes intéressé par edX (facultatif)",
|
||||
"registration.field.gender.options.f": "Femme",
|
||||
"registration.field.gender.options.m": "Homme",
|
||||
"registration.field.gender.options.o": "Autre / Préfère ne pas répondre",
|
||||
"registration.field.education.levels.label": "Plus haut niveau de scolarité atteint (facultatif)",
|
||||
"registration.field.education.levels.p": "Doctorat",
|
||||
"registration.field.education.levels.m": "Master ou diplôme professionnel",
|
||||
"registration.field.education.levels.b": "Diplôme de premier cycle supérieur",
|
||||
"registration.field.education.levels.a": "Grade de l'associé",
|
||||
"registration.field.education.levels.hs": "Lycée / enseignement secondaire",
|
||||
"registration.field.education.levels.jhs": "Collège / enseignement secondaire inférieur",
|
||||
"registration.field.education.levels.el": "Enseignement primaire",
|
||||
"registration.field.education.levels.none": "Sans diplôme",
|
||||
"registration.field.education.levels.other": "Autre niveau d'étude",
|
||||
"registration.username.suggestion.label": "Suggéré :",
|
||||
"registration.using.tpa.form.heading": "Terminer la création de votre compte",
|
||||
"did.you.mean.alert.text": "Vouliez-vous dire",
|
||||
"register.page.terms.of.service": "J'accepte les {termsOfService} {platformName}",
|
||||
"register.page.terms.of.service.and.honor.code": "En créant un compte, vous acceptez le {tosAndHonorCode} et vous reconnaissez que {platformName} et chaque\n membre peut traiter vos données personnelles conformément à la {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Connectez-vous",
|
||||
"reset.password.page.title": "Réinitialiser le mot de passe | {siteName}",
|
||||
"reset.password": "Réinitialiser le mot de passe",
|
||||
@@ -180,40 +154,10 @@
|
||||
"confirm.password.label": "Confirmer le mot de passe",
|
||||
"passwords.do.not.match": "Les mots de passe ne correspondent pas",
|
||||
"confirm.your.password": "Confirmer votre mot de passe",
|
||||
"forgot.password.confirmation.sign.in.link": "connexion",
|
||||
"reset.password.request.forgot.password.text": "Mot de passe oublié",
|
||||
"reset.password.request.invalid.token.header": "Lien de réinitialisation du mot de passe non valide",
|
||||
"reset.password.empty.new.password.field.error": "Veuillez entrer votre nouveau mot de passe.",
|
||||
"reset.password.failure.heading": "Nous n'avons pas pu réinitialiser votre mot de passe.",
|
||||
"reset.password.form.submission.error": "Veuillez vérifier vos réponses et réessayer.",
|
||||
"reset.password.request.server.error": "Échec de la réinitialisation du mot de passe",
|
||||
"reset.password.token.validation.sever.error": "Échec de la validation du jeton",
|
||||
"reset.server.rate.limit.error": "Trop de demandes.",
|
||||
"reset.password.success.heading": "Réinitialisation du mot de passe complétée.",
|
||||
"reset.password.success": "Votre mot de passe a été réinitialisé. Connectez-vous à votre compte.",
|
||||
"progressive.profiling.page.title": "Champs optionnels | {siteName}",
|
||||
"progressive.profiling.page.heading": "Quelques questions pour vous nous aideront à devenir plus intelligents.",
|
||||
"gender.options.label": "Sexe (facultatif)",
|
||||
"gender.options.f": "Femme",
|
||||
"gender.options.m": "Homme",
|
||||
"gender.options.o": "Autre / Préfère ne pas répondre",
|
||||
"education.levels.label": "Plus haut niveau de scolarité atteint (facultatif)",
|
||||
"education.levels.p": "Doctorat",
|
||||
"education.levels.m": "Master ou diplôme professionnel",
|
||||
"education.levels.b": "Diplôme de premier cycle supérieur",
|
||||
"education.levels.a": "Grade de l'associé",
|
||||
"education.levels.hs": "Lycée / enseignement secondaire",
|
||||
"education.levels.jhs": "Collège / enseignement secondaire inférieur",
|
||||
"education.levels.el": "Enseignement primaire",
|
||||
"education.levels.none": "Sans diplôme",
|
||||
"education.levels.other": "Autre niveau d'étude",
|
||||
"year.of.birth.label": "Année de naissance (facultatif)",
|
||||
"optional.fields.information.link": "En savoir plus sur la façon dont nous utilisons ces informations.",
|
||||
"optional.fields.submit.button": "Envoyez",
|
||||
"optional.fields.skip.button": "Ignorer pour l'instant",
|
||||
"continue.to.platform": "Continuer vers {platformName}",
|
||||
"modal.title": "Merci de nous en informer.",
|
||||
"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."
|
||||
"rate.limit.error": "Une erreur s'est produite en raison d'un trop grand nombre de demandes. Veuillez réessayer après un certain temps."
|
||||
}
|
||||
@@ -5,15 +5,9 @@
|
||||
"complete.your.profile.2": "your profile",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
||||
"forgot.password.confirmation.title": "Check your email",
|
||||
"forgot.password.confirmation.support.link": "contact technical support",
|
||||
"forgot.password.confirmation.info": "If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.",
|
||||
"logistration.sign.in": "Sign in",
|
||||
"logistration.register": "Register",
|
||||
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"server.ratelimit.error.message": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
|
||||
"enterprisetpa.sso.button.title": "Sign in using {providerName}",
|
||||
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
|
||||
"sso.sign.in.with": "Sign in with {providerName}",
|
||||
"sso.create.account.using": "Create account using {providerName}",
|
||||
@@ -26,6 +20,7 @@
|
||||
"tpa.alert.heading": "Almost done!",
|
||||
"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",
|
||||
"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}",
|
||||
@@ -38,7 +33,6 @@
|
||||
"forgot.password.error.message.title": "An error occurred.",
|
||||
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
|
||||
"forgot.password.empty.email.field.error": "Enter your email",
|
||||
"forgot.password.invalid.email.message": "The email address you've provided isn't formatted correctly.",
|
||||
"forgot.password.email.help.text": "The email address you used to register with {platformName}",
|
||||
"confirmation.message.title": "Check your email",
|
||||
"confirmation.support.link": "contact technical support",
|
||||
@@ -53,7 +47,6 @@
|
||||
"token.validation.internal.sever.error.heading": "Token validation failure",
|
||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
@@ -65,45 +58,32 @@
|
||||
"login.user.identity.label": "Username or email",
|
||||
"login.password.label": "Password",
|
||||
"sign.in.button": "Sign in",
|
||||
"sign.in.btn.pending.state": "Loading",
|
||||
"need.help.signing.in.collapsible.menu": "Need help signing in?",
|
||||
"forgot.password.link": "Forgot my password",
|
||||
"forgot.password": "Forgot password",
|
||||
"other.sign.in.issues": "Other sign in issues",
|
||||
"need.other.help.signing.in.collapsible.menu": "Need other help signing in?",
|
||||
"institution.login.button": "Institution/campus credentials",
|
||||
"institution.login.page.title": "Sign in with institution/campus credentials",
|
||||
"institution.login.page.back.button": "Back to sign in",
|
||||
"create.an.account": "Create an account",
|
||||
"login.other.options.heading": "Or sign in with:",
|
||||
"non.compliant.password.title": "We recently changed our password requirements",
|
||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
||||
"first.time.here": "First time here?",
|
||||
"email.help.message": "The email address you used to register with edX.",
|
||||
"enterprise.login.btn.text": "Company or school credentials",
|
||||
"email.format.validation.message": "The email address you've provided isn't formatted correctly.",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
||||
"email.validation.message": "Enter your username or email",
|
||||
"password.validation.message": "Password criteria has not been met",
|
||||
"register.link": "Create an account",
|
||||
"sign.in.heading": "Sign in",
|
||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
||||
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
|
||||
"account.activation.info.message": "This account has already been activated.",
|
||||
"account.activation.error.message.title": "Your account could not be activated",
|
||||
"account.activation.support.link": "contact support",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
|
||||
"account.confirmation.success.message": "Sign in to continue.",
|
||||
"account.confirmation.info.message": "This email has already been confirmed.",
|
||||
"account.confirmation.error.message.title": "Your email could not be confirmed",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
|
||||
"login.failure.header.title": "We couldn't sign you in.",
|
||||
"contact.support.link": "contact {platformName} support",
|
||||
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
|
||||
"login.failed.attempt.error": "You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.locked.out.error.message": "To protect your account, it’s been temporarily locked. Try again in {lockedOutPeriod} minutes.",
|
||||
"login.form.invalid.error.message": "Please fill in the fields below.",
|
||||
"login.incorrect.credentials.error.reset.link.text": "reset your password",
|
||||
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
|
||||
@@ -113,8 +93,20 @@
|
||||
"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",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"progressive.profiling.page.title": "Optional Fields | {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",
|
||||
"optional.fields.skip.button": "Skip for now",
|
||||
"optional.fields.next.button": "Next",
|
||||
"continue.to.platform": "Continue to {platformName}",
|
||||
"modal.title": "Thanks for letting us know.",
|
||||
"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.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "Register | {siteName}",
|
||||
"registration.fullname.label": "Full name",
|
||||
"registration.email.label": "Email",
|
||||
@@ -127,22 +119,19 @@
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
"register.institution.login.button": "Institution/campus credentials",
|
||||
"register.institution.login.page.title": "Register with institution/campus credentials",
|
||||
"empty.name.field.error": "Enter your full name",
|
||||
"empty.email.field.error": "Enter your email",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"empty.username.field.error": "Username must be between 2 and 30 characters",
|
||||
"empty.password.field.error": "Password criteria has not been met",
|
||||
"empty.country.field.error": "Select your country or region of residence",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"email.invalid.format.error": "Enter a valid email address",
|
||||
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
|
||||
"username.validation.message": "Username must be between 2 and 30 characters",
|
||||
"name.validation.message": "Enter a valid name",
|
||||
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces",
|
||||
"support.education.research": "Support education research by providing additional information. (Optional)",
|
||||
"registration.request.failure.header": "We couldn't create your account.",
|
||||
"registration.empty.form.submission.error": "Please check your responses and try again.",
|
||||
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
@@ -152,26 +141,11 @@
|
||||
"privacy.policy": "Privacy Policy",
|
||||
"honor.code": "Honor Code",
|
||||
"terms.of.service": "Terms of Service",
|
||||
"registration.year.of.birth.label": "Year of birth (optional)",
|
||||
"registration.field.gender.options.label": "Gender (optional)",
|
||||
"registration.goals.label": "Tell us why you're interested in edX (optional)",
|
||||
"registration.field.gender.options.f": "Female",
|
||||
"registration.field.gender.options.m": "Male",
|
||||
"registration.field.gender.options.o": "Other/Prefer not to say",
|
||||
"registration.field.education.levels.label": "Highest level of education completed (optional)",
|
||||
"registration.field.education.levels.p": "Doctorate",
|
||||
"registration.field.education.levels.m": "Master's or professional degree",
|
||||
"registration.field.education.levels.b": "Bachelor's degree",
|
||||
"registration.field.education.levels.a": "Associate's degree",
|
||||
"registration.field.education.levels.hs": "Secondary/high school",
|
||||
"registration.field.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"registration.field.education.levels.el": "Elementary/primary school",
|
||||
"registration.field.education.levels.none": "No formal education",
|
||||
"registration.field.education.levels.other": "Other education",
|
||||
"registration.username.suggestion.label": "Suggested:",
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"did.you.mean.alert.text": "Did you mean",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Sign in",
|
||||
"reset.password.page.title": "Reset Password | {siteName}",
|
||||
"reset.password": "Reset password",
|
||||
@@ -180,40 +154,10 @@
|
||||
"confirm.password.label": "Confirm password",
|
||||
"passwords.do.not.match": "Passwords do not match",
|
||||
"confirm.your.password": "Confirm your password",
|
||||
"forgot.password.confirmation.sign.in.link": "sign in",
|
||||
"reset.password.request.forgot.password.text": "Forgot password",
|
||||
"reset.password.request.invalid.token.header": "Invalid password reset link",
|
||||
"reset.password.empty.new.password.field.error": "Please enter your new password.",
|
||||
"reset.password.failure.heading": "We couldn't reset your password.",
|
||||
"reset.password.form.submission.error": "Please check your responses and try again.",
|
||||
"reset.password.request.server.error": "Failed to reset password",
|
||||
"reset.password.token.validation.sever.error": "Token validation failure",
|
||||
"reset.server.rate.limit.error": "Too many requests.",
|
||||
"reset.password.success.heading": "Password reset complete.",
|
||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
||||
"progressive.profiling.page.title": "Optional Fields | {siteName}",
|
||||
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
|
||||
"gender.options.label": "Gender (optional)",
|
||||
"gender.options.f": "Female",
|
||||
"gender.options.m": "Male",
|
||||
"gender.options.o": "Other/Prefer not to say",
|
||||
"education.levels.label": "Highest level of education completed (optional)",
|
||||
"education.levels.p": "Doctorate",
|
||||
"education.levels.m": "Master's or professional degree",
|
||||
"education.levels.b": "Bachelor's degree",
|
||||
"education.levels.a": "Associate's degree",
|
||||
"education.levels.hs": "Secondary/high school",
|
||||
"education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"education.levels.el": "Elementary/primary school",
|
||||
"education.levels.none": "No formal education",
|
||||
"education.levels.other": "Other education",
|
||||
"year.of.birth.label": "Year of birth (optional)",
|
||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
||||
"optional.fields.submit.button": "Submit",
|
||||
"optional.fields.skip.button": "Skip for now",
|
||||
"continue.to.platform": "Continue to {platformName}",
|
||||
"modal.title": "Thanks for letting us know.",
|
||||
"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."
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
|
||||
}
|
||||
@@ -5,15 +5,9 @@
|
||||
"complete.your.profile.2": "Il tuo profilo",
|
||||
"welcome.to.platform": "Benvenuto in {siteName}, {username}!",
|
||||
"institution.login.page.sub.heading": "Scegli il tuo istituto dall'elenco sottostante",
|
||||
"forgot.password.confirmation.title": "Controlla la tua casella di posta",
|
||||
"forgot.password.confirmation.support.link": "contatta il supporto tecnico",
|
||||
"forgot.password.confirmation.info": "Se non ricevi un messaggio di reimpostazione della password entro 1 minuto, verifica di aver inserito l'indirizzo e-mail corretto o controlla la cartella della posta indesiderata.",
|
||||
"logistration.sign.in": "Accedi",
|
||||
"logistration.register": "Registrazione",
|
||||
"internal.server.error.message": "Si è verificato un errore. Prova ad aggiornare la pagina oppure verifica la connessione internet.",
|
||||
"server.ratelimit.error.message": "Si è verificato un errore dovuto alle troppe richieste. Prova di nuovo più tardi.",
|
||||
"enterprisetpa.title.heading": "Vuoi accedere utilizzando le credenziali {providerName}?",
|
||||
"enterprisetpa.sso.button.title": "Accedi utilizzando {providerName}",
|
||||
"enterprisetpa.login.button.text": "Mostrami altre modalità di accesso o registrazione",
|
||||
"sso.sign.in.with": "Accedi con {providerName}",
|
||||
"sso.create.account.using": "Crea un account utilizzando {providerName}",
|
||||
@@ -26,6 +20,7 @@
|
||||
"tpa.alert.heading": "Quasi fatto!",
|
||||
"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",
|
||||
"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}",
|
||||
@@ -38,7 +33,6 @@
|
||||
"forgot.password.error.message.title": "Si è verificato un errore. ",
|
||||
"forgot.password.request.in.progress.message": "La tua richiesta precedente è in corso di elaborazione, riprova tra qualche istante. ",
|
||||
"forgot.password.empty.email.field.error": "Inserisci il tuo indirizzo email",
|
||||
"forgot.password.invalid.email.message": "L'indirizzo email che hai fornito non è formattato correttamente. ",
|
||||
"forgot.password.email.help.text": "L'indirizzo email che hai utilizzato per registrarti con {platformName}",
|
||||
"confirmation.message.title": "Controlla la tua casella di posta",
|
||||
"confirmation.support.link": "contatta il supporto tecnico",
|
||||
@@ -53,7 +47,6 @@
|
||||
"token.validation.internal.sever.error.heading": "Errore di convalida del token",
|
||||
"token.validation.internal.sever.error": "Si è verificato un errore. Prova ad aggiornare la pagina oppure verifica la connessione internet.",
|
||||
"internal.server.error": "Si è verificato un errore. Prova ad aggiornare la pagina oppure verifica la connessione internet.",
|
||||
"rate.limit.error": "Si è verificato un errore dovuto alle troppe richieste. Prova di nuovo più tardi.",
|
||||
"account.activation.error.message": "Si è verificato un errore, seleziona {supportLink} per risolvere il problema. ",
|
||||
"login.inactive.user.error": "Per accedere, devi attivare il tuo account.{lineBreak} {lineBreak}Abbiamo appena inviato un link di attivazione a {email}. Se non ricevi un'email, controlla la cartella della posta indesiderata oppure seleziona {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
@@ -65,45 +58,32 @@
|
||||
"login.user.identity.label": "Nome utente o email ",
|
||||
"login.password.label": "Password",
|
||||
"sign.in.button": "Accedi",
|
||||
"sign.in.btn.pending.state": "Caricamento",
|
||||
"need.help.signing.in.collapsible.menu": "Hai bisogno di aiuto per l'accesso?",
|
||||
"forgot.password.link": "Ho dimenticato la mia password",
|
||||
"forgot.password": "Password dimenticata",
|
||||
"other.sign.in.issues": "Altri problemi legati all'accesso",
|
||||
"need.other.help.signing.in.collapsible.menu": "Hai bisogno di ulteriore aiuto per l'accesso?",
|
||||
"institution.login.button": "Credenziali dell'istituto/campus",
|
||||
"institution.login.page.title": "Accedi con le credenziali dell'istituzione/campus",
|
||||
"institution.login.page.back.button": "Torna all'accesso",
|
||||
"create.an.account": "Crea un account",
|
||||
"login.other.options.heading": "Oppure accedi con:",
|
||||
"non.compliant.password.title": "Abbiamo di recente modificato i requisiti per la password ",
|
||||
"non.compliant.password.message": "La tua password attuale non soddisfa i nuovi requisiti di sicurezza. Abbiamo appena inviato un messaggio di reimpostazione della password all'indirizzo e-mail associato a questo account. Grazie per averci aiutato a mantenere i tuoi dati al sicuro.",
|
||||
"account.locked.out.message.1": "Per proteggere il tuo account, è stato temporaneamente bloccato. Riprova tra 30 minuti.",
|
||||
"first.time.here": "È la prima volta che ci visiti?",
|
||||
"email.help.message": "L'indirizzo email che hai utilizzato per registrarti con edX.",
|
||||
"enterprise.login.btn.text": "Credenziali aziendali o scolastiche",
|
||||
"email.format.validation.message": "L'indirizzo email che hai fornito non è formattato correttamente. ",
|
||||
"username.or.email.format.validation.less.chars.message": "Il nome utente o l'e-mail deve contenere almeno 3 caratteri.",
|
||||
"email.validation.message": "Inserisci il tuo nome utente o e-mail",
|
||||
"password.validation.message": "I criteri della password non sono stati soddisfatti",
|
||||
"register.link": "Crea un account",
|
||||
"sign.in.heading": "Accedi",
|
||||
"account.activation.success.message.title": "Completato correttamente! Hai attivato il tuo account. ",
|
||||
"account.activation.success.message": "A breve ti invieremo avvisi e aggiornamenti via email relativi al corso a cui ti sei iscritto. Accedi per proseguire.",
|
||||
"account.activation.info.message": "Questo account è già stato attivato.",
|
||||
"account.activation.error.message.title": "Impossibile attivare il tuo account.",
|
||||
"account.activation.support.link": "contatta il supporto",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"account.confirmation.success.message.title": "Successo! Hai confermato la tua email.",
|
||||
"account.confirmation.success.message": "Accedi per continuare.",
|
||||
"account.confirmation.info.message": "Questa email è già stata confermata.",
|
||||
"account.confirmation.error.message.title": "Impossibile confermare la tua email",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"internal.server.error.message": "Si è verificato un errore. Prova ad aggiornare la pagina oppure verifica la connessione internet.",
|
||||
"login.rate.limit.reached.message": "Troppi tentativi di login falliti. Riprova più tardi.",
|
||||
"login.failure.header.title": "Impossibile autorizzare il tuo accesso.",
|
||||
"contact.support.link": "contatta il supporto {platformName} ",
|
||||
"login.incorrect.credentials.error": "Il nome utente, l'e-mail o la password inseriti non sono corretti. Per favore riprova.",
|
||||
"login.failed.attempt.error": "Hai a disposizione altri {remainingAttempts} tentativi di accesso prima che il tuo account venga temporaneamente bloccato.",
|
||||
"login.locked.out.error.message": "Il tuo account è stato temporaneamente bloccato per motivi di sicurezza. Riprova tra {lockedOutPeriod} minuti.",
|
||||
"login.form.invalid.error.message": "Si prega di compilare i campi sottostanti.",
|
||||
"login.incorrect.credentials.error.reset.link.text": "reimposta la tua password",
|
||||
"login.incorrect.credentials.error.before.account.blocked.text": "clicca qui per ripristinarlo.",
|
||||
@@ -113,8 +93,20 @@
|
||||
"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",
|
||||
"register.page.terms.of.service.and.honor.code": "Creando un account, accetti il {tosAndHonorCode} e riconosci che {platformName} e ciascun Membro trattano i tuoi dati personali in conformità con l' {privacyPolicy}.",
|
||||
"register.page.honor.code": "Accetto il {platformName} {tosAndHonorCode}",
|
||||
"progressive.profiling.page.title": "Campi facoltativi | {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",
|
||||
"optional.fields.skip.button": "Salta per ora",
|
||||
"optional.fields.next.button": "Next",
|
||||
"continue.to.platform": "Continua con {platformName}",
|
||||
"modal.title": "Grazie per averci fatto sapere.",
|
||||
"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.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "Registrazione | {siteName}",
|
||||
"registration.fullname.label": "Nome e Cognome",
|
||||
"registration.email.label": "Email",
|
||||
@@ -127,22 +119,19 @@
|
||||
"help.text.username.2": "Questo non può essere modificato in seguito.",
|
||||
"help.text.email": "Per l'attivazione dell'account e aggiornamenti importanti",
|
||||
"create.account.for.free.button": "Crea un account gratis",
|
||||
"create.an.account.btn.pending.state": "Caricamento",
|
||||
"registration.other.options.heading": "Oppure registrati con:",
|
||||
"register.institution.login.button": "Credenziali dell'istituto/campus",
|
||||
"register.institution.login.page.title": "Registrati con le credenziali dell'istituzione/campus",
|
||||
"empty.name.field.error": "Inserisci il tuo nome e cognome",
|
||||
"empty.email.field.error": "Inserisci il tuo indirizzo email",
|
||||
"email.do.not.match": "Gli indirizzi email non corrispondono.",
|
||||
"empty.username.field.error": "Il nome utente deve essere compreso tra 2 e 30 caratteri",
|
||||
"empty.password.field.error": "I criteri della password non sono stati soddisfatti",
|
||||
"empty.country.field.error": "Seleziona il tuo paese o regione di residenza",
|
||||
"email.do.not.match": "Gli indirizzi email non corrispondono.",
|
||||
"email.invalid.format.error": "Inserisci un indirizzo email valido",
|
||||
"email.ratelimit.less.chars.validation.message": "Email deve avere 3 caratteri.",
|
||||
"username.validation.message": "Il nome utente deve essere compreso tra 2 e 30 caratteri",
|
||||
"name.validation.message": "Inserisci un nome valido",
|
||||
"username.format.validation.message": "I nomi utente possono contenere solo lettere (AZ, az), numeri (0-9), trattini bassi (_) e trattini (-). I nomi utente non possono contenere spazi",
|
||||
"support.education.research": "Supportare la ricerca del livello di istruzione fornendo informazioni aggiuntive. (Facoltativo)",
|
||||
"registration.request.failure.header": "Impossibile creare il tuo account.",
|
||||
"registration.empty.form.submission.error": "Controlla le tue risposte e riprova.",
|
||||
"registration.request.server.error": "Si è verificato un errore. Prova ad aggiornare la pagina oppure verifica la connessione internet.",
|
||||
@@ -152,26 +141,11 @@
|
||||
"privacy.policy": "Informativa sulla privacy",
|
||||
"honor.code": "Codice d'Onore",
|
||||
"terms.of.service": "Termini di Servizio",
|
||||
"registration.year.of.birth.label": "Anno di nascita (facoltativo)",
|
||||
"registration.field.gender.options.label": "Genere (facoltativo)",
|
||||
"registration.goals.label": "Dicci perché sei interessato a edX (facoltativo)",
|
||||
"registration.field.gender.options.f": "Femmina",
|
||||
"registration.field.gender.options.m": "Maschio",
|
||||
"registration.field.gender.options.o": "Altro/Preferisco non dire",
|
||||
"registration.field.education.levels.label": "Livello di istruzione più elevato raggiunto (opzionale) ",
|
||||
"registration.field.education.levels.p": "Dottorato",
|
||||
"registration.field.education.levels.m": "Laurea magistrale o titolo accademico professionale",
|
||||
"registration.field.education.levels.b": "Laurea di primo livello ",
|
||||
"registration.field.education.levels.a": "Diploma Professionale",
|
||||
"registration.field.education.levels.hs": "Scuola Superiore/Liceo",
|
||||
"registration.field.education.levels.jhs": "Scuola Media",
|
||||
"registration.field.education.levels.el": "Scuola Primaria/Elementare",
|
||||
"registration.field.education.levels.none": "Nessun livello educativo formale",
|
||||
"registration.field.education.levels.other": "Altro tipo di formazione",
|
||||
"registration.username.suggestion.label": "Suggerito:",
|
||||
"registration.using.tpa.form.heading": "Completa la creazione del tuo account",
|
||||
"did.you.mean.alert.text": "Intendevi",
|
||||
"register.page.terms.of.service": "Accetto il {platformName} {termsOfService}",
|
||||
"register.page.terms.of.service.and.honor.code": "Creando un account, accetti il {tosAndHonorCode} e riconosci che {platformName} e ciascun Membro trattano i tuoi dati personali in conformità con l' {privacyPolicy}.",
|
||||
"register.page.honor.code": "Accetto {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "Accetto {platformName} {termsOfService}",
|
||||
"sign.in": "Accedi",
|
||||
"reset.password.page.title": "Ripristina password | {siteName}",
|
||||
"reset.password": "Resetta la password",
|
||||
@@ -180,40 +154,10 @@
|
||||
"confirm.password.label": "Conferma password",
|
||||
"passwords.do.not.match": "le passwords non corrispondono",
|
||||
"confirm.your.password": "Conferma la tua password",
|
||||
"forgot.password.confirmation.sign.in.link": "accedi",
|
||||
"reset.password.request.forgot.password.text": "Dimenticato la password",
|
||||
"reset.password.request.invalid.token.header": "Link di ripristino della password non valido",
|
||||
"reset.password.empty.new.password.field.error": "Immetti la nuova password.",
|
||||
"reset.password.failure.heading": "Impossibile ripristinare la tua password.",
|
||||
"reset.password.form.submission.error": "Controlla le tue risposte e riprova.",
|
||||
"reset.password.request.server.error": "Ripristino della password non riuscito",
|
||||
"reset.password.token.validation.sever.error": "Errore di convalida del token",
|
||||
"reset.server.rate.limit.error": "Troppe richieste.",
|
||||
"reset.password.success.heading": "Ripristino della password completato.",
|
||||
"reset.password.success": "La tua password è stata resettata. Accedi al tuo account.",
|
||||
"progressive.profiling.page.title": "Campi facoltativi | {siteName}",
|
||||
"progressive.profiling.page.heading": "Alcune domande per te ci aiuteranno a diventare più intelligenti.",
|
||||
"gender.options.label": "Genere (facoltativo)",
|
||||
"gender.options.f": "Femmina",
|
||||
"gender.options.m": "Maschio",
|
||||
"gender.options.o": "Altro/Preferisco non dire",
|
||||
"education.levels.label": "Livello di istruzione più elevato raggiunto (opzionale) ",
|
||||
"education.levels.p": "Dottorato",
|
||||
"education.levels.m": "Laurea magistrale o titolo accademico professionale",
|
||||
"education.levels.b": "Laurea di primo livello ",
|
||||
"education.levels.a": "Diploma Professionale",
|
||||
"education.levels.hs": "Scuola Superiore/Liceo",
|
||||
"education.levels.jhs": "Scuola Media",
|
||||
"education.levels.el": "Scuola Primaria/Elementare",
|
||||
"education.levels.none": "Nessun livello educativo formale",
|
||||
"education.levels.other": "Altro livello educativo",
|
||||
"year.of.birth.label": "Anno di nascita (facoltativo)",
|
||||
"optional.fields.information.link": "Ulteriori informazioni su come utilizziamo queste informazioni.",
|
||||
"optional.fields.submit.button": "Invia",
|
||||
"optional.fields.skip.button": "Salta per ora",
|
||||
"continue.to.platform": "Continua con {platformName}",
|
||||
"modal.title": "Grazie per averci fatto sapere.",
|
||||
"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."
|
||||
"rate.limit.error": "Si è verificato un errore dovuto alle troppe richieste. Prova di nuovo più tardi."
|
||||
}
|
||||
@@ -5,15 +5,9 @@
|
||||
"complete.your.profile.2": "o seu perfil",
|
||||
"welcome.to.platform": "Bem vindo a {siteName}, {username}!",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
||||
"forgot.password.confirmation.title": "Verifique o seu email",
|
||||
"forgot.password.confirmation.support.link": "contacto o suporte técnico",
|
||||
"forgot.password.confirmation.info": "Se não receber uma mensagem para alterar a palavra-passe após 1 minuto, verifique se introduziu o endereço de correio electrónico correcto, ou verifique a sua pasta de spam.",
|
||||
"logistration.sign.in": "Iniciar sessão",
|
||||
"logistration.register": "Registe-se",
|
||||
"internal.server.error.message": "Ocorreu um erro. Tente actualizar a página, ou verifique a sua ligação à Internet.",
|
||||
"server.ratelimit.error.message": "Ocorreu um erro devido a demasiados pedidos. Por favor, tente novamente após algum tempo.",
|
||||
"enterprisetpa.title.heading": "Gostaria de iniciar sessão usando as suas {providerName} credenciais?",
|
||||
"enterprisetpa.sso.button.title": "Inicie a sessão utilizando {providerName}",
|
||||
"enterprisetpa.login.button.text": "Mostre-me outras formas de iniciar sessão ou registar-se",
|
||||
"sso.sign.in.with": "Inicie sessão com {providerName}",
|
||||
"sso.create.account.using": "Criar conta usando {providerName}",
|
||||
@@ -26,6 +20,7 @@
|
||||
"tpa.alert.heading": "Almost done!",
|
||||
"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",
|
||||
"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}",
|
||||
@@ -38,7 +33,6 @@
|
||||
"forgot.password.error.message.title": "Ocorreu um erro.",
|
||||
"forgot.password.request.in.progress.message": "O seu pedido anterior está a ser processado, por favor tente novamente dentro de momentos.",
|
||||
"forgot.password.empty.email.field.error": "Enter your email",
|
||||
"forgot.password.invalid.email.message": "O endereço de email fornecido não está formatado correctamente.",
|
||||
"forgot.password.email.help.text": "O endereço de e-mail que usou para se registar em {platformName}",
|
||||
"confirmation.message.title": "Verifique o seu email",
|
||||
"confirmation.support.link": "contact technical support",
|
||||
@@ -53,7 +47,6 @@
|
||||
"token.validation.internal.sever.error.heading": "Token validation failure",
|
||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"account.activation.error.message": "Alguma coisa correu mal, siga {supportLink} para resolver esta questão.",
|
||||
"login.inactive.user.error": "Para iniciar sessão, precisa ativar a sua conta. {lineBreak}\n {lineBreak} Acabámos de enviar um link de ativação para {email}. Se não receber um e-mail,\n verifique as suas pastas de spam ou {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
@@ -65,45 +58,32 @@
|
||||
"login.user.identity.label": "Username or email",
|
||||
"login.password.label": "Password",
|
||||
"sign.in.button": "Iniciar Sessão",
|
||||
"sign.in.btn.pending.state": "Loading",
|
||||
"need.help.signing.in.collapsible.menu": "Precisa de ajuda para entrar?",
|
||||
"forgot.password.link": "Esqueci-me da minha palavra-passe",
|
||||
"forgot.password": "Forgot password",
|
||||
"other.sign.in.issues": "Outros problemas de inicio de sessão",
|
||||
"need.other.help.signing.in.collapsible.menu": "Precisa de outra ajuda para entrar?",
|
||||
"institution.login.button": "Institution/campus credentials",
|
||||
"institution.login.page.title": "Inicie sessão com credenciais de instituição/campus",
|
||||
"institution.login.page.back.button": "Voltar para iniciar sessão",
|
||||
"create.an.account": "Criar uma conta",
|
||||
"login.other.options.heading": "Or sign in with:",
|
||||
"non.compliant.password.title": "Recentemente mudámos os nossos requisitos de palavra-passe",
|
||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
||||
"first.time.here": "Está a entrar pela primeira vez?",
|
||||
"email.help.message": "O endereço de e-mail que usou para se registrar no edX.",
|
||||
"enterprise.login.btn.text": "Company or school credentials",
|
||||
"email.format.validation.message": "O endereço de e-mail fornecido não está formatado correctamente.",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
||||
"email.validation.message": "Enter your username or email",
|
||||
"password.validation.message": "Password criteria has not been met",
|
||||
"register.link": "Criar uma conta",
|
||||
"sign.in.heading": "Iniciar Sessão",
|
||||
"account.activation.success.message.title": "Sucesso! Você ativou a sua conta.",
|
||||
"account.activation.success.message": "Receberá agora actualizações por e-mail e alertas nossos relacionados com os cursos em que está inscrito. Inicie a sessão para continuar.",
|
||||
"account.activation.info.message": "This account has already been activated.",
|
||||
"account.activation.error.message.title": "A sua conta não pôde ser ativada",
|
||||
"account.activation.support.link": "contato de suporte",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
|
||||
"account.confirmation.success.message": "Sign in to continue.",
|
||||
"account.confirmation.info.message": "This email has already been confirmed.",
|
||||
"account.confirmation.error.message.title": "Your email could not be confirmed",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"internal.server.error.message": "Ocorreu um erro. Tente actualizar a página, ou verifique a sua ligação à Internet.",
|
||||
"login.rate.limit.reached.message": "Muitas tentativas de login sem sucesso. Tente novamente mais tarde.",
|
||||
"login.failure.header.title": "O seu acesso não foi possível.",
|
||||
"contact.support.link": "contactar o suporte {platformName}",
|
||||
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
|
||||
"login.failed.attempt.error": "Tem mais {remainingAttempts} tentativas de inicio sessão antes que a sua conta seja temporariamente bloqueada.",
|
||||
"login.locked.out.error.message": "Para proteger a sua conta, esta foi temporariamente bloqueada. Tente novamente em {lockedOutPeriod} minutos.",
|
||||
"login.form.invalid.error.message": "Please fill in the fields below.",
|
||||
"login.incorrect.credentials.error.reset.link.text": "reset your password",
|
||||
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
|
||||
@@ -113,8 +93,20 @@
|
||||
"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",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"progressive.profiling.page.title": "Optional Fields | {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",
|
||||
"optional.fields.skip.button": "Skip for now",
|
||||
"optional.fields.next.button": "Next",
|
||||
"continue.to.platform": "Continue to {platformName}",
|
||||
"modal.title": "Thanks for letting us know.",
|
||||
"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.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "Registar | {siteName}",
|
||||
"registration.fullname.label": "Full name",
|
||||
"registration.email.label": "Email",
|
||||
@@ -127,22 +119,19 @@
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
"register.institution.login.button": "Institution/campus credentials",
|
||||
"register.institution.login.page.title": "Registo com credenciais da instituição/campus",
|
||||
"empty.name.field.error": "Enter your full name",
|
||||
"empty.email.field.error": "Enter your email",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"empty.username.field.error": "Username must be between 2 and 30 characters",
|
||||
"empty.password.field.error": "Password criteria has not been met",
|
||||
"empty.country.field.error": "Select your country or region of residence",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"email.invalid.format.error": "Enter a valid email address",
|
||||
"email.ratelimit.less.chars.validation.message": "O e-mail deve ter 3 carateres.",
|
||||
"username.validation.message": "Username must be between 2 and 30 characters",
|
||||
"name.validation.message": "Enter a valid name",
|
||||
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces",
|
||||
"support.education.research": "Apoie a pesquisa em educação fornecendo informações adicionais. (Opcional)",
|
||||
"registration.request.failure.header": "Não foi possível criar a sua conta.",
|
||||
"registration.empty.form.submission.error": "Please check your responses and try again.",
|
||||
"registration.request.server.error": "Ocorreu um erro. Tente actualizar a página, ou verifique a sua ligação à Internet.",
|
||||
@@ -152,26 +141,11 @@
|
||||
"privacy.policy": "Política de Privacidade",
|
||||
"honor.code": "Honor Code",
|
||||
"terms.of.service": "Terms of Service",
|
||||
"registration.year.of.birth.label": "Ano de Nascimento (opcional)",
|
||||
"registration.field.gender.options.label": "Género (opcional)",
|
||||
"registration.goals.label": "Diga-nos porque está interessado no edX (opcional)",
|
||||
"registration.field.gender.options.f": "Feminino",
|
||||
"registration.field.gender.options.m": "Masculino",
|
||||
"registration.field.gender.options.o": "Outros/Prefere não dizer",
|
||||
"registration.field.education.levels.label": "Nível mais elevado de escolaridade concluído (opcional)",
|
||||
"registration.field.education.levels.p": "Doutoramento",
|
||||
"registration.field.education.levels.m": "Mestrado ou Grau Profissional",
|
||||
"registration.field.education.levels.b": "Licenciatura",
|
||||
"registration.field.education.levels.a": "Pós-graduação",
|
||||
"registration.field.education.levels.hs": "Secundário",
|
||||
"registration.field.education.levels.jhs": "2ªciclo/3ºciclo",
|
||||
"registration.field.education.levels.el": "Primária",
|
||||
"registration.field.education.levels.none": "Sem estudos",
|
||||
"registration.field.education.levels.other": "Outra educação",
|
||||
"registration.username.suggestion.label": "Suggested:",
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"did.you.mean.alert.text": "Did you mean",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Sign in",
|
||||
"reset.password.page.title": "Redefinir Palavra-passe | {siteName}",
|
||||
"reset.password": "Reset password",
|
||||
@@ -180,40 +154,10 @@
|
||||
"confirm.password.label": "Confirm password",
|
||||
"passwords.do.not.match": "Passwords do not match",
|
||||
"confirm.your.password": "Confirm your password",
|
||||
"forgot.password.confirmation.sign.in.link": "inicie a sessão",
|
||||
"reset.password.request.forgot.password.text": "Esqueci-me da palavra-passe",
|
||||
"reset.password.request.invalid.token.header": "Link para Redefinir Palavra-passe inválido",
|
||||
"reset.password.empty.new.password.field.error": "Por favor, introduza a sua nova palavra-passe.",
|
||||
"reset.password.failure.heading": "We couldn't reset your password.",
|
||||
"reset.password.form.submission.error": "Please check your responses and try again.",
|
||||
"reset.password.request.server.error": "Falha na redefinição da palavra-passe",
|
||||
"reset.password.token.validation.sever.error": "Falha na validação do Token",
|
||||
"reset.server.rate.limit.error": "Too many requests.",
|
||||
"reset.password.success.heading": "Password reset complete.",
|
||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
||||
"progressive.profiling.page.title": "Optional Fields | {siteName}",
|
||||
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
|
||||
"gender.options.label": "Gender (optional)",
|
||||
"gender.options.f": "Female",
|
||||
"gender.options.m": "Male",
|
||||
"gender.options.o": "Other/Prefer not to say",
|
||||
"education.levels.label": "Highest level of education completed (optional)",
|
||||
"education.levels.p": "Doctorate",
|
||||
"education.levels.m": "Master's or professional degree",
|
||||
"education.levels.b": "Bachelor's degree",
|
||||
"education.levels.a": "Associate's degree",
|
||||
"education.levels.hs": "Secondary/high school",
|
||||
"education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"education.levels.el": "Elementary/primary school",
|
||||
"education.levels.none": "No formal education",
|
||||
"education.levels.other": "Other education",
|
||||
"year.of.birth.label": "Year of birth (optional)",
|
||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
||||
"optional.fields.submit.button": "Submit",
|
||||
"optional.fields.skip.button": "Skip for now",
|
||||
"continue.to.platform": "Continue to {platformName}",
|
||||
"modal.title": "Thanks for letting us know.",
|
||||
"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."
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
|
||||
}
|
||||
@@ -5,15 +5,9 @@
|
||||
"complete.your.profile.2": "your profile",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
||||
"forgot.password.confirmation.title": "Check your email",
|
||||
"forgot.password.confirmation.support.link": "contact technical support",
|
||||
"forgot.password.confirmation.info": "If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.",
|
||||
"logistration.sign.in": "Sign in",
|
||||
"logistration.register": "Register",
|
||||
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"server.ratelimit.error.message": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
|
||||
"enterprisetpa.sso.button.title": "Sign in using {providerName}",
|
||||
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
|
||||
"sso.sign.in.with": "Sign in with {providerName}",
|
||||
"sso.create.account.using": "Create account using {providerName}",
|
||||
@@ -26,6 +20,7 @@
|
||||
"tpa.alert.heading": "Almost done!",
|
||||
"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",
|
||||
"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}",
|
||||
@@ -38,7 +33,6 @@
|
||||
"forgot.password.error.message.title": "An error occurred.",
|
||||
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
|
||||
"forgot.password.empty.email.field.error": "Enter your email",
|
||||
"forgot.password.invalid.email.message": "The email address you've provided isn't formatted correctly.",
|
||||
"forgot.password.email.help.text": "The email address you used to register with {platformName}",
|
||||
"confirmation.message.title": "Check your email",
|
||||
"confirmation.support.link": "contact technical support",
|
||||
@@ -53,7 +47,6 @@
|
||||
"token.validation.internal.sever.error.heading": "Token validation failure",
|
||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
@@ -65,45 +58,32 @@
|
||||
"login.user.identity.label": "Username or email",
|
||||
"login.password.label": "Password",
|
||||
"sign.in.button": "Sign in",
|
||||
"sign.in.btn.pending.state": "Loading",
|
||||
"need.help.signing.in.collapsible.menu": "Need help signing in?",
|
||||
"forgot.password.link": "Forgot my password",
|
||||
"forgot.password": "Forgot password",
|
||||
"other.sign.in.issues": "Other sign in issues",
|
||||
"need.other.help.signing.in.collapsible.menu": "Need other help signing in?",
|
||||
"institution.login.button": "Institution/campus credentials",
|
||||
"institution.login.page.title": "Sign in with institution/campus credentials",
|
||||
"institution.login.page.back.button": "Back to sign in",
|
||||
"create.an.account": "Create an account",
|
||||
"login.other.options.heading": "Or sign in with:",
|
||||
"non.compliant.password.title": "We recently changed our password requirements",
|
||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
||||
"first.time.here": "First time here?",
|
||||
"email.help.message": "The email address you used to register with edX.",
|
||||
"enterprise.login.btn.text": "Company or school credentials",
|
||||
"email.format.validation.message": "The email address you've provided isn't formatted correctly.",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
||||
"email.validation.message": "Enter your username or email",
|
||||
"password.validation.message": "Password criteria has not been met",
|
||||
"register.link": "Create an account",
|
||||
"sign.in.heading": "Sign in",
|
||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
||||
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
|
||||
"account.activation.info.message": "This account has already been activated.",
|
||||
"account.activation.error.message.title": "Your account could not be activated",
|
||||
"account.activation.support.link": "contact support",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
|
||||
"account.confirmation.success.message": "Sign in to continue.",
|
||||
"account.confirmation.info.message": "This email has already been confirmed.",
|
||||
"account.confirmation.error.message.title": "Your email could not be confirmed",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
|
||||
"login.failure.header.title": "We couldn't sign you in.",
|
||||
"contact.support.link": "contact {platformName} support",
|
||||
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
|
||||
"login.failed.attempt.error": "You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.locked.out.error.message": "To protect your account, it’s been temporarily locked. Try again in {lockedOutPeriod} minutes.",
|
||||
"login.form.invalid.error.message": "Please fill in the fields below.",
|
||||
"login.incorrect.credentials.error.reset.link.text": "reset your password",
|
||||
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
|
||||
@@ -113,8 +93,20 @@
|
||||
"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",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"progressive.profiling.page.title": "Optional Fields | {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",
|
||||
"optional.fields.skip.button": "Skip for now",
|
||||
"optional.fields.next.button": "Next",
|
||||
"continue.to.platform": "Continue to {platformName}",
|
||||
"modal.title": "Thanks for letting us know.",
|
||||
"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.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "Register | {siteName}",
|
||||
"registration.fullname.label": "Full name",
|
||||
"registration.email.label": "Email",
|
||||
@@ -127,22 +119,19 @@
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
"register.institution.login.button": "Institution/campus credentials",
|
||||
"register.institution.login.page.title": "Register with institution/campus credentials",
|
||||
"empty.name.field.error": "Enter your full name",
|
||||
"empty.email.field.error": "Enter your email",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"empty.username.field.error": "Username must be between 2 and 30 characters",
|
||||
"empty.password.field.error": "Password criteria has not been met",
|
||||
"empty.country.field.error": "Select your country or region of residence",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"email.invalid.format.error": "Enter a valid email address",
|
||||
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
|
||||
"username.validation.message": "Username must be between 2 and 30 characters",
|
||||
"name.validation.message": "Enter a valid name",
|
||||
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces",
|
||||
"support.education.research": "Support education research by providing additional information. (Optional)",
|
||||
"registration.request.failure.header": "We couldn't create your account.",
|
||||
"registration.empty.form.submission.error": "Please check your responses and try again.",
|
||||
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
@@ -152,26 +141,11 @@
|
||||
"privacy.policy": "Privacy Policy",
|
||||
"honor.code": "Honor Code",
|
||||
"terms.of.service": "Terms of Service",
|
||||
"registration.year.of.birth.label": "Year of birth (optional)",
|
||||
"registration.field.gender.options.label": "Gender (optional)",
|
||||
"registration.goals.label": "Tell us why you're interested in edX (optional)",
|
||||
"registration.field.gender.options.f": "Female",
|
||||
"registration.field.gender.options.m": "Male",
|
||||
"registration.field.gender.options.o": "Other/Prefer not to say",
|
||||
"registration.field.education.levels.label": "Highest level of education completed (optional)",
|
||||
"registration.field.education.levels.p": "Doctorate",
|
||||
"registration.field.education.levels.m": "Master's or professional degree",
|
||||
"registration.field.education.levels.b": "Bachelor's degree",
|
||||
"registration.field.education.levels.a": "Associate's degree",
|
||||
"registration.field.education.levels.hs": "Secondary/high school",
|
||||
"registration.field.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"registration.field.education.levels.el": "Elementary/primary school",
|
||||
"registration.field.education.levels.none": "No formal education",
|
||||
"registration.field.education.levels.other": "Other education",
|
||||
"registration.username.suggestion.label": "Suggested:",
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"did.you.mean.alert.text": "Did you mean",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Sign in",
|
||||
"reset.password.page.title": "Reset Password | {siteName}",
|
||||
"reset.password": "Reset password",
|
||||
@@ -180,40 +154,10 @@
|
||||
"confirm.password.label": "Confirm password",
|
||||
"passwords.do.not.match": "Passwords do not match",
|
||||
"confirm.your.password": "Confirm your password",
|
||||
"forgot.password.confirmation.sign.in.link": "sign in",
|
||||
"reset.password.request.forgot.password.text": "Forgot password",
|
||||
"reset.password.request.invalid.token.header": "Invalid password reset link",
|
||||
"reset.password.empty.new.password.field.error": "Please enter your new password.",
|
||||
"reset.password.failure.heading": "We couldn't reset your password.",
|
||||
"reset.password.form.submission.error": "Please check your responses and try again.",
|
||||
"reset.password.request.server.error": "Failed to reset password",
|
||||
"reset.password.token.validation.sever.error": "Token validation failure",
|
||||
"reset.server.rate.limit.error": "Too many requests.",
|
||||
"reset.password.success.heading": "Password reset complete.",
|
||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
||||
"progressive.profiling.page.title": "Optional Fields | {siteName}",
|
||||
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
|
||||
"gender.options.label": "Gender (optional)",
|
||||
"gender.options.f": "Female",
|
||||
"gender.options.m": "Male",
|
||||
"gender.options.o": "Other/Prefer not to say",
|
||||
"education.levels.label": "Highest level of education completed (optional)",
|
||||
"education.levels.p": "Doctorate",
|
||||
"education.levels.m": "Master's or professional degree",
|
||||
"education.levels.b": "Bachelor's degree",
|
||||
"education.levels.a": "Associate's degree",
|
||||
"education.levels.hs": "Secondary/high school",
|
||||
"education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"education.levels.el": "Elementary/primary school",
|
||||
"education.levels.none": "No formal education",
|
||||
"education.levels.other": "Other education",
|
||||
"year.of.birth.label": "Year of birth (optional)",
|
||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
||||
"optional.fields.submit.button": "Submit",
|
||||
"optional.fields.skip.button": "Skip for now",
|
||||
"continue.to.platform": "Continue to {platformName}",
|
||||
"modal.title": "Thanks for letting us know.",
|
||||
"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."
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
|
||||
}
|
||||
@@ -5,15 +5,9 @@
|
||||
"complete.your.profile.2": "your profile",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
||||
"forgot.password.confirmation.title": "Check your email",
|
||||
"forgot.password.confirmation.support.link": "contact technical support",
|
||||
"forgot.password.confirmation.info": "If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.",
|
||||
"logistration.sign.in": "Sign in",
|
||||
"logistration.register": "Register",
|
||||
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"server.ratelimit.error.message": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
|
||||
"enterprisetpa.sso.button.title": "Sign in using {providerName}",
|
||||
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
|
||||
"sso.sign.in.with": "Sign in with {providerName}",
|
||||
"sso.create.account.using": "Create account using {providerName}",
|
||||
@@ -26,6 +20,7 @@
|
||||
"tpa.alert.heading": "Almost done!",
|
||||
"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",
|
||||
"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}",
|
||||
@@ -38,7 +33,6 @@
|
||||
"forgot.password.error.message.title": "An error occurred.",
|
||||
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
|
||||
"forgot.password.empty.email.field.error": "Enter your email",
|
||||
"forgot.password.invalid.email.message": "The email address you've provided isn't formatted correctly.",
|
||||
"forgot.password.email.help.text": "The email address you used to register with {platformName}",
|
||||
"confirmation.message.title": "Check your email",
|
||||
"confirmation.support.link": "contact technical support",
|
||||
@@ -53,7 +47,6 @@
|
||||
"token.validation.internal.sever.error.heading": "Token validation failure",
|
||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
@@ -65,45 +58,32 @@
|
||||
"login.user.identity.label": "Username or email",
|
||||
"login.password.label": "Password",
|
||||
"sign.in.button": "Sign in",
|
||||
"sign.in.btn.pending.state": "Loading",
|
||||
"need.help.signing.in.collapsible.menu": "Need help signing in?",
|
||||
"forgot.password.link": "Forgot my password",
|
||||
"forgot.password": "Forgot password",
|
||||
"other.sign.in.issues": "Other sign in issues",
|
||||
"need.other.help.signing.in.collapsible.menu": "Need other help signing in?",
|
||||
"institution.login.button": "Institution/campus credentials",
|
||||
"institution.login.page.title": "Sign in with institution/campus credentials",
|
||||
"institution.login.page.back.button": "Back to sign in",
|
||||
"create.an.account": "Create an account",
|
||||
"login.other.options.heading": "Or sign in with:",
|
||||
"non.compliant.password.title": "We recently changed our password requirements",
|
||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
||||
"first.time.here": "First time here?",
|
||||
"email.help.message": "The email address you used to register with edX.",
|
||||
"enterprise.login.btn.text": "Company or school credentials",
|
||||
"email.format.validation.message": "The email address you've provided isn't formatted correctly.",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
||||
"email.validation.message": "Enter your username or email",
|
||||
"password.validation.message": "Password criteria has not been met",
|
||||
"register.link": "Create an account",
|
||||
"sign.in.heading": "Sign in",
|
||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
||||
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
|
||||
"account.activation.info.message": "This account has already been activated.",
|
||||
"account.activation.error.message.title": "Your account could not be activated",
|
||||
"account.activation.support.link": "contact support",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
|
||||
"account.confirmation.success.message": "Sign in to continue.",
|
||||
"account.confirmation.info.message": "This email has already been confirmed.",
|
||||
"account.confirmation.error.message.title": "Your email could not be confirmed",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
|
||||
"login.failure.header.title": "We couldn't sign you in.",
|
||||
"contact.support.link": "contact {platformName} support",
|
||||
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
|
||||
"login.failed.attempt.error": "You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.locked.out.error.message": "To protect your account, it’s been temporarily locked. Try again in {lockedOutPeriod} minutes.",
|
||||
"login.form.invalid.error.message": "Please fill in the fields below.",
|
||||
"login.incorrect.credentials.error.reset.link.text": "reset your password",
|
||||
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
|
||||
@@ -113,8 +93,20 @@
|
||||
"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",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"progressive.profiling.page.title": "Optional Fields | {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",
|
||||
"optional.fields.skip.button": "Skip for now",
|
||||
"optional.fields.next.button": "Next",
|
||||
"continue.to.platform": "Continue to {platformName}",
|
||||
"modal.title": "Thanks for letting us know.",
|
||||
"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.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "Register | {siteName}",
|
||||
"registration.fullname.label": "Full name",
|
||||
"registration.email.label": "Email",
|
||||
@@ -127,22 +119,19 @@
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
"register.institution.login.button": "Institution/campus credentials",
|
||||
"register.institution.login.page.title": "Register with institution/campus credentials",
|
||||
"empty.name.field.error": "Enter your full name",
|
||||
"empty.email.field.error": "Enter your email",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"empty.username.field.error": "Username must be between 2 and 30 characters",
|
||||
"empty.password.field.error": "Password criteria has not been met",
|
||||
"empty.country.field.error": "Select your country or region of residence",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"email.invalid.format.error": "Enter a valid email address",
|
||||
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
|
||||
"username.validation.message": "Username must be between 2 and 30 characters",
|
||||
"name.validation.message": "Enter a valid name",
|
||||
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces",
|
||||
"support.education.research": "Support education research by providing additional information. (Optional)",
|
||||
"registration.request.failure.header": "We couldn't create your account.",
|
||||
"registration.empty.form.submission.error": "Please check your responses and try again.",
|
||||
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
@@ -152,26 +141,11 @@
|
||||
"privacy.policy": "Privacy Policy",
|
||||
"honor.code": "Honor Code",
|
||||
"terms.of.service": "Terms of Service",
|
||||
"registration.year.of.birth.label": "Year of birth (optional)",
|
||||
"registration.field.gender.options.label": "Gender (optional)",
|
||||
"registration.goals.label": "Tell us why you're interested in edX (optional)",
|
||||
"registration.field.gender.options.f": "Female",
|
||||
"registration.field.gender.options.m": "Male",
|
||||
"registration.field.gender.options.o": "Other/Prefer not to say",
|
||||
"registration.field.education.levels.label": "Highest level of education completed (optional)",
|
||||
"registration.field.education.levels.p": "Doctorate",
|
||||
"registration.field.education.levels.m": "Master's or professional degree",
|
||||
"registration.field.education.levels.b": "Bachelor's degree",
|
||||
"registration.field.education.levels.a": "Associate's degree",
|
||||
"registration.field.education.levels.hs": "Secondary/high school",
|
||||
"registration.field.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"registration.field.education.levels.el": "Elementary/primary school",
|
||||
"registration.field.education.levels.none": "No formal education",
|
||||
"registration.field.education.levels.other": "Other education",
|
||||
"registration.username.suggestion.label": "Suggested:",
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"did.you.mean.alert.text": "Did you mean",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Sign in",
|
||||
"reset.password.page.title": "Reset Password | {siteName}",
|
||||
"reset.password": "Reset password",
|
||||
@@ -180,40 +154,10 @@
|
||||
"confirm.password.label": "Confirm password",
|
||||
"passwords.do.not.match": "Passwords do not match",
|
||||
"confirm.your.password": "Confirm your password",
|
||||
"forgot.password.confirmation.sign.in.link": "sign in",
|
||||
"reset.password.request.forgot.password.text": "Forgot password",
|
||||
"reset.password.request.invalid.token.header": "Invalid password reset link",
|
||||
"reset.password.empty.new.password.field.error": "Please enter your new password.",
|
||||
"reset.password.failure.heading": "We couldn't reset your password.",
|
||||
"reset.password.form.submission.error": "Please check your responses and try again.",
|
||||
"reset.password.request.server.error": "Failed to reset password",
|
||||
"reset.password.token.validation.sever.error": "Token validation failure",
|
||||
"reset.server.rate.limit.error": "Too many requests.",
|
||||
"reset.password.success.heading": "Password reset complete.",
|
||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
||||
"progressive.profiling.page.title": "Optional Fields | {siteName}",
|
||||
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
|
||||
"gender.options.label": "Gender (optional)",
|
||||
"gender.options.f": "Female",
|
||||
"gender.options.m": "Male",
|
||||
"gender.options.o": "Other/Prefer not to say",
|
||||
"education.levels.label": "Highest level of education completed (optional)",
|
||||
"education.levels.p": "Doctorate",
|
||||
"education.levels.m": "Master's or professional degree",
|
||||
"education.levels.b": "Bachelor's degree",
|
||||
"education.levels.a": "Associate's degree",
|
||||
"education.levels.hs": "Secondary/high school",
|
||||
"education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"education.levels.el": "Elementary/primary school",
|
||||
"education.levels.none": "No formal education",
|
||||
"education.levels.other": "Other education",
|
||||
"year.of.birth.label": "Year of birth (optional)",
|
||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
||||
"optional.fields.submit.button": "Submit",
|
||||
"optional.fields.skip.button": "Skip for now",
|
||||
"continue.to.platform": "Continue to {platformName}",
|
||||
"modal.title": "Thanks for letting us know.",
|
||||
"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."
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
|
||||
}
|
||||
@@ -5,15 +5,9 @@
|
||||
"complete.your.profile.2": "your profile",
|
||||
"welcome.to.platform": "Welcome to {siteName}, {username}!",
|
||||
"institution.login.page.sub.heading": "Choose your institution from the list below",
|
||||
"forgot.password.confirmation.title": "Check your email",
|
||||
"forgot.password.confirmation.support.link": "contact technical support",
|
||||
"forgot.password.confirmation.info": "If you do not receive a password reset message after 1 minute, verify that you entered the correct email address, or check your spam folder.",
|
||||
"logistration.sign.in": "Sign in",
|
||||
"logistration.register": "Register",
|
||||
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"server.ratelimit.error.message": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"enterprisetpa.title.heading": "Would you like to sign in using your {providerName} credentials?",
|
||||
"enterprisetpa.sso.button.title": "Sign in using {providerName}",
|
||||
"enterprisetpa.login.button.text": "Show me other ways to sign in or register",
|
||||
"sso.sign.in.with": "Sign in with {providerName}",
|
||||
"sso.create.account.using": "Create account using {providerName}",
|
||||
@@ -26,6 +20,7 @@
|
||||
"tpa.alert.heading": "Almost done!",
|
||||
"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",
|
||||
"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}",
|
||||
@@ -38,7 +33,6 @@
|
||||
"forgot.password.error.message.title": "An error occurred.",
|
||||
"forgot.password.request.in.progress.message": "Your previous request is in progress, please try again in a few moments.",
|
||||
"forgot.password.empty.email.field.error": "Enter your email",
|
||||
"forgot.password.invalid.email.message": "The email address you've provided isn't formatted correctly.",
|
||||
"forgot.password.email.help.text": "The email address you used to register with {platformName}",
|
||||
"confirmation.message.title": "Check your email",
|
||||
"confirmation.support.link": "contact technical support",
|
||||
@@ -53,7 +47,6 @@
|
||||
"token.validation.internal.sever.error.heading": "Token validation failure",
|
||||
"token.validation.internal.sever.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"internal.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time.",
|
||||
"account.activation.error.message": "Something went wrong, please {supportLink} to resolve this issue.",
|
||||
"login.inactive.user.error": "In order to sign in, you need to activate your account.{lineBreak}\n {lineBreak}We just sent an activation link to {email}. If you do not receive an email,\n check your spam folders or {supportLink}.",
|
||||
"allowed.domain.login.error": "As {allowedDomain} user, You must login with your {allowedDomain} {tpaLink}.",
|
||||
@@ -65,45 +58,32 @@
|
||||
"login.user.identity.label": "Username or email",
|
||||
"login.password.label": "Password",
|
||||
"sign.in.button": "Sign in",
|
||||
"sign.in.btn.pending.state": "Loading",
|
||||
"need.help.signing.in.collapsible.menu": "Need help signing in?",
|
||||
"forgot.password.link": "Forgot my password",
|
||||
"forgot.password": "Forgot password",
|
||||
"other.sign.in.issues": "Other sign in issues",
|
||||
"need.other.help.signing.in.collapsible.menu": "Need other help signing in?",
|
||||
"institution.login.button": "Institution/campus credentials",
|
||||
"institution.login.page.title": "Sign in with institution/campus credentials",
|
||||
"institution.login.page.back.button": "Back to sign in",
|
||||
"create.an.account": "Create an account",
|
||||
"login.other.options.heading": "Or sign in with:",
|
||||
"non.compliant.password.title": "We recently changed our password requirements",
|
||||
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
|
||||
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
|
||||
"first.time.here": "First time here?",
|
||||
"email.help.message": "The email address you used to register with edX.",
|
||||
"enterprise.login.btn.text": "Company or school credentials",
|
||||
"email.format.validation.message": "The email address you've provided isn't formatted correctly.",
|
||||
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
|
||||
"email.validation.message": "Enter your username or email",
|
||||
"password.validation.message": "Password criteria has not been met",
|
||||
"register.link": "Create an account",
|
||||
"sign.in.heading": "Sign in",
|
||||
"account.activation.success.message.title": "Success! You have activated your account.",
|
||||
"account.activation.success.message": "You will now receive email updates and alerts from us related to the courses you are enrolled in. Sign in to continue.",
|
||||
"account.activation.info.message": "This account has already been activated.",
|
||||
"account.activation.error.message.title": "Your account could not be activated",
|
||||
"account.activation.support.link": "contact support",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"account.confirmation.success.message.title": "Success! You have confirmed your email.",
|
||||
"account.confirmation.success.message": "Sign in to continue.",
|
||||
"account.confirmation.info.message": "This email has already been confirmed.",
|
||||
"account.confirmation.error.message.title": "Your email could not be confirmed",
|
||||
"tpa.account.link": "{provider} account",
|
||||
"internal.server.error.message": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
"login.rate.limit.reached.message": "Too many failed login attempts. Try again later.",
|
||||
"login.failure.header.title": "We couldn't sign you in.",
|
||||
"contact.support.link": "contact {platformName} support",
|
||||
"login.incorrect.credentials.error": "The username, email, or password you entered is incorrect. Please try again.",
|
||||
"login.failed.attempt.error": "You have {remainingAttempts} more sign in attempts before your account is temporarily locked.",
|
||||
"login.locked.out.error.message": "To protect your account, it’s been temporarily locked. Try again in {lockedOutPeriod} minutes.",
|
||||
"login.form.invalid.error.message": "Please fill in the fields below.",
|
||||
"login.incorrect.credentials.error.reset.link.text": "reset your password",
|
||||
"login.incorrect.credentials.error.before.account.blocked.text": "click here to reset it.",
|
||||
@@ -113,8 +93,20 @@
|
||||
"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",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"progressive.profiling.page.title": "Optional Fields | {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",
|
||||
"optional.fields.skip.button": "Skip for now",
|
||||
"optional.fields.next.button": "Next",
|
||||
"continue.to.platform": "Continue to {platformName}",
|
||||
"modal.title": "Thanks for letting us know.",
|
||||
"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.heading": "We have a few recommendations to get you started.",
|
||||
"recommendation.skip.button": "Skip for now",
|
||||
"register.page.title": "Register | {siteName}",
|
||||
"registration.fullname.label": "Full name",
|
||||
"registration.email.label": "Email",
|
||||
@@ -127,22 +119,19 @@
|
||||
"help.text.username.2": "This can not be changed later.",
|
||||
"help.text.email": "For account activation and important updates",
|
||||
"create.account.for.free.button": "Create an account for free",
|
||||
"create.an.account.btn.pending.state": "Loading",
|
||||
"registration.other.options.heading": "Or register with:",
|
||||
"register.institution.login.button": "Institution/campus credentials",
|
||||
"register.institution.login.page.title": "Register with institution/campus credentials",
|
||||
"empty.name.field.error": "Enter your full name",
|
||||
"empty.email.field.error": "Enter your email",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"empty.username.field.error": "Username must be between 2 and 30 characters",
|
||||
"empty.password.field.error": "Password criteria has not been met",
|
||||
"empty.country.field.error": "Select your country or region of residence",
|
||||
"email.do.not.match": "The email addresses do not match.",
|
||||
"email.invalid.format.error": "Enter a valid email address",
|
||||
"email.ratelimit.less.chars.validation.message": "Email must have 3 characters.",
|
||||
"username.validation.message": "Username must be between 2 and 30 characters",
|
||||
"name.validation.message": "Enter a valid name",
|
||||
"username.format.validation.message": "Usernames can only contain letters (A-Z, a-z), numerals (0-9), underscores (_), and hyphens (-). Usernames cannot contain spaces",
|
||||
"support.education.research": "Support education research by providing additional information. (Optional)",
|
||||
"registration.request.failure.header": "We couldn't create your account.",
|
||||
"registration.empty.form.submission.error": "Please check your responses and try again.",
|
||||
"registration.request.server.error": "An error has occurred. Try refreshing the page, or check your internet connection.",
|
||||
@@ -152,26 +141,11 @@
|
||||
"privacy.policy": "Privacy Policy",
|
||||
"honor.code": "Honor Code",
|
||||
"terms.of.service": "Terms of Service",
|
||||
"registration.year.of.birth.label": "Year of birth (optional)",
|
||||
"registration.field.gender.options.label": "Gender (optional)",
|
||||
"registration.goals.label": "Tell us why you're interested in edX (optional)",
|
||||
"registration.field.gender.options.f": "Female",
|
||||
"registration.field.gender.options.m": "Male",
|
||||
"registration.field.gender.options.o": "Other/Prefer not to say",
|
||||
"registration.field.education.levels.label": "Highest level of education completed (optional)",
|
||||
"registration.field.education.levels.p": "Doctorate",
|
||||
"registration.field.education.levels.m": "Master's or professional degree",
|
||||
"registration.field.education.levels.b": "Bachelor's degree",
|
||||
"registration.field.education.levels.a": "Associate's degree",
|
||||
"registration.field.education.levels.hs": "Secondary/high school",
|
||||
"registration.field.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"registration.field.education.levels.el": "Elementary/primary school",
|
||||
"registration.field.education.levels.none": "No formal education",
|
||||
"registration.field.education.levels.other": "Other education",
|
||||
"registration.username.suggestion.label": "Suggested:",
|
||||
"registration.using.tpa.form.heading": "Finish creating your account",
|
||||
"did.you.mean.alert.text": "Did you mean",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
|
||||
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
|
||||
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
|
||||
"sign.in": "Sign in",
|
||||
"reset.password.page.title": "Reset Password | {siteName}",
|
||||
"reset.password": "Reset password",
|
||||
@@ -180,40 +154,10 @@
|
||||
"confirm.password.label": "Confirm password",
|
||||
"passwords.do.not.match": "Passwords do not match",
|
||||
"confirm.your.password": "Confirm your password",
|
||||
"forgot.password.confirmation.sign.in.link": "sign in",
|
||||
"reset.password.request.forgot.password.text": "Forgot password",
|
||||
"reset.password.request.invalid.token.header": "Invalid password reset link",
|
||||
"reset.password.empty.new.password.field.error": "Please enter your new password.",
|
||||
"reset.password.failure.heading": "We couldn't reset your password.",
|
||||
"reset.password.form.submission.error": "Please check your responses and try again.",
|
||||
"reset.password.request.server.error": "Failed to reset password",
|
||||
"reset.password.token.validation.sever.error": "Token validation failure",
|
||||
"reset.server.rate.limit.error": "Too many requests.",
|
||||
"reset.password.success.heading": "Password reset complete.",
|
||||
"reset.password.success": "Your password has been reset. Sign in to your account.",
|
||||
"progressive.profiling.page.title": "Optional Fields | {siteName}",
|
||||
"progressive.profiling.page.heading": "A few questions for you will help us get smarter.",
|
||||
"gender.options.label": "Gender (optional)",
|
||||
"gender.options.f": "Female",
|
||||
"gender.options.m": "Male",
|
||||
"gender.options.o": "Other/Prefer not to say",
|
||||
"education.levels.label": "Highest level of education completed (optional)",
|
||||
"education.levels.p": "Doctorate",
|
||||
"education.levels.m": "Master's or professional degree",
|
||||
"education.levels.b": "Bachelor's degree",
|
||||
"education.levels.a": "Associate's degree",
|
||||
"education.levels.hs": "Secondary/high school",
|
||||
"education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"education.levels.el": "Elementary/primary school",
|
||||
"education.levels.none": "No formal education",
|
||||
"education.levels.other": "Other education",
|
||||
"year.of.birth.label": "Year of birth (optional)",
|
||||
"optional.fields.information.link": "Learn more about how we use this information.",
|
||||
"optional.fields.submit.button": "Submit",
|
||||
"optional.fields.skip.button": "Skip for now",
|
||||
"continue.to.platform": "Continue to {platformName}",
|
||||
"modal.title": "Thanks for letting us know.",
|
||||
"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."
|
||||
"rate.limit.error": "An error has occurred because of too many requests. Please try again after some time."
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
import { messages as paragonMessages } from '@edx/paragon';
|
||||
|
||||
import configuration from './config';
|
||||
import appMessages from './i18n';
|
||||
import MainApp from './MainApp';
|
||||
|
||||
@@ -27,24 +28,7 @@ subscribe(APP_INIT_ERROR, (error) => {
|
||||
initialize({
|
||||
handlers: {
|
||||
config: () => {
|
||||
mergeConfig({
|
||||
LOGIN_ISSUE_SUPPORT_LINK: process.env.LOGIN_ISSUE_SUPPORT_LINK || null,
|
||||
ACTIVATION_EMAIL_SUPPORT_LINK: process.env.ACTIVATION_EMAIL_SUPPORT_LINK || null,
|
||||
PASSWORD_RESET_SUPPORT_LINK: process.env.PASSWORD_RESET_SUPPORT_LINK || null,
|
||||
TOS_AND_HONOR_CODE: process.env.TOS_AND_HONOR_CODE || null,
|
||||
TOS_LINK: process.env.TOS_LINK || null,
|
||||
PRIVACY_POLICY: process.env.PRIVACY_POLICY || null,
|
||||
USER_SURVEY_COOKIE_NAME: process.env.USER_SURVEY_COOKIE_NAME || null,
|
||||
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
|
||||
WELCOME_PAGE_SUPPORT_LINK: process.env.WELCOME_PAGE_SUPPORT_LINK || null,
|
||||
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
|
||||
INFO_EMAIL: process.env.INFO_EMAIL || '',
|
||||
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null,
|
||||
ENABLE_PROGRESSIVE_PROFILING: process.env.ENABLE_PROGRESSIVE_PROFILING || false,
|
||||
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
|
||||
ENABLE_COPPA_COMPLIANCE: process.env.ENABLE_COPPA_COMPLIANCE || '',
|
||||
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
|
||||
});
|
||||
mergeConfig(configuration);
|
||||
},
|
||||
},
|
||||
messages: [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { CheckCircle, Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -70,7 +70,7 @@ const AccountActivationMessage = (props) => {
|
||||
|
||||
AccountActivationMessage.propTypes = {
|
||||
messageType: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AccountActivationMessage);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow, ModalDialog, useToggle,
|
||||
} from '@edx/paragon';
|
||||
@@ -80,7 +80,7 @@ ChangePasswordPrompt.defaultProps = {
|
||||
};
|
||||
|
||||
ChangePasswordPrompt.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
variant: PropTypes.oneOf(['nudge', 'block']),
|
||||
redirectUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthService } from '@edx/frontend-platform/auth';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { FormattedMessage, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Hyperlink } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -193,7 +193,7 @@ LoginFailureMessage.propTypes = {
|
||||
errorCode: PropTypes.string,
|
||||
redirectUrl: PropTypes.string,
|
||||
}),
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(LoginFailureMessage);
|
||||
|
||||
@@ -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, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Form, Hyperlink, Icon, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from '../data/constants';
|
||||
import {
|
||||
getActivationStatus,
|
||||
getAllPossibleQueryParam,
|
||||
getAllPossibleQueryParams,
|
||||
getTpaHint,
|
||||
getTpaProvider,
|
||||
setSurveyCookie,
|
||||
@@ -54,7 +54,7 @@ class LoginPage extends React.Component {
|
||||
},
|
||||
isSubmitted: false,
|
||||
};
|
||||
this.queryParams = getAllPossibleQueryParam();
|
||||
this.queryParams = getAllPossibleQueryParams();
|
||||
this.tpaHint = getTpaHint();
|
||||
}
|
||||
|
||||
@@ -170,12 +170,12 @@ class LoginPage extends React.Component {
|
||||
|
||||
return (
|
||||
<>
|
||||
{((!isEnterpriseLoginDisabled && isSocialAuthActive) || (isEnterpriseLoginDisabled && isInstitutionAuthActive))
|
||||
&& (
|
||||
<div className="mt-4 mb-3 h4">
|
||||
{intl.formatMessage(messages['login.other.options.heading'])}
|
||||
</div>
|
||||
)}
|
||||
{(isSocialAuthActive || (isEnterpriseLoginDisabled && isInstitutionAuthActive))
|
||||
&& (
|
||||
<div className="mt-4 mb-3 h4">
|
||||
{intl.formatMessage(messages['login.other.options.heading'])}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!isEnterpriseLoginDisabled && isSocialAuthActive) && (
|
||||
<Hyperlink className="btn btn-link btn-sm text-body p-0 mb-4" destination={this.getEnterPriseLoginURL()}>
|
||||
@@ -248,13 +248,10 @@ class LoginPage extends React.Component {
|
||||
finishAuthUrl={thirdPartyAuthContext.finishAuthUrl}
|
||||
/>
|
||||
<div className="mw-xs mt-3">
|
||||
{thirdPartyAuthContext.currentProvider
|
||||
&& (
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={thirdPartyAuthContext.currentProvider}
|
||||
platformName={thirdPartyAuthContext.platformName}
|
||||
/>
|
||||
)}
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={thirdPartyAuthContext.currentProvider}
|
||||
platformName={thirdPartyAuthContext.platformName}
|
||||
/>
|
||||
{this.props.loginError ? <LoginFailureMessage loginError={this.props.loginError} /> : null}
|
||||
{submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null}
|
||||
{activationMsgType && <AccountActivationMessage messageType={activationMsgType} />}
|
||||
@@ -372,7 +369,7 @@ LoginPage.defaultProps = {
|
||||
|
||||
LoginPage.propTypes = {
|
||||
getThirdPartyAuthContext: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
loginError: PropTypes.objectOf(PropTypes.any),
|
||||
loginRequest: PropTypes.func.isRequired,
|
||||
loginRequestFailure: PropTypes.func.isRequired,
|
||||
|
||||
@@ -22,36 +22,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Sign in',
|
||||
description: 'Sign in button label that appears on login page',
|
||||
},
|
||||
'sign.in.btn.pending.state': {
|
||||
id: 'sign.in.btn.pending.state',
|
||||
defaultMessage: 'Loading',
|
||||
description: 'Title of icon that appears when button is in pending state',
|
||||
},
|
||||
'need.help.signing.in.collapsible.menu': {
|
||||
id: 'need.help.signing.in.collapsible.menu',
|
||||
defaultMessage: 'Need help signing in?',
|
||||
description: 'A button for collapsible need help signing in menu on login page',
|
||||
},
|
||||
'forgot.password.link': {
|
||||
id: 'forgot.password.link',
|
||||
defaultMessage: 'Forgot my password',
|
||||
description: 'Forgot password link',
|
||||
},
|
||||
'forgot.password': {
|
||||
id: 'forgot.password',
|
||||
defaultMessage: 'Forgot password',
|
||||
description: 'Button text for forgot password',
|
||||
},
|
||||
'other.sign.in.issues': {
|
||||
id: 'other.sign.in.issues',
|
||||
defaultMessage: 'Other sign in issues',
|
||||
description: 'A link that redirects to sign-in issues help',
|
||||
},
|
||||
'need.other.help.signing.in.collapsible.menu': {
|
||||
id: 'need.other.help.signing.in.collapsible.menu',
|
||||
defaultMessage: 'Need other help signing in?',
|
||||
description: 'A button for collapsible need other help signing in menu on forgot password page',
|
||||
},
|
||||
'institution.login.button': {
|
||||
id: 'institution.login.button',
|
||||
defaultMessage: 'Institution/campus credentials',
|
||||
@@ -67,16 +42,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Choose your institution from the list below',
|
||||
description: 'Heading of the institutions list',
|
||||
},
|
||||
'institution.login.page.back.button': {
|
||||
id: 'institution.login.page.back.button',
|
||||
defaultMessage: 'Back to sign in',
|
||||
description: 'return to login page',
|
||||
},
|
||||
'create.an.account': {
|
||||
id: 'create.an.account',
|
||||
defaultMessage: 'Create an account',
|
||||
description: 'Message on button to return to register page',
|
||||
},
|
||||
'login.other.options.heading': {
|
||||
id: 'login.other.options.heading',
|
||||
defaultMessage: 'Or sign in with:',
|
||||
@@ -99,26 +64,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'To protect your account, it\'s been temporarily locked. Try again in 30 minutes.',
|
||||
description: 'Part of message for when user account has been locked out after multiple failed login attempts',
|
||||
},
|
||||
'first.time.here': {
|
||||
id: 'first.time.here',
|
||||
defaultMessage: 'First time here?',
|
||||
description: 'A question that appears before sign up link',
|
||||
},
|
||||
'email.help.message': {
|
||||
id: 'email.help.message',
|
||||
defaultMessage: 'The email address you used to register with edX.',
|
||||
description: 'Message that appears below email field on login page',
|
||||
},
|
||||
'enterprise.login.btn.text': {
|
||||
id: 'enterprise.login.btn.text',
|
||||
defaultMessage: 'Company or school credentials',
|
||||
description: 'Company or school login link text.',
|
||||
},
|
||||
'email.format.validation.message': {
|
||||
id: 'email.format.validation.message',
|
||||
defaultMessage: 'The email address you\'ve provided isn\'t formatted correctly.',
|
||||
description: 'Validation message that appears when email address format is incorrect',
|
||||
},
|
||||
'username.or.email.format.validation.less.chars.message': {
|
||||
id: 'username.or.email.format.validation.less.chars.message',
|
||||
defaultMessage: 'Username or email must have at least 3 characters.',
|
||||
@@ -134,16 +84,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Enter your password',
|
||||
description: 'Validation message that appears when password is empty',
|
||||
},
|
||||
'register.link': {
|
||||
id: 'register.link',
|
||||
defaultMessage: 'Create an account',
|
||||
description: 'Register page link',
|
||||
},
|
||||
'sign.in.heading': {
|
||||
id: 'sign.in.heading',
|
||||
defaultMessage: 'Sign in',
|
||||
description: 'Sign in text',
|
||||
},
|
||||
// Account Activation Strings
|
||||
'account.activation.success.message.title': {
|
||||
id: 'account.activation.success.message.title',
|
||||
@@ -170,11 +110,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'contact support',
|
||||
description: 'Link text used in account activation error message to go to learner help center',
|
||||
},
|
||||
'tpa.account.link': {
|
||||
id: 'tpa.account.link',
|
||||
defaultMessage: '{provider} account',
|
||||
description: 'Link text error message used to go to SSO when staff user try to login through password.',
|
||||
},
|
||||
// Email Confirmation Strings
|
||||
'account.confirmation.success.message.title': {
|
||||
id: 'account.confirmation.success.message.title',
|
||||
@@ -196,6 +131,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Your email could not be confirmed',
|
||||
description: 'Account verification error message title',
|
||||
},
|
||||
'tpa.account.link': {
|
||||
id: 'tpa.account.link',
|
||||
defaultMessage: '{provider} account',
|
||||
description: 'Link text error message used to go to SSO when staff user try to login through password.',
|
||||
},
|
||||
'internal.server.error.message': {
|
||||
id: 'internal.server.error.message',
|
||||
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
|
||||
@@ -221,16 +161,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'The username, email, or password you entered is incorrect. Please try again.',
|
||||
description: 'Error message for incorrect email or password',
|
||||
},
|
||||
'login.failed.attempt.error': {
|
||||
id: 'login.failed.attempt.error',
|
||||
defaultMessage: 'You have {remainingAttempts} more sign in attempts before your account is temporarily locked.',
|
||||
description: 'Failed login attempts error message',
|
||||
},
|
||||
'login.locked.out.error.message': {
|
||||
id: 'login.locked.out.error.message',
|
||||
defaultMessage: 'To protect your account, it’s been temporarily locked. Try again in {lockedOutPeriod} minutes.',
|
||||
description: 'Account locked out user message',
|
||||
},
|
||||
'login.form.invalid.error.message': {
|
||||
id: 'login.form.invalid.error.message',
|
||||
defaultMessage: 'Please fill in the fields below.',
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('LoginFailureMessage', () => {
|
||||
errorCode: INACTIVE_USER,
|
||||
context: {
|
||||
platformName: 'openedX',
|
||||
supportLink: 'https://support.edx.org/',
|
||||
supportLink: 'http://support.openedx.test',
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -80,7 +80,7 @@ describe('LoginFailureMessage', () => {
|
||||
+ 'check your spam folders or contact openedX support.';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(expectedMessage);
|
||||
expect(loginFailureMessage.find('#login-failure-alert').find('a').props().href).toEqual('https://support.edx.org/');
|
||||
expect(loginFailureMessage.find('#login-failure-alert').find('a').props().href).toEqual('http://support.openedx.test');
|
||||
});
|
||||
|
||||
it('test match failed login attempt error', () => {
|
||||
@@ -269,7 +269,7 @@ describe('LoginFailureMessage', () => {
|
||||
email: 'text@example.com',
|
||||
errorCode: ALLOWED_DOMAIN_LOGIN_ERROR,
|
||||
context: {
|
||||
allowedDomain: 'edx.org',
|
||||
allowedDomain: 'test.com',
|
||||
provider: 'Google',
|
||||
tpaHint: 'google-auth2',
|
||||
},
|
||||
@@ -282,7 +282,7 @@ describe('LoginFailureMessage', () => {
|
||||
</IntlProvider>,
|
||||
);
|
||||
|
||||
const errorMessage = "We couldn't sign you in.As edx.org user, You must login with your edx.org Google account.";
|
||||
const errorMessage = "We couldn't sign you in.As test.com user, You must login with your test.com Google account.";
|
||||
const url = 'http://localhost:18000/dashboard/?tpa_hint=google-auth2';
|
||||
|
||||
expect(loginFailureMessage.find('#login-failure-alert').first().text()).toEqual(errorMessage);
|
||||
|
||||
@@ -73,7 +73,7 @@ describe('LoginPage', () => {
|
||||
id: 'oa2-apple-id',
|
||||
name: 'Apple',
|
||||
iconClass: null,
|
||||
iconImage: 'https://edx.devstack.lms/logo.png',
|
||||
iconImage: 'https://openedx.devstack.lms/logo.png',
|
||||
loginUrl: '/auth/login/apple-id/?auth_entry=login&next=/dashboard',
|
||||
};
|
||||
|
||||
@@ -236,6 +236,162 @@ describe('LoginPage', () => {
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(false);
|
||||
});
|
||||
|
||||
it('should show sign-in header providers (ENABLE ENTERPRISE LOGIN)', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [{
|
||||
...ssoProvider,
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(true);
|
||||
expect(loginPage.text().includes('Company or school credentials')).toBe(true);
|
||||
expect(loginPage.text().includes('Institution/campus credentials')).toBe(false);
|
||||
});
|
||||
|
||||
it('should show sign-in header with providers (DISABLE ENTERPRISE LOGIN)', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: true,
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [{
|
||||
...ssoProvider,
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(true);
|
||||
expect(loginPage.text().includes('Company or school credentials')).toBe(false);
|
||||
expect(loginPage.text().includes('Institution/campus credentials')).toBe(false);
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show sign-in header without Providers and secondary Providers (ENABLE ENTERPRISE LOGIN)', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(false);
|
||||
expect(loginPage.text().includes('Company or school credentials')).toBe(false);
|
||||
expect(loginPage.text().includes('Institution/campus credentials')).toBe(false);
|
||||
});
|
||||
|
||||
it('should not show sign-in header without Providers and secondary Providers (DISABLE ENTERPRISE LOGIN)', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: true,
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(false);
|
||||
expect(loginPage.text().includes('Company or school credentials')).toBe(false);
|
||||
expect(loginPage.text().includes('Institution/campus credentials')).toBe(false);
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should show sign-in header with secondary Providers and without Providers', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: true,
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
secondaryProviders: [{
|
||||
...secondaryProviders,
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(true);
|
||||
expect(loginPage.text().includes('Institution/campus credentials')).toBe(true);
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should show sign-in header with Providers and secondary Providers', () => {
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: true,
|
||||
});
|
||||
|
||||
store = mockStore({
|
||||
...initialState,
|
||||
commonComponents: {
|
||||
...initialState.commonComponents,
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
providers: [{
|
||||
...ssoProvider,
|
||||
}],
|
||||
secondaryProviders: [{
|
||||
...secondaryProviders,
|
||||
}],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const loginPage = mount(reduxWrapper(<IntlLoginPage {...props} />));
|
||||
expect(loginPage.text().includes('Or sign in with:')).toBe(true);
|
||||
expect(loginPage.text().includes('Company or school credentials')).toBe(false);
|
||||
expect(loginPage.text().includes('Institution/campus credentials')).toBe(true);
|
||||
|
||||
mergeConfig({
|
||||
DISABLE_ENTERPRISE_LOGIN: '',
|
||||
});
|
||||
});
|
||||
|
||||
// ******** test alert messages ********
|
||||
|
||||
it('should match login error message', () => {
|
||||
@@ -272,7 +428,7 @@ describe('LoginPage', () => {
|
||||
thirdPartyAuthContext: {
|
||||
...initialState.commonComponents.thirdPartyAuthContext,
|
||||
currentProvider: 'Apple',
|
||||
platformName: 'edX',
|
||||
platformName: 'openedX',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
getAuthenticatedUser,
|
||||
hydrateAuthenticatedUser,
|
||||
} from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getLoggingService } from '@edx/frontend-platform/logging';
|
||||
import {
|
||||
Alert,
|
||||
@@ -27,40 +27,65 @@ 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 { saveUserProfile } from './data/actions';
|
||||
import { welcomePageSelector } from './data/selectors';
|
||||
import messages from './messages';
|
||||
import WelcomePageModal from './WelcomePageModal';
|
||||
import ProgressiveProfilingPageModal from './ProgressiveProfilingPageModal';
|
||||
|
||||
const ProgressiveProfiling = (props) => {
|
||||
const {
|
||||
formRenderState, intl, submitState, showError,
|
||||
formRenderState, intl, submitState, showError, location,
|
||||
} = props;
|
||||
const optionalFields = props.location.state.optionalFields.fields;
|
||||
const extendedProfile = props.location.state.optionalFields.extended_profile;
|
||||
const enablePersonalizedRecommendations = getConfig().ENABLE_PERSONALIZED_RECOMMENDATIONS;
|
||||
const registrationResponse = location.state?.registrationResult;
|
||||
const [ready, setReady] = useState(false);
|
||||
const [registrationResult, setRegistrationResult] = useState({ redirectUrl: '' });
|
||||
const [values, setValues] = useState({});
|
||||
const [openDialog, setOpenDialog] = useState(false);
|
||||
|
||||
const [showRecommendationsPage, setShowRecommendationsPage] = useState(false);
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
|
||||
useEffect(() => {
|
||||
configureAuth(AxiosJwtAuthService, { loggingService: getLoggingService(), config: getConfig() });
|
||||
ensureAuthenticatedUser(DASHBOARD_URL).then(() => {
|
||||
hydrateAuthenticatedUser().then(() => {
|
||||
setReady(true);
|
||||
});
|
||||
});
|
||||
ensureAuthenticatedUser(DASHBOARD_URL)
|
||||
.then(() => {
|
||||
hydrateAuthenticatedUser().then(() => {
|
||||
setReady(true);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
if (props.location.state && props.location.state.registrationResult) {
|
||||
setRegistrationResult(props.location.state.registrationResult);
|
||||
if (registrationResponse) {
|
||||
setRegistrationResult(registrationResponse);
|
||||
sendPageEvent('login_and_registration', 'welcome');
|
||||
}
|
||||
}, [DASHBOARD_URL, props.location.state]);
|
||||
}, [DASHBOARD_URL, registrationResponse]);
|
||||
|
||||
if (!props.location.state || !props.location.state.registrationResult || formRenderState === FAILURE_STATE) {
|
||||
useEffect(() => {
|
||||
let queryParams = {};
|
||||
let timer = null;
|
||||
if (registrationResponse) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [authenticatedUser, enablePersonalizedRecommendations, registrationResponse]);
|
||||
|
||||
if (!location.state || !location.state.registrationResult || formRenderState === FAILURE_STATE) {
|
||||
global.location.assign(DASHBOARD_URL);
|
||||
return null;
|
||||
}
|
||||
@@ -69,9 +94,12 @@ const ProgressiveProfiling = (props) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const optionalFields = location.state.optionalFields.fields;
|
||||
const extendedProfile = location.state.optionalFields.extended_profile;
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
window.history.replaceState(location.state, null, '');
|
||||
const payload = { ...values, extendedProfile: [] };
|
||||
if (Object.keys(extendedProfile).length > 0) {
|
||||
extendedProfile.forEach(fieldName => {
|
||||
@@ -95,6 +123,7 @@ const ProgressiveProfiling = (props) => {
|
||||
|
||||
const handleSkip = (e) => {
|
||||
e.preventDefault();
|
||||
window.history.replaceState(props.location.state, null, '');
|
||||
setOpenDialog(true);
|
||||
sendTrackEvent('edx.bi.welcome.page.skip.link.clicked');
|
||||
};
|
||||
@@ -128,11 +157,14 @@ const ProgressiveProfiling = (props) => {
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
<WelcomePageModal isOpen={openDialog} redirectUrl={registrationResult.redirectUrl} />
|
||||
<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 pp-page-content">
|
||||
@@ -152,7 +184,7 @@ const ProgressiveProfiling = (props) => {
|
||||
<Hyperlink
|
||||
isInline
|
||||
variant="muted"
|
||||
destination={getConfig().WELCOME_PAGE_SUPPORT_LINK}
|
||||
destination={getConfig().AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
onClick={() => (sendTrackEvent('edx.bi.welcome.page.support.link.clicked'))}
|
||||
@@ -167,7 +199,7 @@ const ProgressiveProfiling = (props) => {
|
||||
className="login-button-width"
|
||||
state={submitState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['optional.fields.submit.button']),
|
||||
default: showRecommendationsPage ? intl.formatMessage(messages['optional.fields.next.button']) : intl.formatMessage(messages['optional.fields.submit.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={handleSubmit}
|
||||
@@ -192,11 +224,8 @@ const ProgressiveProfiling = (props) => {
|
||||
};
|
||||
|
||||
ProgressiveProfiling.propTypes = {
|
||||
// eslint-disable-next-line react/no-unused-prop-types
|
||||
extendedProfile: PropTypes.arrayOf(PropTypes.string),
|
||||
optionalFields: PropTypes.shape({}),
|
||||
formRenderState: PropTypes.string.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
location: PropTypes.shape({
|
||||
state: PropTypes.object,
|
||||
}),
|
||||
@@ -207,8 +236,6 @@ ProgressiveProfiling.propTypes = {
|
||||
};
|
||||
|
||||
ProgressiveProfiling.defaultProps = {
|
||||
extendedProfile: [],
|
||||
optionalFields: {},
|
||||
location: { state: {} },
|
||||
shouldRedirect: false,
|
||||
showError: false,
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { ActionRow, Button, ModalDialog } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
const WelcomePageModal = (props) => {
|
||||
const ProgressiveProfilingPageModal = (props) => {
|
||||
const { intl, isOpen, redirectUrl } = props;
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
|
||||
@@ -46,14 +46,14 @@ const WelcomePageModal = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
WelcomePageModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
ProgressiveProfilingPageModal.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
isOpen: PropTypes.bool,
|
||||
redirectUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
WelcomePageModal.defaultProps = {
|
||||
ProgressiveProfilingPageModal.defaultProps = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
export default injectIntl(WelcomePageModal);
|
||||
export default injectIntl(ProgressiveProfilingPageModal);
|
||||
62
src/progressive-profiling/messages.jsx
Normal file
62
src/progressive-profiling/messages.jsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'progressive.profiling.page.title': {
|
||||
id: 'progressive.profiling.page.title',
|
||||
defaultMessage: 'Optional Fields | {siteName}',
|
||||
description: 'progressive profiling page title',
|
||||
},
|
||||
'progressive.profiling.page.heading': {
|
||||
id: 'progressive.profiling.page.heading',
|
||||
defaultMessage: 'A few questions for you will help us get smarter.',
|
||||
description: 'The page heading for the progressive profiling page.',
|
||||
},
|
||||
'optional.fields.information.link': {
|
||||
id: 'optional.fields.information.link',
|
||||
defaultMessage: 'Learn more about how we use this information.',
|
||||
description: 'Optional fields page information link',
|
||||
},
|
||||
'optional.fields.submit.button': {
|
||||
id: 'optional.fields.submit.button',
|
||||
defaultMessage: 'Submit',
|
||||
description: 'Submit button text',
|
||||
},
|
||||
'optional.fields.skip.button': {
|
||||
id: 'optional.fields.skip.button',
|
||||
defaultMessage: 'Skip for now',
|
||||
description: 'Skip button text',
|
||||
},
|
||||
'optional.fields.next.button': {
|
||||
id: 'optional.fields.next.button',
|
||||
defaultMessage: 'Next',
|
||||
description: 'Next button text',
|
||||
},
|
||||
// modal dialog box
|
||||
'continue.to.platform': {
|
||||
id: 'continue.to.platform',
|
||||
defaultMessage: 'Continue to {platformName}',
|
||||
description: 'Button text for modal when user chooses "skip for now" option',
|
||||
},
|
||||
'modal.title': {
|
||||
id: 'modal.title',
|
||||
defaultMessage: 'Thanks for letting us know.',
|
||||
description: 'Heading for welcome page modal',
|
||||
},
|
||||
'modal.description': {
|
||||
id: 'modal.description',
|
||||
defaultMessage: 'You can complete your profile in settings at any time if you change your mind.',
|
||||
description: 'Modal body text',
|
||||
},
|
||||
// error message
|
||||
'welcome.page.error.heading': {
|
||||
id: 'welcome.page.error.heading',
|
||||
defaultMessage: 'We couldn\'t update your profile',
|
||||
description: 'Error message heading',
|
||||
},
|
||||
'welcome.page.error.message': {
|
||||
id: 'welcome.page.error.message',
|
||||
defaultMessage: 'An error occurred. You can complete your profile in settings at any time.',
|
||||
description: 'Error message body',
|
||||
},
|
||||
});
|
||||
export default messages;
|
||||
@@ -7,11 +7,13 @@ import * as auth from '@edx/frontend-platform/auth';
|
||||
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import * as logging from '@edx/frontend-platform/logging';
|
||||
import { mount } from 'enzyme';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { MemoryRouter, Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import {
|
||||
COMPLETE_STATE, DEFAULT_REDIRECT_URL, FAILURE_STATE,
|
||||
COMPLETE_STATE, DEFAULT_REDIRECT_URL, FAILURE_STATE, RECOMMENDATIONS,
|
||||
} from '../../data/constants';
|
||||
import { saveUserProfile } from '../data/actions';
|
||||
import ProgressiveProfiling from '../ProgressiveProfiling';
|
||||
@@ -31,9 +33,11 @@ auth.configure = jest.fn();
|
||||
auth.ensureAuthenticatedUser = jest.fn().mockImplementation(() => Promise.resolve(true));
|
||||
auth.hydrateAuthenticatedUser = jest.fn().mockImplementation(() => Promise.resolve(true));
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
describe('ProgressiveProfilingTests', () => {
|
||||
mergeConfig({
|
||||
WELCOME_PAGE_SUPPORT_LINK: 'http://localhost:1999/welcome',
|
||||
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/welcome',
|
||||
});
|
||||
const registrationResult = { redirectUrl: getConfig().LMS_BASE_URL + DEFAULT_REDIRECT_URL, success: true };
|
||||
const fields = {
|
||||
@@ -58,12 +62,18 @@ describe('ProgressiveProfilingTests', () => {
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</MemoryRouter>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const getProgressiveProfilingPage = async () => {
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage {...props} />));
|
||||
const progressiveProfilingPage = mount(reduxWrapper(
|
||||
<Router history={history}>
|
||||
<IntlProgressiveProfilingPage {...props} />
|
||||
</Router>,
|
||||
));
|
||||
await act(async () => {
|
||||
await Promise.resolve(progressiveProfilingPage);
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
@@ -103,12 +113,12 @@ describe('ProgressiveProfilingTests', () => {
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'abc123' }));
|
||||
const formPayload = {
|
||||
gender: 'm',
|
||||
extended_profile: [{ field_name: 'company', field_value: 'edx' }],
|
||||
extended_profile: [{ field_name: 'company', field_value: 'test company' }],
|
||||
};
|
||||
store.dispatch = jest.fn(store.dispatch);
|
||||
const progressiveProfilingPage = await getProgressiveProfilingPage();
|
||||
progressiveProfilingPage.find('select#gender').simulate('change', { target: { value: 'm', name: 'gender' } });
|
||||
progressiveProfilingPage.find('input#company').simulate('change', { target: { value: 'edx', name: 'company' } });
|
||||
progressiveProfilingPage.find('input#company').simulate('change', { target: { value: 'test company', name: 'company' } });
|
||||
|
||||
progressiveProfilingPage.find('button.btn-brand').simulate('click');
|
||||
expect(store.dispatch).toHaveBeenCalledWith(saveUserProfile('abc123', formPayload));
|
||||
@@ -156,4 +166,60 @@ describe('ProgressiveProfilingTests', () => {
|
||||
await getProgressiveProfilingPage();
|
||||
expect(window.location.href).toBe(DASHBOARD_URL);
|
||||
});
|
||||
|
||||
describe('Recommendations test', () => {
|
||||
mergeConfig({
|
||||
ENABLE_PERSONALIZED_RECOMMENDATIONS: true,
|
||||
});
|
||||
|
||||
it.skip('should redirect to recommendations page if recommendations are enabled', async () => {
|
||||
store = mockStore({
|
||||
welcomePage: {
|
||||
...initialState.welcomePage,
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'abc123' }));
|
||||
const progressiveProfilingPage = await getProgressiveProfilingPage();
|
||||
|
||||
expect(progressiveProfilingPage.find('button.btn-brand').text()).toEqual('Next');
|
||||
expect(history.location.pathname).toEqual(RECOMMENDATIONS);
|
||||
});
|
||||
|
||||
it('should not redirect to recommendations page if user is on its way to enroll in a course', async () => {
|
||||
delete window.location;
|
||||
window.location = {
|
||||
href: getConfig().BASE_URL,
|
||||
assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
|
||||
};
|
||||
|
||||
const redirectUrl = `${getConfig().LMS_BASE_URL}${DEFAULT_REDIRECT_URL}?enrollment_action=1`;
|
||||
props = {
|
||||
getFieldData: jest.fn(),
|
||||
location: {
|
||||
state: {
|
||||
registrationResult: {
|
||||
redirectUrl,
|
||||
success: true,
|
||||
},
|
||||
optionalFields,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
store = mockStore({
|
||||
welcomePage: {
|
||||
...initialState.welcomePage,
|
||||
success: true,
|
||||
},
|
||||
});
|
||||
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'abc123' }));
|
||||
const progressiveProfilingPage = await getProgressiveProfilingPage();
|
||||
|
||||
expect(progressiveProfilingPage.find('button.btn-brand').text()).toEqual('Submit');
|
||||
expect(window.location.href).toEqual(redirectUrl);
|
||||
});
|
||||
});
|
||||
});
|
||||
85
src/recommendations/RecommendationCard.jsx
Normal file
85
src/recommendations/RecommendationCard.jsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Card, Hyperlink } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { trackRecommendationsClicked } from './track';
|
||||
|
||||
const RecommendationCard = (props) => {
|
||||
const { recommendation, position, userId } = props;
|
||||
const showPartnerLogo = recommendation.owners.length === 1;
|
||||
|
||||
const getOwners = () => {
|
||||
if (recommendation.owners.length === 1) {
|
||||
return recommendation.owners[0].key;
|
||||
}
|
||||
|
||||
let keys = '';
|
||||
recommendation.owners.forEach((owner) => {
|
||||
keys += `${owner.key }, `;
|
||||
});
|
||||
return keys.slice(0, -2);
|
||||
};
|
||||
|
||||
const handleCardClick = () => {
|
||||
trackRecommendationsClicked(
|
||||
recommendation.courseKey,
|
||||
false,
|
||||
position + 1,
|
||||
userId,
|
||||
recommendation.marketingUrl,
|
||||
recommendation.recommendationType || 'algolia',
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mr-4 recommendation-card">
|
||||
<Hyperlink
|
||||
destination={recommendation.marketingUrl}
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
onClick={handleCardClick}
|
||||
>
|
||||
<Card isClickable>
|
||||
<Card.ImageCap
|
||||
src={recommendation.cardImageUrl}
|
||||
srcAlt="Card image"
|
||||
logoSrc={showPartnerLogo ? recommendation.owners[0].logoImageUrl : ''}
|
||||
logoAlt="Card logo"
|
||||
/>
|
||||
<Card.Header
|
||||
title={recommendation.title}
|
||||
subtitle={getOwners()}
|
||||
/>
|
||||
<Card.Section />
|
||||
<Card.Footer textElement={<small className="footer-text">Course</small>} />
|
||||
</Card>
|
||||
</Hyperlink>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RecommendationCard.propTypes = {
|
||||
recommendation: PropTypes.shape({
|
||||
courseKey: PropTypes.string.isRequired,
|
||||
activeRunKey: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
cardImageUrl: PropTypes.string.isRequired,
|
||||
owners: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
logoImageUrl: PropTypes.string.isRequired,
|
||||
})),
|
||||
marketingUrl: PropTypes.string.isRequired,
|
||||
recommendationType: PropTypes.string,
|
||||
}).isRequired,
|
||||
position: PropTypes.number.isRequired,
|
||||
userId: PropTypes.number,
|
||||
};
|
||||
|
||||
RecommendationCard.defaultProps = {
|
||||
userId: null,
|
||||
};
|
||||
|
||||
export default injectIntl(RecommendationCard);
|
||||
56
src/recommendations/RecommendationsList.jsx
Normal file
56
src/recommendations/RecommendationsList.jsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Container } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import RecommendationCard from './RecommendationCard';
|
||||
|
||||
const RecommendationsList = (props) => {
|
||||
const { title, recommendations, userId } = props;
|
||||
|
||||
return (
|
||||
<Container id="course-recommendations" size="lg" className="recommendations-container">
|
||||
<h2 className="text-sm-center mb-4 text-left recommendations-heading">
|
||||
{title}
|
||||
</h2>
|
||||
<div className="d-flex card-list">
|
||||
{
|
||||
recommendations.map((recommendation, idx) => (
|
||||
<RecommendationCard
|
||||
key={recommendation.activeRunKey}
|
||||
recommendation={recommendation}
|
||||
position={idx}
|
||||
userId={userId}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
RecommendationsList.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
recommendations: PropTypes.arrayOf(PropTypes.shape({
|
||||
courseKey: PropTypes.string.isRequired,
|
||||
activeRunKey: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
cardImageUrl: PropTypes.string.isRequired,
|
||||
owners: PropTypes.arrayOf(PropTypes.shape({
|
||||
key: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
logoImageUrl: PropTypes.string.isRequired,
|
||||
})),
|
||||
marketingUrl: PropTypes.string.isRequired,
|
||||
recommendationType: PropTypes.string,
|
||||
})),
|
||||
userId: PropTypes.number,
|
||||
};
|
||||
|
||||
RecommendationsList.defaultProps = {
|
||||
recommendations: [],
|
||||
userId: null,
|
||||
};
|
||||
|
||||
export default injectIntl(RecommendationsList);
|
||||
141
src/recommendations/RecommendationsPage.jsx
Normal file
141
src/recommendations/RecommendationsPage.jsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Hyperlink, Image, Spinner, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import { DEFAULT_REDIRECT_URL } from '../data/constants';
|
||||
import { EDUCATION_LEVEL_MAPPING, RECOMMENDATIONS_COUNT } from './data/constants';
|
||||
import getPersonalizedRecommendations from './data/service';
|
||||
import { convertCourseRunKeytoCourseKey } from './data/utils';
|
||||
import messages from './messages';
|
||||
import RecommendationsList from './RecommendationsList';
|
||||
import { trackRecommendationsViewed } from './track';
|
||||
|
||||
const RecommendationsPage = (props) => {
|
||||
const { intl, location } = props;
|
||||
const registrationResponse = location.state?.registrationResult;
|
||||
const userId = location.state?.userId;
|
||||
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [recommendations, setRecommendations] = useState([]);
|
||||
const educationLevel = EDUCATION_LEVEL_MAPPING[location.state?.educationLevel];
|
||||
|
||||
useEffect(() => {
|
||||
if (registrationResponse) {
|
||||
const generalRecommendations = JSON.parse(getConfig().GENERAL_RECOMMENDATIONS);
|
||||
let coursesWithKeys = [];
|
||||
getPersonalizedRecommendations(educationLevel).then((response) => {
|
||||
coursesWithKeys = response.map(course => ({
|
||||
...course,
|
||||
courseKey: convertCourseRunKeytoCourseKey(course.activeRunKey),
|
||||
}));
|
||||
|
||||
if (coursesWithKeys.length >= RECOMMENDATIONS_COUNT) {
|
||||
setRecommendations(coursesWithKeys.slice(0, RECOMMENDATIONS_COUNT));
|
||||
} else {
|
||||
const courseRecommendations = coursesWithKeys.concat(generalRecommendations);
|
||||
// Remove duplicate recommendations
|
||||
const uniqueRecommendations = courseRecommendations.filter(
|
||||
(recommendation, index, self) => index === self.findIndex((existingRecommendation) => (
|
||||
existingRecommendation.courseKey === recommendation.courseKey
|
||||
)),
|
||||
);
|
||||
setRecommendations(uniqueRecommendations.slice(0, RECOMMENDATIONS_COUNT));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
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]);
|
||||
|
||||
if (!registrationResponse) {
|
||||
global.location.assign(DASHBOARD_URL);
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleRedirection = () => {
|
||||
window.history.replaceState(location.state, null, '');
|
||||
if (registrationResponse) {
|
||||
window.location.href = registrationResponse.redirectUrl;
|
||||
} else {
|
||||
window.location.href = DASHBOARD_URL;
|
||||
}
|
||||
};
|
||||
|
||||
if (!isLoading && recommendations.length < RECOMMENDATIONS_COUNT) {
|
||||
handleRedirection();
|
||||
}
|
||||
|
||||
const handleSkip = (e) => {
|
||||
e.preventDefault();
|
||||
handleRedirection();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['recommendation.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
</Helmet>
|
||||
<div className="d-flex flex-column vh-100 bg-light-200">
|
||||
<div className="mb-2">
|
||||
<div className="col-md-12 small-screen-top-stripe medium-screen-top-stripe extra-large-screen-top-stripe" />
|
||||
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
|
||||
<Image className="logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
||||
</Hyperlink>
|
||||
</div>
|
||||
{(!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'])}
|
||||
recommendations={recommendations}
|
||||
userId={userId}
|
||||
/>
|
||||
<div className="text-center">
|
||||
<StatefulButton
|
||||
className="font-weight-500"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['recommendation.skip.button']),
|
||||
}}
|
||||
onClick={handleSkip}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<Spinner animation="border" variant="primary" className="centered-align-spinner" />
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
RecommendationsPage.propTypes = {
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
location: PropTypes.shape({
|
||||
state: PropTypes.object,
|
||||
}),
|
||||
|
||||
};
|
||||
|
||||
RecommendationsPage.defaultProps = {
|
||||
location: { state: {} },
|
||||
};
|
||||
|
||||
export default injectIntl(RecommendationsPage);
|
||||
11
src/recommendations/data/constants.js
Normal file
11
src/recommendations/data/constants.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export const RECOMMENDATIONS_COUNT = 4;
|
||||
|
||||
// Education difficulty level mapping
|
||||
export const EDUCATION_LEVEL_MAPPING = {
|
||||
p: 'Advanced',
|
||||
m: 'Advanced',
|
||||
b: 'Intermediate',
|
||||
a: 'Intermediate',
|
||||
hs: 'Introductory',
|
||||
jhs: 'Introductory',
|
||||
};
|
||||
22
src/recommendations/data/service.js
Normal file
22
src/recommendations/data/service.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { camelCaseObject } from '@edx/frontend-platform';
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
|
||||
const INDEX_NAME = process.env.ALGOLIA_AUTHN_RECOMMENDATIONS_INDEX;
|
||||
|
||||
const getPersonalizedRecommendations = async (educationLevel) => {
|
||||
const facetFilters = ['product:Course', 'availability:Available now'];
|
||||
|
||||
if (educationLevel) {
|
||||
facetFilters.push(`level:${educationLevel}`);
|
||||
}
|
||||
const client = algoliasearch(process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_SEARCH_KEY);
|
||||
const index = client.initIndex(INDEX_NAME);
|
||||
const { hits } = await index.search('', {
|
||||
aroundLatLngViaIP: true,
|
||||
facetFilters,
|
||||
});
|
||||
|
||||
return camelCaseObject(hits);
|
||||
};
|
||||
|
||||
export default getPersonalizedRecommendations;
|
||||
17
src/recommendations/data/utils.js
Normal file
17
src/recommendations/data/utils.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export const convertCourseRunKeytoCourseKey = (courseRunKey) => {
|
||||
if (!courseRunKey) {
|
||||
return '';
|
||||
}
|
||||
const newKeyFormat = courseRunKey.includes('+');
|
||||
if (newKeyFormat) {
|
||||
const splitCourseRunKey = courseRunKey.split(':').slice(-1)[0];
|
||||
const splitCourseKey = splitCourseRunKey.split('+').slice(0, 2);
|
||||
return `${splitCourseKey[0]}+${splitCourseKey[1]}`;
|
||||
}
|
||||
const splitCourseKey = courseRunKey.split('/').slice(0, 2);
|
||||
return `${splitCourseKey[0]}+${splitCourseKey[1]}`;
|
||||
};
|
||||
|
||||
export default {
|
||||
convertCourseRunKeytoCourseKey,
|
||||
};
|
||||
1
src/recommendations/index.js
Normal file
1
src/recommendations/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './RecommendationsPage';
|
||||
21
src/recommendations/messages.js
Normal file
21
src/recommendations/messages.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'recommendation.page.title': {
|
||||
id: 'recommendation.page.title',
|
||||
defaultMessage: 'Recommendations | {siteName}',
|
||||
description: 'recommendation page title',
|
||||
},
|
||||
'recommendation.page.heading': {
|
||||
id: 'recommendation.page.heading',
|
||||
defaultMessage: 'We have a few recommendations to get you started.',
|
||||
description: 'recommendation page heading',
|
||||
},
|
||||
'recommendation.skip.button': {
|
||||
id: 'recommendation.skip.button',
|
||||
defaultMessage: 'Skip for now',
|
||||
description: 'Skip button text',
|
||||
},
|
||||
|
||||
});
|
||||
export default messages;
|
||||
21
src/recommendations/optimizelyExperiment.js
Normal file
21
src/recommendations/optimizelyExperiment.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const RECOMMENDATIONS_EXP_ID = process.env.RECOMMENDATIONS_EXPERIMENT_ID;
|
||||
const RECOMMENDATIONS_EXP_VARIATION = 'show_recommendations_page';
|
||||
|
||||
const activateRecommendationsExperiment = () => {
|
||||
if (window.optimizely) {
|
||||
window.optimizely.push({
|
||||
type: 'page',
|
||||
pageName: 'van_1294_personalized_recommendations_on_authn',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const isUserInVariation = () => {
|
||||
if (window.optimizely) {
|
||||
const selectedVariant = window.optimizely.get('state').getVariationMap()[RECOMMENDATIONS_EXP_ID];
|
||||
return selectedVariant?.name === RECOMMENDATIONS_EXP_VARIATION;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export { activateRecommendationsExperiment, isUserInVariation };
|
||||
140
src/recommendations/tests/RecommendationsPage.test.jsx
Normal file
140
src/recommendations/tests/RecommendationsPage.test.jsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig, mergeConfig } from '@edx/frontend-platform';
|
||||
import * as analytics from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { DEFAULT_REDIRECT_URL } from '../../data/constants';
|
||||
import * as getPersonalizedRecommendations from '../data/service';
|
||||
import RecommendationsPage from '../RecommendationsPage';
|
||||
import { mockedGeneralRecommendations, mockedResponse } from './mockedData';
|
||||
|
||||
const IntlRecommendationsPage = injectIntl(RecommendationsPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
jest.mock('../data/service');
|
||||
|
||||
analytics.sendTrackEvent = jest.fn();
|
||||
|
||||
describe('RecommendationsPageTests', () => {
|
||||
mergeConfig({
|
||||
GENERAL_RECOMMENDATIONS: '[]',
|
||||
});
|
||||
|
||||
let defaultProps = {};
|
||||
let store = {};
|
||||
|
||||
const registrationResult = {
|
||||
redirectUrl: getConfig().LMS_BASE_URL.concat('/course-about-page-url'),
|
||||
success: true,
|
||||
};
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const getRecommendationsPage = async (props = defaultProps) => {
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage {...props} />));
|
||||
await act(async () => {
|
||||
await Promise.resolve(recommendationsPage);
|
||||
recommendationsPage.update();
|
||||
});
|
||||
|
||||
return recommendationsPage;
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore({});
|
||||
defaultProps = {
|
||||
location: {
|
||||
state: {
|
||||
registrationResult,
|
||||
userId: 111,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it('redirects to dashboard if user tries to access the page directly', async () => {
|
||||
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
|
||||
delete window.location;
|
||||
window.location = {
|
||||
href: getConfig().BASE_URL,
|
||||
assign: jest.fn().mockImplementation((value) => { window.location.href = value; }),
|
||||
};
|
||||
getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve([]));
|
||||
await getRecommendationsPage({});
|
||||
|
||||
expect(getPersonalizedRecommendations.default).toHaveBeenCalledTimes(0);
|
||||
expect(window.location.href).toEqual(DASHBOARD_URL);
|
||||
});
|
||||
|
||||
it('should show loading state to user', async () => {
|
||||
getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve(mockedResponse));
|
||||
await act(async () => {
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage {...defaultProps} />));
|
||||
expect(recommendationsPage.find('.centered-align-spinner').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call getPersonalizedRecommendations', async () => {
|
||||
delete window.location;
|
||||
window.location = { assign: jest.fn() };
|
||||
getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve([]));
|
||||
await getRecommendationsPage();
|
||||
|
||||
expect(getPersonalizedRecommendations.default).toHaveBeenCalledTimes(1);
|
||||
expect(analytics.sendTrackEvent).toHaveBeenCalledWith(
|
||||
'edx.bi.user.recommendations.viewed',
|
||||
{
|
||||
page: 'authn_recommendations',
|
||||
course_key_array: [],
|
||||
amplitude_recommendations: false,
|
||||
is_control: false,
|
||||
user_id: 111,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should display recommendations returned by Algolia', async () => {
|
||||
getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve(mockedResponse));
|
||||
const recommendationsPage = await getRecommendationsPage();
|
||||
|
||||
expect(recommendationsPage.find('#course-recommendations').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should redirect if recommended courses count is less than RECOMMENDATIONS_COUNT', async () => {
|
||||
delete window.location;
|
||||
window.location = { assign: jest.fn() };
|
||||
getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve([mockedResponse[0]]));
|
||||
const recommendationsPage = await getRecommendationsPage();
|
||||
|
||||
expect(recommendationsPage.find('#course-recommendations').exists()).toBeFalsy();
|
||||
expect(window.location.href).toEqual(registrationResult.redirectUrl);
|
||||
});
|
||||
|
||||
it('should not redirect if fallback recommendations are enabled', async () => {
|
||||
mergeConfig({
|
||||
GENERAL_RECOMMENDATIONS: mockedGeneralRecommendations,
|
||||
});
|
||||
getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve([]));
|
||||
const recommendationsPage = await getRecommendationsPage();
|
||||
|
||||
expect(recommendationsPage.find('#course-recommendations').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should display all owners for a course', async () => {
|
||||
getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve(mockedResponse));
|
||||
const recommendationsPage = await getRecommendationsPage();
|
||||
|
||||
expect(
|
||||
recommendationsPage.find('.pgn__card-header-subtitle-md').getElements()[0].props.children,
|
||||
).toEqual('firstOwnerX, secondOwnerX');
|
||||
});
|
||||
});
|
||||
79
src/recommendations/tests/mockedData.js
Normal file
79
src/recommendations/tests/mockedData.js
Normal file
@@ -0,0 +1,79 @@
|
||||
export const mockedResponse = [
|
||||
{
|
||||
title: 'How to Learn Online 1',
|
||||
marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-1',
|
||||
cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-1.png',
|
||||
activeRunKey: 'course-v1:test+testX+2018',
|
||||
owners: [
|
||||
{
|
||||
key: 'firstOwnerX',
|
||||
logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-1.png',
|
||||
name: 'first owner',
|
||||
},
|
||||
{
|
||||
key: 'secondOwnerX',
|
||||
logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-1.png',
|
||||
name: 'second owner',
|
||||
},
|
||||
],
|
||||
objectId: 'course-how-to-learn-online-key-1',
|
||||
},
|
||||
{
|
||||
title: 'How to Learn Online 2',
|
||||
marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-2',
|
||||
cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-2.png',
|
||||
activeRunKey: 'course-v1:test+testX+2019',
|
||||
owners: [
|
||||
{
|
||||
key: 'testX',
|
||||
logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-2.png',
|
||||
name: 'test',
|
||||
},
|
||||
],
|
||||
objectId: 'course-how-to-learn-online-key-2',
|
||||
},
|
||||
{
|
||||
title: 'How to Learn Online 3',
|
||||
marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-3',
|
||||
cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-3.png',
|
||||
activeRunKey: 'course-v1:test+testX+2020',
|
||||
owners: [
|
||||
{
|
||||
key: 'testX',
|
||||
logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-3.png',
|
||||
name: 'test',
|
||||
},
|
||||
],
|
||||
objectId: 'course-how-to-learn-online-key-3',
|
||||
},
|
||||
{
|
||||
title: 'How to Learn Online 4',
|
||||
marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-4',
|
||||
cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-4.png',
|
||||
activeRunKey: 'course-v1:test+testX+2021',
|
||||
owners: [
|
||||
{
|
||||
key: 'testX',
|
||||
logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-4.png',
|
||||
name: 'test',
|
||||
},
|
||||
],
|
||||
objectId: 'course-how-to-learn-online-key-4',
|
||||
},
|
||||
{
|
||||
title: 'How to Learn Online 5',
|
||||
marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-5',
|
||||
cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-5.png',
|
||||
activeRunKey: 'course-v1:test+testX+2022',
|
||||
owners: [
|
||||
{
|
||||
key: 'testX',
|
||||
logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-5.png',
|
||||
name: 'test',
|
||||
},
|
||||
],
|
||||
objectId: 'course-how-to-learn-online-key-5',
|
||||
},
|
||||
];
|
||||
|
||||
export const mockedGeneralRecommendations = '[{"courseKey":"test+text1","activeRunKey":"course-v1:test+test1+2018","cardImageUrl":"https://test-recommendations.com/text-1.jpg","marketingUrl":"https://test-recommendations.com/test-1","objectId":"test-1","owners":[{"key":"Testx","logoImageUrl":"https://test-recommendations.com/organization/test-1.png","name":"General recommendation org 1"}],"title":"General recommendation 1","recommendationType":"general"},{"courseKey":"test+text2","activeRunKey":"course-v1:test+test2+2018","cardImageUrl":"https://test-recommendations.com/text-2.jpg","marketingUrl":"https://test-recommendations.com/test-2","objectId":"test-2","owners":[{"key":"Testx","logoImageUrl":"https://test-recommendations.com/organization/test-2.png","name":"General recommendation org 2"}],"title":"General recommendation 2","recommendationType":"general"},{"courseKey":"test+text3","activeRunKey":"course-v1:test+test3+2018","cardImageUrl":"https://test-recommendations.com/text-3.jpg","marketingUrl":"https://test-recommendations.com/test-3","objectId":"test-3","owners":[{"key":"Testx","logoImageUrl":"https://test-recommendations.com/organization/test-3.png","name":"General recommendation org 3"}],"title":"General recommendation 3","recommendationType":"general"},{"courseKey":"test+text4","activeRunKey":"course-v1:test+test4+2018","cardImageUrl":"https://test-recommendations.com/text-4.jpg","marketingUrl":"https://test-recommendations.com/test-4","objectId":"test-4","owners":[{"key":"Testx","logoImageUrl":"https://test-recommendations.com/organization/test-4.png","name":"General recommendation org 4"}],"title":"General recommendation 4","recommendationType":"general"},{"courseKey":"test+text5","activeRunKey":"course-v1:test+test5+2018","cardImageUrl":"https://test-recommendations.com/text-5.jpg","marketingUrl":"https://test-recommendations.com/test-5","objectId":"test-5","owners":[{"key":"Testx","logoImageUrl":"https://test-recommendations.com/organization/test-5.png","name":"General recommendation org 5"}],"title":"General recommendation 5","recommendationType":"general"}]';
|
||||
49
src/recommendations/track.js
Normal file
49
src/recommendations/track.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
|
||||
export const LINK_TIMEOUT = 300;
|
||||
|
||||
export const eventNames = {
|
||||
recommendedCourseClicked: 'edx.bi.user.recommended.course.click',
|
||||
recommendationsViewed: 'edx.bi.user.recommendations.viewed',
|
||||
};
|
||||
|
||||
export const createLinkTracker = (tracker, href, openInNewTab = false) => (e) => {
|
||||
e.preventDefault();
|
||||
tracker();
|
||||
if (openInNewTab) {
|
||||
return setTimeout(() => { global.open(href, '_blank'); }, LINK_TIMEOUT);
|
||||
}
|
||||
return setTimeout(() => { global.location.href = href; }, LINK_TIMEOUT);
|
||||
};
|
||||
|
||||
export const trackRecommendationsClicked = (courseKey, isControl, position, userId, href, recommendationType) => {
|
||||
createLinkTracker(
|
||||
sendTrackEvent(eventNames.recommendedCourseClicked, {
|
||||
page: 'authn_recommendations',
|
||||
position,
|
||||
recommendation_type: recommendationType,
|
||||
course_key: courseKey,
|
||||
is_control: isControl,
|
||||
user_id: userId,
|
||||
}),
|
||||
href,
|
||||
true,
|
||||
);
|
||||
};
|
||||
|
||||
export const trackRecommendationsViewed = (recommendedCourseKeys, isControl, userId) => {
|
||||
sendTrackEvent(
|
||||
eventNames.recommendationsViewed, {
|
||||
page: 'authn_recommendations',
|
||||
course_key_array: recommendedCourseKeys,
|
||||
amplitude_recommendations: false,
|
||||
is_control: isControl,
|
||||
user_id: userId,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export default {
|
||||
trackRecommendationsClicked,
|
||||
trackRecommendationsViewed,
|
||||
};
|
||||
226
src/register/ConfigurableRegistrationForm.jsx
Normal file
226
src/register/ConfigurableRegistrationForm.jsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import FormFieldRenderer from '../field-renderer';
|
||||
import { FIELDS } from './data/constants';
|
||||
import { validateCountryField } from './data/utils';
|
||||
import messages from './messages';
|
||||
import { HonorCode, TermsOfService } from './registrationFields';
|
||||
import CountryField from './registrationFields/CountryField';
|
||||
|
||||
/**
|
||||
* Fields on registration page that are not the default required fields (name, email, username, password).
|
||||
* These configurable required fields are defined on the backend using REGISTRATION_EXTRA_FIELDS setting.
|
||||
*
|
||||
* Country and Honor Code/Terms of Services (if enabled) will appear at the bottom of the form, even if they
|
||||
* appear higher in order returned by backend. This is to make the user experience better.
|
||||
*
|
||||
* For edX only:
|
||||
* Country and honor code fields are required by default, and we will continue to show them on
|
||||
* frontend even if the API doesn't return it. The `SHOW_CONFIGURABLE_EDX_FIELDS` flag will enable
|
||||
* it for edX.
|
||||
* */
|
||||
const ConfigurableRegistrationForm = (props) => {
|
||||
const {
|
||||
countryList,
|
||||
email,
|
||||
fieldDescriptions,
|
||||
fieldErrors,
|
||||
formFields,
|
||||
intl,
|
||||
setFieldErrors,
|
||||
setFocusedField,
|
||||
setFormFields,
|
||||
} = props;
|
||||
|
||||
let showTermsOfServiceAndHonorCode = false;
|
||||
let showCountryField = false;
|
||||
|
||||
const formFieldDescriptions = [];
|
||||
const honorCode = [];
|
||||
const flags = {
|
||||
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
|
||||
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
|
||||
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!formFields.country) {
|
||||
setFormFields(prevState => ({ ...prevState, country: { countryCode: '', displayValue: '' } }));
|
||||
}
|
||||
});
|
||||
|
||||
const handleOnChange = (event, countryValue = null) => {
|
||||
const { name, type } = event.target;
|
||||
let value;
|
||||
if (countryValue) {
|
||||
value = { ...countryValue };
|
||||
} else {
|
||||
value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
||||
if (type === 'checkbox') {
|
||||
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
|
||||
}
|
||||
}
|
||||
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
||||
};
|
||||
|
||||
const handleOnBlur = (event) => {
|
||||
const { name, value } = event.target;
|
||||
let error = '';
|
||||
if (name === 'country') {
|
||||
const countryValidation = validateCountryField(
|
||||
value.trim(), countryList, intl.formatMessage(messages['empty.country.field.error']),
|
||||
);
|
||||
const { countryCode, displayValue } = countryValidation;
|
||||
error = countryValidation.error;
|
||||
setFormFields(prevState => ({ ...prevState, country: { countryCode, displayValue } }));
|
||||
} 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']);
|
||||
}
|
||||
setFocusedField(null);
|
||||
setFieldErrors(prevErrors => ({ ...prevErrors, [name]: error }));
|
||||
};
|
||||
|
||||
const handleOnFocus = (event) => {
|
||||
const { name } = event.target;
|
||||
setFieldErrors(prevErrors => ({ ...prevErrors, [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);
|
||||
};
|
||||
|
||||
if (flags.showConfigurableRegistrationFields) {
|
||||
Object.keys(fieldDescriptions).forEach(fieldName => {
|
||||
const fieldData = fieldDescriptions[fieldName];
|
||||
switch (fieldData.name) {
|
||||
case FIELDS.COUNTRY:
|
||||
showCountryField = true;
|
||||
break;
|
||||
case FIELDS.HONOR_CODE:
|
||||
if (fieldData.type === 'tos_and_honor_code') {
|
||||
showTermsOfServiceAndHonorCode = true;
|
||||
} else {
|
||||
honorCode.push(
|
||||
<span key={fieldData.name}>
|
||||
<HonorCode
|
||||
fieldType={fieldData.type}
|
||||
value={formFields[fieldData.name]}
|
||||
onChangeHandler={handleOnChange}
|
||||
errorMessage={fieldErrors[fieldData.name]}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case FIELDS.TERMS_OF_SERVICE:
|
||||
honorCode.push(
|
||||
<span key={fieldData.name}>
|
||||
<TermsOfService
|
||||
value={formFields[fieldData.name]}
|
||||
onChangeHandler={handleOnChange}
|
||||
errorMessage={fieldErrors[fieldData.name]}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
formFieldDescriptions.push(
|
||||
<span key={fieldData.name}>
|
||||
<FormFieldRenderer
|
||||
fieldData={fieldData}
|
||||
value={formFields[fieldData.name]}
|
||||
onChangeHandler={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={fieldErrors[fieldData.name]}
|
||||
isRequired
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (flags.showConfigurableEdxFields || showCountryField) {
|
||||
formFieldDescriptions.push(
|
||||
<span key="country">
|
||||
<CountryField
|
||||
countryList={countryList}
|
||||
selectedCountry={formFields.country}
|
||||
errorMessage={fieldErrors.country || ''}
|
||||
onChangeHandler={handleOnChange}
|
||||
onBlurHandler={handleOnBlur}
|
||||
onFocusHandler={handleOnFocus}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (flags.showMarketingEmailOptInCheckbox) {
|
||||
formFieldDescriptions.push(
|
||||
<span key="marketing_email_opt_in">
|
||||
<FormFieldRenderer
|
||||
fieldData={{
|
||||
type: 'checkbox',
|
||||
label: intl.formatMessage(messages['registration.opt.in.label'], { siteName: getConfig().SITE_NAME }),
|
||||
name: 'marketingEmailsOptIn',
|
||||
}}
|
||||
value={formFields.marketingEmailsOptIn}
|
||||
className="opt-checkbox"
|
||||
onChangeHandler={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
if (flags.showConfigurableEdxFields || showTermsOfServiceAndHonorCode) {
|
||||
formFieldDescriptions.push(
|
||||
<span key="honor_code">
|
||||
<HonorCode fieldType="tos_and_honor_code" onChangeHandler={handleOnChange} value={formFields.honor_code} />
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{formFieldDescriptions}
|
||||
<div>
|
||||
{honorCode}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ConfigurableRegistrationForm.propTypes = {
|
||||
countryList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
email: PropTypes.string.isRequired,
|
||||
fieldDescriptions: PropTypes.shape({}),
|
||||
fieldErrors: PropTypes.shape({
|
||||
country: PropTypes.string,
|
||||
}).isRequired,
|
||||
formFields: PropTypes.shape({
|
||||
country: PropTypes.shape({
|
||||
displayValue: PropTypes.string,
|
||||
countryCode: PropTypes.string,
|
||||
}),
|
||||
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,
|
||||
};
|
||||
|
||||
ConfigurableRegistrationForm.defaultProps = {
|
||||
fieldDescriptions: {},
|
||||
};
|
||||
|
||||
export default injectIntl(ConfigurableRegistrationForm);
|
||||
@@ -1,229 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Icon, IconButton } from '@edx/paragon';
|
||||
import { ExpandLess, ExpandMore } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
import onClickOutside from 'react-onclickoutside';
|
||||
|
||||
import { FormGroup } from '../common-components';
|
||||
import { COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, FORM_SUBMISSION_ERROR } from './data/constants';
|
||||
|
||||
class CountryDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.handleFocus = this.handleFocus.bind(this);
|
||||
this.handleOnBlur = this.handleOnBlur.bind(this);
|
||||
|
||||
this.state = {
|
||||
displayValue: '',
|
||||
icon: this.expandMoreButton(),
|
||||
errorMessage: '',
|
||||
showFieldError: true,
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const selectedCountry = this.props.options.find((o) => o[COUNTRY_CODE_KEY] === nextProps.value);
|
||||
if (this.props.value !== nextProps.value) {
|
||||
if (selectedCountry) {
|
||||
this.setState({
|
||||
displayValue: selectedCountry[COUNTRY_DISPLAY_KEY],
|
||||
showFieldError: false,
|
||||
errorMessage: nextProps.errorMessage,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
// Set persisted country value as display value.
|
||||
this.setState({ displayValue: nextProps.value, showFieldError: true, errorMessage: nextProps.errorMessage });
|
||||
return false;
|
||||
// eslint-disable-next-line no-else-return
|
||||
} else if (nextProps.value && selectedCountry && this.state.displayValue === nextProps.value) {
|
||||
// Handling a case here where user enters a valid country code that needs to be
|
||||
// evaluated and set its display value.
|
||||
this.setState({ displayValue: selectedCountry[COUNTRY_DISPLAY_KEY] });
|
||||
return false;
|
||||
}
|
||||
if (this.props.errorCode !== nextProps.errorCode && nextProps.errorCode === 'invalid-country') {
|
||||
this.setState({ showFieldError: true, errorMessage: nextProps.errorMessage });
|
||||
return false;
|
||||
}
|
||||
if (this.state.errorMessage !== nextProps.errorMessage) {
|
||||
this.setState({ showFieldError: true, errorMessage: nextProps.errorMessage });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (props.errorCode === FORM_SUBMISSION_ERROR && state.showFieldError) {
|
||||
return { errorMessage: props.errorMessage };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getItems(strToFind = '') {
|
||||
let { options } = this.props;
|
||||
if (strToFind.length > 0) {
|
||||
options = options.filter((option) => (option.name.toLowerCase().includes(strToFind.toLowerCase())));
|
||||
}
|
||||
|
||||
return options.map((opt) => {
|
||||
const value = opt[COUNTRY_CODE_KEY];
|
||||
const displayValue = opt[COUNTRY_DISPLAY_KEY];
|
||||
|
||||
return (
|
||||
<button type="button" name="countryItem" className="dropdown-item data-hj-suppress" value={displayValue} key={value} onClick={(e) => { this.handleItemClick(e); }}>
|
||||
{displayValue.length > 30 ? displayValue.substring(0, 30).concat('...') : displayValue}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
handleOnChange = (e) => {
|
||||
const filteredItems = this.getItems(e.target.value);
|
||||
this.setState({
|
||||
dropDownItems: filteredItems,
|
||||
displayValue: e.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
handleClickOutside = () => {
|
||||
if (this.state.dropDownItems?.length > 0) {
|
||||
this.setState(() => ({
|
||||
icon: this.expandMoreButton(),
|
||||
dropDownItems: '',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
handleExpandLess() {
|
||||
this.setState({ dropDownItems: '', icon: this.expandMoreButton() });
|
||||
}
|
||||
|
||||
handleExpandMore() {
|
||||
this.setState(prevState => ({
|
||||
dropDownItems: this.getItems(prevState.displayValue), icon: this.expandLessButton(), errorMessage: '', showFieldError: false,
|
||||
}));
|
||||
}
|
||||
|
||||
handleFocus(e) {
|
||||
const { name, value } = e.target;
|
||||
this.setState(prevState => ({
|
||||
dropDownItems: this.getItems(name === 'country' ? value : prevState.displayValue),
|
||||
icon: this.expandLessButton(),
|
||||
errorMessage: '',
|
||||
showFieldError: false,
|
||||
}));
|
||||
if (this.props.handleFocus) { this.props.handleFocus(e); }
|
||||
}
|
||||
|
||||
handleOnBlur(e, itemClicked = false, country = '') {
|
||||
const { name } = e.target;
|
||||
let countryValue = '';
|
||||
if (country) {
|
||||
countryValue = country;
|
||||
} else {
|
||||
countryValue = itemClicked ? e.target.value : this.state.displayValue;
|
||||
}
|
||||
// For a better user experience, do not validate when focus out from 'country' field
|
||||
// and focus on 'countryItem' or 'countryExpand' button.
|
||||
if (e.relatedTarget && e.relatedTarget.name === 'countryItem' && (name === 'country' || name === 'countryExpand')) {
|
||||
return;
|
||||
}
|
||||
const normalized = countryValue.toLowerCase();
|
||||
const selectedCountry = this.props.options.find((o) => o[COUNTRY_DISPLAY_KEY].toLowerCase() === normalized);
|
||||
if (!selectedCountry) {
|
||||
this.setState({ errorMessage: this.props.errorMessage, showFieldError: true });
|
||||
}
|
||||
if (this.props.handleBlur) { this.props.handleBlur({ target: { name: 'country', value: countryValue } }); }
|
||||
}
|
||||
|
||||
handleItemClick(e) {
|
||||
let countryValue = '';
|
||||
if (!e.target.value) {
|
||||
countryValue = e.target.parentElement.parentElement.value;
|
||||
}
|
||||
this.setState({ dropDownItems: '', icon: this.expandMoreButton() });
|
||||
this.handleOnBlur(e, true, countryValue);
|
||||
}
|
||||
|
||||
expandMoreButton() {
|
||||
return (
|
||||
<IconButton
|
||||
className="expand-more"
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleOnBlur}
|
||||
name="countryExpand"
|
||||
src={ExpandMore}
|
||||
iconAs={Icon}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt="expand-more"
|
||||
onClick={(e) => { this.handleExpandMore(e); }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
expandLessButton() {
|
||||
return (
|
||||
<IconButton
|
||||
className="expand-less"
|
||||
onFocus={this.handleFocus}
|
||||
onBlur={this.handleOnBlur}
|
||||
name="countryExpand"
|
||||
src={ExpandLess}
|
||||
iconAs={Icon}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
alt="expand-less"
|
||||
onClick={(e) => { this.handleExpandLess(e); }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<FormGroup
|
||||
as="input"
|
||||
name={this.props.name}
|
||||
autoComplete="off"
|
||||
className="mb-0"
|
||||
floatingLabel={this.props.floatingLabel}
|
||||
trailingElement={this.state.icon}
|
||||
handleChange={this.handleOnChange}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleFocus}
|
||||
value={this.state.displayValue}
|
||||
errorMessage={this.state.errorMessage}
|
||||
/>
|
||||
<div className="dropdown-container">
|
||||
{ this.state.dropDownItems?.length > 0 ? this.state.dropDownItems : null }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CountryDropdown.defaultProps = {
|
||||
options: null,
|
||||
floatingLabel: null,
|
||||
handleFocus: null,
|
||||
handleBlur: null,
|
||||
value: null,
|
||||
errorMessage: null,
|
||||
errorCode: null,
|
||||
};
|
||||
|
||||
CountryDropdown.propTypes = {
|
||||
options: PropTypes.arrayOf(PropTypes.object),
|
||||
floatingLabel: PropTypes.string,
|
||||
handleFocus: PropTypes.func,
|
||||
handleBlur: PropTypes.func,
|
||||
value: PropTypes.string,
|
||||
errorMessage: PropTypes.string,
|
||||
errorCode: PropTypes.string,
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default onClickOutside(CountryDropdown);
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Error } from '@edx/paragon/icons';
|
||||
import PropTypes from 'prop-types';
|
||||
@@ -18,6 +18,10 @@ const RegistrationFailureMessage = (props) => {
|
||||
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
|
||||
}, [errorCode, failureCount]);
|
||||
|
||||
if (!errorCode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let errorMessage;
|
||||
switch (errorCode) {
|
||||
case INTERNAL_SERVER_ERROR:
|
||||
@@ -52,7 +56,7 @@ RegistrationFailureMessage.propTypes = {
|
||||
}),
|
||||
errorCode: PropTypes.string.isRequired,
|
||||
failureCount: PropTypes.number.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(RegistrationFailureMessage);
|
||||
|
||||
@@ -1,659 +1,187 @@
|
||||
import React from 'react';
|
||||
import React, {
|
||||
useEffect, useMemo, useState,
|
||||
} from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
|
||||
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { sendPageEvent } from '@edx/frontend-platform/analytics';
|
||||
import {
|
||||
getCountryList, getLocale, injectIntl, intlShape,
|
||||
getCountryList, getLocale, injectIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Alert, Form, Icon, StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { Close, Error } from '@edx/paragon/icons';
|
||||
import PropTypes, { string } from 'prop-types';
|
||||
import { Form, StatefulButton } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import {
|
||||
FormGroup, InstitutionLogistration, PasswordField, RedirectLogistration,
|
||||
RenderInstitutionButton, SocialAuthProviders, ThirdPartyAuthAlert,
|
||||
FormGroup, InstitutionLogistration, PasswordField, RedirectLogistration, ThirdPartyAuthAlert,
|
||||
} from '../common-components';
|
||||
import { getThirdPartyAuthContext } from '../common-components/data/actions';
|
||||
import {
|
||||
extendedProfileSelector,
|
||||
fieldDescriptionSelector,
|
||||
optionalFieldsSelector,
|
||||
thirdPartyAuthContextSelector,
|
||||
fieldDescriptionSelector, optionalFieldsSelector, thirdPartyAuthContextSelector,
|
||||
} from '../common-components/data/selectors';
|
||||
import EnterpriseSSO from '../common-components/EnterpriseSSO';
|
||||
import {
|
||||
DEFAULT_STATE, LETTER_REGEX, NUMBER_REGEX, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX, VALID_NAME_REGEX,
|
||||
DEFAULT_STATE, INVALID_NAME_REGEX, LETTER_REGEX, NUMBER_REGEX, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX,
|
||||
} from '../data/constants';
|
||||
import {
|
||||
getAllPossibleQueryParam, getTpaHint, getTpaProvider, setCookie, setSurveyCookie,
|
||||
getAllPossibleQueryParams, getTpaHint, getTpaProvider, setCookie, setSurveyCookie,
|
||||
} from '../data/utils';
|
||||
import FormFieldRenderer from '../field-renderer';
|
||||
import CountryDropdown from './CountryDropdown';
|
||||
import ConfigurableRegistrationForm from './ConfigurableRegistrationForm';
|
||||
import {
|
||||
backupRegistrationFormBegin,
|
||||
clearUsernameSuggestions,
|
||||
fetchRealtimeValidations,
|
||||
registerNewUser,
|
||||
resetRegistrationForm,
|
||||
setRegistrationFormData,
|
||||
setUserPipelineDataLoaded,
|
||||
} from './data/actions';
|
||||
import {
|
||||
COMMON_EMAIL_PROVIDERS, COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, DEFAULT_SERVICE_PROVIDER_DOMAINS,
|
||||
DEFAULT_TOP_LEVEL_DOMAINS, FIELDS, FORM_SUBMISSION_ERROR,
|
||||
COUNTRY_CODE_KEY, COUNTRY_DISPLAY_KEY, FORM_SUBMISSION_ERROR,
|
||||
} from './data/constants';
|
||||
import {
|
||||
registrationErrorSelector,
|
||||
registrationFormDataSelector,
|
||||
registrationRequestSelector,
|
||||
usernameSuggestionsSelector,
|
||||
validationsSelector,
|
||||
} from './data/selectors';
|
||||
import HonorCode from './HonorCode';
|
||||
import { registrationErrorSelector, validationsSelector } from './data/selectors';
|
||||
import { getSuggestionForInvalidEmail, validateCountryField, validateEmailAddress } from './data/utils';
|
||||
import messages from './messages';
|
||||
import RegistrationFailure from './RegistrationFailure';
|
||||
import TermsOfService from './TermsOfService';
|
||||
import UsernameField from './UsernameField';
|
||||
import { getLevenshteinSuggestion, getSuggestionForInvalidEmail } from './utils';
|
||||
import { EmailField, UsernameField } from './registrationFields';
|
||||
import ThirdPartyAuth from './ThirdPartyAuth';
|
||||
|
||||
class RegistrationPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.handleOnClose = this.handleOnClose.bind(this);
|
||||
this.countryList = getCountryList(getLocale());
|
||||
this.queryParams = getAllPossibleQueryParam();
|
||||
// TODO: Once we have tested it and ready for openedX we can remove this flag and make the code
|
||||
// permanent part of Authn and remove extra code
|
||||
this.showDynamicRegistrationFields = getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS;
|
||||
this.tpaHint = getTpaHint();
|
||||
const { registrationFormData } = this.props;
|
||||
this.state = {
|
||||
country: '',
|
||||
email: registrationFormData.email,
|
||||
name: registrationFormData.name,
|
||||
password: registrationFormData.password,
|
||||
username: registrationFormData.username,
|
||||
marketingOptIn: registrationFormData.marketingOptIn,
|
||||
errors: {
|
||||
email: registrationFormData.errors.email,
|
||||
name: registrationFormData.errors.name,
|
||||
username: registrationFormData.errors.username,
|
||||
password: registrationFormData.errors.password,
|
||||
country: '',
|
||||
},
|
||||
emailFieldBorderClass: registrationFormData.emailFieldBorderClass,
|
||||
emailErrorSuggestion: registrationFormData.emailErrorSuggestion,
|
||||
emailWarningSuggestion: registrationFormData.emailWarningSuggestion,
|
||||
errorCode: null,
|
||||
failureCount: 0,
|
||||
startTime: Date.now(),
|
||||
totalRegistrationTime: 0,
|
||||
validatePassword: false,
|
||||
values: {},
|
||||
focusedField: '',
|
||||
};
|
||||
}
|
||||
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||
const urlRegex = new RegExp(INVALID_NAME_REGEX);
|
||||
|
||||
componentDidMount() {
|
||||
sendPageEvent('login_and_registration', 'register');
|
||||
const payload = { ...this.queryParams };
|
||||
window.optimizely = window.optimizely || [];
|
||||
window.optimizely.push({
|
||||
type: 'page',
|
||||
pageName: 'authn_registration_page',
|
||||
isActive: true,
|
||||
});
|
||||
const RegistrationPage = (props) => {
|
||||
const {
|
||||
backedUpFormData,
|
||||
backendCountryCode,
|
||||
backendValidations,
|
||||
fieldDescriptions,
|
||||
handleInstitutionLogin,
|
||||
intl,
|
||||
institutionLogin,
|
||||
optionalFields,
|
||||
registrationErrorCode,
|
||||
registrationResult,
|
||||
shouldBackupState,
|
||||
submitState,
|
||||
thirdPartyAuthApiStatus,
|
||||
thirdPartyAuthContext,
|
||||
usernameSuggestions,
|
||||
validationApiRateLimited,
|
||||
// Actions
|
||||
backupFormState,
|
||||
setUserPipelineDetailsLoaded,
|
||||
getRegistrationDataFromBackend,
|
||||
userPipelineDataLoaded,
|
||||
validateFromBackend,
|
||||
} = props;
|
||||
|
||||
if (payload.save_for_later === 'true') {
|
||||
sendTrackEvent('edx.bi.user.saveforlater.course.enroll.clicked', { category: 'save-for-later' });
|
||||
}
|
||||
|
||||
if (this.tpaHint) {
|
||||
payload.tpa_hint = this.tpaHint;
|
||||
}
|
||||
payload.is_register_page = true;
|
||||
this.props.resetRegistrationForm();
|
||||
this.props.getThirdPartyAuthContext(payload);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (nextProps.registrationFormData && this.props.registrationFormData !== nextProps.registrationFormData) {
|
||||
// Ensuring browser's autofill user credentials get filled and their state persists in the redux store.
|
||||
const nextState = {
|
||||
username: nextProps.registrationFormData.username || this.state.username,
|
||||
password: nextProps.registrationFormData.password || this.state.password,
|
||||
};
|
||||
// Do not set focused field's value from redux store to retain entered data in focused field.
|
||||
let { focusedField } = this.state;
|
||||
|
||||
// Exemption for country field's value as we need to set updated value from the store.
|
||||
if (focusedField === 'country') { focusedField = ''; }
|
||||
const { [focusedField]: _, ...registrationData } = { ...nextProps.registrationFormData, ...nextState };
|
||||
this.setState({
|
||||
...registrationData,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.usernameSuggestions.length > 0 && this.state.username === '') {
|
||||
this.props.setRegistrationFormData({
|
||||
username: ' ',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.props.validationDecisions !== nextProps.validationDecisions) {
|
||||
if (nextProps.validationDecisions) {
|
||||
const state = {};
|
||||
const errors = { ...this.state.errors, ...nextProps.validationDecisions };
|
||||
let validatePassword = false;
|
||||
|
||||
if (errors.password) {
|
||||
validatePassword = true;
|
||||
}
|
||||
|
||||
if (nextProps.registrationErrorCode) {
|
||||
state.errorCode = nextProps.registrationErrorCode;
|
||||
}
|
||||
|
||||
let {
|
||||
suggestedTldMessage,
|
||||
suggestedTopLevelDomain,
|
||||
suggestedSldMessage,
|
||||
suggestedServiceLevelDomain,
|
||||
} = this.state;
|
||||
|
||||
if (errors.email) {
|
||||
suggestedTldMessage = '';
|
||||
suggestedTopLevelDomain = '';
|
||||
suggestedSldMessage = '';
|
||||
suggestedServiceLevelDomain = '';
|
||||
}
|
||||
|
||||
this.setState({
|
||||
...state,
|
||||
suggestedTldMessage,
|
||||
suggestedTopLevelDomain,
|
||||
suggestedSldMessage,
|
||||
suggestedServiceLevelDomain,
|
||||
validatePassword,
|
||||
});
|
||||
|
||||
this.props.setRegistrationFormData({
|
||||
errors,
|
||||
}, true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.props.thirdPartyAuthContext.pipelineUserDetails !== nextProps.thirdPartyAuthContext.pipelineUserDetails) {
|
||||
const { pipelineUserDetails } = nextProps.thirdPartyAuthContext;
|
||||
const { registrationFormData } = this.props;
|
||||
|
||||
// Added a conditional errors check to not fall back on pipelines data when a user explicitly edits the form.
|
||||
this.props.setRegistrationFormData({
|
||||
name: registrationFormData.errors.name ? registrationFormData.name
|
||||
: (registrationFormData.name || pipelineUserDetails.name || ''),
|
||||
email: registrationFormData.errors.email ? registrationFormData.email
|
||||
: (registrationFormData.email || pipelineUserDetails.email || ''),
|
||||
username: registrationFormData.errors.username ? registrationFormData.username
|
||||
: (registrationFormData.username || pipelineUserDetails.username || ''),
|
||||
country: registrationFormData.country || nextProps.thirdPartyAuthContext.countryCode,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.props.thirdPartyAuthContext.countryCode !== nextProps.thirdPartyAuthContext.countryCode) {
|
||||
this.props.setRegistrationFormData({
|
||||
country: this.props.registrationFormData.country || nextProps.thirdPartyAuthContext.countryCode,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
onChangeHandler = (e) => {
|
||||
const { name, value, checked } = e.target;
|
||||
const { errors, values } = this.state;
|
||||
if (e.target.type === 'checkbox') {
|
||||
errors[name] = '';
|
||||
values[name] = checked;
|
||||
} else {
|
||||
values[name] = value;
|
||||
}
|
||||
const state = { errors, values };
|
||||
this.setState({ ...state });
|
||||
const countryList = useMemo(() => getCountryList(getLocale()), []);
|
||||
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
|
||||
const tpaHint = useMemo(() => getTpaHint(), []);
|
||||
const flags = {
|
||||
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
|
||||
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
|
||||
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
|
||||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields });
|
||||
const [configurableFormFields, setConfigurableFormFields] = useState({ ...backedUpFormData.configurableFormFields });
|
||||
const [errors, setErrors] = useState({ ...backedUpFormData.errors });
|
||||
const [emailSuggestion, setEmailSuggestion] = useState({ ...backedUpFormData.emailSuggestion });
|
||||
|
||||
const { startTime } = this.state;
|
||||
const totalRegistrationTime = (Date.now() - startTime) / 1000;
|
||||
const dynamicFieldErrorMessages = {};
|
||||
const [errorCode, setErrorCode] = useState({ type: '', count: 0 });
|
||||
const [formStartTime, setFormStartTime] = useState(null);
|
||||
const [focusedField, setFocusedField] = useState(null);
|
||||
|
||||
let payload = {
|
||||
name: this.state.name,
|
||||
username: this.state.username,
|
||||
email: this.state.email,
|
||||
is_authn_mfe: true,
|
||||
};
|
||||
const {
|
||||
providers, currentProvider, secondaryProviders, finishAuthUrl,
|
||||
} = thirdPartyAuthContext;
|
||||
const platformName = getConfig().SITE_NAME;
|
||||
|
||||
if (this.props.thirdPartyAuthContext.currentProvider) {
|
||||
payload.social_auth_provider = this.props.thirdPartyAuthContext.currentProvider;
|
||||
} else {
|
||||
payload.password = this.state.password;
|
||||
}
|
||||
|
||||
if (this.showDynamicRegistrationFields) {
|
||||
payload.extendedProfile = [];
|
||||
Object.keys(this.props.fieldDescriptions).forEach((fieldName) => {
|
||||
if (this.props.extendedProfile.includes(fieldName)) {
|
||||
payload.extendedProfile.push({ fieldName, fieldValue: this.state.values[fieldName] });
|
||||
} else {
|
||||
payload[fieldName] = this.state.values[fieldName];
|
||||
}
|
||||
dynamicFieldErrorMessages[fieldName] = this.props.fieldDescriptions[fieldName].error_message;
|
||||
});
|
||||
if (
|
||||
this.props.fieldDescriptions[FIELDS.HONOR_CODE]
|
||||
&& this.props.fieldDescriptions[FIELDS.HONOR_CODE].type === 'tos_and_honor_code'
|
||||
) {
|
||||
payload[FIELDS.HONOR_CODE] = true;
|
||||
}
|
||||
} else {
|
||||
payload.country = this.state.country;
|
||||
payload.honor_code = true;
|
||||
}
|
||||
|
||||
if (!this.isFormValid(payload, dynamicFieldErrorMessages)) {
|
||||
this.setState(prevState => ({
|
||||
errorCode: FORM_SUBMISSION_ERROR,
|
||||
failureCount: prevState.failureCount + 1,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
if (getConfig().MARKETING_EMAILS_OPT_IN) {
|
||||
payload.marketing_emails_opt_in = this.state.marketingOptIn;
|
||||
}
|
||||
|
||||
payload = snakeCaseObject(payload);
|
||||
payload.totalRegistrationTime = totalRegistrationTime;
|
||||
|
||||
// add query params to the payload
|
||||
payload = { ...payload, ...this.queryParams };
|
||||
this.setState({
|
||||
totalRegistrationTime,
|
||||
}, () => {
|
||||
this.props.registerNewUser(payload);
|
||||
});
|
||||
}
|
||||
|
||||
handleOnBlur = (e) => {
|
||||
let { name, value } = e.target;
|
||||
this.setState({
|
||||
focusedField: '',
|
||||
});
|
||||
|
||||
if (name === 'passwordValidation') {
|
||||
name = 'password';
|
||||
value = this.state.password;
|
||||
}
|
||||
const payload = {
|
||||
is_authn_mfe: true,
|
||||
form_field_key: name,
|
||||
email: this.state.email,
|
||||
username: this.state.username,
|
||||
password: this.state.password,
|
||||
name: this.state.name,
|
||||
honor_code: true,
|
||||
country: this.state.country,
|
||||
};
|
||||
this.validateInput(name, value, payload);
|
||||
}
|
||||
|
||||
handleOnChange = (e) => {
|
||||
let { value } = e.target;
|
||||
if (e.target.name === 'username') {
|
||||
if (value.length > 30) {
|
||||
return;
|
||||
}
|
||||
if (value.startsWith(' ')) {
|
||||
value = value.substring(1);
|
||||
/**
|
||||
* Set the userPipelineDetails data in formFields for only first time
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!userPipelineDataLoaded) {
|
||||
const { pipelineUserDetails } = thirdPartyAuthContext;
|
||||
if (pipelineUserDetails && Object.keys(pipelineUserDetails).length !== 0) {
|
||||
const { name = '', username = '', email = '' } = pipelineUserDetails;
|
||||
setFormFields(prevState => ({
|
||||
...prevState, name, username, email,
|
||||
}));
|
||||
setUserPipelineDetailsLoaded(true);
|
||||
}
|
||||
}
|
||||
}, [thirdPartyAuthContext, userPipelineDataLoaded, setUserPipelineDetailsLoaded]);
|
||||
|
||||
this.setState({
|
||||
[e.target.name]: value,
|
||||
});
|
||||
}
|
||||
|
||||
handleOnFocus = (e) => {
|
||||
const fieldName = e.target.name;
|
||||
this.setState({
|
||||
focusedField: fieldName,
|
||||
});
|
||||
const { errors } = this.state;
|
||||
errors[fieldName] = '';
|
||||
if (fieldName === 'username') {
|
||||
this.props.clearUsernameSuggestions();
|
||||
}
|
||||
if (fieldName === 'countryExpand') {
|
||||
errors.country = '';
|
||||
}
|
||||
if (fieldName === 'passwordValidation') {
|
||||
errors.password = '';
|
||||
useEffect(() => {
|
||||
if (!formStartTime) {
|
||||
sendPageEvent('login_and_registration', 'register');
|
||||
const payload = { ...queryParams, is_register_page: true };
|
||||
if (tpaHint) {
|
||||
payload.tpa_hint = tpaHint;
|
||||
}
|
||||
getRegistrationDataFromBackend(payload);
|
||||
setFormStartTime(Date.now());
|
||||
}
|
||||
}, [formStartTime, getRegistrationDataFromBackend, queryParams, tpaHint]);
|
||||
|
||||
this.props.setRegistrationFormData({
|
||||
errors,
|
||||
});
|
||||
}
|
||||
|
||||
handleSuggestionClick = (e, suggestion) => {
|
||||
const { errors } = this.state;
|
||||
if (e.target.name === 'username') {
|
||||
errors.username = '';
|
||||
this.props.setRegistrationFormData({
|
||||
username: suggestion,
|
||||
errors,
|
||||
});
|
||||
this.props.clearUsernameSuggestions();
|
||||
} else if (e.target.name === 'email') {
|
||||
e.preventDefault();
|
||||
errors.email = '';
|
||||
this.props.setRegistrationFormData({
|
||||
email: suggestion,
|
||||
emailErrorSuggestion: null,
|
||||
emailWarningSuggestion: null,
|
||||
emailFieldBorderClass: '',
|
||||
errors,
|
||||
/**
|
||||
* Backup the registration form in redux when register page is toggled.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (shouldBackupState) {
|
||||
backupFormState({
|
||||
configurableFormFields: { ...configurableFormFields },
|
||||
formFields: { ...formFields },
|
||||
emailSuggestion: { ...emailSuggestion },
|
||||
errors: { ...errors },
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [shouldBackupState, configurableFormFields, formFields, errors, emailSuggestion, backupFormState]);
|
||||
|
||||
handleUsernameSuggestionClose = () => {
|
||||
this.props.setRegistrationFormData({
|
||||
username: '',
|
||||
});
|
||||
this.props.clearUsernameSuggestions();
|
||||
}
|
||||
|
||||
validateDynamicFields = (e) => {
|
||||
const { intl } = this.props;
|
||||
const { errors } = this.state;
|
||||
const { name, value } = e.target;
|
||||
if (!value.trim()) {
|
||||
errors[name] = this.props.fieldDescriptions[name].error_message;
|
||||
useEffect(() => {
|
||||
if (backendValidations) {
|
||||
setErrors(prevErrors => ({ ...prevErrors, ...backendValidations }));
|
||||
}
|
||||
if (name === 'confirm_email' && value.length > 0 && this.state.email && value !== this.state.email) {
|
||||
errors.confirm_email = intl.formatMessage(messages['email.do.not.match']);
|
||||
}, [backendValidations]);
|
||||
|
||||
useEffect(() => {
|
||||
if (registrationErrorCode) {
|
||||
setErrorCode(prevState => ({ type: registrationErrorCode, count: prevState.count + 1 }));
|
||||
}
|
||||
this.setState({ errors });
|
||||
}
|
||||
}, [registrationErrorCode]);
|
||||
|
||||
isFormValid(payload, dynamicFieldError) {
|
||||
const { errors } = this.state;
|
||||
let isValid = true;
|
||||
Object.keys(payload).forEach(key => {
|
||||
if (!payload[key]) {
|
||||
errors[key] = (key in dynamicFieldError) ? dynamicFieldError[key] : this.props.intl.formatMessage(messages[`empty.${key}.field.error`]);
|
||||
}
|
||||
// Mark form invalid, if there was already a validation error for this key or we added empty field error
|
||||
if (errors[key]) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
const state = { ...payload, errors };
|
||||
this.props.setRegistrationFormData({
|
||||
...state,
|
||||
});
|
||||
return isValid;
|
||||
}
|
||||
|
||||
validateInput(fieldName, value, payload) {
|
||||
let state = {};
|
||||
const { errors } = this.state;
|
||||
const { intl, statusCode } = this.props;
|
||||
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
|
||||
const urlRegex = new RegExp(VALID_NAME_REGEX);
|
||||
|
||||
switch (fieldName) {
|
||||
case 'email':
|
||||
if (!value) {
|
||||
errors.email = intl.formatMessage(messages['empty.email.field.error']);
|
||||
} else if (value.length <= 2) {
|
||||
errors.email = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
} else {
|
||||
let emailWarningSuggestion = null;
|
||||
let emailErrorSuggestion = null;
|
||||
|
||||
const [username, domainName] = value.split('@');
|
||||
|
||||
// Check if email address is invalid. If we have a suggestion for invalid email provide that along with
|
||||
// error message.
|
||||
if (!emailRegex.test(value)) {
|
||||
errors.email = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
emailErrorSuggestion = getSuggestionForInvalidEmail(domainName, username);
|
||||
} else {
|
||||
let suggestion = null;
|
||||
const hasMultipleSubdomains = value.match(/\./g).length > 1;
|
||||
const [serviceLevelDomain, topLevelDomain] = domainName.split('.');
|
||||
const tldSuggestion = !DEFAULT_TOP_LEVEL_DOMAINS.includes(topLevelDomain);
|
||||
const serviceSuggestion = getLevenshteinSuggestion(serviceLevelDomain, DEFAULT_SERVICE_PROVIDER_DOMAINS, 2);
|
||||
|
||||
if (DEFAULT_SERVICE_PROVIDER_DOMAINS.includes(serviceSuggestion || serviceLevelDomain)) {
|
||||
suggestion = `${username}@${serviceSuggestion || serviceLevelDomain}.com`;
|
||||
}
|
||||
|
||||
if (!hasMultipleSubdomains && tldSuggestion) {
|
||||
emailErrorSuggestion = suggestion;
|
||||
} else if (serviceSuggestion) {
|
||||
emailWarningSuggestion = suggestion;
|
||||
} else {
|
||||
suggestion = getLevenshteinSuggestion(domainName, COMMON_EMAIL_PROVIDERS, 3);
|
||||
if (suggestion) {
|
||||
emailWarningSuggestion = `${username}@${suggestion}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMultipleSubdomains && tldSuggestion) {
|
||||
errors.email = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
} else if (payload && statusCode !== 403) {
|
||||
this.props.fetchRealtimeValidations(payload);
|
||||
} else {
|
||||
errors.email = '';
|
||||
}
|
||||
if (this.state.values && this.state.values.confirm_email && value !== this.state.values.confirm_email) {
|
||||
errors.confirm_email = intl.formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
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],
|
||||
},
|
||||
}
|
||||
state = {
|
||||
emailWarningSuggestion,
|
||||
emailErrorSuggestion,
|
||||
emailFieldBorderClass: emailWarningSuggestion ? 'yellow-border' : null,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case 'name':
|
||||
if (!value.trim()) {
|
||||
errors.name = intl.formatMessage(messages['empty.name.field.error']);
|
||||
} else if (value && value.match(urlRegex)) {
|
||||
errors.name = intl.formatMessage(messages['name.validation.message']);
|
||||
} else {
|
||||
errors.name = '';
|
||||
}
|
||||
|
||||
if (!this.state.username.trim() && value) {
|
||||
// fetch username suggestions based on the full name
|
||||
this.props.fetchRealtimeValidations(payload);
|
||||
}
|
||||
break;
|
||||
case 'username':
|
||||
if (value === ' ' && this.props.usernameSuggestions.length > 0) {
|
||||
errors.username = '';
|
||||
break;
|
||||
}
|
||||
if (!value || value.length <= 1 || value.length > 30) {
|
||||
errors.username = intl.formatMessage(messages['username.validation.message']);
|
||||
} else if (!value.match(/^[a-zA-Z0-9_-]*$/i)) {
|
||||
errors.username = intl.formatMessage(messages['username.format.validation.message']);
|
||||
} else if (payload && statusCode !== 403) {
|
||||
this.props.fetchRealtimeValidations(payload);
|
||||
} else {
|
||||
errors.username = '';
|
||||
}
|
||||
|
||||
if (this.state.validatePassword) {
|
||||
this.props.fetchRealtimeValidations({ ...payload, form_field_key: 'password' });
|
||||
}
|
||||
break;
|
||||
case 'password':
|
||||
errors.password = '';
|
||||
if (!value || !LETTER_REGEX.test(value) || !NUMBER_REGEX.test(value) || value.length < 8) {
|
||||
errors.password = intl.formatMessage(messages['password.validation.message']);
|
||||
} else if (payload && statusCode !== 403) {
|
||||
this.props.fetchRealtimeValidations(payload);
|
||||
}
|
||||
break;
|
||||
case 'country':
|
||||
value = value.trim(); // eslint-disable-line no-param-reassign
|
||||
if (value) {
|
||||
const normalizedValue = value.toLowerCase();
|
||||
let selectedCountry = (
|
||||
this.countryList.find((o) => o[COUNTRY_DISPLAY_KEY].toLowerCase().trim() === normalizedValue));
|
||||
if (selectedCountry) {
|
||||
value = selectedCountry[COUNTRY_CODE_KEY]; // eslint-disable-line no-param-reassign
|
||||
errors.country = '';
|
||||
break;
|
||||
} else {
|
||||
// Handling a case here where user enters a valid country code that needs to be
|
||||
// evaluated and set its value as a valid value.
|
||||
selectedCountry = (
|
||||
this.countryList.find((o) => o[COUNTRY_CODE_KEY].toLowerCase().trim() === normalizedValue));
|
||||
if (selectedCountry) {
|
||||
value = selectedCountry[COUNTRY_CODE_KEY]; // eslint-disable-line no-param-reassign
|
||||
errors.country = '';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
errors.country = intl.formatMessage(messages['empty.country.field.error']);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
));
|
||||
}
|
||||
}
|
||||
}, [backendCountryCode, countryList]);
|
||||
|
||||
state = {
|
||||
...state,
|
||||
[fieldName]: value,
|
||||
};
|
||||
this.props.setRegistrationFormData({
|
||||
...state,
|
||||
errors,
|
||||
});
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
handleOnClose() {
|
||||
this.props.setRegistrationFormData({
|
||||
emailErrorSuggestion: null,
|
||||
});
|
||||
}
|
||||
|
||||
renderEmailFeedback() {
|
||||
if (this.state.emailErrorSuggestion) {
|
||||
return (
|
||||
<Alert variant="danger" className="email-error-alert" icon={Error}>
|
||||
<span className="alert-text">
|
||||
{this.props.intl.formatMessage(messages['did.you.mean.alert.text'])}{' '}
|
||||
<Alert.Link
|
||||
href="#"
|
||||
name="email"
|
||||
onClick={e => { this.handleSuggestionClick(e, this.state.emailErrorSuggestion); }}
|
||||
>
|
||||
{this.state.emailErrorSuggestion}
|
||||
</Alert.Link>?<Icon src={Close} className="alert-close" onClick={this.handleOnClose} tabIndex="0" />
|
||||
</span>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
if (this.state.emailWarningSuggestion) {
|
||||
return (
|
||||
<span className="small">
|
||||
{this.props.intl.formatMessage(messages['did.you.mean.alert.text'])}:{' '}
|
||||
<Alert.Link
|
||||
href="#"
|
||||
name="email"
|
||||
className="email-warning-alert-link"
|
||||
onClick={e => { this.handleSuggestionClick(e, this.state.emailWarningSuggestion); }}
|
||||
>
|
||||
{this.state.emailWarningSuggestion}
|
||||
</Alert.Link>?
|
||||
</span>
|
||||
);
|
||||
/**
|
||||
* We need to remove the placeholder from the field, adding a space will do that.
|
||||
* This is needed because we are placing the username suggestions on top of the field.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (usernameSuggestions.length && !formFields.username) {
|
||||
setFormFields(prevState => ({ ...prevState, username: ' ' }));
|
||||
}
|
||||
}, [usernameSuggestions, formFields]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
renderThirdPartyAuth(providers, secondaryProviders, currentProvider, thirdPartyAuthApiStatus, intl) {
|
||||
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
|
||||
const isSocialAuthActive = !!providers.length && !currentProvider;
|
||||
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
|
||||
|
||||
return (
|
||||
<>
|
||||
{((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive) && (
|
||||
<div className="mt-4 mb-3 h4">
|
||||
{intl.formatMessage(messages['registration.other.options.heading'])}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{thirdPartyAuthApiStatus === PENDING_STATE ? (
|
||||
<Skeleton className="tpa-skeleton" height={36} count={2} />
|
||||
) : (
|
||||
<>
|
||||
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
|
||||
<RenderInstitutionButton
|
||||
onSubmitHandler={this.props.handleInstitutionLogin}
|
||||
buttonTitle={intl.formatMessage(messages['register.institution.login.button'])}
|
||||
/>
|
||||
)}
|
||||
{isSocialAuthActive && (
|
||||
<div className="row m-0">
|
||||
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
renderForm(currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthApiStatus,
|
||||
finishAuthUrl,
|
||||
submitState,
|
||||
intl) {
|
||||
if (this.props.institutionLogin) {
|
||||
return (
|
||||
<InstitutionLogistration
|
||||
secondaryProviders={this.props.thirdPartyAuthContext.secondaryProviders}
|
||||
headingTitle={intl.formatMessage(messages['register.institution.login.page.title'])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (this.props.registrationResult.success) {
|
||||
useEffect(() => {
|
||||
if (registrationResult.success) {
|
||||
// TODO: Do we still need this cookie?
|
||||
setSurveyCookie('register');
|
||||
setCookie(getConfig().REGISTER_CONVERSION_COOKIE_NAME, true);
|
||||
setCookie('authn-returning-user');
|
||||
@@ -663,200 +191,362 @@ class RegistrationPage extends React.Component {
|
||||
window.optimizely.push({
|
||||
type: 'event',
|
||||
eventName: 'authn-register-conversion',
|
||||
tags: {
|
||||
value: this.state.totalRegistrationTime,
|
||||
},
|
||||
});
|
||||
|
||||
// Fire GTM event used for integration with impact.com
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
window.dataLayer.push({
|
||||
event: 'ImpactRegistrationEvent',
|
||||
});
|
||||
}
|
||||
}, [registrationResult]);
|
||||
|
||||
const validateInput = (fieldName, value, payload, shouldValidateFromBackend, setError = true) => {
|
||||
let fieldError = '';
|
||||
let confirmEmailError = ''; // This is to handle the use case where the form contains "confirm email" field
|
||||
let countryFieldCode = '';
|
||||
|
||||
switch (fieldName) {
|
||||
case 'name':
|
||||
if (!value.trim()) {
|
||||
fieldError = intl.formatMessage(messages['empty.name.field.error']);
|
||||
} else if (value && value.match(urlRegex)) {
|
||||
fieldError = intl.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']);
|
||||
} else if (value.length <= 2) {
|
||||
fieldError = intl.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']);
|
||||
setEmailSuggestion({
|
||||
suggestion: getSuggestionForInvalidEmail(domainName, username),
|
||||
type: 'error',
|
||||
});
|
||||
} else {
|
||||
const response = validateEmailAddress(value, username, domainName);
|
||||
if (response.hasError) {
|
||||
fieldError = intl.formatMessage(messages['email.invalid.format.error']);
|
||||
delete response.hasError;
|
||||
} else if (shouldValidateFromBackend) {
|
||||
validateFromBackend(payload);
|
||||
}
|
||||
setEmailSuggestion({ ...response });
|
||||
|
||||
if (configurableFormFields.confirm_email && value !== configurableFormFields.confirm_email) {
|
||||
confirmEmailError = intl.formatMessage(messages['email.do.not.match']);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'username':
|
||||
if (!value || value.length <= 1 || value.length > 30) {
|
||||
fieldError = intl.formatMessage(messages['username.validation.message']);
|
||||
} else if (!value.match(/^[a-zA-Z0-9_-]*$/i)) {
|
||||
fieldError = intl.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']);
|
||||
} 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']),
|
||||
);
|
||||
fieldError = error;
|
||||
countryFieldCode = countryCode;
|
||||
setConfigurableFormFields(prevState => ({ ...prevState, country: { countryCode, displayValue } }));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (flags.showConfigurableRegistrationFields) {
|
||||
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']);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (setError) {
|
||||
setErrors(prevErrors => ({
|
||||
...prevErrors,
|
||||
confirm_email: flags.showConfigurableRegistrationFields ? confirmEmailError : '',
|
||||
[fieldName]: fieldError,
|
||||
}));
|
||||
}
|
||||
return { fieldError, countryFieldCode };
|
||||
};
|
||||
|
||||
const isFormValid = (payload, focusedFieldError) => {
|
||||
const fieldErrors = { ...errors };
|
||||
let isValid = !focusedFieldError;
|
||||
Object.keys(payload).forEach(key => {
|
||||
if (!payload[key]) {
|
||||
fieldErrors[key] = intl.formatMessage(messages[`empty.${key}.field.error`]);
|
||||
}
|
||||
if (fieldErrors[key]) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (flags.showConfigurableEdxFields) {
|
||||
if (!configurableFormFields.country.displayValue) {
|
||||
fieldErrors.country = intl.formatMessage(messages['empty.country.field.error']);
|
||||
}
|
||||
if (fieldErrors.country) {
|
||||
isValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (flags.showConfigurableRegistrationFields) {
|
||||
Object.keys(fieldDescriptions).forEach(key => {
|
||||
if (key === 'country' && !configurableFormFields.country.displayValue) {
|
||||
fieldErrors[key] = intl.formatMessage(messages['empty.country.field.error']);
|
||||
} else if (!configurableFormFields[key]) {
|
||||
fieldErrors[key] = fieldDescriptions[key].error_message;
|
||||
}
|
||||
if (fieldErrors[key]) {
|
||||
isValid = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const honorCode = [];
|
||||
const formFields = this.showDynamicRegistrationFields ? (
|
||||
Object.keys(this.props.fieldDescriptions).map((fieldName) => {
|
||||
const fieldData = this.props.fieldDescriptions[fieldName];
|
||||
switch (fieldData.name) {
|
||||
case FIELDS.COUNTRY:
|
||||
return (
|
||||
<span key={fieldData.name}>
|
||||
<CountryDropdown
|
||||
name="country"
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
options={this.countryList}
|
||||
value={this.state.values[fieldData.name]}
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
|
||||
handleChange={
|
||||
(value) => this.setState(prevState => ({ values: { ...prevState.values, country: value } }))
|
||||
}
|
||||
errorCode={this.state.errorCode}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
case FIELDS.HONOR_CODE:
|
||||
honorCode.push(
|
||||
<span key={fieldData.name}>
|
||||
<HonorCode
|
||||
fieldType={fieldData.type}
|
||||
value={this.state.values[fieldData.name]}
|
||||
onChangeHandler={this.onChangeHandler}
|
||||
errorMessage={this.state.errors[fieldData.name]}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
return null;
|
||||
case FIELDS.TERMS_OF_SERVICE:
|
||||
honorCode.push(
|
||||
<span key={fieldData.name}>
|
||||
<TermsOfService
|
||||
value={this.state.values[fieldData.name]}
|
||||
onChangeHandler={this.onChangeHandler}
|
||||
errorMessage={this.state.errors[fieldData.name]}
|
||||
/>
|
||||
</span>,
|
||||
);
|
||||
return null;
|
||||
default:
|
||||
return (
|
||||
<span key={fieldData.name}>
|
||||
<FormFieldRenderer
|
||||
fieldData={fieldData}
|
||||
value={this.state.values[fieldData.name]}
|
||||
onChangeHandler={this.onChangeHandler}
|
||||
handleBlur={this.validateDynamicFields}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors[fieldData.name]}
|
||||
isRequired
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
})
|
||||
) : null;
|
||||
if (focusedField) {
|
||||
fieldErrors[focusedField] = focusedFieldError;
|
||||
}
|
||||
setErrors({ ...fieldErrors });
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const handleSuggestionClick = (event, fieldName, suggestion = '') => {
|
||||
event.preventDefault();
|
||||
setErrors(prevErrors => ({ ...prevErrors, [fieldName]: '' }));
|
||||
switch (fieldName) {
|
||||
case 'email':
|
||||
setFormFields(prevState => ({ ...prevState, email: emailSuggestion.suggestion }));
|
||||
setEmailSuggestion({ suggestion: '', type: '' });
|
||||
break;
|
||||
case 'username':
|
||||
setFormFields(prevState => ({ ...prevState, username: suggestion }));
|
||||
props.resetUsernameSuggestions();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
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 (name === 'username') {
|
||||
if (value.length > 30) {
|
||||
return;
|
||||
}
|
||||
if (value.startsWith(' ')) {
|
||||
value = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
setFormFields(prevState => ({ ...prevState, [name]: value }));
|
||||
};
|
||||
|
||||
const handleOnBlur = (event) => {
|
||||
const { name, value } = event.target;
|
||||
const payload = {
|
||||
name: formFields.name,
|
||||
email: formFields.email,
|
||||
username: formFields.username,
|
||||
password: formFields.password,
|
||||
form_field_key: name,
|
||||
};
|
||||
|
||||
setFocusedField(null);
|
||||
validateInput(name, name === 'password' ? formFields.password : value, payload, !validationApiRateLimited);
|
||||
};
|
||||
|
||||
const handleOnFocus = (event) => {
|
||||
const { name, value } = event.target;
|
||||
setErrors(prevErrors => ({ ...prevErrors, [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);
|
||||
|
||||
if (name === 'username') {
|
||||
props.resetUsernameSuggestions();
|
||||
// If we added a space character to username field to display the suggestion
|
||||
// remove it before user enters the input. This is to ensure user doesn't
|
||||
// have a space prefixed to the username.
|
||||
if (value === ' ') {
|
||||
setFormFields(prevState => ({ ...prevState, [name]: '' }));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const totalRegistrationTime = (Date.now() - formStartTime) / 1000;
|
||||
let payload = { ...formFields };
|
||||
|
||||
if (currentProvider) {
|
||||
delete payload.password;
|
||||
payload.social_auth_provider = currentProvider;
|
||||
}
|
||||
|
||||
const { fieldError: focusedFieldError, countryFieldCode } = focusedField ? (
|
||||
validateInput(
|
||||
focusedField,
|
||||
(focusedField in fieldDescriptions || focusedField === 'country') ? (
|
||||
configurableFormFields[focusedField]
|
||||
) : formFields[focusedField],
|
||||
payload,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
) : '';
|
||||
|
||||
if (!isFormValid(payload, focusedFieldError)) {
|
||||
setErrorCode(prevState => ({ type: FORM_SUBMISSION_ERROR, count: prevState.count + 1 }));
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(configurableFormFields).forEach((fieldName) => {
|
||||
if (fieldName === 'country') {
|
||||
payload[fieldName] = focusedField === 'country' ? countryFieldCode : configurableFormFields[fieldName].countryCode;
|
||||
} else {
|
||||
payload[fieldName] = configurableFormFields[fieldName];
|
||||
}
|
||||
});
|
||||
|
||||
// Don't send the marketing email opt-in value if the flag is turned off
|
||||
if (!flags.showMarketingEmailOptInCheckbox) {
|
||||
delete payload.marketingEmailsOptIn;
|
||||
}
|
||||
|
||||
payload = snakeCaseObject(payload);
|
||||
payload.totalRegistrationTime = totalRegistrationTime;
|
||||
|
||||
// add query params to the payload
|
||||
payload = { ...payload, ...queryParams };
|
||||
props.registerNewUser(payload);
|
||||
};
|
||||
|
||||
const renderForm = () => {
|
||||
if (institutionLogin) {
|
||||
return (
|
||||
<InstitutionLogistration
|
||||
secondaryProviders={secondaryProviders}
|
||||
headingTitle={intl.formatMessage(messages['register.institution.login.page.title'])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{intl.formatMessage(messages['register.page.title'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
</title>
|
||||
<title>{intl.formatMessage(messages['register.page.title'], { siteName: getConfig().SITE_NAME })}</title>
|
||||
</Helmet>
|
||||
<RedirectLogistration
|
||||
success={this.props.registrationResult.success}
|
||||
redirectUrl={this.props.registrationResult.redirectUrl}
|
||||
success={registrationResult.success}
|
||||
redirectUrl={registrationResult.redirectUrl}
|
||||
finishAuthUrl={finishAuthUrl}
|
||||
optionalFields={this.props.optionalFields}
|
||||
redirectToWelcomePage={getConfig().ENABLE_PROGRESSIVE_PROFILING
|
||||
&& Object.keys(this.props.optionalFields).length !== 0}
|
||||
optionalFields={optionalFields}
|
||||
redirectToProgressiveProfilingPage={
|
||||
getConfig().ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN && Object.keys(optionalFields).includes('fields')
|
||||
}
|
||||
/>
|
||||
<div className="mw-xs mt-3">
|
||||
{this.state.errorCode ? (
|
||||
<RegistrationFailure
|
||||
errorCode={this.state.errorCode}
|
||||
failureCount={this.state.failureCount}
|
||||
context={{ provider: currentProvider }}
|
||||
/>
|
||||
) : null}
|
||||
{currentProvider && (
|
||||
<>
|
||||
<ThirdPartyAuthAlert
|
||||
currentProvider={currentProvider}
|
||||
platformName={this.props.thirdPartyAuthContext.platformName}
|
||||
referrer={REGISTER_PAGE}
|
||||
/>
|
||||
<h4 className="mt-4 mb-4">{intl.formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
|
||||
</>
|
||||
)}
|
||||
<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={this.state.name}
|
||||
autoComplete="on"
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.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'])}
|
||||
/>
|
||||
<FormGroup
|
||||
<EmailField
|
||||
name="email"
|
||||
value={this.state.email}
|
||||
autoComplete="on"
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
errorMessage={this.state.errors.email}
|
||||
handleFocus={this.handleOnFocus}
|
||||
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'])}
|
||||
borderClass={this.state.emailFieldBorderClass}
|
||||
>
|
||||
{this.renderEmailFeedback()}
|
||||
</FormGroup>
|
||||
|
||||
/>
|
||||
<UsernameField
|
||||
name="username"
|
||||
spellCheck="false"
|
||||
value={this.state.username}
|
||||
autoComplete="on"
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.username}
|
||||
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'])}
|
||||
handleSuggestionClick={this.handleSuggestionClick}
|
||||
usernameSuggestions={this.props.usernameSuggestions}
|
||||
handleUsernameSuggestionClose={this.handleUsernameSuggestionClose}
|
||||
/>
|
||||
|
||||
{!currentProvider && (
|
||||
<PasswordField
|
||||
name="password"
|
||||
value={this.state.password}
|
||||
autoComplete="off"
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleChange={this.handleOnChange}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.password}
|
||||
value={formFields.password}
|
||||
handleChange={handleOnChange}
|
||||
handleBlur={handleOnBlur}
|
||||
handleFocus={handleOnFocus}
|
||||
errorMessage={errors.password}
|
||||
floatingLabel={intl.formatMessage(messages['registration.password.label'])}
|
||||
/>
|
||||
)}
|
||||
{!(this.showDynamicRegistrationFields)
|
||||
&& (
|
||||
<CountryDropdown
|
||||
name="country"
|
||||
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
|
||||
options={this.countryList}
|
||||
value={this.state.country}
|
||||
autoComplete="on"
|
||||
handleBlur={this.handleOnBlur}
|
||||
handleFocus={this.handleOnFocus}
|
||||
errorMessage={this.state.errors.country}
|
||||
errorCode={this.state.errorCode}
|
||||
/>
|
||||
)}
|
||||
{formFields}
|
||||
{(getConfig().MARKETING_EMAILS_OPT_IN)
|
||||
&& (
|
||||
<Form.Checkbox
|
||||
className="opt-checkbox"
|
||||
name="marketing_emails_opt_in"
|
||||
checked={this.state.marketingOptIn}
|
||||
onChange={(e) => this.props.setRegistrationFormData({
|
||||
marketingOptIn: e.target.checked,
|
||||
})}
|
||||
>
|
||||
{intl.formatMessage(messages['registration.opt.in.label'], { siteName: getConfig().SITE_NAME })}
|
||||
</Form.Checkbox>
|
||||
)}
|
||||
{!(this.showDynamicRegistrationFields) ? (
|
||||
<HonorCode
|
||||
fieldType="tos_and_honor_code"
|
||||
/>
|
||||
) : <div>{honorCode}</div>}
|
||||
<ConfigurableRegistrationForm
|
||||
countryList={countryList}
|
||||
email={formFields.email}
|
||||
fieldErrors={errors}
|
||||
formFields={configurableFormFields}
|
||||
setFieldErrors={setErrors}
|
||||
setFormFields={setConfigurableFormFields}
|
||||
setFocusedField={setFocusedField}
|
||||
fieldDescriptions={fieldDescriptions}
|
||||
/>
|
||||
<StatefulButton
|
||||
name="register-user"
|
||||
id="register-user"
|
||||
name="register-user"
|
||||
type="submit"
|
||||
variant="brand"
|
||||
className="register-stateful-button-width mt-4 mb-4"
|
||||
@@ -865,132 +555,84 @@ class RegistrationPage extends React.Component {
|
||||
default: intl.formatMessage(messages['create.account.for.free.button']),
|
||||
pending: '',
|
||||
}}
|
||||
onClick={this.handleSubmit}
|
||||
onClick={handleSubmit}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
/>
|
||||
{this.renderThirdPartyAuth(providers,
|
||||
secondaryProviders,
|
||||
currentProvider,
|
||||
thirdPartyAuthApiStatus,
|
||||
intl)}
|
||||
<ThirdPartyAuth
|
||||
currentProvider={currentProvider}
|
||||
providers={providers}
|
||||
secondaryProviders={secondaryProviders}
|
||||
handleInstitutionLogin={handleInstitutionLogin}
|
||||
thirdPartyAuthApiStatus={thirdPartyAuthApiStatus}
|
||||
/>
|
||||
</Form>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { intl, submitState, thirdPartyAuthApiStatus } = this.props;
|
||||
const {
|
||||
currentProvider, finishAuthUrl, providers, secondaryProviders,
|
||||
} = this.props.thirdPartyAuthContext;
|
||||
|
||||
if (this.tpaHint) {
|
||||
if (thirdPartyAuthApiStatus === PENDING_STATE) {
|
||||
return <Skeleton height={36} />;
|
||||
}
|
||||
const { provider, skipHintedLogin } = getTpaProvider(this.tpaHint, providers, secondaryProviders);
|
||||
if (skipHintedLogin) {
|
||||
window.location.href = getConfig().LMS_BASE_URL + provider.registerUrl;
|
||||
return null;
|
||||
}
|
||||
return provider ? (<EnterpriseSSO provider={provider} intl={intl} />)
|
||||
: this.renderForm(
|
||||
currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthApiStatus,
|
||||
finishAuthUrl,
|
||||
submitState,
|
||||
intl,
|
||||
);
|
||||
if (tpaHint) {
|
||||
if (thirdPartyAuthApiStatus === PENDING_STATE) {
|
||||
return <Skeleton height={36} />;
|
||||
}
|
||||
return this.renderForm(
|
||||
currentProvider,
|
||||
providers,
|
||||
secondaryProviders,
|
||||
thirdPartyAuthApiStatus,
|
||||
finishAuthUrl,
|
||||
submitState,
|
||||
intl,
|
||||
);
|
||||
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
|
||||
if (skipHintedLogin) {
|
||||
window.location.href = getConfig().LMS_BASE_URL + provider.registerUrl;
|
||||
return null;
|
||||
}
|
||||
return provider ? <EnterpriseSSO provider={provider} intl={intl} /> : renderForm();
|
||||
}
|
||||
}
|
||||
return (
|
||||
renderForm()
|
||||
);
|
||||
};
|
||||
|
||||
RegistrationPage.defaultProps = {
|
||||
extendedProfile: [],
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
registrationResult: null,
|
||||
registerNewUser: null,
|
||||
registrationErrorCode: null,
|
||||
submitState: DEFAULT_STATE,
|
||||
thirdPartyAuthApiStatus: 'pending',
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
},
|
||||
registrationFormData: {
|
||||
country: '',
|
||||
email: '',
|
||||
name: '',
|
||||
password: '',
|
||||
username: '',
|
||||
marketingOptIn: true,
|
||||
errors: {
|
||||
email: '',
|
||||
name: '',
|
||||
username: '',
|
||||
password: '',
|
||||
country: '',
|
||||
},
|
||||
emailFieldBorderClass: '',
|
||||
emailErrorSuggestion: null,
|
||||
emailWarningSuggestion: null,
|
||||
},
|
||||
validationDecisions: null,
|
||||
statusCode: null,
|
||||
usernameSuggestions: [],
|
||||
const mapStateToProps = state => {
|
||||
const registerPageState = state.register;
|
||||
return {
|
||||
backedUpFormData: registerPageState.registrationFormData,
|
||||
backendCountryCode: registerPageState.backendCountryCode,
|
||||
backendValidations: validationsSelector(state),
|
||||
fieldDescriptions: fieldDescriptionSelector(state),
|
||||
optionalFields: optionalFieldsSelector(state),
|
||||
registrationErrorCode: registrationErrorSelector(state),
|
||||
registrationResult: registerPageState.registrationResult,
|
||||
shouldBackupState: registerPageState.shouldBackupState,
|
||||
userPipelineDataLoaded: registerPageState.userPipelineDataLoaded,
|
||||
submitState: registerPageState.submitState,
|
||||
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
|
||||
thirdPartyAuthContext: thirdPartyAuthContextSelector(state),
|
||||
validationApiRateLimited: registerPageState.validationApiRateLimited,
|
||||
usernameSuggestions: registerPageState.usernameSuggestions,
|
||||
};
|
||||
};
|
||||
|
||||
RegistrationPage.propTypes = {
|
||||
extendedProfile: PropTypes.arrayOf(PropTypes.string),
|
||||
backedUpFormData: PropTypes.shape({
|
||||
configurableFormFields: PropTypes.shape({}),
|
||||
formFields: PropTypes.shape({}),
|
||||
errors: PropTypes.shape({}),
|
||||
emailSuggestion: PropTypes.shape({}),
|
||||
}),
|
||||
backendCountryCode: PropTypes.string,
|
||||
backendValidations: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
}),
|
||||
fieldDescriptions: PropTypes.shape({}),
|
||||
institutionLogin: PropTypes.bool.isRequired,
|
||||
intl: PropTypes.objectOf(PropTypes.object).isRequired,
|
||||
optionalFields: PropTypes.shape({}),
|
||||
intl: intlShape.isRequired,
|
||||
getThirdPartyAuthContext: PropTypes.func.isRequired,
|
||||
registerNewUser: PropTypes.func,
|
||||
resetRegistrationForm: PropTypes.func.isRequired,
|
||||
setRegistrationFormData: PropTypes.func.isRequired,
|
||||
registrationErrorCode: PropTypes.string,
|
||||
registrationResult: PropTypes.shape({
|
||||
redirectUrl: PropTypes.string,
|
||||
success: PropTypes.bool,
|
||||
}),
|
||||
registrationErrorCode: PropTypes.string,
|
||||
shouldBackupState: PropTypes.bool,
|
||||
submitState: PropTypes.string,
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
registrationFormData: PropTypes.shape({
|
||||
country: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
marketingOptIn: PropTypes.bool,
|
||||
errors: PropTypes.shape({
|
||||
email: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
country: PropTypes.string,
|
||||
}),
|
||||
emailFieldBorderClass: PropTypes.string,
|
||||
emailErrorSuggestion: PropTypes.string,
|
||||
emailWarningSuggestion: PropTypes.string,
|
||||
}),
|
||||
thirdPartyAuthContext: PropTypes.shape({
|
||||
currentProvider: PropTypes.string,
|
||||
platformName: PropTypes.string,
|
||||
@@ -1006,48 +648,64 @@ RegistrationPage.propTypes = {
|
||||
username: PropTypes.string,
|
||||
}),
|
||||
}),
|
||||
fetchRealtimeValidations: PropTypes.func.isRequired,
|
||||
validationDecisions: PropTypes.shape({
|
||||
country: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
password: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
}),
|
||||
clearUsernameSuggestions: PropTypes.func.isRequired,
|
||||
statusCode: PropTypes.number,
|
||||
usernameSuggestions: PropTypes.arrayOf(string),
|
||||
institutionLogin: PropTypes.bool.isRequired,
|
||||
usernameSuggestions: PropTypes.arrayOf(PropTypes.string),
|
||||
userPipelineDataLoaded: PropTypes.bool,
|
||||
validationApiRateLimited: PropTypes.bool,
|
||||
// Actions
|
||||
backupFormState: PropTypes.func.isRequired,
|
||||
getRegistrationDataFromBackend: PropTypes.func.isRequired,
|
||||
handleInstitutionLogin: PropTypes.func.isRequired,
|
||||
registerNewUser: PropTypes.func.isRequired,
|
||||
resetUsernameSuggestions: PropTypes.func.isRequired,
|
||||
setUserPipelineDetailsLoaded: PropTypes.func.isRequired,
|
||||
validateFromBackend: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
const registrationResult = registrationRequestSelector(state);
|
||||
const thirdPartyAuthContext = thirdPartyAuthContextSelector(state);
|
||||
return {
|
||||
registrationErrorCode: registrationErrorSelector(state),
|
||||
submitState: state.register.submitState,
|
||||
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
|
||||
registrationResult,
|
||||
thirdPartyAuthContext,
|
||||
validationDecisions: validationsSelector(state),
|
||||
statusCode: state.register.statusCode,
|
||||
usernameSuggestions: usernameSuggestionsSelector(state),
|
||||
registrationFormData: registrationFormDataSelector(state),
|
||||
fieldDescriptions: fieldDescriptionSelector(state),
|
||||
optionalFields: optionalFieldsSelector(state),
|
||||
extendedProfile: extendedProfileSelector(state),
|
||||
};
|
||||
RegistrationPage.defaultProps = {
|
||||
backedUpFormData: {
|
||||
configurableFormFields: {
|
||||
marketingEmailsOptIn: true,
|
||||
},
|
||||
formFields: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
},
|
||||
errors: {
|
||||
name: '', email: '', username: '', password: '',
|
||||
},
|
||||
emailSuggestion: {
|
||||
suggestion: '', type: '',
|
||||
},
|
||||
},
|
||||
backendCountryCode: '',
|
||||
backendValidations: null,
|
||||
fieldDescriptions: {},
|
||||
optionalFields: {},
|
||||
registrationErrorCode: '',
|
||||
registrationResult: null,
|
||||
shouldBackupState: false,
|
||||
submitState: DEFAULT_STATE,
|
||||
thirdPartyAuthApiStatus: PENDING_STATE,
|
||||
thirdPartyAuthContext: {
|
||||
currentProvider: null,
|
||||
finishAuthUrl: null,
|
||||
countryCode: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
pipelineUserDetails: null,
|
||||
},
|
||||
usernameSuggestions: [],
|
||||
userPipelineDataLoaded: false,
|
||||
validationApiRateLimited: false,
|
||||
};
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
{
|
||||
clearUsernameSuggestions,
|
||||
getThirdPartyAuthContext,
|
||||
fetchRealtimeValidations,
|
||||
backupFormState: backupRegistrationFormBegin,
|
||||
getRegistrationDataFromBackend: getThirdPartyAuthContext,
|
||||
resetUsernameSuggestions: clearUsernameSuggestions,
|
||||
validateFromBackend: fetchRealtimeValidations,
|
||||
registerNewUser,
|
||||
resetRegistrationForm,
|
||||
setRegistrationFormData,
|
||||
setUserPipelineDetailsLoaded: setUserPipelineDataLoaded,
|
||||
},
|
||||
)(injectIntl(RegistrationPage));
|
||||
|
||||
73
src/register/ThirdPartyAuth.jsx
Normal file
73
src/register/ThirdPartyAuth.jsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
|
||||
import {
|
||||
RenderInstitutionButton,
|
||||
SocialAuthProviders,
|
||||
} from '../common-components';
|
||||
import {
|
||||
PENDING_STATE, REGISTER_PAGE,
|
||||
} from '../data/constants';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* This component renders the Single sign-on (SSO) buttons for the providers passed.
|
||||
* */
|
||||
const ThirdPartyAuth = (props) => {
|
||||
const {
|
||||
providers, secondaryProviders, currentProvider, handleInstitutionLogin, thirdPartyAuthApiStatus, intl,
|
||||
} = props;
|
||||
const isInstitutionAuthActive = !!secondaryProviders.length && !currentProvider;
|
||||
const isSocialAuthActive = !!providers.length && !currentProvider;
|
||||
const isEnterpriseLoginDisabled = getConfig().DISABLE_ENTERPRISE_LOGIN;
|
||||
|
||||
return (
|
||||
<>
|
||||
{((isEnterpriseLoginDisabled && isInstitutionAuthActive) || isSocialAuthActive) && (
|
||||
<div className="mt-4 mb-3 h4">
|
||||
{intl.formatMessage(messages['registration.other.options.heading'])}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{thirdPartyAuthApiStatus === PENDING_STATE ? (
|
||||
<Skeleton className="tpa-skeleton" height={36} count={2} />
|
||||
) : (
|
||||
<>
|
||||
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (
|
||||
<RenderInstitutionButton
|
||||
onSubmitHandler={handleInstitutionLogin}
|
||||
buttonTitle={intl.formatMessage(messages['register.institution.login.button'])}
|
||||
/>
|
||||
)}
|
||||
{isSocialAuthActive && (
|
||||
<div className="row m-0">
|
||||
<SocialAuthProviders socialAuthProviders={providers} referrer={REGISTER_PAGE} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ThirdPartyAuth.defaultProps = {
|
||||
currentProvider: null,
|
||||
providers: [],
|
||||
secondaryProviders: [],
|
||||
thirdPartyAuthApiStatus: 'pending',
|
||||
};
|
||||
|
||||
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),
|
||||
thirdPartyAuthApiStatus: PropTypes.string,
|
||||
};
|
||||
|
||||
export default injectIntl(ThirdPartyAuth);
|
||||
@@ -1,15 +1,39 @@
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const REGISTER_NEW_USER = new AsyncActionType('REGISTRATION', 'REGISTER_NEW_USER');
|
||||
export const BACKUP_REGISTRATION_DATA = new AsyncActionType('REGISTRATION', 'BACKUP_REGISTRATION_DATA');
|
||||
export const REGISTER_FORM_VALIDATIONS = new AsyncActionType('REGISTRATION', 'GET_FORM_VALIDATIONS');
|
||||
export const REGISTRATION_FORM = new AsyncActionType('REGISTRATION', 'REGISTRATION_FORM');
|
||||
export const REGISTER_NEW_USER = new AsyncActionType('REGISTRATION', 'REGISTER_NEW_USER');
|
||||
export const REGISTER_CLEAR_USERNAME_SUGGESTIONS = 'REGISTRATION_CLEAR_USERNAME_SUGGESTIONS';
|
||||
export const REGISTER_PERSIST_FORM_DATA = 'REGISTER_PERSIST_FORM_DATA';
|
||||
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
|
||||
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
|
||||
|
||||
// Reset Form
|
||||
export const resetRegistrationForm = () => ({
|
||||
type: REGISTRATION_FORM.RESET,
|
||||
// Backup registration form
|
||||
export const backupRegistrationForm = () => ({
|
||||
type: BACKUP_REGISTRATION_DATA.BASE,
|
||||
});
|
||||
|
||||
export const backupRegistrationFormBegin = (data) => ({
|
||||
type: BACKUP_REGISTRATION_DATA.BEGIN,
|
||||
payload: { ...data },
|
||||
});
|
||||
|
||||
// Validate fields from the backend
|
||||
export const fetchRealtimeValidations = (formPayload) => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.BASE,
|
||||
payload: { formPayload },
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsBegin = () => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.BEGIN,
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsSuccess = (validations) => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.SUCCESS,
|
||||
payload: { validations },
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsFailure = () => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.FAILURE,
|
||||
});
|
||||
|
||||
// Register
|
||||
@@ -33,35 +57,16 @@ export const registerNewUserFailure = (error) => ({
|
||||
payload: { ...error },
|
||||
});
|
||||
|
||||
// Realtime Field validations
|
||||
export const fetchRealtimeValidations = (formPayload) => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.BASE,
|
||||
payload: { formPayload },
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsBegin = () => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.BEGIN,
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsSuccess = (validations) => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.SUCCESS,
|
||||
payload: { validations },
|
||||
});
|
||||
|
||||
export const fetchRealtimeValidationsFailure = () => ({
|
||||
type: REGISTER_FORM_VALIDATIONS.FAILURE,
|
||||
});
|
||||
|
||||
export const clearUsernameSuggestions = () => ({
|
||||
type: REGISTER_CLEAR_USERNAME_SUGGESTIONS,
|
||||
});
|
||||
|
||||
export const setRegistrationFormData = (formData, clearRegistrationError = false) => ({
|
||||
type: REGISTER_PERSIST_FORM_DATA,
|
||||
payload: { formData, clearRegistrationError },
|
||||
});
|
||||
|
||||
export const setCountryFromThirdPartyAuthContext = (countryCode) => ({
|
||||
type: REGISTER_SET_COUNTRY_CODE,
|
||||
payload: { countryCode },
|
||||
});
|
||||
|
||||
export const setUserPipelineDataLoaded = (value) => ({
|
||||
type: REGISTER_SET_USER_PIPELINE_DATA_LOADED,
|
||||
payload: { value },
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user