Compare commits

...

29 Commits

Author SHA1 Message Date
Régis Behmo
ad8be560b4 build: this MFE should be included in the open releases 2022-06-09 09:27:01 -04:00
dependabot[bot]
5dea66540d build(deps): bump async from 2.6.3 to 2.6.4 (#587)
Bumps [async](https://github.com/caolan/async) from 2.6.3 to 2.6.4.
- [Release notes](https://github.com/caolan/async/releases)
- [Changelog](https://github.com/caolan/async/blob/v2.6.4/CHANGELOG.md)
- [Commits](https://github.com/caolan/async/compare/v2.6.3...v2.6.4)

---
updated-dependencies:
- dependency-name: async
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-08 11:20:59 +05:00
renovate[bot]
400648f09f chore(deps): update dependency semver-regex to 3.1.4 [security] (#584)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-06-08 11:12:29 +05:00
edx-semantic-release
f781f311a2 chore(i18n): update translations 2022-06-03 12:57:48 -04:00
Waheed Ahmad
9469adc386 Revert "fix: Persist Register/Sign in form states on tab switch [VAN-618]" (#582) 2022-06-03 19:22:52 +05:00
Syed Sajjad Hussain Shah
691a248477 Merge pull request #576 from openedx/sajjad/VAN-618
fix: Persist Register/Sign in form states on tab switch [VAN-618]
2022-06-02 16:28:47 +05:00
Syed Sajjad Hussain Shah
4bf0021982 fix: Persist Register/Sign in form states on tab switch [VAN-618] 2022-06-02 16:22:40 +05:00
Mubbshar Anwar
b04257a62d feat: add zendesk help (#580)
Add zendesk js library on frontend-app-authn to connect users with help desk.

VAN-958
2022-06-01 15:24:37 +05:00
Attiya Ishaque
bce8aa712d feat: add required field support (#557) 2022-05-19 18:22:51 +05:00
Attiya Ishaque
25bc11ea8d chore: [VAN-948] Add name and id to the forms (#573) 2022-05-17 15:51:02 +05:00
Zainab Amir
bcece267bc fix: update yellow arc position (#572) 2022-05-13 10:41:00 +05:00
Syed Sajjad Hussain Shah
1575034ed3 Merge pull request #571 from openedx/sajjad/VAN-548
fix: Add aria-Invalid="true" to error-state fields
2022-05-11 11:00:49 +05:00
Syed Sajjad Hussain Shah
922f1e2ac4 Merge branch 'master' into sajjad/VAN-548 2022-05-11 10:56:52 +05:00
Syed Sajjad Hussain Shah
b03fe545ba fix: Add aria-Invalid=true to invalid fields 2022-05-09 18:08:56 +05:00
Jawayria
47fce8aa7d feat: Add check for package-lock file version (#568) 2022-05-09 18:03:18 +05:00
Jawayria
5fd6754025 Merge pull request #556 from openedx/jenkins/drop-node-12-2099e62
chore!: Dropped support for Node 12
2022-05-09 13:17:39 +05:00
Jawayria
428372d198 Merge branch 'master' into jenkins/drop-node-12-2099e62 2022-05-09 13:13:08 +05:00
edx-semantic-release
4f83726387 chore(i18n): update translations 2022-05-01 11:16:39 -04:00
edX requirements bot
40cb949d4e chore!: Dropped support for Node 12 2022-04-29 16:46:06 +05:00
Syed Sajjad Hussain Shah
daa54be1c8 Merge pull request #569 from openedx/sajjad/VAN-930
fix: Styling issue in fields trailing element
2022-04-29 11:55:55 +05:00
Syed Sajjad Hussain Shah
b126635981 fix: Styling issue in fields trailing element 2022-04-28 14:54:34 +05:00
Syed Sajjad Hussain Shah
94e1207d37 Merge pull request #567 from openedx/sajjad/VAN-936
fix: Update position of cross icon on username field
2022-04-26 17:42:25 +05:00
Syed Sajjad Hussain Shah
9ec6e1cf05 fix: Update position of cross icon
Cross icon was overlapping the border the username field
when username suggestions were generated,
This PR fixes the position of cross icon of username
suggestions field

VAN-936
2022-04-26 17:28:47 +05:00
Shafqat Farhan
eddce35a37 feat: VAN-935 - Launch rename register (#566) 2022-04-26 14:13:35 +05:00
Zainab Amir
38cbd4d0d8 fix: headers hierarchy on reset page (#564) 2022-04-25 14:37:29 +05:00
edx-semantic-release
b962b3f4ca chore(i18n): update translations 2022-04-24 11:16:50 -04:00
Mubbshar Anwar
ad9b40d9e9 fix: button width (#563)
text fit in login button.
VAM-889
2022-04-22 15:28:53 +05:00
Shafqat Farhan
7990fe7483 fix: VAN-909 - Fixed the form submission with expired CSRF token (#562) 2022-04-14 17:45:37 +05:00
Mohammad Ahtasham ul Hassan
def1415f14 Upgrade frontend-build (#560)
* fix: upgrade frontend-build
2022-04-11 20:06:09 +05:00
51 changed files with 1004 additions and 293 deletions

2
.env
View File

@@ -26,3 +26,5 @@ ENABLE_PROGRESSIVE_PROFILING=''
MARKETING_EMAILS_OPT_IN=''
ENABLE_COPPA_COMPLIANCE=''
SHOW_DYNAMIC_PROFILING_PAGE=''
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''

View File

@@ -31,3 +31,5 @@ DISABLE_ENTERPRISE_LOGIN=''
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
ENABLE_COPPA_COMPLIANCE=''
MARKETING_EMAILS_OPT_IN=''
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''

View File

@@ -24,3 +24,5 @@ DISABLE_ENTERPRISE_LOGIN=''
REGISTER_CONVERSION_COOKIE_NAME='openedx-user-register-conversion'
MARKETING_EMAILS_OPT_IN=''
ENABLE_COPPA_COMPLIANCE=''
ZENDESK_KEY=''
ZENDESK_LOGO_URL=''

View File

@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-20.04
strategy:
matrix:
node: [12, 14, 16]
node: [16]
steps:
- name: Checkout
uses: actions/checkout@v2

View File

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

View File

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

173
package-lock.json generated
View File

@@ -47,11 +47,11 @@
"regenerator-runtime": "0.13.9",
"reselect": "4.1.5",
"sanitize-html": "2.7.0",
"semver-regex": "3.1.3",
"semver-regex": "3.1.4",
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@edx/frontend-build": "9.1.2",
"@edx/frontend-build": "9.1.4",
"@edx/reactifex": "1.0.3",
"babel-plugin-formatjs": "10.3.18",
"codecov": "3.8.2",
@@ -2005,9 +2005,9 @@
}
},
"node_modules/@edx/frontend-build": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-9.1.2.tgz",
"integrity": "sha512-qC7ReTYGEiAfd1WMMEKR2JnO6/kvTh80COOqA4CslV8l6kchMP7pw9dVPDj+/njWFhK/sn9hoi+Ro4wlcylwyg==",
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-9.1.4.tgz",
"integrity": "sha512-Lw4EqZ8iMnIDLA/xlAEByPzzoyU9faStktYgnRGhPM9DPyQ504NQ0I/qv7vX6qWJ2wII458TNAgGzu9zr2hHWQ==",
"dev": true,
"dependencies": {
"@babel/cli": "7.16.0",
@@ -2051,8 +2051,8 @@
"react-dev-utils": "12.0.0",
"react-refresh": "0.10.0",
"resolve-url-loader": "5.0.0-beta.1",
"sass": "1.26.11",
"sass-loader": "10.0.5",
"sass": "1.49.9",
"sass-loader": "12.6.0",
"source-map-loader": "0.2.4",
"style-loader": "2.0.0",
"url-loader": "4.1.1",
@@ -7063,9 +7063,9 @@
}
},
"node_modules/async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"dependencies": {
"lodash": "^4.17.14"
@@ -14692,6 +14692,12 @@
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
"integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
"dev": true
},
"node_modules/import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -24212,34 +24218,33 @@
}
},
"node_modules/sass": {
"version": "1.26.11",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.11.tgz",
"integrity": "sha512-W1l/+vjGjIamsJ6OnTe0K37U2DBO/dgsv2Z4c89XQ8ZOO6l/VwkqwLSqoYzJeJs6CLuGSTRWc91GbQFL3lvrvw==",
"version": "1.49.9",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz",
"integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==",
"dev": true,
"dependencies": {
"chokidar": ">=2.0.0 <4.0.0"
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
"sass": "sass.js"
},
"engines": {
"node": ">=8.9.0"
"node": ">=12.0.0"
}
},
"node_modules/sass-loader": {
"version": "10.0.5",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.5.tgz",
"integrity": "sha512-2LqoNPtKkZq/XbXNQ4C64GFEleSEHKv6NPSI+bMC/l+jpEXGJhiRYkAQToO24MR7NU4JRY2RpLpJ/gjo2Uf13w==",
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
"integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==",
"dev": true,
"dependencies": {
"klona": "^2.0.4",
"loader-utils": "^2.0.0",
"neo-async": "^2.6.2",
"schema-utils": "^3.0.0",
"semver": "^7.3.2"
"neo-async": "^2.6.2"
},
"engines": {
"node": ">= 10.13.0"
"node": ">= 12.13.0"
},
"funding": {
"type": "opencollective",
@@ -24247,9 +24252,10 @@
},
"peerDependencies": {
"fibers": ">= 3.1.0",
"node-sass": "^4.0.0 || ^5.0.0",
"node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0",
"sass": "^1.3.0",
"webpack": "^4.36.0 || ^5.0.0"
"sass-embedded": "*",
"webpack": "^5.0.0"
},
"peerDependenciesMeta": {
"fibers": {
@@ -24260,42 +24266,12 @@
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
}
}
},
"node_modules/sass-loader/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sass-loader/node_modules/semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sass-loader/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/saxes": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz",
@@ -24387,9 +24363,9 @@
}
},
"node_modules/semver-regex": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.3.tgz",
"integrity": "sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ==",
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz",
"integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA==",
"engines": {
"node": ">=8"
},
@@ -29482,9 +29458,9 @@
"requires": {}
},
"@edx/frontend-build": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-9.1.2.tgz",
"integrity": "sha512-qC7ReTYGEiAfd1WMMEKR2JnO6/kvTh80COOqA4CslV8l6kchMP7pw9dVPDj+/njWFhK/sn9hoi+Ro4wlcylwyg==",
"version": "9.1.4",
"resolved": "https://registry.npmjs.org/@edx/frontend-build/-/frontend-build-9.1.4.tgz",
"integrity": "sha512-Lw4EqZ8iMnIDLA/xlAEByPzzoyU9faStktYgnRGhPM9DPyQ504NQ0I/qv7vX6qWJ2wII458TNAgGzu9zr2hHWQ==",
"dev": true,
"requires": {
"@babel/cli": "7.16.0",
@@ -29528,8 +29504,8 @@
"react-dev-utils": "12.0.0",
"react-refresh": "0.10.0",
"resolve-url-loader": "5.0.0-beta.1",
"sass": "1.26.11",
"sass-loader": "10.0.5",
"sass": "1.49.9",
"sass-loader": "12.6.0",
"source-map-loader": "0.2.4",
"style-loader": "2.0.0",
"url-loader": "4.1.1",
@@ -33486,9 +33462,9 @@
"dev": true
},
"async": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
"integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==",
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
"dev": true,
"requires": {
"lodash": "^4.17.14"
@@ -39416,6 +39392,12 @@
"integrity": "sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA==",
"dev": true
},
"immutable": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
"integrity": "sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw==",
"dev": true
},
"import-fresh": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
@@ -46630,51 +46612,24 @@
}
},
"sass": {
"version": "1.26.11",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.11.tgz",
"integrity": "sha512-W1l/+vjGjIamsJ6OnTe0K37U2DBO/dgsv2Z4c89XQ8ZOO6l/VwkqwLSqoYzJeJs6CLuGSTRWc91GbQFL3lvrvw==",
"version": "1.49.9",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.49.9.tgz",
"integrity": "sha512-YlYWkkHP9fbwaFRZQRXgDi3mXZShslVmmo+FVK3kHLUELHHEYrCmL1x6IUjC7wLS6VuJSAFXRQS/DxdsC4xL1A==",
"dev": true,
"requires": {
"chokidar": ">=2.0.0 <4.0.0"
"chokidar": ">=3.0.0 <4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
}
},
"sass-loader": {
"version": "10.0.5",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.0.5.tgz",
"integrity": "sha512-2LqoNPtKkZq/XbXNQ4C64GFEleSEHKv6NPSI+bMC/l+jpEXGJhiRYkAQToO24MR7NU4JRY2RpLpJ/gjo2Uf13w==",
"version": "12.6.0",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-12.6.0.tgz",
"integrity": "sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA==",
"dev": true,
"requires": {
"klona": "^2.0.4",
"loader-utils": "^2.0.0",
"neo-async": "^2.6.2",
"schema-utils": "^3.0.0",
"semver": "^7.3.2"
},
"dependencies": {
"lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"requires": {
"yallist": "^4.0.0"
}
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
},
"yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
}
"neo-async": "^2.6.2"
}
},
"saxes": {
@@ -46750,9 +46705,9 @@
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
},
"semver-regex": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.3.tgz",
"integrity": "sha512-Aqi54Mk9uYTjVexLnR67rTyBusmwd04cLkHy9hNvk3+G3nT2Oyg7E0l4XVbOaNwIvQ3hHeYxGcyEy+mKreyBFQ=="
"version": "3.1.4",
"resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.4.tgz",
"integrity": "sha512-6IiqeZNgq01qGf0TId0t3NvKzSvUsjcpdEO3AQNeIjR6A2+ckTnQlDpl4qu1bjRv0RzN3FP9hzFmws3lKqRWkA=="
},
"semver-truncate": {
"version": "1.1.2",

View File

@@ -72,11 +72,11 @@
"regenerator-runtime": "0.13.9",
"reselect": "4.1.5",
"sanitize-html": "2.7.0",
"semver-regex": "3.1.3",
"semver-regex": "3.1.4",
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@edx/frontend-build": "9.1.2",
"@edx/frontend-build": "9.1.4",
"@edx/reactifex": "1.0.3",
"babel-plugin-formatjs": "10.3.18",
"codecov": "3.8.2",

View File

@@ -14,6 +14,58 @@
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
></script>
<% } %>
<% if (process.env.ZENDESK_KEY) { %>
<script
id="ze-snippet"
src="https://static.zdassets.com/ekr/snippet.js?key=<%= process.env.ZENDESK_KEY %>"
>
</script>
<script type="text/javascript">
window.zESettings = {
cookies: true,
webWidget: {
contactOptions: {
enabled: false,
},
chat: {
suppress: false,
},
contactForm: {
ticketForms: [
{
id: 360003368814,
subject: false,
fields: [
{
id: 'description',
prefill: {
'*': '',
},
},
],
},
],
selectTicketForm: {
'*': 'Please choose your request type:',
},
attachments: true,
},
helpCenter: {
originalArticleButton: true,
},
answerBot: {
suppress: false,
contactOnlyAfterQuery: true,
title: { '*': 'edX Support' },
avatar: {
url: '<%= process.env.ZENDESK_LOGO_URL %>',
name: { '*': 'edX Support' },
},
},
},
};
</script>
<% } %>
</head>
<body>
<div id="root"></div>

View File

@@ -37,12 +37,12 @@ $accent-a-light: #c9f2f5;
width: 12rem;
}
.stateful-button-variation1-width {
width: 16.4rem;
.register-stateful-button-width {
min-width: 14.4rem;
}
.login-button-width {
width: 6rem;
min-width: 6rem;
}
.tpa-skeleton {
@@ -624,6 +624,7 @@ select.form-control {
position: absolute;
background-color: #fff;
width: 464px;
z-index: 100 !important;
}
.email-error-alert {
@@ -857,7 +858,6 @@ select.form-control {
font-size: 0.75rem;
line-height: 1.25rem;
}
margin-top: 1rem;
margin-left: 3px;
}
.suggested-username {
@@ -866,7 +866,7 @@ select.form-control {
margin-left: 15px;
}
.suggested-username-close-button {
right: 0;
right: 1rem;
position: absolute;
}
.suggested-username-with-error {
@@ -881,3 +881,7 @@ select.form-control {
overflow-x: auto;
display: inline-flex;
}
.pgn__form-control-decorator-trailing {
right: 0 !important;
}

View File

@@ -20,51 +20,53 @@ const LargeLeftLayout = (props) => {
new ClipboardJS('.copyIcon'); // eslint-disable-line no-new
return (
<div className="min-vh-100 pr-0 mt-lg-n2 d-flex align-items-center">
<Toast
onClose={() => setToastShow(false)}
show={showToast}
>
{intl.formatMessage(messages['code.copied'])}
</Toast>
<svg className={classNames(
'large-screen-svg-line',
{
'variation1-bar-color mt-n6 pt-0 ml-5': experimentName === 'variation1' && isRegistrationPage,
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
'ml-5': experimentName !== 'variation1' || !isRegistrationPage,
},
)}
>
<line x1="50" y1="0" x2="10" y2="215" />
</svg>
<div className={classNames({ 'pl-4': experimentName === 'variation1' && isRegistrationPage })}>
<h1 className={classNames('large-heading', { 'mb-4.5': experimentName === 'variation1' && isRegistrationPage })}>
{intl.formatMessage(messages['start.learning'])}
<span
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
>
<br />
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
</h1>
{experimentName === 'variation1' && isRegistrationPage ? (
<span className="text-light-300 dicount-heading">
<span className="lead mr-3">
<SideDiscountBanner />
<div className="min-vh-100 d-flex justify-content-left align-items-center">
<div className="d-flex pr-0 mt-lg-n2">
<Toast
onClose={() => setToastShow(false)}
show={showToast}
>
{intl.formatMessage(messages['code.copied'])}
</Toast>
<svg className={classNames(
'large-screen-svg-line',
{
'variation1-bar-color mt-n6 pt-0 ml-5': experimentName === 'variation1' && isRegistrationPage,
'variation2-bar-color': experimentName === 'variation2' && isRegistrationPage,
'ml-5': experimentName !== 'variation1' || !isRegistrationPage,
},
)}
>
<line x1="50" y1="0" x2="10" y2="215" />
</svg>
<div className={classNames({ 'pl-4': experimentName === 'variation1' && isRegistrationPage })}>
<h1 className={classNames('large-heading', { 'mb-4.5': experimentName === 'variation1' && isRegistrationPage })}>
{intl.formatMessage(messages['start.learning'])}
<span
className={((experimentName === 'variation1' || experimentName === 'variation2') && isRegistrationPage) ? 'text-accent-b' : 'text-accent-a'}
>
<br />
{intl.formatMessage(messages['with.site.name'], { siteName: getConfig().SITE_NAME })}
</span>
<span className="dashed-border d-inline-flex flex-wrap align-items-center">
<span id="edx-welcome" className="text-white edx-welcome font-weight-bold mr-1">EDXWELCOME</span>
<FontAwesomeIcon
className="text-light-700 copyIcon ml-1.5 hover-discount-icon"
icon={faCut}
data-clipboard-action="copy"
data-clipboard-target="#edx-welcome"
onClick={() => setToastShow(true)}
/>
</h1>
{experimentName === 'variation1' && isRegistrationPage ? (
<span className="text-light-300 dicount-heading">
<span className="lead mr-3">
<SideDiscountBanner />
</span>
<span className="dashed-border d-inline-flex flex-wrap align-items-center">
<span id="edx-welcome" className="text-white edx-welcome font-weight-bold mr-1">EDXWELCOME</span>
<FontAwesomeIcon
className="text-light-700 copyIcon ml-1.5 hover-discount-icon"
icon={faCut}
data-clipboard-action="copy"
data-clipboard-target="#edx-welcome"
onClick={() => setToastShow(true)}
/>
</span>
</span>
</span>
) : null}
) : null}
</div>
</div>
</div>
);

View File

@@ -26,6 +26,7 @@ const FormGroup = (props) => {
as={props.as}
readOnly={props.readOnly}
type={props.type}
aria-invalid={props.errorMessage !== ''}
className="form-field"
autoComplete={props.autoComplete}
name={props.name}

View File

@@ -63,6 +63,7 @@ const PasswordField = (props) => {
type={isPasswordHidden ? 'password' : 'text'}
name={props.name}
value={props.value}
aria-invalid={props.errorMessage !== ''}
onFocus={handleFocus}
onBlur={handleBlur}
onChange={props.handleChange}

View File

@@ -12,9 +12,9 @@ export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
});
export const getThirdPartyAuthContextSuccess = (thirdPartyAuthContext) => ({
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, thirdPartyAuthContext) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { thirdPartyAuthContext },
payload: { fieldDescriptions, thirdPartyAuthContext },
});
export const getThirdPartyAuthContextFailure = () => ({

View File

@@ -3,6 +3,8 @@ import { THIRD_PARTY_AUTH_CONTEXT } from './actions';
import { PENDING_STATE, COMPLETE_STATE } from '../../data/constants';
export const defaultState = {
extendedProfile: [],
fieldDescriptions: {},
thirdPartyAuthApiStatus: null,
};
@@ -16,6 +18,8 @@ const reducer = (state = defaultState, action) => {
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS:
return {
...state,
extendedProfile: action.payload.fieldDescriptions.extendedProfile,
fieldDescriptions: action.payload.fieldDescriptions.fields,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};

View File

@@ -18,10 +18,10 @@ import {
export function* fetchThirdPartyAuthContext(action) {
try {
yield put(getThirdPartyAuthContextBegin());
const { thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
const { fieldDescriptions, thirdPartyAuthContext } = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(getThirdPartyAuthContextSuccess(
thirdPartyAuthContext,
fieldDescriptions, thirdPartyAuthContext,
));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());

View File

@@ -8,3 +8,13 @@ export const thirdPartyAuthContextSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.thirdPartyAuthContext,
);
export const fieldDescriptionSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.fieldDescriptions,
);
export const extendedProfileSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.extendedProfile,
);

View File

@@ -18,6 +18,11 @@ export async function getThirdPartyAuthContext(urlParams) {
throw (e);
});
return {
thirdPartyAuthContext: camelCaseObject(convertKeyNames(data, { fullname: 'name' })),
fieldDescriptions: data.registration_fields || {},
// For backward compatibility with the API, once https://github.com/openedx/edx-platform/pull/30198 is merged
// and deployed update it to use data.context_data
thirdPartyAuthContext: camelCaseObject(
convertKeyNames(data.context_data || data, { fullname: 'name' }),
),
};
}

View File

@@ -26,7 +26,7 @@ describe('fetchThirdPartyAuthContext', () => {
it('should call service and dispatch success action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data }));
.mockImplementation(() => Promise.resolve({ thirdPartyAuthContext: data, fieldDescriptions: {} }));
const dispatched = [];
await runSaga(
@@ -38,7 +38,7 @@ describe('fetchThirdPartyAuthContext', () => {
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
actions.getThirdPartyAuthContextSuccess(data),
actions.getThirdPartyAuthContextSuccess({}, data),
]);
getThirdPartyAuthContext.mockClear();
});

View File

@@ -6,7 +6,17 @@ import { ExpandMore } from '@edx/paragon/icons';
const FormFieldRenderer = (props) => {
let formField = null;
const { fieldData, onChangeHandler, value } = props;
const {
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': {
@@ -14,63 +24,95 @@ const FormFieldRenderer = (props) => {
return null;
}
formField = (
<Form.Group controlId={fieldData.name} className="mb-3">
<Form.Group controlId={fieldData.name} isInvalid={isRequired && errorMessage}>
<Form.Control
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} className="mb-3">
<Form.Group controlId={fieldData.name} isInvalid={isRequired && errorMessage}>
<Form.Control
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} className="mb-3">
<Form.Group controlId={fieldData.name} isInvalid={isRequired && errorMessage}>
<Form.Control
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 className="mb-3">
<Form.Group isInvalid={isRequired && errorMessage}>
<Form.Checkbox
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;
@@ -83,6 +125,10 @@ const FormFieldRenderer = (props) => {
};
FormFieldRenderer.defaultProps = {
value: '',
handleBlur: null,
handleFocus: null,
errorMessage: '',
isRequired: false,
};
FormFieldRenderer.propTypes = {
@@ -92,6 +138,10 @@ FormFieldRenderer.propTypes = {
name: PropTypes.string,
}).isRequired,
onChangeHandler: PropTypes.func.isRequired,
handleBlur: PropTypes.func,
handleFocus: PropTypes.func,
errorMessage: PropTypes.string,
isRequired: PropTypes.bool,
value: PropTypes.string,
};

View File

@@ -102,4 +102,96 @@ describe('FieldRendererTests', () => {
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

@@ -97,11 +97,11 @@ const ForgotPasswordPage = (props) => {
{ siteName: getConfig().SITE_NAME })}
</title>
</Helmet>
<Form className="mw-xs">
<Form id="forget-password-form" name="forget-password-form" className="mw-xs">
<ForgotPasswordAlert email={props.email} emailError={errors.email} status={status} />
<h4>
<h2 className="h4">
{intl.formatMessage(messages['forgot.password.page.heading'])}
</h4>
</h2>
<p className="mb-4">
{intl.formatMessage(messages['forgot.password.page.instructions'])}
</p>
@@ -116,6 +116,8 @@ const ForgotPasswordPage = (props) => {
helpText={[intl.formatMessage(messages['forgot.password.email.help.text'], { platformName })]}
/>
<StatefulButton
id="submit-forget-password"
name="submit-forget-password"
type="submit"
variant="brand"
className="login-button-width"
@@ -129,6 +131,7 @@ const ForgotPasswordPage = (props) => {
/>
<Hyperlink
id="forgot-password"
name="forgot-password"
className="ml-4 font-weight-500 text-body"
destination={supportUrl}
onClick={e => {

View File

@@ -118,6 +118,8 @@
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
@@ -129,7 +131,6 @@
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.button": "Create an account",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
@@ -153,6 +154,8 @@
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
@@ -173,7 +176,7 @@
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",

View File

@@ -118,6 +118,8 @@
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
@@ -129,7 +131,6 @@
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.button": "Create an account",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
@@ -153,6 +154,8 @@
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
@@ -173,7 +176,7 @@
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",

View File

@@ -118,6 +118,8 @@
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "Al crear una cuenta, aceptas el {tosAndHonorCode} y reconoces que {platformName} y cada\n Miembro procesa tus datos personales de acuerdo con la {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Nombre completo",
"registration.email.label": "Correo electrónico",
@@ -129,7 +131,6 @@
"help.text.username.1": "El nombre que te identificará en tus cursos.",
"help.text.username.2": "Esto no puede modificarse posteriormente.",
"help.text.email": "Para la activación de la cuenta y las actualizaciones importantes",
"create.account.button": "Crear una cuenta",
"create.account.for.free.button": "Crea una cuenta gratis",
"create.an.account.btn.pending.state": "Cargando",
"registration.other.options.heading": "O regístrese con:",
@@ -153,6 +154,8 @@
"registration.tpa.session.expired": "Inscripción usando {provider} ha expirado.",
"terms.of.service.and.honor.code": "Condiciones de servicio y código de honor",
"privacy.policy": "Política de privacidad ",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Año de nacimiento (opcional)",
"registration.field.gender.options.label": "Género (opcional)",
"registration.goals.label": "Díganos por qué estás interesado en edX (opcional)",
@@ -173,7 +176,7 @@
"registration.using.tpa.form.heading": "Termina de crear tu cuenta",
"did.you.mean.alert.text": "¿Quieres decir",
"certificate.msg": "*No se incluyen los programas de: MicroMasters en Analytics: Essential Tools and Methods de GTx, Certificación Profesional de Corporate Finance de ColumbiaX o cursos o programas ofrecidos por Wharton y NYIF en esta oferta.",
"register.page.terms.of.service.and.honor.code": "Al crear una cuenta, aceptas el {tosAndHonorCode} y reconoces que {platformName} y cada\n Miembro procesa tus datos personales de acuerdo con la {privacyPolicy}.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Iniciar sesión",
"reset.password.page.title": "Restablecer contraseña | {siteName}",
"reset.password": "Restablecer mi contraseña",

View File

@@ -112,12 +112,14 @@
"login.form.invalid.error.message": "Veuillez remplir les champs ci-dessous.",
"login.incorrect.credentials.error.reset.link.text": "réinitialiser votre mot de passe",
"login.incorrect.credentials.error.before.account.blocked.text": "cliquez ici pour le réinitialiser.",
"password.security.nudge.title": "Password security",
"password.security.block.title": "Password change required",
"password.security.nudge.body": "Our system detected that your password is vulnerable. We recommend you change it so that your account stays secure.",
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"password.security.nudge.title": "Sécurité du mot de passe",
"password.security.block.title": "Changement de mot de passe requis",
"password.security.nudge.body": "Notre système a détecté que votre mot de passe est vulnérable. Nous vous recommandons de le modifier afin que votre compte reste sécurisé.",
"password.security.block.body": "Notre système a détecté que votre mot de passe est vulnérable. Changez votre mot de passe afin que votre compte reste sécurisé.",
"password.security.close.button": "Fermer",
"password.security.redirect.to.reset.password.button": "Réinitialiser votre mot de passe",
"register.page.terms.of.service.and.honor.code": "En créant un compte, vous acceptez le {tosAndHonorCode} et vous reconnaissez que {platformName} et chaque\n membre peut traiter vos données personnelles conformément à la {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "S'inscrire | {siteName}",
"registration.fullname.label": "Nom complet",
"registration.email.label": "Email",
@@ -129,7 +131,6 @@
"help.text.username.1": "Le nom qui vous identifiera dans vos cours.",
"help.text.username.2": "Cela ne peut pas être modifié ultérieurement.",
"help.text.email": "Pour l'activation du compte et les mises à jour importantes",
"create.account.button": "Créer un compte",
"create.account.for.free.button": "Créer un compte gratuitement",
"create.an.account.btn.pending.state": "Chargement en cours",
"registration.other.options.heading": "Ou inscrivez-vous avec :",
@@ -153,6 +154,8 @@
"registration.tpa.session.expired": "L'inscription avec {provider} a échouée.",
"terms.of.service.and.honor.code": "Conditions d'utilisation et Code d'honneur",
"privacy.policy": "Politique de confidentialité",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Année de naissance (facultatif)",
"registration.field.gender.options.label": "Sexe (facultatif)",
"registration.goals.label": "Dites-nous pourquoi vous êtes intéressé par edX (facultatif)",
@@ -173,7 +176,7 @@
"registration.using.tpa.form.heading": "Terminer la création de votre compte",
"did.you.mean.alert.text": "Vouliez-vous dire",
"certificate.msg": "*L'offre n'est pas éligible au programme GTx Analytics: Essential Tools and Methods MicroMasters, au programme de certificat professionnel en finance d'entreprise de ColumbiaX, ni aux cours ou programmes proposés par Wharton et NYIF.",
"register.page.terms.of.service.and.honor.code": "En créant un compte, vous acceptez le {tosAndHonorCode} et vous reconnaissez que {platformName} et chaque\n membre peut traiter vos données personnelles conformément à la {privacyPolicy}.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Connectez-vous",
"reset.password.page.title": "Réinitialiser le mot de passe | {siteName}",
"reset.password": "Réinitialiser le mot de passe",

View File

@@ -118,6 +118,8 @@
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
@@ -129,7 +131,6 @@
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.button": "Create an account",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
@@ -153,6 +154,8 @@
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
@@ -173,7 +176,7 @@
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",

View File

@@ -118,6 +118,8 @@
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Registrazione | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
@@ -129,7 +131,6 @@
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.button": "Create an account",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
@@ -153,6 +154,8 @@
"registration.tpa.session.expired": "La registrazione mediante {provider} è andata in timeout.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Anno di nascita (facoltativo)",
"registration.field.gender.options.label": "Genere (facoltativo)",
"registration.goals.label": "Dicci perché sei interessato a edX (facoltativo)",
@@ -173,7 +176,7 @@
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Ripristina password | {siteName}",
"reset.password": "Reset password",

View File

@@ -118,6 +118,8 @@
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Registar | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
@@ -129,7 +131,6 @@
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.button": "Create an account",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
@@ -153,6 +154,8 @@
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Termos de Serviço e Código de Honra",
"privacy.policy": "Política de Privacidade",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Ano de Nascimento (opcional)",
"registration.field.gender.options.label": "Género (opcional)",
"registration.goals.label": "Diga-nos porque está interessado no edX (opcional)",
@@ -173,7 +176,7 @@
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Redefinir Palavra-passe | {siteName}",
"reset.password": "Reset password",

View File

@@ -118,6 +118,8 @@
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
@@ -129,7 +131,6 @@
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.button": "Create an account",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
@@ -153,6 +154,8 @@
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
@@ -173,7 +176,7 @@
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",

View File

@@ -118,6 +118,8 @@
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
@@ -129,7 +131,6 @@
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.button": "Create an account",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
@@ -153,6 +154,8 @@
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
@@ -173,7 +176,7 @@
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",

View File

@@ -118,6 +118,8 @@
"password.security.block.body": "Our system detected that your password is vulnerable. Change your password so that your account stays secure.",
"password.security.close.button": "Close",
"password.security.redirect.to.reset.password.button": "Reset your password",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.honor.code": "I agree to the {platformName} {tosAndHonorCode}",
"register.page.title": "Register | {siteName}",
"registration.fullname.label": "Full name",
"registration.email.label": "Email",
@@ -129,7 +131,6 @@
"help.text.username.1": "The name that will identify you in your courses.",
"help.text.username.2": "This can not be changed later.",
"help.text.email": "For account activation and important updates",
"create.account.button": "Create an account",
"create.account.for.free.button": "Create an account for free",
"create.an.account.btn.pending.state": "Loading",
"registration.other.options.heading": "Or register with:",
@@ -153,6 +154,8 @@
"registration.tpa.session.expired": "Registration using {provider} has timed out.",
"terms.of.service.and.honor.code": "Terms of Service and Honor Code",
"privacy.policy": "Privacy Policy",
"honor.code": "Honor Code",
"terms.of.service": "Terms of Service",
"registration.year.of.birth.label": "Year of birth (optional)",
"registration.field.gender.options.label": "Gender (optional)",
"registration.goals.label": "Tell us why you're interested in edX (optional)",
@@ -173,7 +176,7 @@
"registration.using.tpa.form.heading": "Finish creating your account",
"did.you.mean.alert.text": "Did you mean",
"certificate.msg": "*Offer not eligible for GTxs Analytics: Essential Tools and Methods MicroMasters Program, ColumbiaXs Corporate Finance Professional Certificate Program, or courses or programs offered by Wharton, and NYIF.",
"register.page.terms.of.service.and.honor.code": "By creating an account, you agree to the {tosAndHonorCode} and you acknowledge that {platformName} and each\n Member process your personal data in accordance with the {privacyPolicy}.",
"register.page.terms.of.service": "I agree to the {platformName} {termsOfService}",
"sign.in": "Sign in",
"reset.password.page.title": "Reset Password | {siteName}",
"reset.password": "Reset password",

View File

@@ -41,6 +41,7 @@ initialize({
MARKETING_EMAILS_OPT_IN: process.env.MARKETING_EMAILS_OPT_IN || '',
ENABLE_COPPA_COMPLIANCE: process.env.ENABLE_COPPA_COMPLIANCE || '',
SHOW_DYNAMIC_PROFILING_PAGE: process.env.SHOW_DYNAMIC_PROFILING_PAGE || false,
ENABLE_DYNAMIC_REGISTRATION_FIELDS: process.env.ENABLE_DYNAMIC_REGISTRATION_FIELDS || false,
});
},
},

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getAuthService } from '@edx/frontend-platform/auth';
import { Alert, Hyperlink } from '@edx/paragon';
import { Error } from '@edx/paragon/icons';
import PropTypes from 'prop-types';
@@ -24,6 +25,7 @@ import ChangePasswordPrompt from './ChangePasswordPrompt';
const LoginFailureMessage = (props) => {
const { intl } = props;
const { context, errorCode, value } = props.loginError;
const authService = getAuthService();
let errorList;
let link;
let resetLink = (
@@ -135,6 +137,10 @@ const LoginFailureMessage = (props) => {
}
break;
case NUDGE_PASSWORD_CHANGE:
// Need to clear the CSRF token here to fetch a new one because token is already rotated after successful login.
if (authService) {
authService.getCsrfTokenService().clearCsrfTokenCache();
}
return (
<ChangePasswordPrompt
redirectUrl={props.loginError.redirectUrl}

View File

@@ -224,7 +224,7 @@ class LoginPage extends React.Component {
{submitState === DEFAULT_STATE && this.state.isSubmitted ? windowScrollTo({ left: 0, top: 0, behavior: 'smooth' }) : null}
{activationMsgType && <AccountActivationMessage messageType={activationMsgType} />}
{this.props.resetPassword && !this.props.loginError ? <ResetPasswordSuccess /> : null}
<Form>
<Form name="sign-in-form" id="sign-in-form">
<FormGroup
name="emailOrUsername"
value={this.state.emailOrUsername}
@@ -243,6 +243,8 @@ class LoginPage extends React.Component {
floatingLabel={intl.formatMessage(messages['login.password.label'])}
/>
<StatefulButton
name="sign-in"
id="sign-in"
type="submit"
variant="brand"
className="login-button-width"
@@ -256,6 +258,7 @@ class LoginPage extends React.Component {
/>
<Link
id="forgot-password"
name="forgot-password"
className="btn btn-link font-weight-500 text-body"
to={updatePathWithQueryParams(RESET_PAGE)}
onClick={this.handleForgotPasswordLinkClickEvent}

View File

@@ -3,6 +3,7 @@ import { mount } from 'enzyme';
import { MemoryRouter } from 'react-router-dom';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import * as auth from '@edx/frontend-platform/auth';
import LoginFailureMessage from '../LoginFailure';
import {
@@ -17,6 +18,9 @@ import {
REQUIRE_PASSWORD_CHANGE,
} from '../data/constants';
jest.mock('@edx/frontend-platform/auth');
auth.getAuthService = jest.fn();
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
describe('LoginFailureMessage', () => {

View File

@@ -9,6 +9,7 @@ import renderer from 'react-test-renderer';
import CookiePolicyBanner from '@edx/frontend-component-cookie-policy-banner';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics';
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { loginRequest, loginRequestFailure, loginRequestReset } from '../data/actions';
@@ -18,9 +19,11 @@ import LoginPage from '../LoginPage';
import { COMPLETE_STATE, PENDING_STATE } from '../../data/constants';
jest.mock('@edx/frontend-platform/analytics');
jest.mock('@edx/frontend-platform/auth');
analytics.sendTrackEvent = jest.fn();
analytics.sendPageEvent = jest.fn();
auth.getAuthService = jest.fn();
const IntlLoginFailureMessage = injectIntl(LoginFailureMessage);
const IntlLoginPage = injectIntl(LoginPage);

View File

@@ -176,7 +176,7 @@ class CountryDropdown extends React.Component {
render() {
return (
<div>
<div className="mb-4">
<FormGroup
as="input"
name={this.props.name}

View File

@@ -0,0 +1,88 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink, Form } from '@edx/paragon';
import messages from './messages';
const HonorCode = (props) => {
const {
intl, errorMessage, onChangeHandler, fieldType, value,
} = props;
if (fieldType === 'tos_and_honor_code') {
return (
<div id="honor-code" className="micro text-muted mt-4">
<FormattedMessage
id="register.page.terms.of.service.and.honor.code"
defaultMessage="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}."
description="Text that appears on registration form stating honor code and privacy policy"
values={{
platformName: getConfig().SITE_NAME,
tosAndHonorCode: (
<Hyperlink variant="muted" destination={getConfig().TOS_AND_HONOR_CODE || '#'} target="_blank">
{intl.formatMessage(messages['terms.of.service.and.honor.code'])}
</Hyperlink>
),
privacyPolicy: (
<Hyperlink variant="muted" destination={getConfig().PRIVACY_POLICY || '#'} target="_blank">
{intl.formatMessage(messages['privacy.policy'])}
</Hyperlink>
),
}}
/>
</div>
);
}
return (
<div id="honor-code" className="micro text-muted">
<Form.Checkbox
className="opt-checkbox mt-1"
id="honor-code"
checked={value}
name="honor_code"
value={value}
onChange={onChangeHandler}
>
<FormattedMessage
id="register.page.honor.code"
defaultMessage="I agree to the {platformName} {tosAndHonorCode}"
description="Text that appears on registration form stating honor code"
values={{
platformName: getConfig().SITE_NAME,
tosAndHonorCode: (
<Hyperlink variant="muted" destination={getConfig().TOS_AND_HONOR_CODE || '#'} target="_blank">
{intl.formatMessage(messages['honor.code'])}
</Hyperlink>
),
}}
/>
</Form.Checkbox>
{errorMessage && (
<Form.Control.Feedback type="invalid" className="form-text-size" hasIcon={false}>
{errorMessage}
</Form.Control.Feedback>
)}
</div>
);
};
HonorCode.defaultProps = {
errorMessage: '',
onChangeHandler: null,
fieldType: 'honor_code',
value: false,
};
HonorCode.propTypes = {
intl: intlShape.isRequired,
errorMessage: PropTypes.string,
onChangeHandler: PropTypes.func,
fieldType: PropTypes.string,
value: PropTypes.bool,
};
export default injectIntl(HonorCode);

View File

@@ -4,37 +4,44 @@ import { connect } from 'react-redux';
import Skeleton from 'react-loading-skeleton';
import { Helmet } from 'react-helmet';
import PropTypes, { string } from 'prop-types';
import classNames from 'classnames';
import { getConfig } from '@edx/frontend-platform';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import {
injectIntl, intlShape, getCountryList, getLocale, FormattedMessage,
injectIntl, intlShape, getCountryList, getLocale,
} from '@edx/frontend-platform/i18n';
import {
Alert, Form, Hyperlink, StatefulButton, Icon,
Alert, Form, StatefulButton, Icon,
} from '@edx/paragon';
import { Error, Close } from '@edx/paragon/icons';
import FormFieldRenderer from '../field-renderer';
import {
clearUsernameSuggestions, registerNewUser, resetRegistrationForm, fetchRealtimeValidations,
} from './data/actions';
import {
FORM_SUBMISSION_ERROR, DEFAULT_SERVICE_PROVIDER_DOMAINS, DEFAULT_TOP_LEVEL_DOMAINS, COMMON_EMAIL_PROVIDERS,
FIELDS, FORM_SUBMISSION_ERROR, DEFAULT_SERVICE_PROVIDER_DOMAINS, DEFAULT_TOP_LEVEL_DOMAINS, COMMON_EMAIL_PROVIDERS,
} from './data/constants';
import {
registrationErrorSelector, registrationRequestSelector, validationsSelector, usernameSuggestionsSelector,
registrationErrorSelector,
registrationRequestSelector,
validationsSelector,
usernameSuggestionsSelector,
} from './data/selectors';
import messages from './messages';
import RegistrationFailure from './RegistrationFailure';
import UsernameField from './UsernameField';
import HonorCode from './HonorCode';
import {
RedirectLogistration, SocialAuthProviders, ThirdPartyAuthAlert, RenderInstitutionButton,
InstitutionLogistration, FormGroup, PasswordField,
} from '../common-components';
import { getThirdPartyAuthContext } from '../common-components/data/actions';
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
import {
extendedProfileSelector,
fieldDescriptionSelector,
thirdPartyAuthContextSelector,
} from '../common-components/data/selectors';
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import {
DEFAULT_STATE, PENDING_STATE, REGISTER_PAGE, VALID_EMAIL_REGEX, LETTER_REGEX, NUMBER_REGEX, VALID_NAME_REGEX,
@@ -44,6 +51,7 @@ import {
} from '../data/utils';
import CountryDropdown from './CountryDropdown';
import { getLevenshteinSuggestion, getSuggestionForInvalidEmail } from './utils';
import TermsOfService from './TermsOfService';
class RegistrationPage extends React.Component {
constructor(props, context) {
@@ -52,6 +60,9 @@ class RegistrationPage extends React.Component {
this.handleOnClose = this.handleOnClose.bind(this);
this.queryParams = getAllPossibleQueryParam();
// TODO: Once we have tested it and ready for openedX we can remove this flag and make the code
// permanent part of Authn and remove extra code
this.showDynamicRegistrationFields = getConfig().ENABLE_DYNAMIC_REGISTRATION_FIELDS;
this.tpaHint = getTpaHint();
this.state = {
country: '',
@@ -76,8 +87,7 @@ class RegistrationPage extends React.Component {
optimizelyExperimentName: '',
readOnly: true,
validatePassword: false,
// TODO: Remove after VAN-876 experimentation is complete.
registerRenameExpVariation: '',
values: {},
};
}
@@ -90,13 +100,6 @@ class RegistrationPage extends React.Component {
isActive: true,
});
if (payload.register_for_free === 'true') {
window.optimizely.push({
type: 'event',
eventName: 'van-876-authn-registration-page',
});
}
if (payload.save_for_later === 'true') {
sendTrackEvent('edx.bi.user.saveforlater.course.enroll.clicked', { category: 'save-for-later' });
}
@@ -174,27 +177,35 @@ class RegistrationPage extends React.Component {
}
getExperiments = () => {
const { experimentName, renameRegisterExperiment } = window;
const { experimentName } = window;
if (experimentName) {
this.setState({ optimizelyExperimentName: experimentName });
}
};
if (renameRegisterExperiment) {
this.setState({ registerRenameExpVariation: renameRegisterExperiment });
onChangeHandler = (e) => {
const { name, value, checked } = e.target;
const { errors, values } = this.state;
if (e.target.type === 'checkbox') {
errors[name] = '';
values[name] = checked;
} else {
values[name] = value;
}
const state = { errors, values };
this.setState({ ...state });
};
handleSubmit = (e) => {
e.preventDefault();
const { startTime } = this.state;
const totalRegistrationTime = (Date.now() - startTime) / 1000;
const payload = {
const dynamicFieldErrorMessages = {};
let payload = {
name: this.state.name,
username: this.state.username,
email: this.state.email,
country: this.state.country,
honor_code: true,
is_authn_mfe: true,
};
@@ -204,7 +215,28 @@ class RegistrationPage extends React.Component {
payload.password = this.state.password;
}
if (!this.isFormValid(payload)) {
if (this.showDynamicRegistrationFields) {
payload.extendedProfile = [];
Object.keys(this.props.fieldDescriptions).forEach((fieldName) => {
if (this.props.extendedProfile.includes(fieldName)) {
payload.extendedProfile.push({ fieldName, fieldValue: this.state.values[fieldName] });
} else {
payload[fieldName] = this.state.values[fieldName];
}
dynamicFieldErrorMessages[fieldName] = this.props.fieldDescriptions[fieldName].error_message;
});
if (
this.props.fieldDescriptions[FIELDS.HONOR_CODE]
&& this.props.fieldDescriptions[FIELDS.HONOR_CODE].type === 'tos_and_honor_code'
) {
payload[FIELDS.HONOR_CODE] = true;
}
} else {
payload.country = this.state.country;
payload.honor_code = true;
}
if (!this.isFormValid(payload, dynamicFieldErrorMessages)) {
this.setState(prevState => ({
errorCode: FORM_SUBMISSION_ERROR,
failureCount: prevState.failureCount + 1,
@@ -216,6 +248,7 @@ class RegistrationPage extends React.Component {
payload.marketing_emails_opt_in = this.state.marketingOptIn;
}
payload = snakeCaseObject(payload);
payload.totalRegistrationTime = totalRegistrationTime;
this.setState({
totalRegistrationTime,
@@ -296,13 +329,22 @@ class RegistrationPage extends React.Component {
this.props.clearUsernameSuggestions();
}
isFormValid(payload) {
validateDynamicFields = (e) => {
const { errors } = this.state;
const { name, value } = e.target;
if (!value) {
errors[name] = this.props.fieldDescriptions[name].error_message;
}
this.setState({ errors });
}
isFormValid(payload, dynamicFieldError) {
const { errors } = this.state;
let isValid = true;
Object.keys(payload).forEach(key => {
if (!payload[key]) {
errors[key] = this.props.intl.formatMessage(messages[`empty.${key}.field.error`]);
errors[key] = (key in dynamicFieldError) ? dynamicFieldError[key] : this.props.intl.formatMessage(messages[`empty.${key}.field.error`]);
}
// Mark form invalid, if there was already a validation error for this key or we added empty field error
if (errors[key]) {
@@ -523,7 +565,6 @@ class RegistrationPage extends React.Component {
setSurveyCookie('register');
setCookie(getConfig().REGISTER_CONVERSION_COOKIE_NAME, true);
setCookie('authn-returning-user');
const payload = { ...this.queryParams };
// Fire optimizely events
window.optimizely = window.optimizely || [];
@@ -534,15 +575,75 @@ class RegistrationPage extends React.Component {
value: this.state.totalRegistrationTime,
},
});
if (payload.register_for_free === 'true') {
window.optimizely.push({
type: 'event',
eventName: 'van-876-authn-register-for-free-conversion',
});
}
}
const honorCode = [];
const formFields = this.showDynamicRegistrationFields ? (
Object.keys(this.props.fieldDescriptions).map((fieldName) => {
const fieldData = this.props.fieldDescriptions[fieldName];
switch (fieldData.name) {
case FIELDS.COUNTRY:
return (
<span key={fieldData.name}>
<CountryDropdown
name="country"
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
options={getCountryList(getLocale())}
valueKey="code"
displayValueKey="name"
value={this.state.values[fieldData.name]}
handleBlur={this.handleOnBlur}
handleFocus={this.handleOnFocus}
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
handleChange={
(value) => this.setState(prevState => ({ values: { ...prevState.values, country: value } }))
}
errorCode={this.state.errorCode}
readOnly={this.state.readOnly}
/>
</span>
);
case FIELDS.HONOR_CODE:
honorCode.push(
<span key={fieldData.name}>
<HonorCode
fieldType={fieldData.type}
value={this.state.values[fieldData.name]}
onChangeHandler={this.onChangeHandler}
errorMessage={this.state.errors[fieldData.name]}
/>
</span>,
);
return null;
case FIELDS.TERMS_OF_SERVICE:
honorCode.push(
<span key={fieldData.name}>
<TermsOfService
value={this.state.values[fieldData.name]}
onChangeHandler={this.onChangeHandler}
errorMessage={this.state.errors[fieldData.name]}
/>
</span>,
);
return null;
default:
return (
<span key={fieldData.name}>
<FormFieldRenderer
fieldData={fieldData}
value={this.state.values[fieldData.name]}
onChangeHandler={this.onChangeHandler}
handleBlur={this.validateDynamicFields}
handleFocus={this.handleOnFocus}
errorMessage={this.state.errors[fieldData.name]}
isRequired
/>
</span>
);
}
})
) : null;
return (
<>
<Helmet>
@@ -574,7 +675,7 @@ class RegistrationPage extends React.Component {
<h4 className="mt-4 mb-4">{intl.formatMessage(messages['registration.using.tpa.form.heading'])}</h4>
</>
)}
<Form>
<Form id="registration-form" name="registration-form">
<FormGroup
name="name"
value={this.state.name}
@@ -624,20 +725,24 @@ class RegistrationPage extends React.Component {
floatingLabel={intl.formatMessage(messages['registration.password.label'])}
/>
)}
<CountryDropdown
name="country"
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
options={getCountryList(getLocale())}
valueKey="code"
displayValueKey="name"
value={this.state.country}
handleBlur={this.handleOnBlur}
handleFocus={this.handleOnFocus}
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
handleChange={(value) => this.setState({ country: value })}
errorCode={this.state.errorCode}
readOnly={this.state.readOnly}
/>
{!(this.showDynamicRegistrationFields)
&& (
<CountryDropdown
name="country"
floatingLabel={intl.formatMessage(messages['registration.country.label'])}
options={getCountryList(getLocale())}
valueKey="code"
displayValueKey="name"
value={this.state.country}
handleBlur={this.handleOnBlur}
handleFocus={this.handleOnFocus}
errorMessage={intl.formatMessage(messages['empty.country.field.error'])}
handleChange={(value) => this.setState({ country: value })}
errorCode={this.state.errorCode}
readOnly={this.state.readOnly}
/>
)}
{formFields}
{(getConfig().MARKETING_EMAILS_OPT_IN)
&& (
<Form.Checkbox
@@ -649,40 +754,20 @@ class RegistrationPage extends React.Component {
{intl.formatMessage(messages['registration.opt.in.label'], { siteName: getConfig().SITE_NAME })}
</Form.Checkbox>
)}
<div id="honor-code" className="micro text-muted mt-4">
<FormattedMessage
id="register.page.terms.of.service.and.honor.code"
defaultMessage="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}."
description="Text that appears on registration form stating honor code and privacy policy"
values={{
platformName: getConfig().SITE_NAME,
tosAndHonorCode: (
<Hyperlink variant="muted" destination={getConfig().TOS_AND_HONOR_CODE || '#'} target="_blank">
{intl.formatMessage(messages['terms.of.service.and.honor.code'])}
</Hyperlink>
),
privacyPolicy: (
<Hyperlink variant="muted" destination={getConfig().PRIVACY_POLICY || '#'} target="_blank">
{intl.formatMessage(messages['privacy.policy'])}
</Hyperlink>
),
}}
{!(this.showDynamicRegistrationFields) ? (
<HonorCode
fieldType="tos_and_honor_code"
/>
</div>
) : <div className="mt-4">{honorCode}</div>}
<StatefulButton
name="register-user"
id="register-user"
type="submit"
variant="brand"
className={classNames(
'mt-4 mb-4',
{ 'stateful-button-variation1-width': this.state.registerRenameExpVariation === 'variation1' },
{ 'stateful-button-width': this.state.registerRenameExpVariation !== 'variation1' },
)}
className="register-stateful-button-width mt-4 mb-4"
state={submitState}
labels={{
default: this.state.registerRenameExpVariation === 'variation1' ? (
intl.formatMessage(messages['create.account.for.free.button'])
) : intl.formatMessage(messages['create.account.button']),
default: intl.formatMessage(messages['create.account.for.free.button']),
pending: '',
}}
onClick={this.handleSubmit}
@@ -745,6 +830,8 @@ class RegistrationPage extends React.Component {
}
RegistrationPage.defaultProps = {
extendedProfile: [],
fieldDescriptions: {},
registrationResult: null,
registerNewUser: null,
registrationErrorCode: null,
@@ -764,6 +851,8 @@ RegistrationPage.defaultProps = {
};
RegistrationPage.propTypes = {
extendedProfile: PropTypes.arrayOf(PropTypes.string),
fieldDescriptions: PropTypes.shape({}),
intl: intlShape.isRequired,
getThirdPartyAuthContext: PropTypes.func.isRequired,
registerNewUser: PropTypes.func,
@@ -817,6 +906,8 @@ const mapStateToProps = state => {
validationDecisions: validationsSelector(state),
statusCode: state.register.statusCode,
usernameSuggestions: usernameSuggestionsSelector(state),
fieldDescriptions: fieldDescriptionSelector(state),
extendedProfile: extendedProfileSelector(state),
};
};

View File

@@ -0,0 +1,60 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink, Form } from '@edx/paragon';
import messages from './messages';
const TermsOfService = (props) => {
const {
intl, errorMessage, onChangeHandler, value,
} = props;
return (
<div id="terms-of-service" className="micro text-muted">
<Form.Checkbox
className="opt-checkbox mt-1"
id="tos"
checked={value}
name="terms_of_service"
value={value}
onChange={onChangeHandler}
>
<FormattedMessage
id="register.page.terms.of.service"
defaultMessage="I agree to the {platformName} {termsOfService}"
description="Text that appears on registration form stating terms of service.
It is a legal document that users must agree to."
values={{
platformName: getConfig().SITE_NAME,
termsOfService: (
<Hyperlink variant="muted" destination={getConfig().TOS_LINK || '#'} target="_blank">
{intl.formatMessage(messages['terms.of.service'])}
</Hyperlink>
),
}}
/>
</Form.Checkbox>
{errorMessage && (
<Form.Control.Feedback type="invalid" className="form-text-size" hasIcon={false}>
{errorMessage}
</Form.Control.Feedback>
)}
</div>
);
};
TermsOfService.defaultProps = {
errorMessage: '',
value: false,
};
TermsOfService.propTypes = {
intl: intlShape.isRequired,
errorMessage: PropTypes.string,
onChangeHandler: PropTypes.func.isRequired,
value: PropTypes.bool,
};
export default injectIntl(TermsOfService);

View File

@@ -1,3 +1,10 @@
// Registration Fields
export const FIELDS = {
COUNTRY: 'country',
HONOR_CODE: 'honor_code',
TERMS_OF_SERVICE: 'terms_of_service',
};
// Registration Error Codes
export const FORBIDDEN_REQUEST = 'forbidden-request';
export const FORM_SUBMISSION_ERROR = 'form-submission-error';

View File

@@ -1,8 +1,14 @@
import {
REGISTRATION_FORM, REGISTER_NEW_USER, REGISTER_FORM_VALIDATIONS, REGISTER_CLEAR_USERNAME_SUGGESTIONS,
REGISTRATION_FORM,
REGISTER_NEW_USER,
REGISTER_FORM_VALIDATIONS,
REGISTER_CLEAR_USERNAME_SUGGESTIONS,
} from './actions';
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
import {
DEFAULT_STATE,
PENDING_STATE,
} from '../../data/constants';
export const defaultState = {
registrationError: {},
@@ -11,6 +17,9 @@ export const defaultState = {
validations: null,
statusCode: null,
usernameSuggestions: [],
extendedProfile: [],
fieldDescriptions: {},
formRenderState: DEFAULT_STATE,
};
const reducer = (state = defaultState, action) => {

View File

@@ -55,7 +55,6 @@ export function* fetchRealtimeValidations(action) {
}
}
}
export default function* saga() {
yield takeEvery(REGISTER_NEW_USER.BASE, handleNewUserRegistration);
yield takeEvery(REGISTER_FORM_VALIDATIONS.BASE, fetchRealtimeValidations);

View File

@@ -14,6 +14,9 @@ describe('register reducer', () => {
validations: null,
statusCode: null,
usernameSuggestions: [],
extendedProfile: [],
fieldDescriptions: {},
formRenderState: DEFAULT_STATE,
},
);
});

View File

@@ -59,11 +59,6 @@ const messages = defineMessages({
description: 'Help text for email field on registration page',
},
// Form buttons
'create.account.button': {
id: 'create.account.button',
defaultMessage: 'Create an account',
description: 'Button label that appears on register page',
},
'create.account.for.free.button': {
id: 'create.account.for.free.button',
defaultMessage: 'Create an account for free',
@@ -193,6 +188,16 @@ const messages = defineMessages({
defaultMessage: 'Privacy Policy',
description: 'Text for the hyperlink that redirects user to privacy policy',
},
'honor.code': {
id: 'honor.code',
defaultMessage: 'Honor Code',
description: 'Text for the hyperlink that redirects user to the honor code',
},
'terms.of.service': {
id: 'terms.of.service',
defaultMessage: 'Terms of Service',
description: 'Text for the hyperlink that redirects user to the terms of service',
},
// Optional fields
'registration.year.of.birth.label': {
id: 'registration.year.of.birth.label',

View File

@@ -0,0 +1,59 @@
import React from 'react';
import { mount } from 'enzyme';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mergeConfig } from '@edx/frontend-platform';
import HonorCode from '../HonorCode';
const IntlHonorCode = injectIntl(HonorCode);
describe('HonorCodeTest', () => {
mergeConfig({
PRIVACY_POLICY: 'http://privacy-policy.com',
TOS_AND_HONOR_CODE: 'http://tos-and-honot-code.com',
});
let value = false;
const changeHandler = (e) => {
value = e.target.checked;
};
beforeEach(() => {
value = false;
});
it('should render error msg if honor code is not checked', () => {
const honorCode = mount(
<IntlProvider locale="en">
<IntlHonorCode errorMessage="You must agree to the edx Honor Code" onChangeHandler={changeHandler} />
</IntlProvider>,
);
expect(honorCode.find('.form-text-size').last().text()).toEqual('You must agree to the edx Honor Code');
});
it('should render Honor code field', () => {
const expectedMsg = 'I agree to the Your Platform Name Here Honor Codein a new tab';
const honorCode = mount(
<IntlProvider locale="en">
<IntlHonorCode onChangeHandler={changeHandler} />
</IntlProvider>,
);
honorCode.find('#honor-code').last().simulate('change', { target: { checked: true, type: 'checkbox' } });
expect(honorCode.find('#honor-code').find('label').text()).toEqual(expectedMsg);
expect(value).toEqual(true);
});
it('should render Terms of Service and Honor code field', () => {
const HonorCodeProps = mount(
<IntlProvider locale="en">
<IntlHonorCode fieldType="tos_and_honor_code" onChangeHandler={changeHandler} />
</IntlProvider>,
);
const expectedMsg = 'By creating an account, you agree to the Terms of Service and Honor Codein a new tab and you '
+ 'acknowledge that Your Platform Name Here and each Member process your personal data in '
+ 'accordance with the Privacy Policyin a new tab.';
const field = HonorCodeProps.find('#honor-code');
expect(field.text()).toEqual(expectedMsg);
});
});

View File

@@ -16,7 +16,9 @@ import {
registerNewUser,
resetRegistrationForm,
} from '../data/actions';
import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_SESSION_EXPIRED } from '../data/constants';
import {
FIELDS, FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR, TPA_SESSION_EXPIRED,
} from '../data/constants';
import RegistrationFailureMessage from '../RegistrationFailure';
import RegistrationPage from '../RegistrationPage';
@@ -404,7 +406,7 @@ describe('RegistrationPage', () => {
it('should match default button state', () => {
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual('Create an account');
expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual('Create an account for free');
});
it('should match pending button state', () => {
@@ -773,4 +775,87 @@ describe('RegistrationPage', () => {
});
});
});
describe('TestDynamicFields', () => {
it('should render fields returned by backend', () => {
mergeConfig({
ENABLE_DYNAMIC_REGISTRATION_FIELDS: true,
});
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
fieldDescriptions: {
country: { name: 'country', error_message: true },
profession: { name: 'profession', type: 'text', label: 'Profession' },
honor_code: { name: FIELDS.HONOR_CODE, error_message: 'You must agree to Honor Code of our site' },
terms_of_service: {
name: FIELDS.TERMS_OF_SERVICE,
error_message: 'You must agree to the Terms and Service agreement of our site',
},
},
},
});
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
expect(registerPage.find('#country').exists()).toBeTruthy();
expect(registerPage.find('#profession').exists()).toBeTruthy();
expect(registerPage.find('#honor-code').exists()).toBeTruthy();
expect(registerPage.find('#tos').exists()).toBeTruthy();
});
it('should submit form with fields returned by backend in payload', () => {
jest.spyOn(global.Date, 'now').mockImplementation(() => 0);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
fieldDescriptions: {
country: { name: 'country', error_message: true },
profession: { name: 'profession', type: 'text', label: 'Profession' },
honor_code: { name: 'honor_code', type: 'tos_and_honor_code' },
},
extendedProfile: ['profession'],
},
});
const payload = {
name: 'John Doe',
username: 'john_doe',
email: 'john.doe@example.com',
password: 'password1',
country: 'Pakistan',
totalRegistrationTime: 0,
is_authn_mfe: true,
honor_code: true,
extended_profile: [{ field_name: 'profession', field_value: 'Engineer' }],
};
store.dispatch = jest.fn(store.dispatch);
const registerPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
populateRequiredFields(registerPage, payload);
registerPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } });
registerPage.find('button.btn-brand').simulate('click');
expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' }));
});
it('should show error message for fields returned by backend', () => {
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
fieldDescriptions: {
profession: {
name: 'profession', type: 'text', label: 'Profession', error_message: 'Enter profession',
},
},
},
});
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
registrationPage.find('button.btn-brand').simulate('click');
expect(registrationPage.find('#profession-error').last().text()).toEqual('Enter profession');
});
});
});

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { mount } from 'enzyme';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { mergeConfig } from '@edx/frontend-platform';
import TermsOfService from '../TermsOfService';
const IntlTermsOfService = injectIntl(TermsOfService);
describe('TermsOfServiceTest', () => {
mergeConfig({
TOS_LINK: 'http://tos-and-honot-code.com',
});
let value = false;
const changeHandler = (e) => {
value = e.target.checked;
};
beforeEach(() => {
value = false;
});
it('should render error msg if Terms of Service checkbox is not checked', () => {
const errorMessage = 'You must agree to the edx Terms of Service';
const termsOfService = mount(
<IntlProvider locale="en">
<IntlTermsOfService errorMessage={errorMessage} onChangeHandler={changeHandler} />
</IntlProvider>,
);
expect(termsOfService.find('.form-text-size').last().text()).toEqual(errorMessage);
});
it('should render Terms of Service field', () => {
const termsOfService = mount(
<IntlProvider locale="en">
<IntlTermsOfService onChangeHandler={changeHandler} />
</IntlProvider>,
);
const expectedMsg = 'I agree to the Your Platform Name Here Terms of Servicein a new tab';
expect(termsOfService.find('#terms-of-service').find('label').text()).toEqual(expectedMsg);
expect(value).toEqual(false);
});
it('should change value when Terms of Service field is checked', () => {
const termsOfService = mount(
<IntlProvider locale="en">
<IntlTermsOfService onChangeHandler={changeHandler} />
</IntlProvider>,
);
const field = termsOfService.find('input#tos');
field.simulate('change', { target: { checked: true, type: 'checkbox' } });
expect(value).toEqual(true);
});
});

View File

@@ -174,7 +174,7 @@ const ResetPasswordPage = (props) => {
<ResetPasswordFailure errorCode={errorCode} errorMsg={props.errorMsg} />
<h4>{intl.formatMessage(messages['reset.password'])}</h4>
<p className="mb-4">{intl.formatMessage(messages['reset.password.page.instructions'])}</p>
<Form>
<Form id="set-reset-password-form" name="set-reset-password-form">
<PasswordField
name="newPassword"
value={newPassword}
@@ -194,6 +194,8 @@ const ResetPasswordPage = (props) => {
floatingLabel={intl.formatMessage(messages['confirm.password.label'])}
/>
<StatefulButton
id="submit-new-password"
name="submit-new-password"
type="submit"
variant="brand"
className="stateful-button-width"

View File

@@ -162,7 +162,7 @@ const WelcomePage = (props) => {
<p>{intl.formatMessage(messages['welcome.page.error.message'])}</p>
</Alert>
) : null}
<Form>
<Form id="welcome-page-profile-form" name="welcome-page-profile-form">
<Form.Group controlId="levelOfEducation">
<Form.Control
as="select"
@@ -217,6 +217,8 @@ const WelcomePage = (props) => {
</span>
<div className="d-flex mt-4">
<StatefulButton
name="submit-profile"
id="submit-profile"
type="submit"
variant="brand"
className="login-button-width"
@@ -229,6 +231,8 @@ const WelcomePage = (props) => {
onMouseDown={(e) => e.preventDefault()}
/>
<StatefulButton
id="skip-profile"
name="skip-profile"
className="text-gray-700 font-weight-500"
type="submit"
variant="link"