Compare commits

..

3 Commits

Author SHA1 Message Date
Alexander Sheehan
63bbf8d2a3 WIP: iterative steps towards demoable (non-real world functioning) product 2021-08-12 12:08:33 -04:00
manny-m
39e046f09d feat: added PartnetWelcome componenet 2021-08-11 16:52:38 -04:00
Alexander Sheehan
14c1f5b944 feat: beginnings of the recruit me some money exploits 2021-08-11 10:48:16 -04:00
197 changed files with 28813 additions and 33349 deletions

28
.env
View File

@@ -13,24 +13,14 @@ ORDER_HISTORY_URL=null
REFRESH_ACCESS_TOKEN_ENDPOINT=null
SEGMENT_KEY=''
SITE_NAME=null
INFO_EMAIL=''
# ***** Cookies *****
REGISTER_CONVERSION_COOKIE_NAME=null
USER_SURVEY_COOKIE_NAME=null
# ***** Links *****
USER_INFO_COOKIE_NAME=null
AUTHN_MINIMAL_HEADER=true
LOGIN_ISSUE_SUPPORT_LINK=''
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK=null
# ***** Features flags *****
REGISTRATION_OPTIONAL_FIELDS=''
USER_SURVEY_COOKIE_NAME=null
COOKIE_DOMAIN=null
WELCOME_PAGE_SUPPORT_LINK=null
INFO_EMAIL=''
DISABLE_ENTERPRISE_LOGIN=''
ENABLE_COOKIE_POLICY_BANNER=''
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
ENABLE_PERSONALIZED_RECOMMENDATIONS=''
MARKETING_EMAILS_OPT_IN=''
SHOW_CONFIGURABLE_EDX_FIELDS=''
# ***** Zendesk related keys *****
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''
# ***** Miscellaneous *****
APP_ID=''
MFE_CONFIG_API_URL=''
REGISTER_CONVERSION_COOKIE_NAME=null
ENABLE_PROGRESSIVE_PROFILING=''

View File

@@ -17,21 +17,16 @@ MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='http://localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
INFO_EMAIL='info@example.com'
# ***** Cookies *****
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
SESSION_COOKIE_DOMAIN='localhost'
SITE_NAME='edX'
USER_INFO_COOKIE_NAME='edx-user-info'
USER_SURVEY_COOKIE_NAME='openedx-user-survey-type'
# ***** Links *****
LOGIN_ISSUE_SUPPORT_LINK='http://localhost:18000/login-issue-support-url'
AUTHN_MINIMAL_HEADER=true
LOGIN_ISSUE_SUPPORT_LINK='/login-issue-support-url'
TOS_AND_HONOR_CODE='http://localhost:18000/honor'
TOS_LINK='http://localhost:18000/tos'
PRIVACY_POLICY='http://localhost:18000/privacy'
AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK='http://localhost:1999/welcome'
# ***** Miscellaneous *****
APP_ID=''
MFE_CONFIG_API_URL=''
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''
REGISTRATION_OPTIONAL_FIELDS=''
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'

View File

@@ -1,5 +0,0 @@
# 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'

View File

@@ -15,8 +15,10 @@ MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='http://localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=''
SITE_NAME='Your Platform Name Here'
SITE_NAME='edX'
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'
APP_ID=''
MFE_CONFIG_API_URL=''

View File

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

View File

@@ -1,17 +1,16 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('eslint', {
rules: {
// Temporarily update the 'indent', 'template-curly-spacing' and
// 'no-multiple-empty-lines' rules since they are causing eslint
// to fail for no apparent reason since upgrading
// to fail for no apparent reason since upgrading
// @edx/frontend-build from v3 to v5:
// - TypeError: Cannot read property 'range' of null
indent: [
'indent': [
'error',
2,
{ ignoredNodes: ['TemplateLiteral', 'SwitchCase'] },
{ 'ignoredNodes': ['TemplateLiteral', 'SwitchCase'] }
],
'template-curly-spacing': 'off',
'jsx-a11y/label-has-associated-control': ['error', {
@@ -19,36 +18,7 @@ module.exports = createConfig('eslint', {
labelAttributes: [],
controlComponents: [],
assert: 'htmlFor',
depth: 25,
depth: 25
}],
'sort-imports': ['error', { ignoreCase: true, ignoreDeclarationSort: true }],
'import/order': [
'error',
{
groups: [
'builtin',
'external',
'internal',
['sibling', 'parent'],
'index',
],
pathGroups: [
{
pattern: '@(react|react-dom|react-redux)',
group: 'external',
position: 'before',
},
],
pathGroupsExcludedImportTypes: ['react'],
'newlines-between': 'always',
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
},
],
'function-paren-newline': 'off',
'no-import-assign': 'off',
'react/no-unstable-nested-components': 'off',
},
});

View File

@@ -1,29 +0,0 @@
### Description
Include a description of your changes here, along with a link to any relevant Jira tickets and/or Github issues.
#### JIRA
[XXX-XXXX](https://2u-internal.atlassian.net/browse/XXX-XXXX)
#### How Has This Been Tested?
Please describe in detail how you tested your changes.
#### Screenshots/sandbox (optional):
Include a link to the sandbox for design changes or screenshot for before and after. **Remove this section if its not applicable.**
|Before|After|
|-------|-----|
| | |
#### Merge Checklist
* [ ] If your update includes visual changes, have they been reviewed by a designer? Send them a link to the Sandbox, if applicable.
* [ ] Is there adequate test coverage for your changes?
#### Post-merge Checklist
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/vanguards** to do it.
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.

View File

@@ -1,19 +0,0 @@
# Run the workflow that adds new tickets that are either:
# - labelled "DEPR"
# - title starts with "[DEPR]"
# - body starts with "Proposal Date" (this is the first template field)
# to the org-wide DEPR project board
name: Add newly created DEPR issues to the DEPR project board
on:
issues:
types: [opened]
jobs:
routeissue:
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
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 }}

View File

@@ -1,20 +0,0 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "label: " it tries to apply
# the label indicated in rest of comment.
# If the comment starts with "remove label: ", it tries
# to remove the indicated label.
# Note: Labels are allowed to have spaces and this script does
# not parse spaces (as often a space is legitimate), so the command
# "label: really long lots of words label" will apply the
# label "really long lots of words label"
name: Allows for the adding and removing of labels via comment
on:
issue_comment:
types: [created]
jobs:
add_remove_labels:
uses: openedx/.github/.github/workflows/add-remove-label-on-comment.yml@master

View File

@@ -1,21 +0,0 @@
name: autoupdate
on:
push:
branches:
- master
jobs:
autoupdate:
name: autoupdate
runs-on: ubuntu-22.04
steps:
- uses: docker://chinthakagodawita/autoupdate-action:v1
env:
GITHUB_TOKEN: "${{ secrets.CC_GITHUB_TOKEN }}"
DRY_RUN: "false"
PR_FILTER: "labelled"
PR_LABELS: "autoupdate"
EXCLUDED_LABELS: "dependencies,wontfix"
MERGE_MSG: "Branch was auto-updated."
RETRY_COUNT: "5"
RETRY_SLEEP: "300"
MERGE_CONFLICT_ACTION: "fail"

View File

@@ -1,44 +0,0 @@
name: node_CI
on:
push:
branches:
- master
pull_request:
branches:
- '**'
jobs:
tests:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- name: Setup Nodejs
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VER }}
- name: Install Dependencies
run: npm ci
- name: Verify No Uncommitted Package-Lock Changes
run: make validate-no-uncommitted-package-lock-changes
- name: Run i18n_extract
run: npm run i18n_extract
- name: Lint
run: npm run lint
- name: Test
run: npm run test
- name: Build
run: npm run build
- name: Run Code Coverage
uses: codecov/codecov-action@v3

View File

@@ -1,10 +0,0 @@
# Run commitlint on the commit messages in a pull request.
name: Lint Commit Messages
on:
- pull_request
jobs:
commitlint:
uses: openedx/.github/.github/workflows/commitlint.yml@master

View File

@@ -1,13 +0,0 @@
#check package-lock file version
name: Lockfile Version check
on:
push:
branches:
- master
pull_request:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

View File

@@ -1,12 +0,0 @@
# This workflow runs when a comment is made on the ticket
# If the comment starts with "assign me" it assigns the author to the
# ticket (case insensitive)
name: Assign comment author to ticket if they say "assign me"
on:
issue_comment:
types: [created]
jobs:
self_assign_by_comment:
uses: openedx/.github/.github/workflows/self-assign-issue.yml@master

1
.gitignore vendored
View File

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

View File

@@ -1,6 +1,7 @@
.eslintignore
.eslintrc.json
.gitignore
.travis.yml
docker-compose.yml
Dockerfile
Makefile

1
.nvmrc
View File

@@ -1 +0,0 @@
18

13
.travis.yml Executable file
View File

@@ -0,0 +1,13 @@
language: node_js
node_js: 12
install:
- npm ci
script:
- make validate-no-uncommitted-package-lock-changes
- npm run i18n_extract
- npm run lint
- npm run test
- npm run build
- npm run is-es5
after_success:
- codecov

View File

@@ -1,9 +1,8 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-authn]
[edx-platform.frontend-app-authn]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON
type = KEYVALUEJSON

View File

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

16
Makefile Normal file → Executable file
View File

@@ -1,9 +1,11 @@
export TRANSIFEX_RESOURCE = frontend-app-authn
transifex_langs = "ar,fr,es_419,zh_CN,it_IT,pt_PT,de_DE,uk,ru,hi"
transifex_resource = frontend-app-authn
transifex_langs = "ar,fr,es_419,zh_CN"
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
@@ -36,17 +38,17 @@ push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
$(transifex_utils) $(transifex_temp) --comments
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
tx pull -f --mode reviewed --language=$(transifex_langs)
# This target is used by CI.
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:
# Checking for package-lock.json changes...
git diff --exit-code package-lock.json

View File

@@ -1,193 +1,48 @@
|Build Status| |ci-badge| |Codecov| |semantic-release|
|Build Status| |Codecov| |license|
frontend-app-authn
====================
Please tag **@openedx/vanguards** on any PRs or issues. Thanks!
Introduction
------------
=================================
This is a micro-frontend application responsible for the login, registration and password reset functionality.
**What is the domain of this MFE?**
Development
-----------
- Register page
Start Devstack
^^^^^^^^^^^^^^
- Login page
To use this application `devstack <https://github.com/edx/devstack>`__ must be running.
- Forgot password page
- Start devstack
- Reset password page
Start the development server
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Progressive profiling page
In this project, install requirements and start the development server by running:
.. code:: bash
Installation
------------
npm install
npm start # The server will run on port 1999
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
Once the dev server is up visit http://localhost:1999/login.
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
Configuration and Deployment
----------------------------
2. Start up LMS, if it's not already started.
This MFE is configured via node environment variables supplied at build time. See the .env file for the list of required environment variables. Example build syntax with a single environment variable:
4. Within this project (frontend-app-authn), install requirements and start the development server:
.. code:: bash
.. code-block::
NODE_ENV=development ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
npm install
npm start # The server will run on port 1999
5. Once the dev server is up, visit http://localhost:1999 to access the MFE
.. image:: ./docs/images/frontend-app-authn-localhost-preview.png
**Note:** Follow `Enable social auth locally <docs/how_tos/enable_social_auth.rst>`_ for enabling Social Sign-on Buttons (SSO) locally
Environment Variables/Setup Notes
---------------------------------
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
The authentication micro-frontend also requires the following additional variable:
.. list-table:: Environment Variables
:widths: 30 50 20
:header-rows: 1
* - Name
- Description / Usage
- Example
* - ``LOGIN_ISSUE_SUPPORT_LINK``
- The fully-qualified URL to the login issue support page in the target environment.
- ``https://support.example.com``
* - ``ACTIVATION_EMAIL_SUPPORT_LINK``
- The fully-qualified URL to the activation email support page in the target environment.
- ``https://support.example.com``
* - ``PASSWORD_RESET_SUPPORT_LINK``
- The fully-qualified URL to the password reset support page in the target environment.
- ``https://support.example.com``
* - ``AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK``
- The fully-qualified URL to the progressive profiling support page in the target environment.
- ``https://support.example.com``
* - ``TOS_AND_HONOR_CODE``
- The fully-qualified URL to the Honor code page in the target environment.
- ``https://example.com/honor``
* - ``TOS_LINK``
- The fully-qualified URL to the Terms of service page in the target environment.
- ``https://example.com/tos``
* - ``PRIVACY_POLICY``
- The fully-qualified URL to the Privacy policy page in the target environment.
- ``https://example.com/privacy``
* - ``INFO_EMAIL``
- The valid email address for information query regarding the target environment.
- ``info@example.com``
* - ``ENABLE_DYNAMIC_REGISTRATION_FIELDS``
- Enables support for configurable registration fields on the MFE. This flag must be enabled to show any required registration field besides the default fields (name, email, username, password).
- ``true`` | ``''`` (empty strings are falsy)
* - ``ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN``
- 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)
* - ``DISABLE_ENTERPRISE_LOGIN``
- Disables the enterprise login from Authn MFE.
- ``true`` | ``''`` (empty strings are falsy)
* - ``MFE_CONFIG_API_URL``
- Link of the API to get runtime mfe configuration variables from the site configuration or django settings.
- ``/api/v1/mfe_config`` | ``''`` (empty strings are falsy)
* - ``APP_ID``
- Name of MFE, this will be used by the API to get runtime configurations for the specific micro frontend. For a frontend repo `frontend-app-appName`, use `appName` as APP_ID.
- ``authn`` | ``''``
* - ``ENABLE_COOKIE_POLICY_BANNER``
- Enables support for displaying the cookies acceptance banner.
- ``true`` | ``''`` (empty strings are falsy)
edX-specific Environment Variables
**********************************
Furthermore, there are several edX-specific environment variables that enable integrations with closed-source services private to the edX organization, and might be unsupported in Open edX.
.. list-table:: edX-specific Environment Variables
:widths: 30 50 20
:header-rows: 1
* - Name
- Description / Usage
- Example
* - ``MARKETING_EMAILS_OPT_IN``
- Enables support for opting in marketing emails that helps us getting user consent for sending marketing emails.
- ``true`` | ``''`` (empty strings are falsy)
* - ``SHOW_CONFIGURABLE_EDX_FIELDS``
- For edX, country and honor code fields are required by default. This flag enables edX specific required fields.
- ``true`` | ``''`` (empty strings are falsy)
For more information see the document: `Micro-frontend applications in Open
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
How To Contribute
------------
Contributions are very welcome, and strongly encouraged! We've
put together `some documentation that describes our contribution process <https://edx.readthedocs.org/projects/edx-developer-guide/en/latest/process/index.html>`_.
Even though they were written with edx-platform in mind, the guidelines should be followed for Open edX code in general.
PR description template should be automatically applied if you are sending PR from github interface; otherwise you
can find it it at `PULL_REQUEST_TEMPLATE.md <https://github.com/openedx/frontend-app-authn/blob/master/.github/pull_request_template.md>`_
This project is currently accepting all types of contributions, bug fixes and security fixes.
Open edX Code of Conduct
------------------------
All community members are expected to follow the `Open edX Code of Conduct <https://openedx.org/code-of-conduct/>`_.
People
------
The assigned maintainers for this component and other project details may be
found in `Backstage <https://backstage.openedx.org/catalog/default/group/vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml``
file in this repo.
Reporting Security Issues
-------------------------
Please do not report security issues in public. Please email security@edx.org.
Known Issues
------------
None
License
-------
The code in this repository is licensed under the GNU Affero General Public License v3.0, unless
otherwise noted.
Please see `LICENSE <https://github.com/openedx/frontend-app-authn/blob/master/LICENSE>`_ for details.
==============================
edX <https://github.com/edx/edx-developer-docs/blob/5191e800bf16cf42f25c58c58f983bdaf7f9305d/docs/micro-frontends-in-open-edx.rst>`__.
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-authn.svg?branch=master
:target: https://travis-ci.com/edx/frontend-app-authn
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-authn
:target: https://codecov.io/gh/edx/frontend-app-authn
.. |ci-badge| image:: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml/badge.svg
:target: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml
:alt: Continuous Integration
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
:target: https://github.com/semantic-release/semantic-release
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-authn.svg
:target: @edx/frontend-app-authn

View File

@@ -1,18 +0,0 @@
# This file records information about this repo. Its use is described in OEP-55:
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: 'frontend-app-authn'
description: "Micro-frontend for authentication service. It contains views for login, registration and password reset functionality."
links:
- url: 'https://github.com/openedx/frontend-app-authn/blob/master/README.rst'
title: 'Documentation'
icon: 'Article'
annotations:
openedx.org/arch-interest-groups: ""
spec:
owner: group:vanguards
type: 'service'
lifecycle: 'production'

View File

@@ -91,7 +91,7 @@ In the data sub-directory, the file names describe what each piece of code does.
/ProfilePhotoUploader.jsx // supporting view
/data // Note: most files here are named with a plural, as they contain many of the things in question.
/actions.js
/mockedData.js
/constants.js
/reducers.js
/sagas.js
/selectors.js

View File

@@ -1,14 +0,0 @@
Enable Social Auth Locally
--------------------------
Please follow the steps below to enable social auth (SSO) locally.
1. Follow `Enabling Third Party Authentication <https://edx.readthedocs.io/projects/edx-installing-configuring-and-running/en/latest/configuration/tpa/index.html>`_ for backend configuration.
2. Authn has a component for rendering Social Auth providers at frontend which goes through each provider.
* If the provider has an ``iconImage``, then it will be rendered as image in SSO button.
* If ``iconImage`` is not available in provider, but the provider's ``iconClass`` is from the supported icon classes ``['apple', 'facebook', 'google', 'microsoft']`` then it is used as icon image.
* If ``iconClass`` doesn't match the supported icon classes then the ``faSignInAlt`` from font awesome icons is used as icon image for SSO button.

View File

@@ -2,4 +2,4 @@
React App i18n HOWTO
####################
This document has moved to the frontend-platform repo: https://github.com/openedx/frontend-platform/blob/master/docs/how_tos/i18n.rst
This document has moved to the frontend-platform repo: https://github.com/edx/frontend-platform/blob/master/docs/how_tos/i18n.rst

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

View File

@@ -8,7 +8,5 @@ module.exports = createConfig('jest', {
'src/setupTest.js',
'src/i18n',
'src/index.jsx',
'MainApp.jsx',
],
testEnvironment: 'jsdom',
});

View File

@@ -3,6 +3,7 @@
nick: Authn MFE
oeps: {}
owner: openedx/vanguards
owner: edx/vanguards
openedx-release:
maybe: true # Delete this "maybe" line when you have decided about Open edX inclusion.
ref: master

47937
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,14 +4,16 @@
"description": "Frontend application template",
"repository": {
"type": "git",
"url": "git+https://github.com/openedx/frontend-app-authn.git"
"url": "git+https://github.com/edx/frontend-app-authn.git"
},
"browserslist": [
"extends @edx/browserslist-config"
"last 2 versions",
"ie 11"
],
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"is-es5": "es-check es5 ./dist/*.js",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
@@ -24,68 +26,64 @@
},
"author": "edX",
"license": "AGPL-3.0",
"homepage": "https://github.com/openedx/frontend-app-authn#readme",
"homepage": "https://github.com/edx/frontend-app-authn#readme",
"publishConfig": {
"access": "public"
},
"bugs": {
"url": "https://github.com/openedx/frontend-app-authn/issues"
"url": "https://github.com/edx/frontend-app-authn/issues"
},
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-component-cookie-policy-banner": "2.2.2",
"@edx/frontend-platform": "4.2.0",
"@edx/paragon": "20.30.1",
"@fortawesome/fontawesome-svg-core": "6.2.1",
"@fortawesome/free-brands-svg-icons": "6.2.1",
"@fortawesome/free-regular-svg-icons": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/react-fontawesome": "0.2.0",
"@optimizely/react-sdk": "^2.9.1",
"@redux-devtools/extension": "3.2.5",
"algoliasearch": "^4.14.3",
"classnames": "2.3.2",
"core-js": "3.30.0",
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-cookie-policy-banner": "2.1.12",
"@edx/frontend-platform": "1.12.0",
"@edx/paragon": "16.6.1",
"@fortawesome/fontawesome-svg-core": "1.2.32",
"@fortawesome/free-brands-svg-icons": "5.15.1",
"@fortawesome/free-regular-svg-icons": "5.15.1",
"@fortawesome/free-solid-svg-icons": "5.15.1",
"@fortawesome/react-fontawesome": "0.1.13",
"classnames": "2.2.6",
"core-js": "3.9.1",
"extract-react-intl-messages": "4.1.1",
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.0",
"fastest-levenshtein": "1.0.12",
"form-urlencoded": "4.2.1",
"formik": "2.2.6",
"lodash.camelcase": "4.3.0",
"lodash.snakecase": "4.1.1",
"prop-types": "15.8.1",
"prop-types": "15.7.2",
"query-string": "5.1.1",
"react": "16.14.0",
"react-dom": "16.14.0",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.2.0",
"react-onclickoutside": "6.13.0",
"react-redux": "7.2.9",
"react-loading-skeleton": "2.2.0",
"react-onclickoutside": "6.11.2",
"react-redux": "7.2.3",
"react-responsive": "8.2.0",
"react-router": "5.3.4",
"react-router-dom": "5.3.4",
"react-zendesk": "^0.1.13",
"redux": "4.2.0",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"redux": "4.0.5",
"redux-devtools-extension": "2.13.8",
"redux-logger": "3.0.6",
"redux-mock-store": "1.5.4",
"redux-saga": "1.2.3",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.13.11",
"reselect": "4.1.7",
"sanitize-html": "2.10.0",
"semver-regex": "3.1.4",
"universal-cookie": "4.0.4"
"redux-saga": "1.1.3",
"redux-thunk": "2.3.0",
"regenerator-runtime": "0.13.9",
"reselect": "4.0.0",
"universal-cookie": "^4.0.4"
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "12.8.6",
"@edx/reactifex": "1.1.0",
"babel-plugin-formatjs": "10.4.0",
"@edx/frontend-build": "5.6.11",
"babel-plugin-react-intl": "8.2.25",
"codecov": "3.8.1",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.7",
"eslint-plugin-import": "2.26.0",
"glob": "7.2.3",
"history": "5.3.0",
"husky": "7.0.4",
"jest": "29.5.0",
"react-test-renderer": "16.14.0"
"enzyme-adapter-react-16": "1.15.6",
"es-check": "5.2.3",
"glob": "7.1.6",
"history": "5.0.0",
"husky": "4.3.8",
"jest": "26.6.3",
"react-test-renderer": "16.14.0",
"reactifex": "1.1.1"
}
}

View File

@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en-us">
<head>
<title>Authn | <%= process.env.SITE_NAME %></title>
<title>Authn | edX</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />

View File

@@ -1,20 +1,9 @@
{
"extends": [
"config:base",
":automergeLinters",
":automergeTesters",
":automergeMinor",
":noUnscheduledUpdates",
":semanticCommits"
"config:base"
],
"rebaseStalePrs": true,
"schedule": [
"every weekday"
],
"packageRules": [
{
"matchPackageNames": ["node", "npm"],
"enabled": false
}
]
"patch": {
"automerge": true
},
"rebaseStalePrs": true
}

View File

@@ -1,38 +1,25 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { Helmet } from 'react-helmet';
import { Redirect, Route, Switch } from 'react-router-dom';
import { AppProvider } from '@edx/frontend-platform/react';
import {
Logistration, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
UnAuthOnlyRoute, registerIcons, NotFoundPage, Logistration,
} from './common-components';
import configureStore from './data/configureStore';
import {
AUTHN_PROGRESSIVE_PROFILING,
LOGIN_PAGE,
PAGE_NOT_FOUND,
PASSWORD_RESET_CONFIRM,
RECOMMENDATIONS,
REGISTER_PAGE,
RESET_PAGE,
LOGIN_PAGE, PAGE_NOT_FOUND, REGISTER_PAGE, RESET_PAGE, PASSWORD_RESET_CONFIRM, WELCOME_PAGE,
} from './data/constants';
import configureStore from './data/configureStore';
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 ForgotPasswordPage from './forgot-password';
import ResetPasswordPage from './reset-password';
import WelcomePage from './welcome';
import './index.scss';
registerIcons();
const MainApp = () => (
<AppProvider store={configureStore()}>
<Helmet>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
{getConfig().ZENDESK_KEY && <Zendesk />}
<Switch>
<Route exact path="/">
<Redirect to={updatePathWithQueryParams(REGISTER_PAGE)} />
@@ -41,8 +28,7 @@ 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={AUTHN_PROGRESSIVE_PROFILING} component={ProgressiveProfiling} />
<Route exact path={RECOMMENDATIONS} component={RecommendationsPage} />
<Route exact path={WELCOME_PAGE} component={WelcomePage} />
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
<Route path="*">
<Redirect to={PAGE_NOT_FOUND} />

View File

@@ -1,8 +1,3 @@
// Load component based styles
@import "_base_component.scss";
@import "_registration.scss";
@import "_recommendations_page.scss";
//
// ----------------------------
// #COLORS
// ----------------------------
@@ -18,22 +13,7 @@ $microsoft-black: #2f2f2f;
$microsoft-focus-black: #000;
$apple-black: #000000;
$apple-focus-black: $apple-black;
$elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
// Forgot Password Page
.forgot-password-button-width {
min-width: 6rem;
}
.centered-align-spinner {
left: 0;
right: 0;
bottom: 0;
top: 0;
position: absolute;
margin: auto;
}
$accent-a-light: #c9f2f5;
.main-content {
@extend .pt-4;
@@ -49,7 +29,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.login-button-width {
min-width: 6rem;
width: 6rem;
}
.tpa-skeleton {
@@ -80,15 +60,31 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
.alert-link {
font-weight: normal;
text-decoration: underline;
color: $info-300 !important;
color: #0075b4 !important;
&:hover {
color: $info-500 !important;
color: #065683 !important;
}
}
.authn-header {
border-bottom: 1px solid #e7e7e7;
height: 3.75rem;
position: relative;
z-index: 1000;
}
.authn-header img {
height: 1.75rem;
margin-left: 2rem;
padding: 1rem 0;
display: block;
position: relative;
box-sizing: content-box;
}
.form-control {
background-color: $white !important;
background-color: white !important;
font-size: 0.875rem;
line-height: 1.5;
height: 2.75rem;
@@ -161,10 +157,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
background-color: $google-blue;
.icon-image {
margin-left: -6px;
max-height: 34px;
max-width: 34px;
height: 34px;
margin-left: 2px;
}
&:hover,
@@ -287,48 +280,257 @@ select.form-control {
.pt-10 {
padding-top: 10px;
}
.tooltip-shadow {
box-shadow: 0px 5px 15px 0px rgba(0,0,0,0.3) !important;
}
#password-requirement-left {
opacity: 1;
@extend .x-small;
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
right: 0.2rem !important;
.tooltip-inner {
background: $white;
@extend .tooltip-shadow;
background: white;
display: block;
color: $gray-500;
color: #707070;
}
.arrow::before {
border-left-color: $white;
border-left-color: #fff;
@extend .tooltip-shadow;
}
}
#password-requirement-top {
@extend .x-small;
filter: drop-shadow($elevation-level-2-shadow) drop-shadow($elevation-level-2-shadow) !important;
opacity: 1;
width: 90%;
bottom: 10px !important;
display: flex;
justify-content: center;
margin-bottom: 10px;
.tooltip-inner {
min-width: 464px !important;
background: $white;
max-width: inherit;
background: white;
display: block;
color: $gray-500;
@extend .tooltip-shadow;
color: #707070;
}
.arrow::before {
border-top-color: $white;
border-top-color: #fff;
@extend .tooltip-shadow;
}
}
#forgotpassword-success-alert {
.alert-link {
color: $gray-700 !important;
color: #454545 !important;
}
}
.screen-header-light {
background-color: $light-200;
}
.screen-header-primary {
background-color: $primary-400;
}
.large-screen-container {
background-color: $white;
width: 50vw;
}
.large-screen-svg-light,
.large-screen-svg-primary {
fill: $light-200;
overflow: hidden;
position: absolute;
}
.large-screen-svg-primary {
fill: $primary-400;
}
.medium-screen-container {
flex-wrap: nowrap;
max-width: inherit;
height: 282px;
}
.medium-screen-svg-light,
.medium-screen-svg-primary {
fill: $light-200;
overflow: inherit;
position: absolute;
}
.medium-screen-svg-primary {
fill: $primary-400;
}
.screen-polygon {
background-color: $white;
}
.small-screen-header-light,
.small-screen-header-primary {
background-color: $light-200;
width: 100vw;
}
.small-screen-header-primary {
background-color: $primary-400;
}
.extra-large-screen-top-stripe {
height: 0.5rem;
background-image: linear-gradient(
102.02deg,
$brand-700 10%,
$brand 10%,
$brand 45%,
$primary-700 45%,
$primary-700 55%,
$accent-a 55%,
$accent-a 75%,
$accent-a-light 75%,
);
background-repeat: no-repeat;
}
.medium-screen-top-stripe {
height: 0.5rem;
background-image: linear-gradient(
102.02deg,
$brand-700,
$brand-700 10%,
$brand 10%,
$brand 90%,
$primary-700 90%,
$primary-700 100%,
);
background-repeat: no-repeat;
}
.large-screen-top-stripe {
height: 0.5rem;
background-image: linear-gradient(
102.02deg,
$brand-700 10%,
$brand 10%,
$brand 65%,
$primary-700 65%,
$primary-700 75%,
$accent-a 75%,
$accent-a 75%);
background-repeat: no-repeat;
}
.small-screen-top-stripe {
height: 0.5rem;
background-image: linear-gradient(
102.02deg,
$brand-700,
$brand-700 20%,
$brand 20%,
);
background-repeat: no-repeat;
}
// Progressive profiling base component classes
.medium-container {
flex-wrap: nowrap;
max-width: inherit;
height: 260px;
}
.extra-extra-large-svg-line {
stroke: $accent-b;
stroke-width: 0.5rem;
width: 5.5rem;
height: 150px;
}
.extra-large-svg-line {
stroke: $accent-b;
stroke-width: 0.5rem;
width: 5.5rem;
height: 110px;
}
.medium-svg-line {
stroke: $accent-b;
stroke-width: 0.5rem;
width: 7em;
height: 110px;
}
.small-svg-line {
stroke: $accent-b;
stroke-width: 0.25rem;
width: 4em;
height: 90px;
}
.extra-small-svg-line {
stroke: $accent-b;
stroke-width: 0.25rem;
width: 4em;
height: 80px;
}
// Non-Auth Screen Svg Lines
.large-screen-svg-line {
padding-top: 0.5rem;
stroke: $accent-b;
stroke-width: 0.5rem;
width: 5.5rem;
height: 240px;
}
.medium-screen-svg-line {
padding-top: 0.5rem;
stroke: $accent-b;
stroke-width: 0.5rem;
width: 7em;
height: 115px;
}
.small-screen-svg-line {
padding-top: 0.5rem;
stroke: $accent-b;
stroke-width: 0.25rem;
width: 4em;
height: 72px;
}
.large-heading {
margin-left: 7px;
color: $white;
max-width: 24.5rem;
line-height: 78px;
font-size: 78px;
}
.medium-heading {
padding-left: 1rem;
color: $white;
max-width: 27rem;
line-height: 60px;
font-size: 60px;
}
.small-heading {
padding-left: 0.5rem;
color: $white;
line-height: 40px;
font-size: 36px;
}
.logo {
width: 4.44rem;
margin-top: 1.5rem;
margin-left: 1.5rem;
}
.username-suggestion {
padding: 1px 0.5rem;
margin: 0.25rem;
@@ -346,7 +548,12 @@ select.form-control {
}
.yellow-border {
border: 2px solid $accent-b;
border: 2px solid #F0CC00;
}
.one-rem-font {
font-size: 0.99rem;
color: #707070;
}
.institute-heading {
@@ -363,7 +570,7 @@ select.form-control {
}
.dropdown-item:active {
background-color: $light-300;
background-color: #F2F0EF;
}
.dropdown-container {
@@ -375,9 +582,8 @@ select.form-control {
line-height: 1.25rem;
overflow-y: scroll;
position: absolute;
background-color: $white;
background-color: #fff;
width: 464px;
z-index: 100 !important;
}
.email-error-alert {
@@ -421,13 +627,16 @@ select.form-control {
}
}
.arrow-back-icon {
margin-top:2px;
}
.icon-size {
width: 2.3rem;
}
.has-floating-label {
color: $gray-500;
}
.pgn__form-control-floating-label .pgn__form-control-floating-label-content {
font-size: 0.875rem;
line-height: 1.5;
@@ -461,17 +670,6 @@ select.form-control {
font-weight: 400;
}
.pp-page-heading {
line-height: 1.75rem;
font-size: 1.375rem;
margin-bottom: 0.5rem;
font-weight: 700;
@include media-breakpoint-down('md') {
line-height: 1.5rem;
font-size: 1.125rem;
}
}
@media (min-width: 1024px) {
.mw-500 {
@@ -501,6 +699,36 @@ select.form-control {
.welcome-page-content {
padding-top: 1.5rem !important;
}
.layout {
display: flex;
align-items: center;
flex-direction:column;
justify-content: center;
}
}
@media (max-width: 1199px) and (min-width: 768px) {
.layout {
display: flex;
align-items: center;
flex-direction:column;
justify-content: center;
}
}
@media (min-width: 1200px) {
.layout{
display: flex;
justify-content: space-between;
}
.content {
width: 50vw;
display: flex;
justify-content: center;
margin-top: 4rem;
}
}
@media (max-width: 464px) {
@@ -512,6 +740,12 @@ select.form-control {
}
}
@media (max-height: 500px) {
.large-screen-svg-line, .large-heading {
margin-top: 5rem;
}
}
.alert {
p:last-child {
margin-bottom: 0;
@@ -539,14 +773,6 @@ select.form-control {
padding: 1.5rem !important;
}
#password-requirement-top {
display: unset;
.tooltip-inner {
max-width: inherit;
min-width: unset !important;
}
}
.progressive-profiling-support {
font-size: 0.688rem;
font-weight: normal;
@@ -564,36 +790,3 @@ select.form-control {
line-height: 1.5rem;
color: $primary-700
}
.opt-checkbox {
.pgn__form-label {
font-size: 0.75rem;
line-height: 1.25rem;
}
margin-left: 3px;
}
.suggested-username {
position: relative;
margin-top: -8.7%;
margin-left: 15px;
}
.suggested-username-close-button {
right: 1rem;
position: absolute;
}
.suggested-username-with-error {
position: relative;
margin-top: -13.7%;
margin-bottom: 11%;
margin-left: 15px;
}
.scroll-suggested-username {
width: 21rem;
white-space: nowrap;
overflow-x: auto;
display: inline-flex;
}
.pgn__form-control-decorator-trailing {
right: 0 !important;
}

View File

@@ -0,0 +1,101 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Col, Hyperlink, Image, Row,
} from '@edx/paragon';
import messages from './messages';
const AuthExtraLargeLayout = (props) => {
const { intl, username, variant, toggleWelcomeText } = props;
return (
<div className="container row p-0 m-0 large-screen-container">
<div className="col-md-9 p-0 screen-header-light">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image alt="edx" className="logo position-absolute" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="min-vh-100 d-flex align-items-center">
<div>
<Row>
<Col xs={3}>
<svg className={classNames(
'ml-5 mt-5',
{
'extra-large-svg-line': variant === 'xl',
'extra-extra-large-svg-line': variant === 'xxl',
},
)}
>
<line x1="60" y1="0" x2="5" y2="220" />
</svg>
</Col>
<Col xs={9}>
<div className={classNames(
'data-hj-suppress',
{
h3: variant === 'xl',
h2: variant === 'xxl',
},
)}
>
{intl.formatMessage(
messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username },
)}
</div>
{ !toggleWelcomeText ? (
<div
className={classNames(
'text-primary',
{
'display-1': variant === 'xl',
'display-2': variant === 'xxl',
},
)}
>
{intl.formatMessage(messages['complete.your.profile.1'])}
<span className="text-accent-a">
<br />
{intl.formatMessage(messages['complete.your.profile.2'])}
</span>
</div>
) : null}
</Col>
</Row>
</div>
</div>
</div>
<div className="col-md-3 p-0 screen-polygon">
<svg
width="100%"
height="100%"
className="m1-n1 large-screen-svg-light"
preserveAspectRatio="xMaxYMin meet"
>
<g transform="skewX(171.6)">
<rect x="0" y="0" height="100%" width="100%" />
</g>
</svg>
</div>
</div>
);
};
AuthExtraLargeLayout.defaultProps = {
variant: 'xl',
toggleWelcomeText: false,
};
AuthExtraLargeLayout.propTypes = {
intl: intlShape.isRequired,
username: PropTypes.string.isRequired,
variant: PropTypes.oneOf(['xl', 'xxl']),
toggleWelcomeText: PropTypes.bool,
};
export default injectIntl(AuthExtraLargeLayout);

View File

@@ -1,49 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import PropTypes from 'prop-types';
import messages from './messages';
const AuthLargeLayout = ({ username }) => {
const { formatMessage } = useIntl();
return (
<div className="w-50 d-flex">
<div className="col-md-10 bg-light-200 p-0">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo position-absolute" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
</Hyperlink>
<div className="min-vh-100 d-flex align-items-center">
<div className="large-screen-left-container mr-n4.5 large-yellow-line mt-5" />
<div>
<h1 className="welcome-to-platform data-hj-suppress">
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
</h1>
<h2 className="complete-your-profile">
{formatMessage(messages['complete.your.profile.1'])}
<div className="text-accent-a">
{formatMessage(messages['complete.your.profile.2'])}
</div>
</h2>
</div>
</div>
</div>
<div className="col-md-2 bg-white p-0">
<svg className="m1-n1 w-100 h-100 large-screen-svg-light" preserveAspectRatio="xMaxYMin meet">
<g transform="skewX(171.6)">
<rect x="0" y="0" height="100%" width="100%" />
</g>
</svg>
</div>
</div>
);
};
AuthLargeLayout.propTypes = {
username: PropTypes.string.isRequired,
};
export default AuthLargeLayout;

View File

@@ -1,52 +1,77 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import {
Col, Hyperlink, Image, Row,
} from '@edx/paragon';
import messages from './messages';
const AuthMediumLayout = ({ username }) => {
const { formatMessage } = useIntl();
const AuthMediumLayout = (props) => {
const { intl, username, toggleWelcomeText } = props;
return (
<>
<div className="w-100 medium-screen-top-stripe" />
<div className="w-100 p-0 mb-3 d-flex">
<div className="col-md-10 bg-light-200">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
</Hyperlink>
<div className="d-flex align-items-center justify-content-center mb-4 ml-5">
<div className="medium-yellow-line mt-5 mr-n2" />
<div>
<h1 className="h3 data-hj-suppress mw-320">
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
</h1>
<h2 className="display-1">
{formatMessage(messages['complete.your.profile.1'])}
<div className="text-accent-a">
{formatMessage(messages['complete.your.profile.2'])}
</div>
</h2>
</div>
<div className="container row p-0 mb-3 medium-container">
<div className="col-md-10 p-0 screen-header-light">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image alt="edx" className="logo" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="d-flex align-items-center justify-content-center ml-6">
<div>
<Row>
<Col xs={3}>
<svg className="medium-svg-line ml-5 mt-5">
<line x1="60" y1="0" x2="5" y2="220" />
</svg>
</Col>
<Col xs={9}>
<h3 className="data-hj-suppress">
{intl.formatMessage(
messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username },
)}
</h3>
{ !toggleWelcomeText ? (
<div className="display-1 text-primary">
{intl.formatMessage(messages['complete.your.profile.1'])}
<span className="text-accent-a">
<br />
{intl.formatMessage(messages['complete.your.profile.2'])}
</span>
</div>
) : null}
</Col>
</Row>
</div>
</div>
<div className="col-md-2 bg-white p-0">
<svg className="w-100 h-100 medium-screen-svg-light" preserveAspectRatio="xMaxYMin meet">
<g transform="skewX(168)">
<rect x="0" y="0" height="100%" width="100%" />
</g>
</svg>
</div>
</div>
</>
<div className="col-md-2 p-0 screen-polygon">
<svg
width="100%"
height="100%"
className="medium-screen-svg-light"
preserveAspectRatio="xMaxYMin meet"
>
<g transform="skewX(168)">
<rect x="0" y="0" height="100%" width="100%" />
</g>
</svg>
</div>
</div>
);
};
AuthMediumLayout.propTypes = {
username: PropTypes.string.isRequired,
AuthMediumLayout.defaultProps = {
toggleWelcomeText: false,
};
export default AuthMediumLayout;
AuthMediumLayout.propTypes = {
intl: intlShape.isRequired,
username: PropTypes.string.isRequired,
toggleWelcomeText: PropTypes.bool,
};
export default injectIntl(AuthMediumLayout);

View File

@@ -1,41 +1,72 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Col, Hyperlink, Image, Row,
} from '@edx/paragon';
import messages from './messages';
const AuthSmallLayout = ({ username }) => {
const { formatMessage } = useIntl();
const AuthSmallLayout = (props) => {
const { intl, username, variant, toggleWelcomeText } = props;
return (
<div className="min-vw-100 bg-light-200">
<div className="col-md-12 small-screen-top-stripe" />
<div className="small-screen-header-light">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
<Image alt="edx" className="logo" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="d-flex align-items-center m-3.5">
<div className="small-yellow-line mt-4.5" />
<div className={classNames('d-flex mt-3', { 'pl-6': variant === 'sm' })}>
<div>
<h1 className="h5 data-hj-suppress">
{formatMessage(messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username })}
</h1>
<h2 className="h1">
{formatMessage(messages['complete.your.profile.1'])}
<div className="text-accent-a">
{formatMessage(messages['complete.your.profile.2'])}
</div>
</h2>
<Row>
<Col xs={3}>
<svg className={classNames(
'mt-4\.5', // eslint-disable-line no-useless-escape
{
'extra-small-svg-line': variant === 'xs',
'small-svg-line': variant === 'sm',
},
)}
>
<line x1="60" y1="0" x2="5" y2="220" />
</svg>
</Col>
<Col xs={9}>
<h5 className="data-hj-suppress">
{intl.formatMessage(
messages['welcome.to.platform'], { siteName: getConfig().SITE_NAME, username },
)}
</h5>
{ !toggleWelcomeText ? (
<h1>
{intl.formatMessage(messages['complete.your.profile.1'])}
<br />
<span className="text-accent-a">
{intl.formatMessage(messages['complete.your.profile.2'])}
</span>
</h1>
) : null}
</Col>
</Row>
</div>
</div>
</div>
);
};
AuthSmallLayout.propTypes = {
username: PropTypes.string.isRequired,
AuthSmallLayout.defaultProps = {
variant: 'sm',
toggleWelcomeText: false,
};
export default AuthSmallLayout;
AuthSmallLayout.propTypes = {
intl: intlShape.isRequired,
username: PropTypes.string.isRequired,
variant: PropTypes.oneOf(['sm', 'xs']),
toggleWelcomeText: PropTypes.bool,
};
export default injectIntl(AuthSmallLayout);

View File

@@ -1,39 +1,58 @@
import React from 'react';
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { getLocale } from '@edx/frontend-platform/i18n';
import { breakpoints } from '@edx/paragon';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import MediaQuery from 'react-responsive';
import AuthLargeLayout from './AuthLargeLayout';
import AuthMediumLayout from './AuthMediumLayout';
import AuthSmallLayout from './AuthSmallLayout';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import {
ExtraSmall, Small, Medium, Large, ExtraLarge, ExtraExtraLarge,
} from '@edx/paragon';
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
import { getLocale } from '@edx/frontend-platform/i18n';
import LargeLayout from './LargeLayout';
import MediumLayout from './MediumLayout';
import SmallLayout from './SmallLayout';
const BaseComponent = ({ children, showWelcomeBanner }) => {
const authenticatedUser = showWelcomeBanner ? getAuthenticatedUser() : null;
const username = authenticatedUser ? authenticatedUser.username : null;
import AuthExtraLargeLayout from './AuthExtraLargeLayout';
import AuthMediumLayout from './AuthMediumLayout';
import AuthSmallLayout from './AuthSmallLayout';
const BaseComponent = ({ children, toggleWelcomeText }) => {
const authenticatedUser = getAuthenticatedUser();
return (
<>
{getConfig().ENABLE_COOKIE_POLICY_BANNER ? <CookiePolicyBanner languageCode={getLocale()} /> : null}
<div className="col-md-12 extra-large-screen-top-stripe" />
<div className="layout">
<MediaQuery maxWidth={breakpoints.small.maxWidth - 1}>
{authenticatedUser ? <AuthSmallLayout username={username} /> : <SmallLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.medium.minWidth} maxWidth={breakpoints.large.maxWidth - 1}>
{authenticatedUser ? <AuthMediumLayout username={username} /> : <MediumLayout />}
</MediaQuery>
<MediaQuery minWidth={breakpoints.extraLarge.minWidth} maxWidth={breakpoints.extraExtraLarge.maxWidth}>
{authenticatedUser ? <AuthLargeLayout username={username} /> : <LargeLayout />}
</MediaQuery>
<CookiePolicyBanner languageCode={getLocale()} />
<ExtraLarge>
<div className="col-md-12 extra-large-screen-top-stripe" />
</ExtraLarge>
<ExtraExtraLarge>
<div className="col-md-12 extra-large-screen-top-stripe" />
</ExtraExtraLarge>
<div className={classNames('layout', { authenticated: authenticatedUser })}>
<ExtraSmall>
<div className="col-md-12 small-screen-top-stripe" />
{authenticatedUser ? <AuthSmallLayout toggleWelcomeText={toggleWelcomeText} variant="xs" username={authenticatedUser.username} /> : <SmallLayout />}
</ExtraSmall>
<Small>
<div className="col-md-12 small-screen-top-stripe" />
{authenticatedUser ? <AuthSmallLayout toggleWelcomeText={toggleWelcomeText} username={authenticatedUser.username} /> : <SmallLayout />}
</Small>
<Medium>
<div className="w-100 medium-screen-top-stripe" />
{authenticatedUser ? <AuthMediumLayout toggleWelcomeText={toggleWelcomeText} username={authenticatedUser.username} /> : <MediumLayout />}
</Medium>
<Large>
<div className="w-100 large-screen-top-stripe" />
{authenticatedUser ? <AuthMediumLayout toggleWelcomeText={toggleWelcomeText} username={authenticatedUser.username} /> : <MediumLayout />}
</Large>
<ExtraLarge>
{authenticatedUser ? <AuthExtraLargeLayout toggleWelcomeText={toggleWelcomeText} username={authenticatedUser.username} /> : <LargeLayout />}
</ExtraLarge>
<ExtraExtraLarge>
{authenticatedUser ? <AuthExtraLargeLayout toggleWelcomeText={toggleWelcomeText} variant="xxl" username={authenticatedUser.username} /> : <LargeLayout />}
</ExtraExtraLarge>
<div className={classNames('content', { 'align-items-center mt-0': authenticatedUser })}>
{children}
@@ -43,13 +62,13 @@ const BaseComponent = ({ children, showWelcomeBanner }) => {
);
};
BaseComponent.defaultProps = {
showWelcomeBanner: false,
};
BaseComponent.propTypes = {
children: PropTypes.node.isRequired,
showWelcomeBanner: PropTypes.bool,
toggleWelcomeText: PropTypes.bool,
};
BaseComponent.defaultProps = {
toggleWelcomeText: false,
}
export default BaseComponent;

View File

@@ -1,45 +1,31 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import classNames from 'classnames';
import messages from './messages';
import LargeScreenLeftLayout from './LargeLeftLayout';
const LargeLayout = () => {
const { formatMessage } = useIntl();
return (
<div className="w-50 d-flex">
<div className="col-md-9 bg-primary-400">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo position-absolute" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="min-vh-100 d-flex align-items-center">
<div className={classNames({ 'large-yellow-line mr-n4.5': getConfig().SITE_NAME === 'edX' })} />
<h1
className={classNames(
'display-2 text-white mw-xs',
{ 'ml-6': getConfig().SITE_NAME !== 'edX' },
)}
>
{formatMessage(messages['start.learning'])}
<div className="text-accent-a">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</div>
</h1>
</div>
</div>
<div className="col-md-3 bg-white p-0">
<svg className="ml-n1 w-100 h-100 large-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
<g transform="skewX(171.6)">
<rect x="0" y="0" height="100%" width="100%" />
</g>
</svg>
</div>
const LargeLayout = () => (
<div className="container row p-0 m-0 large-screen-container">
<div className="col-md-9 p-0 screen-header-primary">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image alt="edx" className="logo position-absolute" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<LargeScreenLeftLayout />
</div>
);
};
<div className="col-md-3 p-0 screen-polygon">
<svg
width="100%"
height="100%"
className="ml-n1 large-screen-svg-primary"
preserveAspectRatio="xMaxYMin meet"
>
<g transform="skewX(171.6)">
<rect x="0" y="0" height="100%" width="100%" />
</g>
</svg>
</div>
</div>
);
export default LargeLayout;

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
const LargeLeftLayout = (props) => {
const { intl } = props;
return (
<div className="min-vh-100 pr-0 mt-lg-n2 d-flex align-items-center">
<svg className="large-screen-svg-line ml-5">
<line x1="50" y1="0" x2="10" y2="215" />
</svg>
<h1 className="large-heading">
{intl.formatMessage(messages['start.learning'])}
<span className="text-accent-a"><br />
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</h1>
</div>
);
};
LargeLeftLayout.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(LargeLeftLayout);

View File

@@ -1,52 +1,45 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import classNames from 'classnames';
import messages from './messages';
const MediumLayout = () => {
const { formatMessage } = useIntl();
const MediumLayout = (props) => {
const { intl } = props;
return (
<>
<div className="w-100 medium-screen-top-stripe" />
<div className="w-100 p-0 mb-3 d-flex">
<div className="col-md-10 bg-primary-400">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image alt={getConfig().SITE_NAME} className="logo" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="d-flex align-items-center justify-content-center mb-4 ">
<div className={classNames({ 'mt-1 medium-yellow-line': getConfig().SITE_NAME === 'edX' })} />
<div>
<h1
className={classNames(
'display-1 text-white mt-5 mb-5 mr-2 main-heading',
{ 'ml-4.5': getConfig().SITE_NAME !== 'edX' },
)}
>
<span>
{formatMessage(messages['start.learning'])}{' '}
<span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</span>
</h1>
</div>
</div>
</div>
<div className="col-md-2 bg-white p-0">
<svg className="w-100 h-100 medium-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
<g transform="skewX(168)">
<rect x="0" y="0" height="100%" width="100%" />
</g>
<div className="container row p-0 mb-3 medium-screen-container">
<div className="col-md-10 p-0 screen-header-primary">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image alt="edx" className="logo" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="row mt-4 justify-content-center">
<svg className="medium-screen-svg-line pl-5">
<line x1="50" y1="0" x2="10" y2="215" />
</svg>
<h1 className="medium-heading pb-4">
{intl.formatMessage(messages['start.learning'])}
<span className="text-accent-a"><br />
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</h1>
</div>
</div>
</>
<div className="col-md-2 p-0 screen-polygon">
<svg width="100%" height="100%" className="medium-screen-svg-primary" preserveAspectRatio="xMaxYMin meet">
<g transform="skewX(168)">
<rect x="0" y="0" height="100%" width="100%" />
</g>
</svg>
</div>
</div>
);
};
export default MediumLayout;
MediumLayout.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(MediumLayout);

View File

@@ -1,40 +1,39 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@edx/paragon';
import classNames from 'classnames';
import messages from './messages';
const SmallLayout = () => {
const { formatMessage } = useIntl();
const SmallLayout = (props) => {
const { intl } = props;
return (
<span className="bg-primary-400 w-100">
<div className="col-md-12 small-screen-top-stripe" />
<div>
<>
<div className="small-screen-header-primary">
<Hyperlink destination={getConfig().MARKETING_SITE_BASE_URL}>
<Image className="logo-small" alt={getConfig().SITE_NAME} src={getConfig().LOGO_WHITE_URL} />
<Image alt="edx" className="logo" src={getConfig().LOGO_WHITE_URL} />
</Hyperlink>
<div className="d-flex align-items-center m-3.5">
<div className={classNames({ 'small-yellow-line mr-n2.5': getConfig().SITE_NAME === 'edX' })} />
<h1
className={classNames(
'text-white mt-3.5 mb-3.5',
)}
>
<span>
{formatMessage(messages['start.learning'])}{' '}
<span className="text-accent-a d-inline-block">
{formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
<div className="d-flex mt-3">
<svg className="small-screen-svg-line">
<line x1="55" y1="0" x2="40" y2="65" />
</svg>
<h1 className="small-heading pb-3">
{intl.formatMessage(messages['start.learning'])}
<br />
<span className="text-accent-a">
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</h1>
</div>
</div>
</span>
</>
);
};
export default SmallLayout;
SmallLayout.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(SmallLayout);

View File

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

View File

@@ -1,7 +1,6 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import LargeLayout from '../LargeLayout';
import MediumLayout from '../MediumLayout';

View File

@@ -1,24 +1,21 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Button, Form,
} from '@edx/paragon';
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
import {
Form, Button,
} from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
import messages from './messages';
/**
* This component renders the Single sign-on (SSO) button only for the tpa provider passed
* */
const EnterpriseSSO = (props) => {
const { formatMessage } = useIntl();
const { intl } = props;
const tpaProvider = props.provider;
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
const handleSubmit = (e, url) => {
e.preventDefault();
@@ -36,7 +33,7 @@ const EnterpriseSSO = (props) => {
<div className="d-flex flex-column">
<div className="mw-450">
<Form className="m-0">
<p>{formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
<p>{intl.formatMessage(messages['enterprisetpa.title.heading'], { providerName: tpaProvider.name })}</p>
<Button
id={tpaProvider.id}
key={tpaProvider.id}
@@ -65,15 +62,12 @@ const EnterpriseSSO = (props) => {
<div className="mb-4" />
<Button
type="submit"
id="other-ways-to-sign-in"
variant="outline-primary"
state="Complete"
className="w-100"
onClick={(e) => handleClick(e)}
>
{disablePublicAccountCreation
? formatMessage(messages['enterprisetpa.login.button.text.public.account.creation.disabled'])
: formatMessage(messages['enterprisetpa.login.button.text'])}
{intl.formatMessage(messages['enterprisetpa.login.button.text'])}
</Button>
</Form>
</div>
@@ -104,6 +98,7 @@ EnterpriseSSO.propTypes = {
loginUrl: PropTypes.string,
registerUrl: PropTypes.string,
}),
intl: intlShape.isRequired,
};
export default EnterpriseSSO;
export default injectIntl(EnterpriseSSO);

View File

@@ -24,12 +24,9 @@ const FormGroup = (props) => {
<Form.Group controlId={props.name} className={props.className} isInvalid={props.errorMessage !== ''}>
<Form.Control
as={props.as}
readOnly={props.readOnly}
type={props.type}
aria-invalid={props.errorMessage !== ''}
className="form-field"
autoComplete={props.autoComplete}
spellCheck={props.spellCheck}
name={props.name}
value={props.value}
onFocus={handleFocus}
@@ -37,6 +34,7 @@ const FormGroup = (props) => {
onClick={handleClick}
onChange={props.handleChange}
controlClassName={props.borderClass}
trailingElement={props.trailingElement}
floatingLabel={props.floatingLabel}
>
@@ -64,43 +62,39 @@ const FormGroup = (props) => {
FormGroup.defaultProps = {
as: 'input',
autoComplete: null,
borderClass: '',
children: null,
className: '',
errorMessage: '',
borderClass: '',
autoComplete: null,
handleBlur: null,
handleChange: () => {},
handleClick: null,
handleFocus: null,
handleClick: null,
helpText: [],
options: null,
readOnly: false,
spellCheck: null,
trailingElement: null,
type: 'text',
children: null,
className: '',
};
FormGroup.propTypes = {
as: PropTypes.string,
autoComplete: PropTypes.string,
borderClass: PropTypes.string,
children: PropTypes.element,
className: PropTypes.string,
errorMessage: PropTypes.string,
borderClass: PropTypes.string,
autoComplete: PropTypes.string,
floatingLabel: PropTypes.string.isRequired,
handleBlur: PropTypes.func,
handleChange: PropTypes.func,
handleClick: PropTypes.func,
handleFocus: PropTypes.func,
handleClick: PropTypes.func,
helpText: PropTypes.arrayOf(PropTypes.string),
name: PropTypes.string.isRequired,
options: PropTypes.func,
readOnly: PropTypes.bool,
spellCheck: PropTypes.string,
trailingElement: PropTypes.element,
type: PropTypes.string,
value: PropTypes.string.isRequired,
children: PropTypes.element,
className: PropTypes.string,
};
export default FormGroup;

View File

@@ -1,16 +1,11 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Button, Hyperlink, Icon } from '@edx/paragon';
import { Institution } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import messages from './messages';
/**
* This component renders the Institution login button
* */
export const RenderInstitutionButton = props => {
const { onSubmitHandler, buttonTitle } = props;
@@ -27,13 +22,10 @@ export const RenderInstitutionButton = props => {
);
};
/**
* This component renders the page list of available institutions for login
* */
const InstitutionLogistration = props => {
const lmsBaseUrl = getConfig().LMS_BASE_URL;
const { formatMessage } = useIntl();
const {
intl,
secondaryProviders,
headingTitle,
} = props;
@@ -46,7 +38,7 @@ const InstitutionLogistration = props => {
{headingTitle}
</h4>
<p className="mb-2">
{formatMessage(messages['institution.login.page.sub.heading'])}
{intl.formatMessage(messages['institution.login.page.sub.heading'])}
</p>
</div>
</div>
@@ -78,7 +70,7 @@ const LogistrationDefaultProps = {
};
const LogistrationProps = {
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
name: PropTypes.string.isRequried,
loginUrl: PropTypes.string.isRequired,
})),
};
@@ -95,6 +87,7 @@ RenderInstitutionButton.defaultProps = {
InstitutionLogistration.propTypes = {
...LogistrationProps,
intl: intlShape.isRequired,
headingTitle: PropTypes.string,
};
InstitutionLogistration.defaultProps = {
@@ -102,4 +95,4 @@ InstitutionLogistration.defaultProps = {
headingTitle: '',
};
export default InstitutionLogistration;
export default injectIntl(InstitutionLogistration);

View File

@@ -1,48 +1,28 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthService } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Icon,
Tab,
Tabs,
} from '@edx/paragon';
import { ChevronLeft } from '@edx/paragon/icons';
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import { BaseComponent } from '../base-component';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Tabs,
Tab,
Icon,
} from '@edx/paragon';
import { ChevronLeft } from '@edx/paragon/icons';
import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import { getTpaHint, getTpaProvider, updatePathWithQueryParams } from '../data/utils';
import { updatePathWithQueryParams, getTpaHint } from '../data/utils';
import { LoginPage } from '../login';
import { RegistrationPage } from '../register';
import { backupRegistrationForm } from '../register/data/actions';
import { clearThirdPartyAuthContextErrorMessage } from './data/actions';
import {
tpaProvidersSelector,
} from './data/selectors';
import messages from './messages';
import BaseComponent from '../base-component';
const Logistration = (props) => {
const { selectedPage, tpaProviders } = props;
const tpaHint = getTpaHint();
const {
providers, secondaryProviders,
} = tpaProviders;
const { formatMessage } = useIntl();
const { intl, selectedPage } = props;
const tpa = getTpaHint();
const [institutionLogin, setInstitutionLogin] = useState(false);
const [key, setKey] = useState('');
const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false;
useEffect(() => {
const authService = getAuthService();
if (authService) {
authService.getCsrfTokenService().getCsrfToken(getConfig().LMS_BASE_URL);
}
});
const handleInstitutionLogin = (e) => {
sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
@@ -57,10 +37,6 @@ const Logistration = (props) => {
const handleOnSelect = (tabKey) => {
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
props.clearThirdPartyAuthContextErrorMessage();
if (tabKey === LOGIN_PAGE) {
props.backupRegistrationForm();
}
setKey(tabKey);
};
@@ -69,100 +45,51 @@ const Logistration = (props) => {
<Icon src={ChevronLeft} className="left-icon" />
<span className="ml-2">
{selectedPage === LOGIN_PAGE
? formatMessage(messages['logistration.sign.in'])
: formatMessage(messages['logistration.register'])}
? intl.formatMessage(messages['logistration.sign.in'])
: intl.formatMessage(messages['logistration.register'])}
</span>
</div>
);
const isValidTpaHint = () => {
const { provider } = getTpaProvider(tpaHint, providers, secondaryProviders);
return !!provider;
};
return (
<BaseComponent>
<div>
{disablePublicAccountCreation
{institutionLogin
? (
<>
<Redirect to={updatePathWithQueryParams(LOGIN_PAGE)} />
{institutionLogin && (
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
<Tab title={tabTitle} eventKey={LOGIN_PAGE} />
</Tabs>
)}
<div id="main-content" className="main-content">
{!institutionLogin && (
<h3 className="mb-4.5">{formatMessage(messages['logistration.sign.in'])}</h3>
)}
<LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
</div>
</>
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
<Tab title={tabTitle} eventKey={selectedPage === LOGIN_PAGE ? LOGIN_PAGE : REGISTER_PAGE} />
</Tabs>
)
: (
<div>
{institutionLogin
? (
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
<Tab title={tabTitle} eventKey={selectedPage === LOGIN_PAGE ? LOGIN_PAGE : REGISTER_PAGE} />
</Tabs>
)
: (!isValidTpaHint() && (
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={handleOnSelect}>
<Tab title={formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
<Tab title={formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
</Tabs>
))}
{ key && (
<Redirect to={updatePathWithQueryParams(key)} />
<>
{!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>
)}
<div id="main-content" className="main-content">
{selectedPage === LOGIN_PAGE
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
: (
<RegistrationPage
institutionLogin={institutionLogin}
handleInstitutionLogin={handleInstitutionLogin}
/>
)}
</div>
</div>
</>
)}
{ key && (
<Redirect to={updatePathWithQueryParams(key)} />
)}
<div id="main-content" className="main-content">
{selectedPage === LOGIN_PAGE
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
: <RegistrationPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />}
</div>
</div>
</BaseComponent>
);
};
Logistration.propTypes = {
intl: intlShape.isRequired,
selectedPage: PropTypes.string,
backupRegistrationForm: PropTypes.func.isRequired,
clearThirdPartyAuthContextErrorMessage: PropTypes.func.isRequired,
tpaProviders: PropTypes.shape({
providers: PropTypes.arrayOf(PropTypes.shape({})),
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
}),
};
Logistration.defaultProps = {
tpaProviders: {
providers: [],
secondaryProviders: [],
},
};
Logistration.defaultProps = {
selectedPage: REGISTER_PAGE,
};
const mapStateToProps = state => ({
tpaProviders: tpaProvidersSelector(state),
});
export default connect(
mapStateToProps,
{
backupRegistrationForm,
clearThirdPartyAuthContextErrorMessage,
},
)(Logistration);
export default injectIntl(Logistration);

View File

@@ -1,17 +1,16 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
const NotFoundPage = () => (
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
<p className="my-0 py-5 text-muted mw-32em">
<FormattedMessage
id="error.notfound.message"
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
description="error message when a page does not exist"
/>
</p>
</div>
);
export default NotFoundPage;
export default function NotFoundPage() {
return (
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
<p className="my-0 py-5 text-muted mw-32em">
<FormattedMessage
id="error.notfound.message"
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
description="error message when a page does not exist"
/>
</p>
</div>
);
}

View File

@@ -1,19 +1,19 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form, Icon, IconButton, OverlayTrigger, Tooltip, useToggle,
Form, IconButton, useToggle, Tooltip, OverlayTrigger, Icon,
} from '@edx/paragon';
import {
Check, Remove, Visibility, VisibilityOff,
} from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
import messages from './messages';
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
const PasswordField = (props) => {
const { formatMessage } = useIntl();
const { formatMessage } = props.intl;
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
const [showTooltip, setShowTooltip] = useState(false);
@@ -30,25 +30,25 @@ const PasswordField = (props) => {
};
const HideButton = (
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={VisibilityOff} iconAs={Icon} onClick={setHiddenTrue} size="sm" variant="secondary" alt={formatMessage(messages['hide.password'])} />
);
const ShowButton = (
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="password" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
<IconButton onFocus={handleFocus} onBlur={handleBlur} name="passwordValidation" src={Visibility} iconAs={Icon} onClick={setHiddenFalse} size="sm" variant="secondary" alt={formatMessage(messages['show.password'])} />
);
const placement = window.innerWidth < 768 ? 'top' : 'left';
const tooltip = (
<Tooltip id={`password-requirement-${placement}`}>
<span id="letter-check" className="d-flex align-items-center">
{LETTER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
<span id="letter-check" className="d-flex position-relative align-content-start">
{LETTER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1" src={Remove} />}
{formatMessage(messages['one.letter'])}
</span>
<span id="number-check" className="d-flex align-items-center">
{NUMBER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
<span id="number-check" className="d-flex position-relative align-content-start">
{NUMBER_REGEX.test(props.value) ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1" src={Remove} />}
{formatMessage(messages['one.number'])}
</span>
<span id="characters-check" className="d-flex align-items-center">
{props.value.length >= 8 ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1 text-light-700" src={Remove} />}
<span id="characters-check" className="d-flex position-relative align-content-start">
{props.value.length >= 8 ? <Icon className="text-success mr-1" src={Check} /> : <Icon className="mr-1" src={Remove} />}
{formatMessage(messages['eight.characters'])}
</span>
</Tooltip>
@@ -63,8 +63,6 @@ const PasswordField = (props) => {
type={isPasswordHidden ? 'password' : 'text'}
name={props.name}
value={props.value}
autoComplete={props.autoComplete}
aria-invalid={props.errorMessage !== ''}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={props.handleChange}
@@ -90,7 +88,6 @@ PasswordField.defaultProps = {
handleFocus: null,
handleChange: () => {},
showRequirements: true,
autoComplete: null,
};
PasswordField.propTypes = {
@@ -100,10 +97,10 @@ PasswordField.propTypes = {
handleBlur: PropTypes.func,
handleFocus: PropTypes.func,
handleChange: PropTypes.func,
intl: intlShape.isRequired,
name: PropTypes.string.isRequired,
showRequirements: PropTypes.bool,
value: PropTypes.string.isRequired,
autoComplete: PropTypes.string,
};
export default PasswordField;
export default injectIntl(PasswordField);

View File

@@ -1,22 +1,16 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router-dom';
import { AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS } from '../data/constants';
import { getConfig } from '@edx/frontend-platform';
import { WELCOME_PAGE } from '../data/constants';
import { setCookie } from '../data/utils';
const RedirectLogistration = (props) => {
function RedirectLogistration(props) {
const {
finishAuthUrl,
redirectUrl,
redirectToProgressiveProfilingPage,
success,
optionalFields,
redirectToRecommendationsPage,
educationLevel,
userId,
finishAuthUrl, redirectUrl, redirectToWelcomePage, success,
} = props;
let finalRedirectUrl = '';
@@ -31,65 +25,31 @@ const RedirectLogistration = (props) => {
finalRedirectUrl = redirectUrl;
}
// Redirect to Progressive Profiling after successful registration
if (redirectToProgressiveProfilingPage) {
// TODO: Do we still need this cookie?
if (redirectToWelcomePage) {
setCookie('van-504-returning-user', true);
// use this component to redirect WelcomePage after successful registration
// return <Redirect to={WELCOME_PAGE} />;
const registrationResult = { redirectUrl: finalRedirectUrl, success };
return (
<Redirect to={{
pathname: AUTHN_PROGRESSIVE_PROFILING,
state: {
registrationResult,
optionalFields,
},
}}
/>
);
}
// Redirect to Recommendation page
if (redirectToRecommendationsPage) {
const registrationResult = { redirectUrl: finalRedirectUrl, success };
return (
<Redirect to={{
pathname: RECOMMENDATIONS,
state: {
registrationResult,
educationLevel,
userId,
},
}}
/>
);
return <Redirect to={{ pathname: WELCOME_PAGE, state: { registrationResult } }} />;
}
window.location.href = finalRedirectUrl;
}
return null;
};
return <></>;
}
RedirectLogistration.defaultProps = {
educationLevel: null,
finishAuthUrl: null,
success: false,
redirectUrl: '',
redirectToProgressiveProfilingPage: false,
optionalFields: {},
redirectToRecommendationsPage: false,
userId: null,
redirectToWelcomePage: false,
};
RedirectLogistration.propTypes = {
educationLevel: PropTypes.string,
finishAuthUrl: PropTypes.string,
success: PropTypes.bool,
redirectUrl: PropTypes.string,
redirectToProgressiveProfilingPage: PropTypes.bool,
optionalFields: PropTypes.shape({}),
redirectToRecommendationsPage: PropTypes.bool,
userId: PropTypes.number,
redirectToWelcomePage: PropTypes.bool,
};
export default RedirectLogistration;

View File

@@ -1,17 +1,16 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
import messages from './messages';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons';
const SocialAuthProviders = (props) => {
const { formatMessage } = useIntl();
const { referrer, socialAuthProviders } = props;
import messages from './messages';
import { LOGIN_PAGE, SUPPORTED_ICON_CLASSES } from '../data/constants';
function SocialAuthProviders(props) {
const { intl, referrer, socialAuthProviders } = props;
function handleSubmit(e) {
e.preventDefault();
@@ -35,24 +34,25 @@ const SocialAuthProviders = (props) => {
</div>
)
: (
<div className="font-container" aria-hidden="true">
<FontAwesomeIcon
icon={SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? ['fab', provider.iconClass] : faSignInAlt}
/>
</div>
<>
<div className="font-container" aria-hidden="true">
<FontAwesomeIcon
icon={SUPPORTED_ICON_CLASSES.includes(provider.iconClass) ? ['fab', provider.iconClass] : faSignInAlt}
/>
</div>
</>
)}
<span id="provider-name" className="notranslate mr-auto pl-2" aria-hidden="true">{provider.name}</span>
<span id="provider-name" className="mr-auto pl-2" aria-hidden="true">{provider.name}</span>
<span className="sr-only">
{referrer === LOGIN_PAGE
? formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
: formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
? intl.formatMessage(messages['sso.sign.in.with'], { providerName: provider.name })
: intl.formatMessage(messages['sso.create.account.using'], { providerName: provider.name })}
</span>
</button>
));
// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{socialAuth}</>;
};
}
SocialAuthProviders.defaultProps = {
referrer: LOGIN_PAGE,
@@ -60,6 +60,7 @@ SocialAuthProviders.defaultProps = {
};
SocialAuthProviders.propTypes = {
intl: intlShape.isRequired,
referrer: PropTypes.string,
socialAuthProviders: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.string,
@@ -68,8 +69,7 @@ SocialAuthProviders.propTypes = {
iconImage: PropTypes.string,
loginUrl: PropTypes.string,
registerUrl: PropTypes.string,
skipRegistrationForm: PropTypes.bool,
})),
};
export default SocialAuthProviders;
export default injectIntl(SocialAuthProviders);

View File

@@ -0,0 +1,61 @@
import React from 'react';
import PropTypes from 'prop-types';
import { TransitionReplace } from '@edx/paragon';
const onChildExit = (htmlNode) => {
// If the leaving child has focus, take control and redirect it
if (htmlNode.contains(document.activeElement)) {
// Get the newly entering sibling.
// It's the previousSibling, but not for any explicit reason. So checking for both.
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
// There's no replacement, do nothing.
if (!enteringChild) return; // eslint-disable-line curly
// Get all the focusable elements in the entering child and focus the first one
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
if (focusableElements.length) {
focusableElements[0].focus();
}
}
};
function SwitchContent({ expression, cases, className }) {
const getContent = (caseKey) => {
if (cases[caseKey]) {
if (typeof cases[caseKey] === 'string') {
return getContent(cases[caseKey]);
}
return React.cloneElement(cases[caseKey], { key: caseKey });
} else if (cases.default) { // eslint-disable-line no-else-return
if (typeof cases.default === 'string') {
return getContent(cases.default);
}
React.cloneElement(cases.default, { key: 'default' });
}
return null;
};
return (
<TransitionReplace
className={className}
onChildExit={onChildExit}
>
{getContent(expression)}
</TransitionReplace>
);
}
SwitchContent.propTypes = {
expression: PropTypes.string,
cases: PropTypes.objectOf(PropTypes.node).isRequired,
className: PropTypes.string,
};
SwitchContent.defaultProps = {
expression: null,
className: null,
};
export default SwitchContent;

View File

@@ -1,52 +1,42 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import PropTypes from 'prop-types';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import messages from './messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
const ThirdPartyAuthAlert = (props) => {
const { formatMessage } = useIntl();
const { currentProvider, referrer } = props;
const { currentProvider, intl, referrer } = props;
const platformName = getConfig().SITE_NAME;
let message;
if (referrer === LOGIN_PAGE) {
message = formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
message = intl.formatMessage(messages['login.third.party.auth.account.not.linked'], { currentProvider, platformName });
} else {
message = formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
}
if (!currentProvider) {
return null;
message = intl.formatMessage(messages['register.third.party.auth.account.not.linked'], { currentProvider, platformName });
}
return (
<>
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2 mb-5' : 'alert-warning mt-n2 mb-5'}>
{referrer === REGISTER_PAGE ? (
<Alert.Heading>{formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
) : null}
<p>{ message }</p>
</Alert>
<Alert id="tpa-alert" className={referrer === REGISTER_PAGE ? 'alert-success mt-n2' : 'alert-warning mt-n2'}>
{referrer === REGISTER_PAGE ? (
<h4 className="mt-4 mb-4">{formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
<Alert.Heading>{intl.formatMessage(messages['tpa.alert.heading'])}</Alert.Heading>
) : null}
</>
<p>{ message }</p>
</Alert>
);
};
ThirdPartyAuthAlert.defaultProps = {
currentProvider: '',
referrer: LOGIN_PAGE,
};
ThirdPartyAuthAlert.propTypes = {
currentProvider: PropTypes.string,
currentProvider: PropTypes.string.isRequired,
intl: intlShape.isRequired,
referrer: PropTypes.string,
};
export default ThirdPartyAuthAlert;
export default injectIntl(ThirdPartyAuthAlert);

View File

@@ -1,9 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Route } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { Route } from 'react-router-dom';
import { DEFAULT_REDIRECT_URL } from '../data/constants';
/**
@@ -30,7 +28,7 @@ const UnAuthOnlyRoute = (props) => {
return <Route {...props} />;
}
return null;
return <></>;
};
export default UnAuthOnlyRoute;

View File

@@ -1,53 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import Zendesk from 'react-zendesk';
import messages from './messages';
const ZendeskHelp = () => {
const { formatMessage } = useIntl();
const setting = {
cookies: true,
webWidget: {
contactOptions: {
enabled: false,
},
chat: {
suppress: false,
},
contactForm: {
ticketForms: [
{
id: 360003368814,
subject: false,
fields: [{ id: 'description', prefill: { '*': '' } }],
},
],
selectTicketForm: {
'*': formatMessage(messages.selectTicketForm),
},
attachments: true,
},
helpCenter: {
originalArticleButton: true,
},
answerBot: {
suppress: false,
contactOnlyAfterQuery: true,
title: { '*': formatMessage(messages.supportTitle) },
avatar: {
url: getConfig().ZENDESK_LOGO_URL,
name: { '*': formatMessage(messages.supportTitle) },
},
},
},
};
return (
<Zendesk defer zendeskKey={getConfig().ZENDESK_KEY} {...setting} />
);
};
export default ZendeskHelp;

View File

@@ -1,7 +1,6 @@
import { AsyncActionType } from '../../data/utils';
export const THIRD_PARTY_AUTH_CONTEXT = new AsyncActionType('THIRD_PARTY_AUTH', 'GET_THIRD_PARTY_AUTH_CONTEXT');
export const THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG = 'THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG';
// Third party auth context
export const getThirdPartyAuthContext = (urlParams) => ({
@@ -13,15 +12,11 @@ export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
});
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({
export const getThirdPartyAuthContextSuccess = (thirdPartyAuthContext) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
payload: { thirdPartyAuthContext },
});
export const getThirdPartyAuthContextFailure = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
});
export const clearThirdPartyAuthContextErrorMessage = () => ({
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
});

View File

@@ -1,51 +1,29 @@
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions';
import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
import { PENDING_STATE, COMPLETE_STATE } from '../../data/constants';
export const defaultState = {
fieldDescriptions: {},
optionalFields: {},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
},
};
const reducer = (state = defaultState, action = {}) => {
const reducer = (state = defaultState, action) => {
switch (action.type) {
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
return {
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS: {
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
return {
...state,
fieldDescriptions: action.payload.fieldDescriptions.fields,
optionalFields: action.payload.optionalFields,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};
}
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
return {
...state,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG:
return {
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
thirdPartyAuthContext: {
...state.thirdPartyAuthContext,
errorMessage: null,
},
};
default:
return state;
}

View File

@@ -1,13 +1,16 @@
import { logError } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
import { logError } from '@edx/frontend-platform/logging';
// Actions
import {
getThirdPartyAuthContextBegin,
getThirdPartyAuthContextFailure,
getThirdPartyAuthContextSuccess,
THIRD_PARTY_AUTH_CONTEXT,
getThirdPartyAuthContextBegin,
getThirdPartyAuthContextSuccess,
getThirdPartyAuthContextFailure,
} from './actions';
// Services
import {
getThirdPartyAuthContext,
} from './service';
@@ -15,12 +18,11 @@ import {
export function* fetchThirdPartyAuthContext(action) {
try {
yield put(getThirdPartyAuthContextBegin());
const {
fieldDescriptions, optionalFields, thirdPartyAuthContext,
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
const { thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
yield put(getThirdPartyAuthContextSuccess(
thirdPartyAuthContext,
));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);

View File

@@ -8,21 +8,3 @@ export const thirdPartyAuthContextSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.thirdPartyAuthContext,
);
export const fieldDescriptionSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.fieldDescriptions,
);
export const optionalFieldsSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.optionalFields,
);
export const tpaProvidersSelector = createSelector(
commonComponentsSelector,
commonComponents => ({
providers: commonComponents.thirdPartyAuthContext.providers,
secondaryProviders: commonComponents.thirdPartyAuthContext.secondaryProviders,
}),
);

View File

@@ -1,4 +1,4 @@
import { getConfig } from '@edx/frontend-platform';
import { camelCaseObject, convertKeyNames, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
// eslint-disable-next-line import/prefer-default-export
@@ -18,8 +18,6 @@ export async function getThirdPartyAuthContext(urlParams) {
throw (e);
});
return {
fieldDescriptions: data.registrationFields || data.registration_fields,
optionalFields: data.optionalFields || data.optional_fields,
thirdPartyAuthContext: data.contextData || data.context_data,
thirdPartyAuthContext: camelCaseObject(convertKeyNames(data, { fullname: 'name' })),
};
}

View File

@@ -1,82 +0,0 @@
import { PENDING_STATE } from '../../../data/constants';
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from '../actions';
import reducer from '../reducers';
describe('common components reducer', () => {
it('test mfe context response', () => {
const state = {
fieldDescriptions: {},
optionalFields: {},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
},
};
const fieldDescriptions = {
fields: [],
};
const optionalFields = {
fields: [],
extended_profile: {},
};
const thirdPartyAuthContext = { ...state.thirdPartyAuthContext };
const action = {
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
};
expect(
reducer(state, action),
).toEqual(
{
...state,
fieldDescriptions: [],
optionalFields: {
fields: [],
extended_profile: {},
},
thirdPartyAuthApiStatus: 'complete',
},
);
});
it('should clear tpa context error message', () => {
const state = {
fieldDescriptions: {},
optionalFields: {},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: 'An error occured',
},
};
const action = {
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
};
expect(
reducer(state, action),
).toEqual(
{
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
thirdPartyAuthContext: {
...state.thirdPartyAuthContext,
errorMessage: null,
},
},
);
});
});

View File

@@ -1,10 +1,9 @@
import { runSaga } from 'redux-saga';
import { setCountryFromThirdPartyAuthContext } from '../../../register/data/actions';
import initializeMockLogging from '../../../setupTest';
import * as actions from '../actions';
import { fetchThirdPartyAuthContext } from '../sagas';
import * as api from '../service';
import initializeMockLogging from '../../../setupTest';
const { loggingService } = initializeMockLogging();
@@ -27,11 +26,7 @@ describe('fetchThirdPartyAuthContext', () => {
it('should call service and dispatch success action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.resolve({
thirdPartyAuthContext: data,
fieldDescriptions: {},
optionalFields: {},
}));
.mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data }));
const dispatched = [];
await runSaga(
@@ -43,8 +38,7 @@ describe('fetchThirdPartyAuthContext', () => {
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
setCountryFromThirdPartyAuthContext(),
actions.getThirdPartyAuthContextSuccess({}, {}, data),
actions.getThirdPartyAuthContextSuccess(data),
]);
getThirdPartyAuthContext.mockClear();
});

View File

@@ -12,4 +12,3 @@ export { storeName } from './data/selectors';
export { default as FormGroup } from './FormGroup';
export { default as PasswordField } from './PasswordField';
export { default as Logistration } from './Logistration';
export { default as Zendesk } from './Zendesk';

View File

@@ -1,13 +1,29 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
// institution login strings
'institution.login.page.sub.heading': {
id: 'institution.login.page.sub.heading',
defaultMessage: 'Choose your institution from the list below',
description: 'Heading of the institutions list',
},
// logistration strings
// Confirmation Alert Message
'forgot.password.confirmation.title': {
id: 'forgot.password.confirmation.title',
defaultMessage: 'Check your email',
description: 'Forgot password confirmation message title',
},
'forgot.password.confirmation.support.link': {
id: 'forgot.password.confirmation.support.link',
defaultMessage: 'contact technical support',
description: 'Technical support link text',
},
'forgot.password.confirmation.info': {
id: 'forgot.password.confirmation.info',
defaultMessage: 'If you do not receive a password reset message after 1 minute, verify that you entered the correct '
+ 'email address, or check your spam folder.',
description: 'Part of message that appears after user requests password change',
},
// Logistration strinsg
'logistration.sign.in': {
id: 'logistration.sign.in',
defaultMessage: 'Sign in',
@@ -18,22 +34,32 @@ const messages = defineMessages({
defaultMessage: 'Register',
description: 'Text that appears on the tab to switch between login and register',
},
'internal.server.error.message': {
id: 'internal.server.error.message',
defaultMessage: 'An error has occurred. Try refreshing the page, or check your internet connection.',
description: 'Error message that appears when server responds with 500 error code',
},
'server.ratelimit.error.message': {
id: 'server.ratelimit.error.message',
defaultMessage: 'An error has occurred because of too many requests. Please try again after some time.',
description: 'Error message that appears when server responds with 429 error code',
},
// enterprise sso strings
'enterprisetpa.title.heading': {
id: 'enterprisetpa.title.heading',
defaultMessage: 'Would you like to sign in using your {providerName} credentials?',
description: 'Header text used in enterprise third party authentication',
},
'enterprisetpa.sso.button.title': {
id: 'enterprisetpa.sso.button.title',
defaultMessage: 'Sign in using {providerName}',
description: 'Text for third party auth provider buttons',
},
'enterprisetpa.login.button.text': {
id: 'enterprisetpa.login.button.text',
defaultMessage: 'Show me other ways to sign in or register',
description: 'Button text for login',
},
'enterprisetpa.login.button.text.public.account.creation.disabled': {
id: 'enterprisetpa.login.button.text.public.account.creation.disabled',
defaultMessage: 'Show me other ways to sign in',
description: 'Button text for login when account creation is disabled',
},
// social auth providers
'sso.sign.in.with': {
id: 'sso.sign.in.with',
@@ -58,17 +84,17 @@ const messages = defineMessages({
},
'one.letter': {
id: 'one.letter',
defaultMessage: '1 letter',
defaultMessage: '1 Letter',
description: 'password requirement to have 1 letter',
},
'one.number': {
id: 'one.number',
defaultMessage: '1 number',
defaultMessage: '1 Number',
description: 'password requirement to have 1 number',
},
'eight.characters': {
id: 'eight.characters',
defaultMessage: '8 characters',
defaultMessage: '8 Characters',
description: 'password requirement to have a minimum of 8 characters',
},
'password.sr.only.helping.text': {
@@ -97,21 +123,6 @@ const messages = defineMessages({
description: 'Message that appears on register page if user has successfully authenticated with TPA '
+ 'but no associated platform account exists',
},
'registration.using.tpa.form.heading': {
id: 'registration.using.tpa.form.heading',
defaultMessage: 'Finish creating your account',
description: 'Heading that appears above form when user is trying to create account using social auth',
},
supportTitle: {
id: 'zendesk.supportTitle',
description: 'Title for the support button',
defaultMessage: 'edX Support',
},
selectTicketForm: {
id: 'zendesk.selectTicketForm',
description: 'Select ticket form',
defaultMessage: 'Please choose your request type:',
},
});
export default messages;

View File

@@ -1,9 +1,9 @@
import React from 'react';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import FormGroup from '../FormGroup';
import PasswordField from '../PasswordField';
@@ -74,9 +74,9 @@ describe('PasswordField', () => {
});
passwordField.update();
expect(passwordField.find('#letter-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon mr-1 text-light-700');
expect(passwordField.find('#letter-check span').prop('className')).toEqual('pgn__icon mr-1');
expect(passwordField.find('#number-check span').prop('className')).toEqual('pgn__icon mr-1');
expect(passwordField.find('#characters-check span').prop('className')).toEqual('pgn__icon mr-1');
});
it('should update password requirement checks', async () => {

View File

@@ -1,19 +1,17 @@
import React from 'react';
import { mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics';
import * as auth from '@edx/frontend-platform/auth';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import { COMPLETE_STATE, LOGIN_PAGE } from '../../data/constants';
import { backupRegistrationForm } from '../../register/data/actions';
import { clearThirdPartyAuthContextErrorMessage } from '../data/actions';
import { RenderInstitutionButton } from '../InstitutionLogistration';
import * as auth from '@edx/frontend-platform/auth';
import Logistration from '../Logistration';
import { RenderInstitutionButton } from '../InstitutionLogistration';
import { COMPLETE_STATE, LOGIN_PAGE } from '../../data/constants';
jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-platform/auth');
@@ -41,7 +39,9 @@ describe('Logistration', () => {
);
beforeEach(() => {
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'test-user' }));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'edX' }));
});
it('should render registration page', () => {
configure({
loggingService: { logError: jest.fn() },
config: {
@@ -50,22 +50,14 @@ describe('Logistration', () => {
},
messages: { 'es-419': {}, de: {}, 'en-us': {} },
});
});
it('should render registration page', () => {
mergeConfig({
ALLOW_PUBLIC_ACCOUNT_CREATION: true,
});
store = mockStore({
register: {
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
},
commonComponents: {
thirdPartyAuthContext: {
providers: [],
secondaryProviders: [],
},
thirdPartyAuthApiStatus: null,
},
});
const logistration = mount(reduxWrapper(<IntlLogistration />));
@@ -79,10 +71,7 @@ describe('Logistration', () => {
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: {
thirdPartyAuthContext: {
providers: [],
secondaryProviders: [],
},
thirdPartyAuthApiStatus: null,
},
});
@@ -92,42 +81,9 @@ describe('Logistration', () => {
expect(logistration.find('#main-content').find('LoginPage').exists()).toBeTruthy();
});
it('should render only login page when public account creation is disabled', () => {
mergeConfig({
ALLOW_PUBLIC_ACCOUNT_CREATION: false,
DISABLE_ENTERPRISE_LOGIN: 'true',
});
store = mockStore({
login: {
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [secondaryProviders],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
const props = { selectedPage: LOGIN_PAGE };
const logistration = mount(reduxWrapper(<IntlLogistration {...props} />));
// verifying sign in heading for institution login false
expect(logistration.find('#main-content').find('h3').text()).toEqual('Sign in');
// verifying tabs heading for institution login true
logistration.find(RenderInstitutionButton).simulate('click', { institutionLogin: true });
expect(logistration.find('#controlled-tab').exists()).toBeTruthy();
});
it('should display institution login option when secondary providers are present', () => {
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: 'true',
ALLOW_PUBLIC_ACCOUNT_CREATION: 'true',
});
store = mockStore({
@@ -222,50 +178,4 @@ 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());
});
it('should clear tpa context errorMessage tab click', () => {
store = mockStore({
login: {
loginResult: { success: false, redirectUrl: '' },
},
register: {
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
},
commonComponents: {
thirdPartyAuthContext: {
providers: [],
secondaryProviders: [],
},
},
});
store.dispatch = jest.fn(store.dispatch);
const logistration = mount(reduxWrapper(<IntlLogistration />));
logistration.find('a[data-rb-event-key="/login"]').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage());
});
});

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import registerIcons from '../RegisterFaIcons';
import SocialAuthProviders from '../SocialAuthProviders';
import registerIcons from '../RegisterFaIcons';
registerIcons();

View File

@@ -1,10 +1,9 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { REGISTER_PAGE } from '../../data/constants';
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
import { REGISTER_PAGE } from '../../data/constants';
describe('ThirdPartyAuthAlert', () => {
let props = {};

View File

@@ -1,15 +1,12 @@
/* eslint-disable import/no-import-module-exports */
/* eslint-disable react/function-component-definition */
import React from 'react';
import { mount } from 'enzyme';
import { BrowserRouter as Router, MemoryRouter, Switch } from 'react-router-dom';
import * as auth from '@edx/frontend-platform/auth';
import { mount } from 'enzyme';
import { UnAuthOnlyRoute } from '..';
import { LOGIN_PAGE } from '../../data/constants';
import { MemoryRouter, BrowserRouter as Router, Switch } from 'react-router-dom';
jest.mock('@edx/frontend-platform/auth');
const RRD = require('react-router-dom');

View File

@@ -1,17 +0,0 @@
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
import Zendesk from '../Zendesk';
jest.mock('react-zendesk', () => 'Zendesk');
describe('Zendesk Help', () => {
it('should match login page third party auth alert message snapshot', () => {
const tree = renderer.create(
<IntlProvider locale="en">
<Zendesk />
</IntlProvider>,
).toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -14,8 +14,8 @@ exports[`SocialAuthProviders should match social auth provider with default icon
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-right-to-bracket "
data-icon="right-to-bracket"
className="svg-inline--fa fa-sign-in-alt fa-w-16 "
data-icon="sign-in-alt"
data-prefix="fas"
focusable="false"
role="img"
@@ -24,7 +24,7 @@ exports[`SocialAuthProviders should match social auth provider with default icon
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M352 96h64c17.7 0 32 14.3 32 32V384c0 17.7-14.3 32-32 32H352c-17.7 0-32 14.3-32 32s14.3 32 32 32h64c53 0 96-43 96-96V128c0-53-43-96-96-96H352c-17.7 0-32 14.3-32 32s14.3 32 32 32zm-7.5 177.4c4.8-4.5 7.5-10.8 7.5-17.4s-2.7-12.9-7.5-17.4l-144-136c-7-6.6-17.2-8.4-26-4.6s-14.5 12.5-14.5 22v72H32c-17.7 0-32 14.3-32 32v64c0 17.7 14.3 32 32 32H160v72c0 9.6 5.7 18.2 14.5 22s19 2 26-4.6l144-136z"
d="M416 448h-84c-6.6 0-12-5.4-12-12v-40c0-6.6 5.4-12 12-12h84c17.7 0 32-14.3 32-32V160c0-17.7-14.3-32-32-32h-84c-6.6 0-12-5.4-12-12V76c0-6.6 5.4-12 12-12h84c53 0 96 43 96 96v192c0 53-43 96-96 96zm-47-201L201 79c-15-15-41-4.5-41 17v96H24c-13.3 0-24 10.7-24 24v96c0 13.3 10.7 24 24 24h136v96c0 21.5 26 32 41 17l168-168c9.3-9.4 9.3-24.6 0-34z"
fill="currentColor"
style={Object {}}
/>
@@ -32,7 +32,7 @@ exports[`SocialAuthProviders should match social auth provider with default icon
</div>
<span
aria-hidden="true"
className="notranslate mr-auto pl-2"
className="mr-auto pl-2"
id="provider-name"
>
Apple
@@ -59,7 +59,7 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
>
<svg
aria-hidden="true"
className="svg-inline--fa fa-google "
className="svg-inline--fa fa-google fa-w-16 "
data-icon="google"
data-prefix="fab"
focusable="false"
@@ -77,7 +77,7 @@ exports[`SocialAuthProviders should match social auth provider with iconClass sn
</div>
<span
aria-hidden="true"
className="notranslate mr-auto pl-2"
className="mr-auto pl-2"
id="provider-name"
>
Apple
@@ -110,7 +110,7 @@ Array [
</div>
<span
aria-hidden="true"
className="notranslate mr-auto pl-2"
className="mr-auto pl-2"
id="provider-name"
>
Apple
@@ -139,7 +139,7 @@ Array [
</div>
<span
aria-hidden="true"
className="notranslate mr-auto pl-2"
className="mr-auto pl-2"
id="provider-name"
>
Facebook

View File

@@ -2,7 +2,7 @@
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
<div
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
className="fade alert-content alert-warning mt-n2 alert show"
id="tpa-alert"
role="alert"
>
@@ -13,7 +13,7 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
className="alert-message-content"
>
<p>
You have successfully signed into Google, but your Google account does not have a linked Your Platform Name Here account. To link your accounts, sign in now using your Your Platform Name Here password.
You have successfully signed into Google, but your Google account does not have a linked edX account. To link your accounts, sign in now using your edX password.
</p>
</div>
</div>
@@ -21,33 +21,26 @@ exports[`ThirdPartyAuthAlert should match login page third party auth alert mess
`;
exports[`ThirdPartyAuthAlert should match register page third party auth alert message snapshot 1`] = `
Array [
<div
className="fade alert-content alert-success mt-n2 alert show"
id="tpa-alert"
role="alert"
>
<div
className="fade alert-content alert-success mt-n2 mb-5 alert show"
id="tpa-alert"
role="alert"
className="pgn__alert-message-wrapper"
>
<div
className="pgn__alert-message-wrapper"
className="alert-message-content"
>
<div
className="alert-message-content"
className="alert-heading h4"
>
<div
className="alert-heading h4"
>
Almost done!
</div>
<p>
You've successfully signed into Google! We just need a little more information before you start learning with Your Platform Name Here.
</p>
Almost done!
</div>
<p>
You've successfully signed into Google! We just need a little more information before you start learning with edX.
</p>
</div>
</div>,
<h4
className="mt-4 mb-4"
>
Finish creating your account
</h4>,
]
</div>
</div>
`;

View File

@@ -1,54 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Zendesk Help should match login page third party auth alert message snapshot 1`] = `
<Zendesk
cookies={true}
defer={true}
webWidget={
Object {
"answerBot": Object {
"avatar": Object {
"name": Object {
"*": "edX Support",
},
"url": undefined,
},
"contactOnlyAfterQuery": true,
"suppress": false,
"title": Object {
"*": "edX Support",
},
},
"chat": Object {
"suppress": false,
},
"contactForm": Object {
"attachments": true,
"selectTicketForm": Object {
"*": "Please choose your request type:",
},
"ticketForms": Array [
Object {
"fields": Array [
Object {
"id": "description",
"prefill": Object {
"*": "",
},
},
],
"id": 360003368814,
"subject": false,
},
],
},
"contactOptions": Object {
"enabled": false,
},
"helpCenter": Object {
"originalArticleButton": true,
},
}
}
/>
`;

View File

@@ -1,29 +0,0 @@
const configuration = {
// Cookies related configs
SESSION_COOKIE_DOMAIN: process.env.SESSION_COOKIE_DOMAIN,
REGISTER_CONVERSION_COOKIE_NAME: process.env.REGISTER_CONVERSION_COOKIE_NAME || null,
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 || '',
ZENDESK_KEY: process.env.ZENDESK_KEY,
ZENDESK_LOGO_URL: process.env.ZENDESK_LOGO_URL,
};
export default configuration;

View File

@@ -1,9 +1,9 @@
import { getConfig } from '@edx/frontend-platform';
import { composeWithDevTools } from '@redux-devtools/extension';
import { applyMiddleware, compose, createStore } from 'redux';
import { applyMiddleware, createStore, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import thunkMiddleware from 'redux-thunk';
import createRootReducer from './reducers';
import rootSaga from './sagas';

View File

@@ -2,9 +2,8 @@
export const LOGIN_PAGE = '/login';
export const REGISTER_PAGE = '/register';
export const RESET_PAGE = '/reset';
export const AUTHN_PROGRESSIVE_PROFILING = '/welcome';
export const WELCOME_PAGE = '/welcome';
export const DEFAULT_REDIRECT_URL = '/dashboard';
export const RECOMMENDATIONS = '/recommendations';
export const PASSWORD_RESET_CONFIRM = '/password_reset_confirm/:token/';
export const PAGE_NOT_FOUND = '/notfound';
export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
@@ -13,16 +12,13 @@ export const ENTERPRISE_LOGIN_URL = '/enterprise/login';
export const SUPPORTED_ICON_CLASSES = ['apple', 'facebook', 'google', 'microsoft'];
// Error Codes
export const FORM_SUBMISSION_ERROR = 'form-submission-error';
export const INTERNAL_SERVER_ERROR = 'internal-server-error';
export const API_RATELIMIT_ERROR = 'api-ratelimit-error';
// Common States
// States
export const DEFAULT_STATE = 'default';
export const PENDING_STATE = 'pending';
export const COMPLETE_STATE = 'complete';
export const FAILURE_STATE = 'failure';
export const FORBIDDEN_STATE = 'forbidden';
// Regex
export const VALID_EMAIL_REGEX = '(^[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&\'*+/=?^_`{}|~0-9A-Z]+)*'
@@ -31,8 +27,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 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.
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'save_for_later', 'register_for_free', 'track', 'is_account_recovery'];
export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next'];

View File

@@ -1,11 +0,0 @@
import {
createInstance,
} from '@optimizely/react-sdk';
const OPTIMIZELY_SDK_KEY = process.env.OPTIMIZELY_FULL_STACK_SDK_KEY;
const optimizely = createInstance({
sdkKey: OPTIMIZELY_SDK_KEY,
});
export default optimizely;

View File

@@ -1,5 +1,13 @@
import { combineReducers } from 'redux';
import {
reducer as loginReducer,
storeName as loginStoreName,
} from '../login';
import {
reducer as registerReducer,
storeName as registerStoreName,
} from '../register';
import {
reducer as commonComponentsReducer,
storeName as commonComponentsStoreName,
@@ -8,29 +16,22 @@ import {
reducer as forgotPasswordReducer,
storeName as forgotPasswordStoreName,
} from '../forgot-password';
import {
reducer as loginReducer,
storeName as loginStoreName,
} from '../login';
import {
reducer as authnProgressiveProfilingReducers,
storeName as authnProgressiveProfilingStoreName,
} from '../progressive-profiling';
import {
reducer as registerReducer,
storeName as registerStoreName,
} from '../register';
import {
reducer as resetPasswordReducer,
storeName as resetPasswordStoreName,
} from '../reset-password';
import {
reducer as welcomePageReducers,
storeName as welcomePageStoreName,
} from '../welcome';
const createRootReducer = () => combineReducers({
[loginStoreName]: loginReducer,
[registerStoreName]: registerReducer,
[commonComponentsStoreName]: commonComponentsReducer,
[forgotPasswordStoreName]: forgotPasswordReducer,
[resetPasswordStoreName]: resetPasswordReducer,
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
[welcomePageStoreName]: welcomePageReducers,
});
export default createRootReducer;

View File

@@ -1,11 +1,11 @@
import { all } from 'redux-saga/effects';
import { saga as registrationSaga } from '../register';
import { saga as loginSaga } from '../login';
import { saga as commonComponentsSaga } from '../common-components';
import { saga as forgotPasswordSaga } from '../forgot-password';
import { saga as loginSaga } from '../login';
import { saga as authnProgressiveProfilingSaga } from '../progressive-profiling';
import { saga as registrationSaga } from '../register';
import { saga as resetPasswordSaga } from '../reset-password';
import { saga as welcomePageSaga } from '../welcome';
export default function* rootSaga() {
yield all([
@@ -14,6 +14,6 @@ export default function* rootSaga() {
commonComponentsSaga(),
forgotPasswordSaga(),
resetPasswordSaga(),
authnProgressiveProfilingSaga(),
welcomePageSaga(),
]);
}

View File

@@ -1,9 +1,9 @@
import { getConfig } from '@edx/frontend-platform';
import Cookies from 'universal-cookie';
import { getConfig } from '@edx/frontend-platform';
export function setCookie(cookieName, cookieValue, cookieExpiry) {
const cookies = new Cookies();
const options = { domain: getConfig().SESSION_COOKIE_DOMAIN, path: '/' };
const options = { domain: getConfig().COOKIE_DOMAIN, path: '/' };
if (cookieExpiry) {
options.expires = cookieExpiry;
}

View File

@@ -1,8 +1,16 @@
// Utility functions
import * as QueryString from 'query-string';
import * as QueryString from 'query-string';
import { AUTH_PARAMS } from '../constants';
export default function processLink(link) {
let matches;
link.replace(/(.*?)<a href=["']([^"']*).*?>([^<]+)<\/a>(.*)/g, function () { // eslint-disable-line func-names
matches = Array.prototype.slice.call(arguments, 1, 5); // eslint-disable-line prefer-rest-params
});
return matches;
}
export const getTpaProvider = (tpaHintProvider, primaryProviders, secondaryProviders) => {
let tpaProvider = null;
let skipHintedLogin = false;
@@ -49,8 +57,8 @@ export const updatePathWithQueryParams = (path) => {
return `${path}${queryParams}`;
};
export const getAllPossibleQueryParams = (locationURl = null) => {
const urlParams = locationURl ? QueryString.parseUrl(locationURl).query : QueryString.parse(window.location.search);
export const getAllPossibleQueryParam = () => {
const urlParams = QueryString.parse(window.location.search);
const params = {};
Object.entries(urlParams).forEach(([key, value]) => {
if (AUTH_PARAMS.indexOf(key) > -1) {

View File

@@ -1,5 +1,18 @@
import { LOGIN_PAGE } from '../constants';
import { updatePathWithQueryParams } from './dataUtils';
import processLink, { updatePathWithQueryParams } from './dataUtils';
describe('processLink', () => {
it('should use the provided processLink function to', () => {
const expectedHref = 'http://test.server.com/';
const expectedText = 'test link';
const link = `<a href="${expectedHref}">${expectedText}</a>`;
const matches = processLink(link);
expect(matches[1]).toEqual(expectedHref);
expect(matches[2]).toEqual(expectedText);
});
});
describe('updatePathWithQueryParams', () => {
it('should append query params into the path', () => {

View File

@@ -1,8 +1,9 @@
export {
default,
getTpaProvider,
getTpaHint,
updatePathWithQueryParams,
getAllPossibleQueryParams,
getAllPossibleQueryParam,
getActivationStatus,
windowScrollTo,
} from './dataUtils';

View File

@@ -1,24 +0,0 @@
import { useEffect, useState } from 'react';
import { breakpoints } from '@edx/paragon';
/**
* A react hook used to determine if the current window is mobile or not.
* returns true if the window is of mobile size.
* Code source: https://github.com/edx/prospectus/blob/master/src/utils/useMobileResponsive.js
*/
const useMobileResponsive = (breakpoint) => {
const [isMobileWindow, setIsMobileWindow] = useState();
const checkForMobile = () => {
setIsMobileWindow(window.matchMedia(`(max-width: ${breakpoint || breakpoints.small.maxWidth}px)`).matches);
};
useEffect(() => {
checkForMobile();
window.addEventListener('resize', checkForMobile);
// return this function here to clean up the event listener
return () => window.removeEventListener('resize', checkForMobile);
});
return isMobileWindow;
};
export default useMobileResponsive;

View File

@@ -1,158 +0,0 @@
import React from 'react';
import { Form, Icon } from '@edx/paragon';
import { ExpandMore } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
const FormFieldRenderer = (props) => {
let formField = null;
const {
className, errorMessage, fieldData, onChangeHandler, isRequired, value,
} = props;
const handleFocus = (e) => {
if (props.handleFocus) { props.handleFocus(e); }
};
const handleOnBlur = (e) => {
if (props.handleBlur) { props.handleBlur(e); }
};
switch (fieldData.type) {
case 'select': {
if (!fieldData.options) {
return null;
}
formField = (
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
<Form.Control
className={className}
as="select"
name={fieldData.name}
value={value}
aria-invalid={isRequired && Boolean(errorMessage)}
onChange={(e) => onChangeHandler(e)}
trailingElement={<Icon src={ExpandMore} />}
floatingLabel={fieldData.label}
onBlur={handleOnBlur}
onFocus={handleFocus}
>
<option key="default" value="">{fieldData.label}</option>
{fieldData.options.map(option => (
<option className="data-hj-suppress" key={option[0]} value={option[0]}>{option[1]}</option>
))}
</Form.Control>
{isRequired && errorMessage && (
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
{errorMessage}
</Form.Control.Feedback>
)}
</Form.Group>
);
break;
}
case 'textarea': {
formField = (
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
<Form.Control
className={className}
as="textarea"
name={fieldData.name}
value={value}
aria-invalid={isRequired && Boolean(errorMessage)}
onChange={(e) => onChangeHandler(e)}
floatingLabel={fieldData.label}
onBlur={handleOnBlur}
onFocus={handleFocus}
/>
{isRequired && errorMessage && (
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
{errorMessage}
</Form.Control.Feedback>
)}
</Form.Group>
);
break;
}
case 'text': {
formField = (
<Form.Group controlId={fieldData.name} isInvalid={!!(isRequired && errorMessage)}>
<Form.Control
className={className}
name={fieldData.name}
value={value}
aria-invalid={isRequired && Boolean(errorMessage)}
onChange={(e) => onChangeHandler(e)}
floatingLabel={fieldData.label}
onBlur={handleOnBlur}
onFocus={handleFocus}
/>
{isRequired && errorMessage && (
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
{errorMessage}
</Form.Control.Feedback>
)}
</Form.Group>
);
break;
}
case 'checkbox': {
formField = (
<Form.Group isInvalid={!!(isRequired && errorMessage)}>
<Form.Checkbox
className={className}
id={fieldData.name}
checked={!!value}
name={fieldData.name}
value={value}
aria-invalid={isRequired && Boolean(errorMessage)}
onChange={(e) => onChangeHandler(e)}
onBlur={handleOnBlur}
onFocus={handleFocus}
>
{fieldData.label}
</Form.Checkbox>
{isRequired && errorMessage && (
<Form.Control.Feedback id={`${fieldData.name}-error`} type="invalid" className="form-text-size" hasIcon={false}>
{errorMessage}
</Form.Control.Feedback>
)}
</Form.Group>
);
break;
}
default:
break;
}
return formField;
};
FormFieldRenderer.defaultProps = {
className: '',
value: '',
handleBlur: null,
handleFocus: null,
errorMessage: '',
isRequired: false,
};
FormFieldRenderer.propTypes = {
className: PropTypes.string,
fieldData: PropTypes.shape({
type: PropTypes.string,
label: PropTypes.string,
name: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)),
}).isRequired,
onChangeHandler: PropTypes.func.isRequired,
handleBlur: PropTypes.func,
handleFocus: PropTypes.func,
errorMessage: PropTypes.string,
isRequired: PropTypes.bool,
value: PropTypes.oneOfType([
PropTypes.string,
PropTypes.bool,
]),
};
export default FormFieldRenderer;

View File

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

View File

@@ -1,199 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { mount } from 'enzyme';
import FieldRenderer from '../FieldRenderer';
describe('FieldRendererTests', () => {
let value = '';
const changeHandler = (e) => {
if (e.target.type === 'checkbox') {
value = e.target.checked;
} else {
value = e.target.value;
}
};
beforeEach(() => {
value = '';
});
it('should render select field type', () => {
const fieldData = {
type: 'select',
label: 'Year of Birth',
name: 'yob-field',
options: [['1997', 1997], ['1998', 1998]],
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('select#yob-field');
field.simulate('change', { target: { value: 1997 } });
expect(field.type()).toEqual('select');
expect(fieldRenderer.find('label').text()).toEqual('Year of Birth');
expect(value).toEqual(1997);
});
it('should return null if no options are provided for select field', () => {
const fieldData = {
type: 'select',
label: 'Year of Birth',
name: 'yob-field',
};
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
expect(fieldRenderer.html()).toBeNull();
});
it('should render textarea field', () => {
const fieldData = {
type: 'textarea',
label: 'Why do you want to join this platform?',
name: 'goals-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('#goals-field').last();
field.simulate('change', { target: { value: 'These are my goals.' } });
expect(field.type()).toEqual('textarea');
expect(fieldRenderer.find('label').text()).toEqual('Why do you want to join this platform?');
expect(value).toEqual('These are my goals.');
});
it('should render an input field', () => {
const fieldData = {
type: 'text',
label: 'Company',
name: 'company-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('#company-field').last();
field.simulate('change', { target: { value: 'ABC' } });
expect(field.type()).toEqual('input');
expect(fieldRenderer.find('label').text()).toEqual('Company');
expect(value).toEqual('ABC');
});
it('should render checkbox field', () => {
const fieldData = {
type: 'checkbox',
label: `I agree that ${getConfig().SITE_NAME} may send me marketing messages.`,
name: 'marketing-emails-opt-in-field',
};
const fieldRenderer = mount(<FieldRenderer value={value} fieldData={fieldData} onChangeHandler={changeHandler} />);
const field = fieldRenderer.find('input#marketing-emails-opt-in-field');
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
expect(field.prop('type')).toEqual('checkbox');
expect(fieldRenderer.find('label').text()).toEqual(fieldData.label);
expect(value).toEqual(true);
});
it('should return null if field type is unknown', () => {
const fieldData = {
type: 'unknown',
};
const fieldRenderer = mount(<FieldRenderer fieldData={fieldData} onChangeHandler={() => {}} />);
expect(fieldRenderer.html()).toBeNull();
});
it('should run onBlur and onFocus functions for a field if given', () => {
const fieldData = { type: 'text', label: 'Test', name: 'test-field' };
let functionValue = '';
const onBlur = (e) => {
functionValue = `${e.target.name} blurred`;
};
const onFocus = (e) => {
functionValue = `${e.target.name} focussed`;
};
const fieldRenderer = mount(
<FieldRenderer
handleFocus={onFocus}
handleBlur={onBlur}
value={value}
fieldData={fieldData}
onChangeHandler={changeHandler}
/>,
);
const field = fieldRenderer.find('#test-field').last();
field.simulate('focus');
expect(functionValue).toEqual('test-field focussed');
field.simulate('blur');
expect(functionValue).toEqual('test-field blurred');
});
it('should render error message for required text fields', () => {
const fieldData = { type: 'text', label: 'First Name', name: 'first-name-field' };
const fieldRenderer = mount(
<FieldRenderer
isRequired
fieldData={fieldData}
onChangeHandler={changeHandler}
errorMessage="Enter your first name"
/>,
);
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Enter your first name');
});
it('should render error message for required select fields', () => {
const fieldData = {
type: 'select', label: 'Preference', name: 'preference-field', options: [['a', 'Opt 1'], ['b', 'Opt 2']],
};
const fieldRenderer = mount(
<FieldRenderer
isRequired
fieldData={fieldData}
onChangeHandler={changeHandler}
errorMessage="Select your preference"
/>,
);
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Select your preference');
});
it('should render error message for required textarea fields', () => {
const fieldData = { type: 'textarea', label: 'Goals', name: 'goals-field' };
const fieldRenderer = mount(
<FieldRenderer
isRequired
fieldData={fieldData}
onChangeHandler={changeHandler}
errorMessage="Tell us your goals"
/>,
);
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('Tell us your goals');
});
it('should render error message for required checkbox fields', () => {
const fieldData = { type: 'checkbox', label: 'Honor Code', name: 'honor-code-field' };
const fieldRenderer = mount(
<FieldRenderer
isRequired
fieldData={fieldData}
onChangeHandler={changeHandler}
errorMessage="You must agree to our Honor Code"
/>,
);
expect(fieldRenderer.find('.form-text-size').last().text()).toEqual('You must agree to our Honor Code');
});
});

View File

@@ -1,31 +1,29 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import { CheckCircle, Error } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
import {
COMPLETE_STATE, FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR,
} from '../data/constants';
import { PASSWORD_RESET } from '../reset-password/data/constants';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';
import { CheckCircle, Error } from '@edx/paragon/icons';
import messages from './messages';
import { INTERNAL_SERVER_ERROR } from '../data/constants';
import { PASSWORD_RESET } from '../reset-password/data/constants';
const ForgotPasswordAlert = (props) => {
const { formatMessage } = useIntl();
const { email, emailError } = props;
let message = '';
let heading = formatMessage(messages['forgot.password.error.alert.title']);
const { email, emailError, intl } = props;
let { status } = props;
if (emailError) {
status = FORM_SUBMISSION_ERROR;
status = 'form-submission-error';
}
let message = '';
let heading = intl.formatMessage(messages['forgot.password.error.alert.title']);
const supportUrl = getConfig().PASSWORD_RESET_SUPPORT_LINK;
switch (status) {
case COMPLETE_STATE:
heading = formatMessage(messages['confirmation.message.title']);
case 'complete':
heading = intl.formatMessage(messages['confirmation.message.title']);
message = (
<FormattedMessage
id="forgot.password.confirmation.message"
@@ -36,8 +34,15 @@ const ForgotPasswordAlert = (props) => {
values={{
email: <span className="data-hj-suppress">{email}</span>,
supportLink: (
<Alert.Link href={getConfig().PASSWORD_RESET_SUPPORT_LINK} target="_blank">
{formatMessage(messages['confirmation.support.link'])}
<Alert.Link
className="alert-link"
href={supportUrl}
onClick={e => {
e.preventDefault();
window.open(supportUrl, '_blank');
}}
>
{intl.formatMessage(messages['confirmation.support.link'])}
</Alert.Link>
),
}}
@@ -45,26 +50,26 @@ const ForgotPasswordAlert = (props) => {
);
break;
case INTERNAL_SERVER_ERROR:
message = formatMessage(messages['internal.server.error']);
message = intl.formatMessage(messages['internal.server.error']);
break;
case FORBIDDEN_STATE:
heading = formatMessage(messages['forgot.password.error.message.title']);
message = formatMessage(messages['forgot.password.request.in.progress.message']);
case 'forbidden':
heading = intl.formatMessage(messages['forgot.password.error.message.title']);
message = intl.formatMessage(messages['forgot.password.request.in.progress.message']);
break;
case FORM_SUBMISSION_ERROR:
message = formatMessage(messages['extend.field.errors'], { emailError });
case 'form-submission-error':
message = intl.formatMessage(messages['extend.field.errors'], { emailError });
break;
case PASSWORD_RESET.INVALID_TOKEN:
heading = formatMessage(messages['invalid.token.heading']);
message = formatMessage(messages['invalid.token.error.message']);
heading = intl.formatMessage(messages['invalid.token.heading']);
message = intl.formatMessage(messages['invalid.token.error.message']);
break;
case PASSWORD_RESET.FORBIDDEN_REQUEST:
heading = formatMessage(messages['token.validation.rate.limit.error.heading']);
message = formatMessage(messages['token.validation.rate.limit.error']);
heading = intl.formatMessage(messages['token.validation.rate.limit.error.heading']);
message = intl.formatMessage(messages['token.validation.rate.limit.error']);
break;
case PASSWORD_RESET.INTERNAL_SERVER_ERROR:
heading = formatMessage(messages['token.validation.internal.sever.error.heading']);
message = formatMessage(messages['token.validation.internal.sever.error']);
heading = intl.formatMessage(messages['token.validation.internal.sever.error.heading']);
message = intl.formatMessage(messages['token.validation.internal.sever.error']);
break;
default:
break;
@@ -74,7 +79,7 @@ const ForgotPasswordAlert = (props) => {
return (
<Alert
id="validation-errors"
className="mb-4"
className="mb-5"
variant={`${status === 'complete' ? 'success' : 'danger'}`}
icon={status === 'complete' ? CheckCircle : Error}
>
@@ -94,7 +99,8 @@ ForgotPasswordAlert.defaultProps = {
ForgotPasswordAlert.propTypes = {
status: PropTypes.string.isRequired,
email: PropTypes.string,
intl: intlShape.isRequired,
emailError: PropTypes.string,
};
export default ForgotPasswordAlert;
export default injectIntl(ForgotPasswordAlert);

View File

@@ -1,166 +1,149 @@
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import React, { useState, useEffect } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
Form,
Hyperlink,
Icon,
StatefulButton,
Tab,
Tabs,
} from '@edx/paragon';
import { ChevronLeft } from '@edx/paragon/icons';
import { Formik } from 'formik';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Helmet } from 'react-helmet';
import { Redirect } from 'react-router-dom';
import { BaseComponent } from '../base-component';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Form,
StatefulButton,
Hyperlink,
Tabs,
Tab,
Icon,
} from '@edx/paragon';
import { ChevronLeft } from '@edx/paragon/icons';
import { forgotPassword } from './data/actions';
import { forgotPasswordResultSelector } from './data/selectors';
import messages from './messages';
import { FormGroup } from '../common-components';
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
import { forgotPassword, setForgotPasswordFormData } from './data/actions';
import { forgotPasswordResultSelector } from './data/selectors';
import ForgotPasswordAlert from './ForgotPasswordAlert';
import messages from './messages';
import BaseComponent from '../base-component';
const ForgotPasswordPage = (props) => {
const platformName = getConfig().SITE_NAME;
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
const {
status, submitState, emailValidationError,
} = props;
const { intl, status, submitState } = props;
const { formatMessage } = useIntl();
const [email, setEmail] = useState(props.email);
const [bannerEmail, setBannerEmail] = useState('');
const [formErrors, setFormErrors] = useState('');
const [validationError, setValidationError] = useState(emailValidationError);
const platformName = getConfig().SITE_NAME;
const regex = new RegExp(VALID_EMAIL_REGEX, 'i');
const [validationError, setValidationError] = useState('');
const [key, setKey] = useState('');
const supportUrl = getConfig().LOGIN_ISSUE_SUPPORT_LINK;
useEffect(() => {
sendPageEvent('login_and_registration', 'reset');
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
}, []);
useEffect(() => {
setValidationError(emailValidationError);
}, [emailValidationError]);
useEffect(() => {
if (status === 'complete') {
setEmail('');
}
}, [status]);
const getValidationMessage = (value) => {
const getValidationMessage = (email) => {
let error = '';
if (value === '') {
error = formatMessage(messages['forgot.password.empty.email.field.error']);
} else if (!emailRegex.test(value)) {
error = formatMessage(messages['forgot.password.page.invalid.email.message']);
if (email === '') {
error = intl.formatMessage(messages['forgot.password.empty.email.field.error']);
} else if (!regex.test(email)) {
error = intl.formatMessage(messages['forgot.password.page.invalid.email.message']);
}
setValidationError(error);
return error;
};
const handleBlur = () => {
props.setForgotPasswordFormData({ email, emailValidationError: getValidationMessage(email) });
};
const handleFocus = () => props.setForgotPasswordFormData({ emailValidationError: '' });
const handleSubmit = (e) => {
e.preventDefault();
setBannerEmail(email);
const error = getValidationMessage(email);
if (error) {
setFormErrors(error);
props.setForgotPasswordFormData({ email, emailValidationError: error });
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
} else {
props.forgotPassword(email);
}
};
const tabTitle = (
<div className="d-inline-flex flex-wrap align-items-center">
<Icon src={ChevronLeft} />
<span className="ml-2">{formatMessage(messages['sign.in.text'])}</span>
<div className="d-flex">
<Icon src={ChevronLeft} className="arrow-back-icon" />
<span className="ml-2">{intl.formatMessage(messages['sign.in.text'])}</span>
</div>
);
return (
<BaseComponent>
<Helmet>
<title>{formatMessage(messages['forgot.password.page.title'],
{ siteName: getConfig().SITE_NAME })}
</title>
</Helmet>
<div>
<Tabs activeKey="" id="controlled-tab" onSelect={(k) => setKey(k)}>
<Tabs activeKey="" id="controlled-tab-example" onSelect={(k) => setKey(k)}>
<Tab title={tabTitle} eventKey={LOGIN_PAGE} />
</Tabs>
{ key && (
<Redirect to={updatePathWithQueryParams(key)} />
)}
<div id="main-content" className="main-content">
<Form id="forget-password-form" name="forget-password-form" className="mw-xs">
<ForgotPasswordAlert email={bannerEmail} emailError={formErrors} status={status} />
<h2 className="h4">
{formatMessage(messages['forgot.password.page.heading'])}
</h2>
<p className="mb-4">
{formatMessage(messages['forgot.password.page.instructions'])}
</p>
<FormGroup
floatingLabel={formatMessage(messages['forgot.password.page.email.field.label'])}
name="email"
value={email}
autoComplete="on"
errorMessage={validationError}
handleChange={(e) => setEmail(e.target.value)}
handleBlur={handleBlur}
handleFocus={handleFocus}
helpText={[formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
/>
<StatefulButton
id="submit-forget-password"
name="submit-forget-password"
type="submit"
variant="brand"
className="forgot-password-button-width"
state={submitState}
labels={{
default: formatMessage(messages['forgot.password.page.submit.button']),
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
{(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}
>
{formatMessage(messages['need.help.sign.in.text'])}
</Hyperlink>
<Formik
initialValues={{ email: '' }}
validateOnChange={false}
validate={(values) => {
const validationMessage = getValidationMessage(values.email);
if (validationMessage !== '') {
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
return { email: validationMessage };
}
return {};
}}
onSubmit={(values) => { props.forgotPassword(values.email); }}
>
{({
errors, handleSubmit, setFieldValue, values,
}) => (
<>
<Helmet>
<title>{intl.formatMessage(messages['forgot.password.page.title'],
{ siteName: getConfig().SITE_NAME })}
</title>
</Helmet>
<Form className="mw-xs">
<ForgotPasswordAlert email={props.email} emailError={errors.email} status={status} />
<h4>
{intl.formatMessage(messages['forgot.password.page.heading'])}
</h4>
<p className="mb-4">
{intl.formatMessage(messages['forgot.password.page.instructions'])}
</p>
<FormGroup
floatingLabel={intl.formatMessage(messages['forgot.password.page.email.field.label'])}
name="email"
errorMessage={validationError}
value={values.email}
handleBlur={() => getValidationMessage(values.email)}
handleChange={e => setFieldValue('email', e.target.value)}
handleFocus={() => setValidationError('')}
helpText={[intl.formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
/>
<StatefulButton
type="submit"
variant="brand"
className="login-button-width"
state={submitState}
labels={{
default: intl.formatMessage(messages['forgot.password.page.submit.button']),
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(e) => e.preventDefault()}
/>
<Hyperlink
id="forgot-password"
className="ml-4 font-weight-500 text-body"
destination={supportUrl}
onClick={e => {
e.preventDefault();
window.open(supportUrl, '_blank');
}}
>{intl.formatMessage(messages['need.help.sign.in.text'])}
</Hyperlink>
<p className="mt-5 one-rem-font">{intl.formatMessage(messages['additional.help.text'])}
<span><Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink></span>
</p>
</Form>
</>
)}
<p className="mt-5.5 small text-gray-700">
{formatMessage(messages['additional.help.text'], { platformName })}
<span>
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
</span>
</p>
</Form>
</Formik>
</div>
</div>
</BaseComponent>
@@ -168,17 +151,15 @@ const ForgotPasswordPage = (props) => {
};
ForgotPasswordPage.propTypes = {
intl: intlShape.isRequired,
email: PropTypes.string,
emailValidationError: PropTypes.string,
forgotPassword: PropTypes.func.isRequired,
setForgotPasswordFormData: PropTypes.func.isRequired,
status: PropTypes.string,
submitState: PropTypes.string,
};
ForgotPasswordPage.defaultProps = {
email: '',
emailValidationError: '',
status: null,
submitState: DEFAULT_STATE,
};
@@ -187,6 +168,5 @@ export default connect(
forgotPasswordResultSelector,
{
forgotPassword,
setForgotPasswordFormData,
},
)(ForgotPasswordPage);
)(injectIntl(ForgotPasswordPage));

View File

@@ -1,7 +1,6 @@
import { AsyncActionType } from '../../data/utils';
export const FORGOT_PASSWORD = new AsyncActionType('FORGOT', 'PASSWORD');
export const FORGOT_PASSWORD_PERSIST_FORM_DATA = 'FORGOT_PASSWORD_PERSIST_FORM_DATA';
// Forgot Password
export const forgotPassword = email => ({
@@ -25,8 +24,3 @@ export const forgotPasswordForbidden = () => ({
export const forgotPasswordServerError = () => ({
type: FORGOT_PASSWORD.FAILURE,
});
export const setForgotPasswordFormData = (forgotPasswordFormData) => ({
type: FORGOT_PASSWORD_PERSIST_FORM_DATA,
payload: { forgotPasswordFormData },
});

View File

@@ -1,12 +1,11 @@
import { FORGOT_PASSWORD } from './actions';
import { INTERNAL_SERVER_ERROR, PENDING_STATE } from '../../data/constants';
import { PASSWORD_RESET_FAILURE } from '../../reset-password/data/actions';
import { FORGOT_PASSWORD, FORGOT_PASSWORD_PERSIST_FORM_DATA } from './actions';
export const defaultState = {
status: '',
submitState: '',
email: '',
emailValidationError: '',
};
const reducer = (state = defaultState, action = null) => {
@@ -14,42 +13,28 @@ const reducer = (state = defaultState, action = null) => {
switch (action.type) {
case FORGOT_PASSWORD.BEGIN:
return {
email: state.email,
status: 'pending',
submitState: PENDING_STATE,
};
case FORGOT_PASSWORD.SUCCESS:
return {
...defaultState,
...action.payload,
status: 'complete',
};
case FORGOT_PASSWORD.FORBIDDEN:
return {
email: state.email,
status: 'forbidden',
};
case FORGOT_PASSWORD.FAILURE:
return {
email: state.email,
status: INTERNAL_SERVER_ERROR,
};
case PASSWORD_RESET_FAILURE:
return {
status: action.payload.errorCode,
};
case FORGOT_PASSWORD_PERSIST_FORM_DATA: {
const { forgotPasswordFormData } = action.payload;
return {
...state,
...forgotPasswordFormData,
};
}
default:
return {
...defaultState,
email: state.email,
emailValidationError: state.emailValidationError,
};
return defaultState;
}
}
return state;

View File

@@ -5,10 +5,11 @@ import { call, put, takeEvery } from 'redux-saga/effects';
import {
FORGOT_PASSWORD,
forgotPasswordBegin,
forgotPasswordSuccess,
forgotPasswordForbidden,
forgotPasswordServerError,
forgotPasswordSuccess,
} from './actions';
import { forgotPassword } from './service';
// Services

View File

@@ -1,34 +0,0 @@
import {
FORGOT_PASSWORD_PERSIST_FORM_DATA,
} from '../actions';
import reducer from '../reducers';
describe('forgot password reducer', () => {
it('should set email and emailValidationError', () => {
const state = {
status: '',
submitState: '',
email: '',
emailValidationError: '',
};
const forgotPasswordFormData = {
email: 'test@gmail',
emailValidationError: 'Enter a valid email address',
};
const action = {
type: FORGOT_PASSWORD_PERSIST_FORM_DATA,
payload: { forgotPasswordFormData },
};
expect(
reducer(state, action),
).toEqual(
{
status: '',
submitState: '',
email: 'test@gmail',
emailValidationError: 'Enter a valid email address',
},
);
});
});

View File

@@ -1,16 +1,16 @@
import { runSaga } from 'redux-saga';
import initializeMockLogging from '../../../setupTest';
import * as actions from '../actions';
import { handleForgotPassword } from '../sagas';
import * as api from '../service';
import initializeMockLogging from '../../../setupTest';
const { loggingService } = initializeMockLogging();
describe('handleForgotPassword', () => {
const params = {
payload: {
forgotPasswordFormData: {
formData: {
email: 'test@test.com',
},
},

View File

@@ -1,4 +1,4 @@
export { default as ForgotPasswordPage } from './ForgotPasswordPage';
export { default } from './ForgotPasswordPage';
export { default as reducer } from './data/reducers';
export { FORGOT_PASSWORD } from './data/actions';
export { default as saga } from './data/sagas';

View File

@@ -51,6 +51,16 @@ const messages = defineMessages({
defaultMessage: 'Enter your email',
description: 'Error message that appears when user tries to submit empty email field',
},
'forgot.password.invalid.email.heading': {
id: 'forgot.password.invalid.email',
defaultMessage: 'An error occurred.',
description: 'heading for invalid email',
},
'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}',
@@ -74,7 +84,7 @@ const messages = defineMessages({
},
'additional.help.text': {
id: 'additional.help.text',
defaultMessage: 'For additional help, contact {platformName} support at ',
defaultMessage: 'For additional help, contact edX support at ',
description: 'additional help text on forgot password page',
},
'sign.in.text': {
@@ -124,5 +134,10 @@ 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;

View File

@@ -1,21 +1,21 @@
import React from 'react';
import { Provider } from 'react-redux';
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
import { mergeConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics';
import * as auth from '@edx/frontend-platform/auth';
import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mount } from 'enzyme';
import { createMemoryHistory } from 'history';
import { act } from 'react-dom/test-utils';
import { Provider } from 'react-redux';
import { MemoryRouter, Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import { createMemoryHistory } from 'history';
import * as analytics from '@edx/frontend-platform/analytics';
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
import { mergeConfig } from '@edx/frontend-platform';
import { IntlProvider, injectIntl, configure } from '@edx/frontend-platform/i18n';
import * as auth from '@edx/frontend-platform/auth';
import ForgotPasswordPage from '../ForgotPasswordPage';
import { INTERNAL_SERVER_ERROR, LOGIN_PAGE } from '../../data/constants';
import { PASSWORD_RESET } from '../../reset-password/data/constants';
import { setForgotPasswordFormData } from '../data/actions';
import ForgotPasswordPage from '../ForgotPasswordPage';
jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-platform/auth');
@@ -51,7 +51,7 @@ describe('ForgotPasswordPage', () => {
beforeEach(() => {
store = mockStore(initialState);
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'test-user' }));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'edX' }));
configure({
loggingService: { logError: jest.fn() },
config: {
@@ -66,15 +66,7 @@ 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?');
});
@@ -133,61 +125,28 @@ describe('ForgotPasswordPage', () => {
expect(forgotPasswordPage.find('#email-invalid-feedback').exists()).toEqual(false);
});
it('should set error in redux store on onBlur', () => {
const forgotPasswordFormData = {
email: 'test@gmail',
emailValidationError: 'Enter a valid email address',
};
props = {
...props,
email: 'test@gmail',
emailValidationError: '',
};
store.dispatch = jest.fn(store.dispatch);
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
forgotPasswordPage.find('input#email').simulate('blur');
expect(store.dispatch).toHaveBeenCalledWith(setForgotPasswordFormData(forgotPasswordFormData));
});
it('should display error message if available in props', async () => {
it('should display error message on blur event', async () => {
const validationMessage = 'Enter your email';
props = {
...props,
emailValidationError: validationMessage,
email: '',
};
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
const emailInput = forgotPasswordPage.find('input#email');
await act(async () => {
await emailInput.simulate('blur', { target: { value: '', name: 'email' } });
});
forgotPasswordPage.update();
expect(forgotPasswordPage.find('.pgn__form-text-invalid').text()).toEqual(validationMessage);
});
it('should clear error in redux store on onFocus', () => {
const forgotPasswordFormData = {
emailValidationError: '',
};
props = {
...props,
email: 'test@gmail',
emailValidationError: 'Enter a valid email address',
};
store.dispatch = jest.fn(store.dispatch);
it('should clear error message on focus event', async () => {
const validationMessage = 'Enter your email';
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
forgotPasswordPage.find('input#email').simulate('focus');
expect(store.dispatch).toHaveBeenCalledWith(setForgotPasswordFormData(forgotPasswordFormData));
});
await act(async () => { await forgotPasswordPage.find('button.btn-brand').simulate('click'); });
it('should clear error message when cleared in props on focus', async () => {
props = {
...props,
emailValidationError: '',
email: '',
};
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
forgotPasswordPage.update();
expect(forgotPasswordPage.find('.pgn__form-text-invalid').text()).toEqual(validationMessage);
forgotPasswordPage.find('input#email').simulate('focus');
expect(forgotPasswordPage.find('#email-invalid-feedback').exists()).toEqual(false);
});

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