Compare commits

..

33 Commits

Author SHA1 Message Date
Attiya Ishaque
e29a461a34 fix: change for testing sandbox purpose 2024-03-26 19:00:40 +05:00
Attiya Ishaque
9276f25200 feat: remove username from the registration from (#1201) 2024-03-20 15:21:25 +05:00
Dmytro
dc90cf9ce5 fix: Registration with password that doesn't meet the requirements (#1184)
* fix: Registration with password that doesn't meet the requirements
---------

Co-authored-by: Dima Alipov <dimaalipov@MacBook-Pro-Dima.local>
Co-authored-by: Syed Sajjad  Hussain Shah <ssajjad@2u.com>
2024-03-15 11:53:42 +05:00
Attiya Ishaque
36354761cc feat: Remove simplify registration experiment code (#1194) 2024-03-15 10:58:54 +05:00
renovate[bot]
f53add81f3 fix(deps): update react-router monorepo to v6.22.3 2024-03-08 12:18:55 +00:00
renovate[bot]
bca59ebd40 fix(deps): update dependency algoliasearch-helper to v3.16.3 2024-03-08 10:45:07 +00:00
renovate[bot]
dcb5da42b0 fix(deps): update dependency @edx/frontend-platform to v7.1.1 2024-03-08 06:51:41 +00:00
renovate[bot]
b346c22b57 chore(deps): update codecov/codecov-action action to v4 (#1172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-07 19:28:01 -08:00
Zainab Amir
8be350e35f Fix TPA skeleton loader (#1189)
* feat: update TPA skeleton
* fix: Prevent wrong appearance of skeleton after second tab click

---------

Co-authored-by: Stanislav Lunyachek <lunyachek@MacBook-Pro-M1.local>
2024-03-06 06:11:54 -08:00
Syed Sajjad Hussain Shah
6695fb6f61 fix: move country field to simplify registration second step (#1195) 2024-03-06 12:21:56 +05:00
Syed Sajjad Hussain Shah
be5b0bb461 fix: invalid form submit btn event error (#1191) 2024-03-05 14:57:10 +05:00
Syed Sajjad Hussain Shah
5f93278326 feat: add invalid form submit click event: (#1190) 2024-03-05 13:13:30 +05:00
Blue
488644f50d fix: remove rebrand experiment from authn (#1187)
Description:
Remove rebrand experiment code from Authn
VAN-1858
2024-03-04 16:14:41 +05:00
Hina Khadim
56bab26018 fix: browser header showing null and replace authn with Authentication (#1185) 2024-03-04 00:53:26 -08:00
Attiya Ishaque
ca42f3851d fix: Fix post registration recommendations card issue (#1183) 2024-03-01 15:25:56 +05:00
Syed Sajjad Hussain Shah
e4bddc2db0 fix: remove password field duplicate validation (#1181) 2024-02-29 17:47:13 +05:00
Syed Sajjad Hussain Shah
8aeacaa001 fix: fix error banner alert (#1180) 2024-02-29 12:14:52 +05:00
Syed Sajjad Hussain Shah
80435d3e5b fix: change submit cta text for experiment (#1179) 2024-02-29 11:25:43 +05:00
Syed Sajjad Hussain Shah
d6c5415c9a feat: add submit btn click event (#1178) 2024-02-28 11:38:07 +05:00
Zainab Amir
0306763eeb feat: update code owner information (#1177) 2024-02-27 06:40:48 -08:00
Zainab Amir
e4ac1288a9 feat: add submit btn click event for default register page (#1176)
Co-authored-by: Syed Sajjad  Hussain Shah <ssajjad@2u.com>
2024-02-27 01:24:52 -08:00
Attiya Ishaque
e1f489838c fix: browser header showing null (#1168) 2024-02-27 12:52:25 +05:00
Syed Sajjad Hussain Shah
9b046146a0 fix: fix simplify registration experiment bugs (#1175) 2024-02-27 10:12:16 +05:00
Syed Sajjad Hussain Shah
e0d605582e fix: fix simplify registration experiment bugs (#1174) 2024-02-26 18:54:16 +05:00
Blue
21b5a01cab fix: Update custom SSO buttons to use brand colors (#1165)
Description: Update custom SSO buttons to use brand colors
VAN-1838
2024-02-26 18:51:12 +05:00
Syed Sajjad Hussain Shah
955ea6485f fix: update comment (#1173) 2024-02-26 17:50:19 +05:00
Syed Sajjad Hussain Shah
9f8a1af7e3 feat: simplify registration experiment (#1164) 2024-02-26 17:22:24 +05:00
renovate[bot]
e617a3ba40 fix(deps): update react-router monorepo to v6.22.1 2024-02-26 12:16:52 +00:00
renovate[bot]
fb3f962039 fix(deps): update dependency react-loading-skeleton to v3.4.0 2024-02-26 11:25:17 +00:00
renovate[bot]
64da54f17c fix(deps): update dependency core-js to v3.36.0 2024-02-26 11:15:39 +00:00
renovate[bot]
74741a1be6 fix(deps): update dependency @edx/frontend-platform to v7.1.0 2024-02-26 10:46:33 +00:00
Stanislav
e9aaf7024a fix: Enabling the ability to log in with a username consisting of 2 characters (#1073)
Co-authored-by: Stanislav Lunyachek <lunyachek@MacBook-Pro-M1.local>
2024-02-15 15:07:24 +05:00
Blue
e3d96385ee fix: remove username and bring in name field for embedded case (#1163)
Description:
Remove username and bring in name field in case embedded registration
VAN-1824
2024-02-14 16:39:31 +05:00
52 changed files with 453 additions and 324 deletions

1
.env
View File

@@ -23,6 +23,7 @@ POST_REGISTRATION_REDIRECT_URL=''
SEARCH_CATALOG_URL=''
# ***** Features flags *****
DISABLE_ENTERPRISE_LOGIN=''
ENABLE_AUTO_GENERATED_USERNAME=''
ENABLE_DYNAMIC_REGISTRATION_FIELDS=''
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN=''
ENABLE_POST_REGISTRATION_RECOMMENDATIONS=''

View File

@@ -25,5 +25,5 @@ Include a link to the sandbox for design changes or screenshot for before and af
#### Post-merge Checklist
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/vanguards** to do it.
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-vanguards** to do it.
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.

View File

@@ -41,4 +41,4 @@ jobs:
run: npm run build
- name: Run Code Coverage
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4

View File

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

View File

@@ -187,7 +187,7 @@ All community members are expected to follow the `Open edX Code of Conduct <http
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``
found in `Backstage <https://backstage.openedx.org/catalog/default/group/2u-vanguards>`_. Backstage pulls this data from the ``catalog-info.yaml``
file in this repo.
Reporting Security Issues

View File

@@ -13,6 +13,6 @@ metadata:
annotations:
openedx.org/arch-interest-groups: ""
spec:
owner: group:vanguards
owner: group:2u-vanguards
type: 'service'
lifecycle: 'production'

View File

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

66
package-lock.json generated
View File

@@ -10,7 +10,7 @@
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-platform": "7.0.1",
"@edx/frontend-platform": "7.1.1",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-brands-svg-icons": "6.5.1",
@@ -24,7 +24,7 @@
"algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.14.0",
"classnames": "2.5.1",
"core-js": "3.35.1",
"core-js": "3.36.0",
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.4",
"prop-types": "15.8.1",
@@ -32,11 +32,11 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.3.1",
"react-loading-skeleton": "3.4.0",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router": "6.21.3",
"react-router-dom": "6.21.3",
"react-router": "6.22.3",
"react-router-dom": "6.22.3",
"react-zendesk": "^0.1.13",
"redux": "4.2.0",
"redux-logger": "3.0.6",
@@ -2159,9 +2159,9 @@
}
},
"node_modules/@cospired/i18n-iso-languages": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@cospired/i18n-iso-languages/-/i18n-iso-languages-4.1.0.tgz",
"integrity": "sha512-5+JK7YiO9r/FmwtlEPL1tQNt04/9AuN1t9GO/0C2yitqhKwFRa1r7VohNNUnFgB84MW5v4Lwq8ZAUZexuJh1nQ==",
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@cospired/i18n-iso-languages/-/i18n-iso-languages-4.2.0.tgz",
"integrity": "sha512-vy8cq1176MTxVwB1X9niQjcIYOH29F8Huxtx8hLmT5Uz3l1ztGDGri8KN/4zE7LV2mCT7JrcAoNV/I9yb+lNUw==",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
@@ -2283,11 +2283,11 @@
}
},
"node_modules/@edx/frontend-platform": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-7.0.1.tgz",
"integrity": "sha512-q5/S8V+hsX6ENYtTgMw0IfhMNshlBZ6pPfMft/ciVgCuTgW1XNZtgL98HhAEcBwmPshWgTRWVEQ83dniFrxwew==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-7.1.1.tgz",
"integrity": "sha512-sr4ExW/ZxyTLp0ielhJnH9fzDIj7Bg7uuYL96ebYamMIHv4WeFQ92gad023vlaoVg60JdMp9XVoDhsf7dHFPLA==",
"dependencies": {
"@cospired/i18n-iso-languages": "4.1.0",
"@cospired/i18n-iso-languages": "4.2.0",
"@formatjs/intl-pluralrules": "4.3.3",
"@formatjs/intl-relativetimeformat": "10.0.1",
"axios": "0.27.2",
@@ -5505,9 +5505,9 @@
"integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA=="
},
"node_modules/@remix-run/router": {
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.14.2.tgz",
"integrity": "sha512-ACXpdMM9hmKZww21yEqWwiLws/UPLhNKvimN8RrYSqPSvB3ov7sLvAcfvaxePeLvccTQKGdkDIhLYApZVDFuKg==",
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.15.3.tgz",
"integrity": "sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==",
"engines": {
"node": ">=14.0.0"
}
@@ -6647,9 +6647,9 @@
}
},
"node_modules/algoliasearch-helper": {
"version": "3.16.2",
"resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.16.2.tgz",
"integrity": "sha512-Yl/Gu5Cq4Z5s/AJ0jR37OPI1H3+z7PHz657ibyaXgMOaWvPlZ3OACN13N+7HCLPUlB0BN+8BtmrG/CqTilowBA==",
"version": "3.16.3",
"resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.16.3.tgz",
"integrity": "sha512-1OuJT6sONAa9PxcOmWo5WCAT3jQSpCR9/m5Azujja7nhUQwAUDvaaAYrcmUySsrvHh74usZHbE3jFfGnWtZj8w==",
"dependencies": {
"@algolia/events": "^4.0.1"
},
@@ -8257,9 +8257,9 @@
}
},
"node_modules/core-js": {
"version": "3.35.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.35.1.tgz",
"integrity": "sha512-IgdsbxNyMskrTFxa9lWHyMwAJU5gXOPP+1yO+K59d50VLVAIDAbs7gIv705KzALModfK3ZrSZTPNpC0PQgIZuw==",
"version": "3.36.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.0.tgz",
"integrity": "sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw==",
"hasInstallScript": true,
"funding": {
"type": "opencollective",
@@ -18381,9 +18381,9 @@
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"node_modules/react-loading-skeleton": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.3.1.tgz",
"integrity": "sha512-NilqqwMh2v9omN7LteiDloEVpFyMIa0VGqF+ukqp0ncVlYu1sKYbYGX9JEl+GtOT9TKsh04zCHAbavnQ2USldA==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.4.0.tgz",
"integrity": "sha512-1oJEBc9+wn7BbkQQk7YodlYEIjgeR+GrRjD+QXkVjwZN7LGIcAFHrx4NhT7UHGBxNY1+zax3c+Fo6XQM4R7CgA==",
"peerDependencies": {
"react": ">=16.8.0"
}
@@ -18526,11 +18526,11 @@
}
},
"node_modules/react-router": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.21.3.tgz",
"integrity": "sha512-a0H638ZXULv1OdkmiK6s6itNhoy33ywxmUFT/xtSoVyf9VnC7n7+VT4LjVzdIHSaF5TIh9ylUgxMXksHTgGrKg==",
"version": "6.22.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.22.3.tgz",
"integrity": "sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==",
"dependencies": {
"@remix-run/router": "1.14.2"
"@remix-run/router": "1.15.3"
},
"engines": {
"node": ">=14.0.0"
@@ -18540,12 +18540,12 @@
}
},
"node_modules/react-router-dom": {
"version": "6.21.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.21.3.tgz",
"integrity": "sha512-kNzubk7n4YHSrErzjLK72j0B5i969GsuCGazRl3G6j1zqZBLjuSlYBdVdkDOgzGdPIffUOc9nmgiadTEVoq91g==",
"version": "6.22.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.22.3.tgz",
"integrity": "sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==",
"dependencies": {
"@remix-run/router": "1.14.2",
"react-router": "6.21.3"
"@remix-run/router": "1.15.3",
"react-router": "6.22.3"
},
"engines": {
"node": ">=14.0.0"

View File

@@ -33,7 +33,7 @@
},
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-platform": "7.0.1",
"@edx/frontend-platform": "7.1.1",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-brands-svg-icons": "6.5.1",
@@ -47,7 +47,7 @@
"algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.14.0",
"classnames": "2.5.1",
"core-js": "3.35.1",
"core-js": "3.36.0",
"fastest-levenshtein": "1.0.16",
"form-urlencoded": "6.1.4",
"prop-types": "15.8.1",
@@ -55,11 +55,11 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.3.1",
"react-loading-skeleton": "3.4.0",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router": "6.21.3",
"react-router-dom": "6.21.3",
"react-router": "6.22.3",
"react-router-dom": "6.22.3",
"react-zendesk": "^0.1.13",
"redux": "4.2.0",
"redux-logger": "3.0.6",

View File

@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en-us">
<head>
<title>Authn | <%= process.env.SITE_NAME %></title>
<title><%= (process.env.SITE_NAME && process.env.SITE_NAME != 'null') ? 'Authentication | ' + process.env.SITE_NAME : 'Authentication' %></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,4 +0,0 @@
const IMAGE_LAYOUT = 'image-layout';
const DEFAULT_LAYOUT = 'default-layout';
export { DEFAULT_LAYOUT, IMAGE_LAYOUT };

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { breakpoints } from '@openedx/paragon';
@@ -11,28 +11,11 @@ import {
ImageExtraSmallLayout, ImageLargeLayout, ImageMediumLayout, ImageSmallLayout,
} from './components/image-layout';
import { AuthLargeLayout, AuthMediumLayout, AuthSmallLayout } from './components/welcome-page-layout';
import { DEFAULT_LAYOUT, IMAGE_LAYOUT } from './data/constants';
const BaseContainer = ({ children, showWelcomeBanner, fullName }) => {
const [baseContainerVersion, setBaseContainerVersion] = useState(DEFAULT_LAYOUT);
const enableImageLayout = getConfig().ENABLE_IMAGE_LAYOUT;
useEffect(() => {
const initRebrandExperiment = () => {
if (window.experiments?.rebrandExperiment) {
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
} else {
window.experiments = window.experiments || {};
window.experiments.rebrandExperiment = {};
window.experiments.rebrandExperiment.handleLoaded = () => {
setBaseContainerVersion(window.experiments?.rebrandExperiment?.variation);
};
}
};
initRebrandExperiment();
}, []);
if (baseContainerVersion === IMAGE_LAYOUT || enableImageLayout) {
if (enableImageLayout) {
return (
<div className="layout">
<MediaQuery maxWidth={breakpoints.extraSmall.maxWidth - 1}>

View File

@@ -25,23 +25,6 @@ describe('Base component tests', () => {
expect(container.querySelector('.large-screen-svg-primary')).toBeDefined();
});
it('[experiment] should show image layout for treatment group', () => {
window.experiments = {
rebrandExperiment: {
variation: 'image-layout',
},
};
const { container } = render(
<IntlProvider locale="en">
<BaseContainer />
</IntlProvider>,
LargeScreen,
);
expect(container.querySelector('.banner__image')).toBeDefined();
});
it('renders Image layout when ENABLE_IMAGE_LAYOUT configuration is enabled', () => {
mergeConfig({
ENABLE_IMAGE_LAYOUT: true,

View File

@@ -6,6 +6,7 @@ import {
Hyperlink, Icon,
} from '@openedx/paragon';
import { Institution } from '@openedx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import Skeleton from 'react-loading-skeleton';
@@ -47,14 +48,23 @@ const ThirdPartyAuth = (props) => {
</div>
)}
{(isLoginPage && !isEnterpriseLoginDisabled && isSocialAuthActive) && (
<Hyperlink className="btn btn-link btn-sm text-body p-0 mb-4" destination={enterpriseLoginURL}>
<Hyperlink
className={classNames(
'btn btn-link btn-sm text-body p-0',
{ 'mb-0': thirdPartyAuthApiStatus === PENDING_STATE },
{ 'mb-4': thirdPartyAuthApiStatus !== PENDING_STATE },
)}
destination={enterpriseLoginURL}
>
<Icon src={Institution} className="institute-icon" />
{formatMessage(messages['enterprise.login.btn.text'])}
</Hyperlink>
)}
{thirdPartyAuthApiStatus === PENDING_STATE ? (
<Skeleton className="tpa-skeleton" height={36} count={2} />
<div className="mt-4">
<Skeleton className="tpa-skeleton" height={36} count={2} />
</div>
) : (
<>
{(isEnterpriseLoginDisabled && isInstitutionAuthActive) && (

View File

@@ -4,6 +4,7 @@ const configuration = {
USER_RETENTION_COOKIE_NAME: process.env.USER_RETENTION_COOKIE_NAME || '',
// Features
DISABLE_ENTERPRISE_LOGIN: process.env.DISABLE_ENTERPRISE_LOGIN || '',
ENABLE_AUTO_GENERATED_USERNAME: process.env.ENABLE_AUTO_GENERATED_USERNAME || true,
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN: process.env.ENABLE_PROGRESSIVE_PROFILING_ON_AUTHN || false,
ENABLE_POST_REGISTRATION_RECOMMENDATIONS: process.env.ENABLE_POST_REGISTRATION_RECOMMENDATIONS || false,

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "كلمة مرورك الحالية لا تستسجيب لمتطلبات الأمان الجديدة. لقد أرسلنا للتو رسالة لإعادة ضبط كلمة المرور إلى عنوان البريد الإلكتروني المرتبط بهذا الحساب. شكرًا لك على مساعدتنا في الحفاظ على سلامة بياناتك.",
"account.locked.out.message.1": "لحماية حسابك، تم إقفاله مؤقتًا. حاول مرة أخرى بعد 30 دقيقة.",
"enterprise.login.btn.text": "بيانات الشركة أو المدرسة",
"username.or.email.format.validation.less.chars.message": "يجب أن يحتوي اسم المستخدم أو البريد الإلكتروني على 3 أحرف على الأقل.",
"username.or.email.format.validation.less.chars.message": "يجب أن يحتوي اسم المستخدم أو البريد الإلكتروني على 2 أحرف على الأقل.",
"email.validation.message": "أدخل اسم المستخدم أو البريد الإلكتروني الخاص بك",
"password.validation.message": "لم يتم استيفاء معايير كلمة المرور",
"account.activation.success.message.title": "نجح الأمر! لقد قمت بتفعيل حسابك.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"enterprise.login.btn.text": "Company or school credentials",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"account.activation.success.message.title": "Success! You have activated your account.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "Ihr aktuelles Passwort entspricht nicht den neuen Sicherheitsanforderungen. Wir haben gerade eine Nachricht zum Zurücksetzen des Passworts an die mit diesem Konto verknüpfte E-Mail-Adresse gesendet. Vielen Dank, dass Sie uns helfen, Ihre Daten zu schützen.",
"account.locked.out.message.1": "Um Ihr Konto zu schützen, wurde es vorübergehend gesperrt. Versuchen Sie es in 30 Minuten erneut.",
"enterprise.login.btn.text": "Arbeits- oder Schulzeugnisse",
"username.or.email.format.validation.less.chars.message": "Benutzername oder E-Mail müssen mindestens 3 Zeichen lang sein.",
"username.or.email.format.validation.less.chars.message": "Benutzername oder E-Mail müssen mindestens 2 Zeichen lang sein.",
"email.validation.message": "Geben Sie Ihren Benutzernamen oder Ihre E-Mail-Adresse ein",
"password.validation.message": "Die Passwortkriterien wurden nicht erfüllt",
"account.activation.success.message.title": "Super! Sie haben Ihr Konto aktiviert.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "Tu contraseña actual no cumple con los nuevos requisitos de seguridad. Acabamos de enviar un mensaje de restablecimiento de contraseña a la dirección de correo electrónico asociada a esta cuenta. Gracias por ayudarnos a mantener tus datos seguros.",
"account.locked.out.message.1": "Para proteger tu cuenta, se ha bloqueado temporalmente. Inténtalo de nuevo en 30 minutos.",
"enterprise.login.btn.text": "Credenciales de la empresa o de la institución ",
"username.or.email.format.validation.less.chars.message": "El nombre de usuario o el correo electrónico deben tener al menos 3 caracteres.",
"username.or.email.format.validation.less.chars.message": "El nombre de usuario o el correo electrónico deben tener al menos 2 caracteres.",
"email.validation.message": "Introduce tu nombre de usuario o correo electrónico",
"password.validation.message": "No se han cumplido los criterios de la contraseña",
"account.activation.success.message.title": "Ha sido un éxito. Has activado tu cuenta.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "Al crear una cuenta, acepta {tosAndHonorCode} y reconoce que {platformName} y cada miembro procesan sus datos personales de acuerdo con {privacyPolicy}.",
"register.page.honor.code": "Acepto las {platformName} {tosAndHonorCode}",
"register.page.terms.of.service": "Acepto las {platformName} {termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "گذرواژه فعلی شما الزامات امنیتی جدید را برآورده نمی‌کند. ما فقط یک پیام بازتنظیم گذرواژه به نشانی رایانامه مرتبط با این حساب کاربری ارسال کردیم. از اینکه به ما کمک می‌کنید تا داده‌های شما را ایمن نگه دارید متشکریم.",
"account.locked.out.message.1": "حساب کاربری شما، به دلیل حفاظت، به‌طور موقت قفل شده است. 30 دقیقه دیگر دوباره امتحان کنید.",
"enterprise.login.btn.text": "اعتبار دانشکده یا شرکت",
"username.or.email.format.validation.less.chars.message": "نام کاربری یا نشانی رایانامه حداقل باید 3 نویسه داشته باشد",
"username.or.email.format.validation.less.chars.message": "نام کاربری یا نشانی رایانامه حداقل باید 2 نویسه داشته باشد",
"email.validation.message": "نام کاربری یا رایانامه خود را وارد کنید",
"password.validation.message": "معیارهای گذرواژه رعایت نشده است",
"account.activation.success.message.title": "موفق شدید! شما حساب کاربری خود را فعال کردید.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "Votre mot de passe actuel ne répond pas aux nouvelles exigences de sécurité. Nous venons d'envoyer un message de réinitialisation de mot de passe à l'adresse courriel associée à ce compte. Merci de nous aider à protéger vos données.",
"account.locked.out.message.1": "Pour protéger votre compte, il a été temporairement verrouillé. Réessayez dans 30 minutes.",
"enterprise.login.btn.text": "Identifiants de la compagnie ou de l'école",
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 3 caractères.",
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 2 caractères.",
"email.validation.message": "Entrez votre nom d'utilisateur ou votre adresse courriel",
"password.validation.message": "Les critères de mot de passe n'ont pas été remplis",
"account.activation.success.message.title": "Succès! Vous avez activé votre compte.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "Votre mot de passe actuel ne répond pas aux nouvelles exigences de sécurité. Nous venons d'envoyer un message de réinitialisation de mot de passe à l'adresse courriel associée à ce compte. Merci de nous aider à protéger vos données.",
"account.locked.out.message.1": "Pour protéger votre compte, il a été temporairement verrouillé. Réessayez dans 30 minutes.",
"enterprise.login.btn.text": "Informations d'identification de la compagnie ou de l'école",
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 3 caractères.",
"username.or.email.format.validation.less.chars.message": "Le nom d'utilisateur ou l'adresse courriel doit comporter au moins 2 caractères.",
"email.validation.message": "Entrez votre nom d'utilisateur ou votre adresse courriel",
"password.validation.message": "Les critères de mot de passe n'ont pas été remplis",
"account.activation.success.message.title": "Succès! Vous avez activé votre compte.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "En créant un compte, vous acceptez le {tosAndHonorCode} et vous reconnaissez que {platformName} et chaque membre traitent vos données personnelles conformément au {privacyPolicy}.",
"register.page.honor.code": "J'accepte le {tosAndHonorCode} {platformName}",
"register.page.terms.of.service": "J'accepte les {termsOfService} {platformName}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"enterprise.login.btn.text": "Company or school credentials",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"account.activation.success.message.title": "Success! You have activated your account.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"enterprise.login.btn.text": "Company or school credentials",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"account.activation.success.message.title": "Success! You have activated your account.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "La tua password attuale non soddisfa i nuovi requisiti di sicurezza. Abbiamo appena inviato un messaggio di reimpostazione della password all&#39;indirizzo e-mail associato a questo account. Grazie per averci aiutato a mantenere i tuoi dati al sicuro.",
"account.locked.out.message.1": "Per proteggere il tuo account, è stato temporaneamente bloccato. Riprova tra 30 minuti.",
"enterprise.login.btn.text": "Credenziali aziendali o scolastiche",
"username.or.email.format.validation.less.chars.message": "Il nome utente o l&#39;e-mail deve contenere almeno 3 caratteri.",
"username.or.email.format.validation.less.chars.message": "Il nome utente o l&#39;e-mail deve contenere almeno 2 caratteri.",
"email.validation.message": "Inserisci il tuo nome utente o e-mail",
"password.validation.message": "I criteri della password non sono stati soddisfatti",
"account.activation.success.message.title": "Completato correttamente! Hai attivato il tuo account. ",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"enterprise.login.btn.text": "Company or school credentials",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"account.activation.success.message.title": "Success! You have activated your account.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "A sua palavra-passe atual não satisfaz os novos requisitos de segurança. Acabámos de enviar uma mensagem de redefinição da palavra-passe para o endereço de email associado a esta conta. Obrigado por nos ajudar a manter os seus dados em segurança.",
"account.locked.out.message.1": "Para proteger sua conta, esta foi temporariamente bloqueada. Tente novamente dentro de 30 minutos.",
"enterprise.login.btn.text": "Credenciais da empresa ou escola",
"username.or.email.format.validation.less.chars.message": "O nome de utilizador ou email deve ter pelo menos 3 carateres.",
"username.or.email.format.validation.less.chars.message": "O nome de utilizador ou email deve ter pelo menos 2 carateres.",
"email.validation.message": "Insira o seu nome de utilizador ou email",
"password.validation.message": "Os critérios de palavra-passe não foram cumpridos",
"account.activation.success.message.title": "Sucesso! Você ativou a sua conta.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"enterprise.login.btn.text": "Company or school credentials",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"account.activation.success.message.title": "Success! You have activated your account.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "Your current password does not meet the new security requirements. We just sent a password-reset message to the email address associated with this account. Thank you for helping us keep your data safe.",
"account.locked.out.message.1": "To protect your account, it's been temporarily locked. Try again in 30 minutes.",
"enterprise.login.btn.text": "Company or school credentials",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 3 characters.",
"username.or.email.format.validation.less.chars.message": "Username or email must have at least 2 characters.",
"email.validation.message": "Enter your username or email",
"password.validation.message": "Password criteria has not been met",
"account.activation.success.message.title": "Success! You have activated your account.",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -64,7 +64,7 @@
"non.compliant.password.message": "您当前的密码不符合新的安全要求。我们刚刚向与此帐户关联的电子邮件地址发送了密码重置邮件。感谢您帮助我们保护您的数据安全。",
"account.locked.out.message.1": "为了保护您的帐户,它已被暂时锁定。请在 30 分钟后重试。",
"enterprise.login.btn.text": "单位或学校证书",
"username.or.email.format.validation.less.chars.message": "用户名或电子邮件必须至少包含 3 个字符。",
"username.or.email.format.validation.less.chars.message": "用户名或电子邮件必须至少包含 2 个字符。",
"email.validation.message": "输入您的用户名或电子邮件",
"password.validation.message": "未满足密码条件",
"account.activation.success.message.title": "成功!您已激活您的帐户。",
@@ -178,4 +178,4 @@
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName}&nbsp;{tosAndHonorCode}",
"register.page.terms.of.service": "I agree to the {platformName}&nbsp;{termsOfService}"
}
}

View File

@@ -128,7 +128,7 @@ const LoginPage = (props) => {
if (emailOrUsername === '') {
fieldErrors.emailOrUsername = formatMessage(messages['email.validation.message']);
} else if (emailOrUsername.length < 3) {
} else if (emailOrUsername.length < 2) {
fieldErrors.emailOrUsername = formatMessage(messages['username.or.email.format.validation.less.chars.message']);
}
if (password === '') {

View File

@@ -61,8 +61,8 @@ const messages = defineMessages({
},
'username.or.email.format.validation.less.chars.message': {
id: 'username.or.email.format.validation.less.chars.message',
defaultMessage: 'Username or email must have at least 3 characters.',
description: 'Validation message that appears when username or email address is less than 3 characters',
defaultMessage: 'Username or email must have at least 2 characters.',
description: 'Validation message that appears when username or email address is less than 2 characters',
},
'email.validation.message': {
id: 'email.validation.message',

View File

@@ -139,7 +139,7 @@ describe('LoginPage', () => {
// ******** test login form validations ********
it('should match state for invalid email (less than 3 characters), on form submission', () => {
it('should match state for invalid email (less than 2 characters), on form submission', () => {
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<IntlLoginPage {...props} />));
@@ -151,14 +151,14 @@ describe('LoginPage', () => {
fireEvent.change(screen.getByText(
'',
{ selector: '#emailOrUsername' },
), { target: { value: 'te' } });
), { target: { value: 't' } });
fireEvent.click(screen.getByText(
'',
{ selector: '.btn-brand' },
));
expect(screen.getByText('Username or email must have at least 3 characters.')).toBeDefined();
expect(screen.getByText('Username or email must have at least 2 characters.')).toBeDefined();
});
it('should show error messages for required fields on empty form submission', () => {
@@ -181,14 +181,14 @@ describe('LoginPage', () => {
fireEvent.change(screen.getByText(
'',
{ selector: '#emailOrUsername' },
), { target: { value: 'te', name: 'emailOrUsername' } });
), { target: { value: 't', name: 'emailOrUsername' } });
fireEvent.click(screen.getByText(
'',
{ selector: '.btn-brand' },
));
expect(container.querySelector('div[feedback-for="emailOrUsername"]').textContent).toEqual('Username or email must have at least 3 characters.');
expect(container.querySelector('div[feedback-for="emailOrUsername"]').textContent).toEqual('Username or email must have at least 2 characters.');
});
// ******** test field focus in functionality ********

View File

@@ -66,7 +66,10 @@ const Logistration = (props) => {
setInstitutionLogin(!institutionLogin);
};
const handleOnSelect = (tabKey) => {
const handleOnSelect = (tabKey, currentTab) => {
if (tabKey === currentTab) {
return;
}
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
props.clearThirdPartyAuthContextErrorMessage();
if (tabKey === LOGIN_PAGE) {
@@ -121,7 +124,7 @@ const Logistration = (props) => {
</Tabs>
)
: (!isValidTpaHint() && !hideRegistrationLink && (
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={handleOnSelect}>
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={(tabKey) => handleOnSelect(tabKey, selectedPage)}>
<Tab title={formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
<Tab title={formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
</Tabs>

View File

@@ -94,6 +94,14 @@ describe('Logistration', () => {
});
});
it('should do nothing when user clicks on the same tab (login/register) again', () => {
const { container } = render(reduxWrapper(<IntlLogistration />));
// While staying on the registration form, clicking the register tab again
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
expect(sendTrackEvent).not.toHaveBeenCalledWith('edx.bi.register_form.toggled', { category: 'user-engagement' });
});
it('should render registration page', () => {
mergeConfig({
ALLOW_PUBLIC_ACCOUNT_CREATION: true,
@@ -260,9 +268,11 @@ describe('Logistration', () => {
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm());
});
it('should fire action to backup login form on tab click', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper(<IntlLogistration />));
const props = { selectedPage: LOGIN_PAGE };
const { container } = render(reduxWrapper(<IntlLogistration {...props} />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginForm());
});

View File

@@ -196,7 +196,7 @@ const ProgressiveProfiling = (props) => {
});
return (
<BaseContainer showWelcomeBanner fullName={authenticatedUser?.fullName || authenticatedUser?.username}>
<BaseContainer showWelcomeBanner fullName={authenticatedUser?.fullName || authenticatedUser?.name}>
<Helmet>
<title>{formatMessage(messages['progressive.profiling.page.title'],
{ siteName: getConfig().SITE_NAME })}

View File

@@ -20,7 +20,7 @@ const BaseCard = ({
<div className="recommendation-card" key={`container-${uuid}`}>
<Hyperlink
target="_blank"
className="card-box"
className="card-box d-inline"
showLaunchIcon={false}
onClick={handleOnClick}
>

View File

@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -9,9 +9,9 @@ import PropTypes from 'prop-types';
import validateEmail from './validator';
import { FormGroup } from '../../../common-components';
import {
backupRegistrationFormBegin,
clearRegistrationBackendError,
fetchRealtimeValidations,
setEmailSuggestionInStore,
} from '../../data/actions';
import messages from '../../messages';
@@ -44,6 +44,10 @@ const EmailField = (props) => {
const [emailSuggestion, setEmailSuggestion] = useState({ ...backedUpFormData?.emailSuggestion });
useEffect(() => {
setEmailSuggestion(backedUpFormData.emailSuggestion);
}, [backedUpFormData.emailSuggestion]);
const handleOnBlur = (e) => {
const { value } = e.target;
const { fieldError, confirmEmailError, suggestion } = validateEmail(value, confirmEmailValue, formatMessage);
@@ -52,10 +56,7 @@ const EmailField = (props) => {
handleErrorChange('confirm_email', confirmEmailError);
}
dispatch(backupRegistrationFormBegin({
...backedUpFormData,
emailSuggestion: { ...suggestion },
}));
dispatch(setEmailSuggestionInStore(suggestion));
setEmailSuggestion(suggestion);
if (fieldError) {

View File

@@ -46,7 +46,14 @@ describe('EmailField', () => {
);
const initialState = {
register: {},
register: {
registrationFormData: {
emailSuggestion: {
suggestion: 'example@gmail.com',
type: 'warning',
},
},
},
};
beforeEach(() => {

View File

@@ -91,7 +91,7 @@ export const validateEmailAddress = (value, username, domainName) => {
const validateEmail = (value, confirmEmailValue, formatMessage) => {
let fieldError = '';
let confirmEmailError = '';
let emailSuggestion = {};
let emailSuggestion = { suggestion: '', type: '' };
if (!value) {
fieldError = formatMessage(messages['empty.email.field.error']);

View File

@@ -27,23 +27,21 @@ const NameField = (props) => {
const {
handleErrorChange,
shouldFetchUsernameSuggestions,
name,
fullName,
} = props;
const handleOnBlur = (e) => {
const { value } = e.target;
const fieldError = validateName(value, name, formatMessage);
const fieldError = validateName(value, formatMessage);
if (fieldError) {
handleErrorChange(name, fieldError);
handleErrorChange('name', fieldError);
} else if (shouldFetchUsernameSuggestions && !validationApiRateLimited) {
dispatch(fetchRealtimeValidations({ name: fullName.trim() }));
dispatch(fetchRealtimeValidations({ name: value }));
}
};
const handleOnFocus = () => {
handleErrorChange(name, '');
dispatch(clearRegistrationBackendError(name));
handleErrorChange('name', '');
dispatch(clearRegistrationBackendError('name'));
};
return (
@@ -58,7 +56,6 @@ const NameField = (props) => {
NameField.defaultProps = {
errorMessage: '',
shouldFetchUsernameSuggestions: false,
fullName: '',
};
NameField.propTypes = {
@@ -67,8 +64,6 @@ NameField.propTypes = {
value: PropTypes.string.isRequired,
handleChange: PropTypes.func.isRequired,
handleErrorChange: PropTypes.func.isRequired,
name: PropTypes.string.isRequired,
fullName: PropTypes.string,
};
export default NameField;

View File

@@ -51,7 +51,7 @@ describe('NameField', () => {
beforeEach(() => {
store = mockStore(initialState);
props = {
name: '',
name: 'name',
value: '',
errorMessage: '',
handleChange: jest.fn(),
@@ -66,44 +66,43 @@ describe('NameField', () => {
});
describe('Test Name Field', () => {
it('should run first name field validation when onBlur is fired', () => {
props.name = 'firstName';
const fieldValidation = { name: 'Enter your full name' };
it('should run name field validation when onBlur is fired', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const firstNameInput = container.querySelector('input#firstName');
fireEvent.blur(firstNameInput, { target: { value: '', name: 'firstName' } });
const nameInput = container.querySelector('input#name');
fireEvent.blur(nameInput, { target: { value: '', name: 'name' } });
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
expect(props.handleErrorChange).toHaveBeenCalledWith(
'firstName',
'Enter your first name',
'name',
fieldValidation.name,
);
});
it('should update first name field error for frontend validations', () => {
props.name = 'firstName';
it('should update errors for frontend validations', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const firstNameInput = container.querySelector('input#firstName');
fireEvent.blur(firstNameInput, { target: { value: 'https://invalid-name.com', name: 'firstName' } });
const nameInput = container.querySelector('input#name');
fireEvent.blur(nameInput, { target: { value: 'https://invalid-name.com', name: 'name' } });
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
expect(props.handleErrorChange).toHaveBeenCalledWith(
'firstName',
'Enter a valid first name',
'name',
'Enter a valid name',
);
});
it('should clear first name error on focus', () => {
props.name = 'firstName';
it('should clear error on focus', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const firstNameInput = container.querySelector('input#firstName');
fireEvent.focus(firstNameInput, { target: { value: '', name: 'firstName' } });
const nameInput = container.querySelector('input#name');
fireEvent.focus(nameInput, { target: { value: '', name: 'name' } });
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
expect(props.handleErrorChange).toHaveBeenCalledWith(
'firstName',
'name',
'',
);
});
@@ -113,16 +112,14 @@ describe('NameField', () => {
props = {
...props,
shouldFetchUsernameSuggestions: true,
fullName: 'test test',
};
props.name = 'lastName';
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const lastNameInput = container.querySelector('input#lastName');
const nameInput = container.querySelector('input#name');
// Enter a valid name so that frontend validations are passed
fireEvent.blur(lastNameInput, { target: { value: 'test', name: 'lastName' } });
fireEvent.blur(nameInput, { target: { value: 'test', name: 'name' } });
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ name: props.fullName }));
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ name: 'test' }));
});
it('should clear the registration validation error on focus on field', () => {
@@ -137,43 +134,14 @@ describe('NameField', () => {
},
});
props.name = 'lastName';
store.dispatch = jest.fn(store.dispatch);
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const lastNameInput = container.querySelector('input#lastName');
const nameInput = container.querySelector('input#name');
fireEvent.focus(lastNameInput, { target: { value: 'test', name: 'lastName' } });
fireEvent.focus(nameInput, { target: { value: 'test', name: 'name' } });
expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('lastName'));
});
it('should run last name field validation when onBlur is fired', () => {
props.name = 'lastName';
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const lastNameInput = container.querySelector('input#lastName');
fireEvent.blur(lastNameInput, { target: { value: '', name: 'lastName' } });
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
expect(props.handleErrorChange).toHaveBeenCalledWith(
'lastName',
'Enter your last name',
);
});
it('should update last name field error for frontend validation', () => {
props.name = 'lastName';
const { container } = render(routerWrapper(reduxWrapper(<IntlNameField {...props} />)));
const lastNameInput = container.querySelector('input#lastName');
fireEvent.blur(lastNameInput, { target: { value: 'https://invalid-name.com', name: 'lastName' } });
expect(props.handleErrorChange).toHaveBeenCalledTimes(1);
expect(props.handleErrorChange).toHaveBeenCalledWith(
'lastName',
'Enter a valid last name',
);
expect(store.dispatch).toHaveBeenCalledWith(clearRegistrationBackendError('name'));
});
});
});

View File

@@ -9,16 +9,12 @@ export const HTML_REGEX = /<|>/u;
// regex from backend
export const INVALID_NAME_REGEX = /https?:\/\/(?:[-\w.]|(?:%[\da-fA-F]{2}))*/g;
const validateName = (value, fieldName, formatMessage) => {
let fieldError;
const validateName = (value, formatMessage) => {
let fieldError = '';
if (!value.trim()) {
fieldError = fieldName === 'lastName'
? formatMessage(messages['empty.lastName.field.error'])
: formatMessage(messages['empty.firstName.field.error']);
fieldError = formatMessage(messages['empty.name.field.error']);
} else if (URL_REGEX.test(value) || HTML_REGEX.test(value) || INVALID_NAME_REGEX.test(value)) {
fieldError = fieldName === 'lastName'
? formatMessage(messages['lastName.validation.message'])
: formatMessage(messages['firstName.validation.message']);
fieldError = formatMessage(messages['name.validation.message']);
}
return fieldError;
};

View File

@@ -18,13 +18,16 @@ import {
backupRegistrationFormBegin,
clearRegistrationBackendError,
registerNewUser,
setEmailSuggestionInStore,
setUserPipelineDataLoaded,
} from './data/actions';
import {
FORM_SUBMISSION_ERROR,
TPA_AUTHENTICATION_FAILURE,
} from './data/constants';
import { getBackendValidations, isFormValid, prepareRegistrationPayload } from './data/utils';
import {
getBackendValidations, isFormValid, prepareRegistrationPayload,
} from './data/utils';
import messages from './messages';
import { EmailField, NameField, UsernameField } from './RegistrationFields';
import {
@@ -56,6 +59,7 @@ const RegistrationPage = (props) => {
showConfigurableEdxFields: getConfig().SHOW_CONFIGURABLE_EDX_FIELDS,
showConfigurableRegistrationFields: getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS,
showMarketingEmailOptInCheckbox: getConfig().MARKETING_EMAILS_OPT_IN,
autoGeneratedUsernameEnabled: getConfig().ENABLE_AUTO_GENERATED_USERNAME,
};
const {
handleInstitutionLogin,
@@ -119,11 +123,9 @@ const RegistrationPage = (props) => {
setErrorCode(prevState => ({ type: TPA_AUTHENTICATION_FAILURE, count: prevState.count + 1 }));
}
if (pipelineUserDetails && Object.keys(pipelineUserDetails).length !== 0) {
const {
firstName = '', lastName = '', username = '', email = '',
} = pipelineUserDetails;
const { name = '', username = '', email = '' } = pipelineUserDetails;
setFormFields(prevState => ({
...prevState, firstName, lastName, username, email,
...prevState, name, username, email,
}));
dispatch(setUserPipelineDataLoaded(true));
}
@@ -190,8 +192,8 @@ const RegistrationPage = (props) => {
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
if (registrationError[name]) {
dispatch(clearRegistrationBackendError(name));
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
}
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
setFormFields(prevState => ({ ...prevState, [name]: value }));
};
@@ -223,9 +225,12 @@ const RegistrationPage = (props) => {
delete payload.password;
payload.social_auth_provider = currentProvider;
}
if (flags.autoGeneratedUsernameEnabled) {
delete payload.username;
}
// Validating form data before submitting
const { isValid, fieldErrors } = isFormValid(
const { isValid, fieldErrors, emailSuggestion } = isFormValid(
payload,
registrationEmbedded ? temporaryErrors : errors,
configurableFormFields,
@@ -233,6 +238,7 @@ const RegistrationPage = (props) => {
formatMessage,
);
setErrors({ ...fieldErrors });
dispatch(setEmailSuggestionInStore(emailSuggestion));
// returning if not valid
if (!isValid) {
@@ -312,22 +318,14 @@ const RegistrationPage = (props) => {
/>
<Form id="registration-form" name="registration-form">
<NameField
name="firstName"
value={formFields.firstName}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.firstName}
floatingLabel={formatMessage(messages['registration.firstName.label'])}
/>
<NameField
name="lastName"
value={formFields.lastName}
name="name"
value={formFields.name}
shouldFetchUsernameSuggestions={!formFields.username.trim()}
fullName={`${formFields.firstName} ${formFields.lastName}`}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.lastName}
floatingLabel={formatMessage(messages['registration.lastName.label'])}
errorMessage={errors.name}
helpText={[formatMessage(messages['help.text.name'])]}
floatingLabel={formatMessage(messages['registration.fullname.label'])}
/>
<EmailField
name="email"
@@ -339,16 +337,18 @@ const RegistrationPage = (props) => {
helpText={[formatMessage(messages['help.text.email'])]}
floatingLabel={formatMessage(messages['registration.email.label'])}
/>
<UsernameField
name="username"
spellCheck="false"
value={formFields.username}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.username}
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
floatingLabel={formatMessage(messages['registration.username.label'])}
/>
{!flags.autoGeneratedUsernameEnabled && (
<UsernameField
name="username"
spellCheck="false"
value={formFields.username}
handleChange={handleOnChange}
handleErrorChange={handleErrorChange}
errorMessage={errors.username}
helpText={[formatMessage(messages['help.text.username.1']), formatMessage(messages['help.text.username.2'])]}
floatingLabel={formatMessage(messages['registration.username.label'])}
/>
)}
{!currentProvider && (
<PasswordField
name="password"

View File

@@ -64,13 +64,13 @@ describe('RegistrationPage', () => {
marketingEmailsOptIn: true,
},
formFields: {
firstName: '', lastName: '', email: '', username: '', password: '',
name: '', email: '', username: '', password: '',
},
emailSuggestion: {
suggestion: '', type: '',
},
errors: {
firstName: '', lastName: '', email: '', username: '', password: '',
name: '', email: '', username: '', password: '',
},
};
@@ -100,6 +100,7 @@ describe('RegistrationPage', () => {
registrationError: {},
registrationFormData,
usernameSuggestions: [],
},
commonComponents: {
thirdPartyAuthApiStatus: null,
@@ -133,10 +134,16 @@ describe('RegistrationPage', () => {
jest.clearAllMocks();
});
const populateRequiredFields = (getByLabelText, payload, isThirdPartyAuth = false) => {
fireEvent.change(getByLabelText('First name'), { target: { value: payload.first_name, name: 'firstName' } });
fireEvent.change(getByLabelText('Last name'), { target: { value: payload.last_name, name: 'lastName' } });
fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } });
const populateRequiredFields = (
getByLabelText,
payload,
isThirdPartyAuth = false,
autoGeneratedUsernameEnabled = false,
) => {
fireEvent.change(getByLabelText('Full name'), { target: { value: payload.name, name: 'name' } });
if (!autoGeneratedUsernameEnabled) {
fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } });
}
fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } });
fireEvent.change(getByLabelText('Country/Region'), { target: { value: payload.country, name: 'country' } });
@@ -153,8 +160,7 @@ describe('RegistrationPage', () => {
});
const emptyFieldValidation = {
firstName: 'Enter your first name',
lastName: 'Enter your last name',
name: 'Enter your full name',
username: 'Username must be between 2 and 30 characters',
email: 'Enter your email',
password: 'Password criteria has not been met',
@@ -171,8 +177,7 @@ describe('RegistrationPage', () => {
window.location = { href: getConfig().BASE_URL, search: '?next=/course/demo-course-url' };
const payload = {
first_name: 'John',
last_name: 'Doe',
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@gmail.com',
password: 'password1',
@@ -195,8 +200,7 @@ describe('RegistrationPage', () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
const formPayload = {
first_name: 'John',
last_name: 'Doe',
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@example.com',
country: 'Pakistan',
@@ -224,6 +228,54 @@ describe('RegistrationPage', () => {
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...formPayload, country: 'PK' }));
});
it('should display an error when form is submitted with an invalid email', () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
const emailError = "We couldn't create your account.Please check your responses and try again.";
const formPayload = {
name: 'Petro',
username: 'petro_qa',
email: 'petro @example.com',
password: 'password1',
country: 'Ukraine',
honor_code: true,
totalRegistrationTime: 0,
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
const validationErrors = container.querySelector('#validation-errors');
expect(validationErrors.textContent).toContain(emailError);
});
it('should display an error when form is submitted with an invalid username', () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
const usernameError = "We couldn't create your account.Please check your responses and try again.";
const formPayload = {
name: 'Petro',
username: 'petro qa',
email: 'petro@example.com',
password: 'password1',
country: 'Ukraine',
honor_code: true,
totalRegistrationTime: 0,
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
const validationErrors = container.querySelector('#validation-errors');
expect(validationErrors.textContent).toContain(usernameError);
});
it('should submit form with marketing email opt in value', () => {
mergeConfig({
MARKETING_EMAILS_OPT_IN: 'true',
@@ -232,8 +284,7 @@ describe('RegistrationPage', () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
const payload = {
first_name: 'John',
last_name: 'Doe',
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@gmail.com',
password: 'password1',
@@ -255,6 +306,44 @@ describe('RegistrationPage', () => {
});
});
it('should submit form without UsernameField when autoGeneratedUsernameEnabled is true', () => {
mergeConfig({
ENABLE_AUTO_GENERATED_USERNAME: true,
});
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
const payload = {
name: 'John Doe',
email: 'john.doe@gmail.com',
password: 'password1',
country: 'Pakistan',
honor_code: true,
totalRegistrationTime: 0,
};
store.dispatch = jest.fn(store.dispatch);
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, payload, false, true);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
mergeConfig({
ENABLE_AUTO_GENERATED_USERNAME: false,
});
});
it('should not display UsernameField when ENABLE_AUTO_GENERATED_USERNAME is true', () => {
mergeConfig({
ENABLE_AUTO_GENERATED_USERNAME: true,
});
const { queryByLabelText } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(queryByLabelText('Username')).toBeNull();
mergeConfig({
ENABLE_AUTO_GENERATED_USERNAME: false,
});
});
it('should not dispatch registerNewUser on empty form Submission', () => {
store.dispatch = jest.fn(store.dispatch);
@@ -584,8 +673,7 @@ describe('RegistrationPage', () => {
registrationFormData: {
...registrationFormData,
formFields: {
firstName: 'John',
lastName: 'Doe',
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@yopmail.com',
password: 'password1',
@@ -599,15 +687,13 @@ describe('RegistrationPage', () => {
const { container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
const firstNameInput = container.querySelector('input#firstName');
const lastNameInput = container.querySelector('input#lastName');
const fullNameInput = container.querySelector('input#name');
const usernameInput = container.querySelector('input#username');
const emailInput = container.querySelector('input#email');
const passwordInput = container.querySelector('input#password');
const emailSuggestion = container.querySelector('.email-suggestion-alert-warning');
expect(firstNameInput.value).toEqual('John');
expect(lastNameInput.value).toEqual('Doe');
expect(fullNameInput.value).toEqual('John Doe');
expect(usernameInput.value).toEqual('john_doe');
expect(emailInput.value).toEqual('john.doe@yopmail.com');
expect(passwordInput.value).toEqual('password1');
@@ -728,8 +814,7 @@ describe('RegistrationPage', () => {
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
pipelineUserDetails: {
firstName: 'John',
lastName: 'Doe',
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@example.com',
},
@@ -760,8 +845,7 @@ describe('RegistrationPage', () => {
registrationFormData: {
...registrationFormData,
formFields: {
firstName: 'John',
lastName: 'Doe',
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@example.com',
},
@@ -781,8 +865,7 @@ describe('RegistrationPage', () => {
...initialState.commonComponents.thirdPartyAuthContext,
currentProvider: 'Apple',
pipelineUserDetails: {
firstName: 'John',
lastName: 'Doe',
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@example.com',
},
@@ -794,8 +877,7 @@ describe('RegistrationPage', () => {
render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({
first_name: 'John',
last_name: 'Doe',
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@example.com',
country: 'PK',

View File

@@ -56,13 +56,13 @@ describe('ConfigurableRegistrationForm', () => {
marketingEmailsOptIn: true,
},
formFields: {
firstName: '', lastName: '', email: '', username: '', password: '',
name: '', email: '', username: '', password: '',
},
emailSuggestion: {
suggestion: '', type: '',
},
errors: {
firstName: '', lastName: '', email: '', username: '', password: '',
name: '', email: '', username: '', password: '',
},
};
@@ -128,8 +128,7 @@ describe('ConfigurableRegistrationForm', () => {
});
const populateRequiredFields = (getByLabelText, payload, isThirdPartyAuth = false) => {
fireEvent.change(getByLabelText('First name'), { target: { value: payload.first_name, name: 'firstName' } });
fireEvent.change(getByLabelText('Last name'), { target: { value: payload.last_name, name: 'lastName' } });
fireEvent.change(getByLabelText('Full name'), { target: { value: payload.name, name: 'name' } });
fireEvent.change(getByLabelText('Public username'), { target: { value: payload.username, name: 'username' } });
fireEvent.change(getByLabelText('Email'), { target: { value: payload.email, name: 'email' } });
@@ -239,8 +238,7 @@ describe('ConfigurableRegistrationForm', () => {
});
const payload = {
first_name: 'John',
last_name: 'Doe',
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@example.com',
password: 'password1',
@@ -350,6 +348,49 @@ describe('ConfigurableRegistrationForm', () => {
expect(confirmEmailErrorElement.textContent).toEqual('The email addresses do not match.');
});
it('should show error if email and confirm email fields do not match on submit click', () => {
const formPayload = {
name: 'Petro',
username: 'petro_qa',
email: 'petro@example.com',
password: 'password1',
country: 'Ukraine',
honor_code: true,
totalRegistrationTime: 0,
};
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
fieldDescriptions: {
confirm_email: {
name: 'confirm_email', type: 'text', label: 'Confirm Email',
},
country: { name: 'country' },
},
},
});
const { getByLabelText, container } = render(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(getByLabelText, formPayload, true);
fireEvent.change(
getByLabelText('Confirm Email'),
{ target: { value: 'test2@gmail.com', name: 'confirm_email' } },
);
const button = container.querySelector('button.btn-brand');
fireEvent.click(button);
const confirmEmailErrorElement = container.querySelector('div#confirm_email-error');
expect(confirmEmailErrorElement.textContent).toEqual('The email addresses do not match.');
const validationErrors = container.querySelector('#validation-errors');
expect(validationErrors.textContent).toContain(
"We couldn't create your account.Please check your responses and try again.",
);
});
it('should run validations for configurable focused field on form submission', () => {
const professionError = 'Enter your profession';
store = mockStore({

View File

@@ -7,6 +7,7 @@ export const REGISTER_CLEAR_USERNAME_SUGGESTIONS = 'REGISTRATION_CLEAR_USERNAME_
export const REGISTRATION_CLEAR_BACKEND_ERROR = 'REGISTRATION_CLEAR_BACKEND_ERROR';
export const REGISTER_SET_COUNTRY_CODE = 'REGISTER_SET_COUNTRY_CODE';
export const REGISTER_SET_USER_PIPELINE_DATA_LOADED = 'REGISTER_SET_USER_PIPELINE_DATA_LOADED';
export const REGISTER_SET_EMAIL_SUGGESTIONS = 'REGISTER_SET_EMAIL_SUGGESTIONS';
// Backup registration form
export const backupRegistrationForm = () => ({
@@ -37,6 +38,12 @@ export const fetchRealtimeValidationsFailure = () => ({
type: REGISTER_FORM_VALIDATIONS.FAILURE,
});
// Set email field frontend validations
export const setEmailSuggestionInStore = (emailSuggestion) => ({
type: REGISTER_SET_EMAIL_SUGGESTIONS,
payload: { emailSuggestion },
});
// Register
export const registerNewUser = registrationInfo => ({
type: REGISTER_NEW_USER.BASE,

View File

@@ -3,7 +3,9 @@ import {
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
REGISTER_FORM_VALIDATIONS,
REGISTER_NEW_USER,
REGISTER_SET_COUNTRY_CODE, REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTER_SET_COUNTRY_CODE,
REGISTER_SET_EMAIL_SUGGESTIONS,
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTRATION_CLEAR_BACKEND_ERROR,
} from './actions';
import {
@@ -22,13 +24,13 @@ export const defaultState = {
marketingEmailsOptIn: true,
},
formFields: {
firstName: '', lastName: '', email: '', username: '', password: '',
name: '', email: '', username: '', password: '',
},
emailSuggestion: {
suggestion: '', type: '',
},
errors: {
firstName: '', lastName: '', email: '', username: '', password: '',
name: '', email: '', username: '', password: '',
},
},
validations: null,
@@ -48,7 +50,7 @@ const reducer = (state = defaultState, action = {}) => {
};
case BACKUP_REGISTRATION_DATA.BEGIN:
return {
...defaultState,
...state,
usernameSuggestions: state.usernameSuggestions,
registrationFormData: { ...action.payload },
userPipelineDataLoaded: state.userPipelineDataLoaded,
@@ -119,6 +121,14 @@ const reducer = (state = defaultState, action = {}) => {
userPipelineDataLoaded: value,
};
}
case REGISTER_SET_EMAIL_SUGGESTIONS:
return {
...state,
registrationFormData: {
...state.registrationFormData,
emailSuggestion: action.payload.emailSuggestion,
},
};
default:
return {
...state,

View File

@@ -7,6 +7,7 @@ import {
REGISTER_FORM_VALIDATIONS,
REGISTER_NEW_USER,
REGISTER_SET_COUNTRY_CODE,
REGISTER_SET_EMAIL_SUGGESTIONS,
REGISTER_SET_USER_PIPELINE_DATA_LOADED,
REGISTRATION_CLEAR_BACKEND_ERROR,
} from '../actions';
@@ -22,13 +23,13 @@ describe('Registration Reducer Tests', () => {
marketingEmailsOptIn: true,
},
formFields: {
firstName: '', lastName: '', email: '', username: '', password: '',
name: '', email: '', username: '', password: '',
},
emailSuggestion: {
suggestion: '', type: '',
},
errors: {
firstName: '', lastName: '', email: '', username: '', password: '',
name: '', email: '', username: '', password: '',
},
},
validations: null,
@@ -65,6 +66,28 @@ describe('Registration Reducer Tests', () => {
);
});
it('should set email suggestions', () => {
const emailSuggestion = {
type: 'test type',
suggestion: 'test suggestion',
};
const action = {
type: REGISTER_SET_EMAIL_SUGGESTIONS,
payload: { emailSuggestion },
};
expect(reducer(defaultState, action)).toEqual(
{
...defaultState,
registrationFormData: {
...defaultState.registrationFormData,
emailSuggestion: {
type: 'test type', suggestion: 'test suggestion',
},
},
});
});
it('should set redirect url dashboard on registration success action', () => {
const payload = {
redirectUrl: `${getConfig().BASE_URL}${DEFAULT_REDIRECT_URL}`,

View File

@@ -2,6 +2,9 @@ import { snakeCaseObject } from '@edx/frontend-platform';
import { LETTER_REGEX, NUMBER_REGEX } from '../../data/constants';
import messages from '../messages';
import validateEmail from '../RegistrationFields/EmailField/validator';
import validateName from '../RegistrationFields/NameField/validator';
import validateUsername from '../RegistrationFields/UsernameField/validator';
/**
* It validates the password field value
@@ -35,12 +38,39 @@ export const isFormValid = (
) => {
const fieldErrors = { ...errors };
let isValid = true;
let emailSuggestion = { suggestion: '', type: '' };
Object.keys(payload).forEach(key => {
if (!payload[key]) {
fieldErrors[key] = formatMessage(messages[`empty.${key}.field.error`]);
switch (key) {
case 'name':
fieldErrors.name = validateName(payload.name, formatMessage);
if (fieldErrors.name) { isValid = false; }
break;
case 'email': {
const {
fieldError, confirmEmailError, suggestion,
} = validateEmail(payload.email, configurableFormFields?.confirm_email, formatMessage);
if (fieldError) {
fieldErrors.email = fieldError;
isValid = false;
}
if (confirmEmailError) {
fieldErrors.confirm_email = confirmEmailError;
isValid = false;
}
emailSuggestion = suggestion;
break;
}
if (fieldErrors[key]) {
isValid = false;
case 'username':
fieldErrors.username = validateUsername(payload.username, formatMessage);
if (fieldErrors.username) { isValid = false; }
break;
case 'password':
fieldErrors.password = validatePasswordField(payload.password, formatMessage);
if (fieldErrors.password) { isValid = false; }
break;
default:
break;
}
});
@@ -54,17 +84,15 @@ export const isFormValid = (
}
Object.keys(fieldDescriptions).forEach(key => {
if (key === 'country' && !configurableFormFields.country.displayValue) {
if (key === 'country' && !configurableFormFields?.country?.displayValue) {
fieldErrors[key] = formatMessage(messages['empty.country.field.error']);
} else if (!configurableFormFields[key]) {
fieldErrors[key] = fieldDescriptions[key].error_message;
}
if (fieldErrors[key]) {
isValid = false;
}
if (fieldErrors[key]) { isValid = false; }
});
return { isValid, fieldErrors };
return { isValid, fieldErrors, emailSuggestion };
};
/**

View File

@@ -7,15 +7,10 @@ const messages = defineMessages({
description: 'register page title',
},
// Field labels
'registration.firstName.label': {
id: 'registration.firstName.label',
defaultMessage: 'First name',
description: 'Label that appears above first name field',
},
'registration.lastName.label': {
id: 'registration.lastName.label',
defaultMessage: 'Last name',
description: 'Label that appears above last name field',
'registration.fullname.label': {
id: 'registration.fullname.label',
defaultMessage: 'Full name',
description: 'Label that appears above fullname field',
},
'registration.email.label': {
id: 'registration.email.label',
@@ -43,10 +38,10 @@ const messages = defineMessages({
description: 'Text for opt in option on register page.',
},
// Help text
'help.text.firstName': {
id: 'help.text.firstName',
'help.text.name': {
id: 'help.text.name',
defaultMessage: 'This name will be used by any certificates that you earn.',
description: 'Help text for first name field on registration page',
description: 'Help text for fullname field on registration page',
},
'help.text.username.1': {
id: 'help.text.username.1',
@@ -81,15 +76,10 @@ const messages = defineMessages({
description: 'Heading of institution page',
},
// Validation messages
'empty.firstName.field.error': {
id: 'empty.firstName.field.error',
defaultMessage: 'Enter your first name',
description: 'Error message for empty first name field',
},
'empty.lastName.field.error': {
id: 'empty.lastName.field.error',
defaultMessage: 'Enter your last name',
description: 'Error message for empty last name field',
'empty.name.field.error': {
id: 'empty.name.field.error',
defaultMessage: 'Enter your full name',
description: 'Error message for empty fullname field',
},
'empty.email.field.error': {
id: 'empty.email.field.error',
@@ -131,15 +121,10 @@ const messages = defineMessages({
defaultMessage: 'Username must be between 2 and 30 characters',
description: 'Error message for empty username field',
},
'firstName.validation.message': {
id: 'firstName.validation.message',
defaultMessage: 'Enter a valid first name',
description: 'Validation message that appears when first name contain URL',
},
'lastName.validation.message': {
id: 'lastName.validation.message',
defaultMessage: 'Enter a valid last name',
description: 'Validation message that appears when last name contain URL',
'name.validation.message': {
id: 'name.validation.message',
defaultMessage: 'Enter a valid name',
description: 'Validation message that appears when fullname contain URL',
},
'password.validation.message': {
id: 'password.validation.message',

View File

@@ -11,7 +11,6 @@
// ----------------------------
// #COLORS
// ----------------------------
$font-blue: #126f9a;
$white: #FFFFFF;
// social platforms
@@ -105,10 +104,10 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
font-size: 14px;
background-color: $white;
border: 1px solid $font-blue;
border: 1px solid $primary;
width: 224px;
height: 36px;
color: $font-blue;
color: $primary;
.btn-tpa__image-icon{
background-color: transparent;
@@ -133,7 +132,7 @@ $elevation-level-2-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.15);
}
.btn-tpa__font-container {
background-color: $font-blue;
background-color: $primary;
color: $white;
font-size: 11px;