Compare commits

...

492 Commits

Author SHA1 Message Date
edX requirements bot
3a4e699507 feat: deprecated bable-plugin-react-intl in favour of babel-plugin-formatjs 2023-10-04 01:53:48 -04:00
Mashal Malik
40103a2386 refactor: add @openedx in renovate automate configuration (#886) 2023-10-02 16:39:21 +05:00
edx-transifex-bot
b6c18bb439 chore(i18n): update translations (#888)
Co-authored-by: Jenkins <sre+jenkins@edx.org>
2023-10-02 11:32:15 +05:00
renovate[bot]
ee51939f2d fix(deps): update dependency core-js to v3.33.0 2023-10-02 02:28:08 -04:00
renovate[bot]
fc127ccd98 chore(deps): update dependency @edx/frontend-build to v12.9.17 2023-10-02 02:27:36 -04:00
edx-transifex-bot
22faebac50 chore(i18n): update translations (#872)
Co-authored-by: Jenkins <sre+jenkins@edx.org>
2023-10-02 11:27:02 +05:00
renovate[bot]
5ce3995f5b fix(deps): update dependency regenerator-runtime to v0.14.0 2023-09-25 03:59:00 -04:00
renovate[bot]
e181269703 fix(deps): update dependency @edx/frontend-component-header to v4.6.1 2023-09-25 03:58:25 -04:00
renovate[bot]
5d212ec6b5 fix(deps): update dependency @edx/frontend-platform to v5.4.0 2023-09-25 04:21:30 +00:00
renovate[bot]
363096c4f0 fix(deps): update dependency form-urlencoded to v6.1.4 2023-09-25 00:21:09 -04:00
Dmytro
85c5902559 feat: add toggling for the hardcode support link (for master) (#864)
* feat: add toggling for the hardcode support link (master)

Add a toggling mechanism for the "unlink all social media
accounts" text to show it as a link or text depending on
the MFE env setting.

* fix(deps): update dependency @edx/frontend-platform to v5.3.0

* Revert "fix(deps): update dependency @edx/frontend-platform to v5.3.0" (#870)

This reverts commit 757e446be7.

* feat: add toggling for the hardcode support link (master)

Add a toggling mechanism for the "unlink all social media
accounts" text to show it as a link or text depending on
the MFE env setting.

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Muhammad Abdullah Waheed <42172960+abdullahwaheed@users.noreply.github.com>
2023-09-21 13:28:45 +05:00
edx-transifex-bot
fca6da2df7 chore(i18n): update translations (#877)
Co-authored-by: Jenkins <sre+jenkins@edx.org>
2023-09-19 11:18:14 +05:00
sundasnoreen12
7a74e5d29b Merge pull request #876 from openedx/sundas/INF-1061
fix: removed underline from course name
2023-09-18 13:53:44 +05:00
renovate[bot]
4b94344dd9 fix(deps): update dependency core-js to v3.32.2 2023-09-18 02:04:27 -04:00
renovate[bot]
5245254de5 fix(deps): update dependency form-urlencoded to v6.1.3 2023-09-18 02:04:07 -04:00
SundasNoreen
afda76ff11 fix: removed underline from course name 2023-09-12 16:56:49 +05:00
Muhammad Abdullah Waheed
da52ddd35a fix: bumped frontend-platform to latest version to fix ci issues (#875) 2023-09-11 19:25:24 +05:00
renovate[bot]
bb2aef3878 fix(deps): update dependency @edx/frontend-platform to v5.3.1 2023-09-11 02:38:15 -04:00
renovate[bot]
4d6f76d9b3 fix(deps): update dependency form-urlencoded to v6.1.2 2023-09-11 02:37:54 -04:00
Awais Ansari
869e083a2a Merge pull request #869 from openedx/sundas/INF-1016
feat: Added link to documentation on preferences UI
2023-09-06 16:36:35 +05:00
SundasNoreen
ee876b5c84 refactor: added paragon class 2023-09-05 23:11:27 +05:00
Muhammad Abdullah Waheed
a2b25449de Revert "fix(deps): update dependency @edx/frontend-platform to v5.3.0" (#870)
This reverts commit 757e446be7.
2023-09-05 20:24:23 +05:00
SundasNoreen
16218252f1 feat: Added link to documentation on preferences UI 2023-09-05 13:39:46 +05:00
renovate[bot]
757e446be7 fix(deps): update dependency @edx/frontend-platform to v5.3.0 2023-09-04 03:06:25 -04:00
Mashal Malik
db0f8f80bc refactor: update lock file version (#855) 2023-08-31 12:43:54 +05:00
Jonas Burigo Martins
49bc817f2d feat: Allow disable account deletion (#817)
* feat: allow disable account deletion

* test: add disable account deletion to test case

* style: fix lint errors

* docs: Add ENABLE_ACCOUNT_DELETION to README.rst
2023-08-28 15:40:48 +05:00
Bilal Qamar
c31beeef96 Revert "fix(deps): update dependency @tensorflow-models/blazeface to v0.1.0" (#861)
This reverts commit 09970d7935.
2023-08-28 14:33:52 +05:00
renovate[bot]
09970d7935 fix(deps): update dependency @tensorflow-models/blazeface to v0.1.0 2023-08-28 06:25:25 +00:00
renovate[bot]
d15b0baf74 fix(deps): update dependency @edx/frontend-component-footer to v12.2.1 2023-08-28 06:25:03 +00:00
edx-transifex-bot
b9802a130e chore(i18n): update translations (#856)
Co-authored-by: Jenkins <sre+jenkins@edx.org>
2023-08-28 11:20:41 +05:00
Syed Ali Abbas Zaidi
7c0ea75e21 refactor: remove history pacakge (#853)
* refactor: remove history pacakge

* chore: improve test coverage
2023-08-23 18:21:29 +05:00
edX requirements bot
e1b02de7de chore: update browserslist DB (#850)
Co-authored-by: abdullahwaheed <abdullahwaheed@users.noreply.github.com>
2023-08-23 15:52:19 +05:00
Muhammad Adeel Tajamul
6c4dbc5db0 fix: changed overlay position and updated notification info text (#849) 2023-08-22 10:46:14 +05:00
Awais Ansari
d180626122 Merge pull request #848 from openedx/aansari/INF-1018
fix: moved feedback widget behind env variable
2023-08-21 14:49:29 +05:00
renovate[bot]
a9518b7388 fix(deps): update dependency @edx/frontend-platform to v5.1.0 2023-08-21 05:17:34 +00:00
renovate[bot]
51b18e9c52 chore(deps): update dependency @edx/frontend-build to v12.9.10 2023-08-21 05:17:18 +00:00
Syed Ali Abbas Zaidi
3d98558bf6 feat: upgrade react rotuer to v6 (#793)
* feat: upgrade react rotuer to v6

* fix: test cases

* build: update header and footer

* refactor: update jumpnav test case
2023-08-18 12:29:07 +05:00
edX requirements bot
e7e7f518bf chore: update browserslist DB (#840)
Co-authored-by: abdullahwaheed <abdullahwaheed@users.noreply.github.com>
2023-08-17 11:38:51 +05:00
Awais Ansari
cb06c8778a fix: notification courses failed test cases 2023-08-16 17:42:32 +05:00
Awais Ansari
ab5c205a7f fix: moved feedback widget behind env variable 2023-08-15 19:00:29 +05:00
edx-transifex-bot
2f85902d2c chore(i18n): update translations (#843)
Co-authored-by: Jenkins <sre+jenkins@edx.org>
2023-08-15 17:43:34 +05:00
renovate[bot]
b75e78bdda fix(deps): update dependency @edx/frontend-platform to v4.6.1 2023-08-14 14:01:36 +00:00
renovate[bot]
182a0251a4 fix(deps): update dependency @edx/paragon to v20.46.2 2023-08-14 02:14:20 -04:00
renovate[bot]
89881c64a6 fix(deps): update dependency @edx/frontend-component-header to v4.5.0 2023-08-14 02:13:51 -04:00
renovate[bot]
7321e2a159 chore(deps): update dependency @edx/frontend-build to v12.9.4 2023-08-14 00:22:28 -04:00
Awais Ansari
9b45aa3bc9 Merge pull request #842 from openedx/aansari/INF-990
style: change feedback widget location
2023-08-08 13:59:57 +05:00
Awais Ansari
dfadac08d3 style: change feedback widget location 2023-08-08 13:46:02 +05:00
Awais Ansari
2e87f0bd9f Merge pull request #839 from openedx/aansari/INF-990
feat: remove other channels then web preferences
2023-08-08 13:02:49 +05:00
Awais Ansari
7f8086545c test: define lightningjs in window object 2023-08-07 15:00:05 +05:00
Awais Ansari
de6e3c2010 feat: add feedback widget for notification preferences 2023-08-04 15:54:11 +05:00
edX requirements bot
eb6d0125c6 chore: update browserslist DB (#790)
Co-authored-by: abdullahwaheed <abdullahwaheed@users.noreply.github.com>
2023-08-04 15:52:16 +05:00
Awais Ansari
e8f754c10b style: update the notification preferences page style according to figma 2023-08-03 18:01:13 +05:00
Awais Ansari
a72bbf2f58 feat: remove other channels then web preferences 2023-08-03 16:15:47 +05:00
renovate[bot]
c465f51e66 chore(deps): update dependency @testing-library/jest-dom to v5.17.0 2023-07-31 00:24:18 -04:00
renovate[bot]
599e658742 chore(deps): update dependency @edx/frontend-build to v12.9.3 2023-07-31 00:24:02 -04:00
Omar Al-Ithawi
929a669cad feat: include paragon in atlas pull (#833)
This pull request is part of the [FC-0012 project](https://openedx.atlassian.net/l/cp/XGS0iCcQ) which is sparked by the [Translation Infrastructure update OEP-58](https://open-edx-proposals.readthedocs.io/en/latest/architectural-decisions/oep-0058-arch-translations-management.html#specification).
2023-07-25 18:23:08 +05:00
renovate[bot]
503b8b5176 chore(deps): update dependency @edx/frontend-build to v12.9.2 2023-07-24 07:57:00 +00:00
renovate[bot]
d2aa727c12 fix(deps): update dependency @edx/frontend-component-header to v4.4.4 2023-07-24 03:51:58 -04:00
Bilal Qamar
d66dcecd2f feat: update react & react-dom to v17 (#803)
* feat: update react & react-dom to v17

* feat: update react & react-dom to v17

* build: update lock file

* refactor: updated edx packages

* refactor: removed unnecessary lint disable

---------

Co-authored-by: mashal-m <mashal.malik@arbisoft.com>
2023-07-18 12:28:32 +05:00
edx-transifex-bot
80d0c44b40 chore(i18n): update translations (#829)
Co-authored-by: Jenkins <sre+jenkins@edx.org>
2023-07-17 17:53:27 +05:00
dependabot[bot]
0e5cd30d01 chore(deps): bump tough-cookie from 4.1.2 to 4.1.3 (#820)
Bumps [tough-cookie](https://github.com/salesforce/tough-cookie) from 4.1.2 to 4.1.3.
- [Release notes](https://github.com/salesforce/tough-cookie/releases)
- [Changelog](https://github.com/salesforce/tough-cookie/blob/master/CHANGELOG.md)
- [Commits](https://github.com/salesforce/tough-cookie/compare/v4.1.2...v4.1.3)

---
updated-dependencies:
- dependency-name: tough-cookie
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-17 10:49:01 +05:00
renovate[bot]
56b9fe3998 fix(deps): update dependency @edx/frontend-component-footer to v12.1.2 2023-07-17 01:08:58 -04:00
renovate[bot]
a22f1298eb chore(deps): update dependency @edx/frontend-build to v12.8.65 2023-07-17 05:08:34 +00:00
dependabot[bot]
b7bd6a2846 chore(deps): bump semver from 5.7.1 to 5.7.2 (#827)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-17 10:07:10 +05:00
Awais Ansari
a52ab171de Merge pull request #828 from openedx/aansari/INF-952
feat: implement showPreferences flag in notification preferences
2023-07-14 12:54:34 +05:00
Awais Ansari
13a7508f26 test: fix JumpNav test cases 2023-07-13 15:51:45 +05:00
Awais Ansari
a499aa4cc5 feat: implement showPreferences flag in notification preferences 2023-07-13 14:01:03 +05:00
Awais Ansari
e7207878d4 Merge pull request #825 from openedx/aansari/INF-948
refactor: update notEditable to nonEditable in notification preferences
2023-07-11 12:21:24 +05:00
Awais Ansari
47d64c1cf5 refactor: update notEditable to nonEditable in notification preferences 2023-07-10 18:12:11 +05:00
renovate[bot]
c75dc86263 fix(deps): update dependency @edx/frontend-component-header to v4.4.0 2023-07-10 05:01:18 -04:00
Mashal Malik
105a8d1a3c chore: add paragon messages (#818)
* fix(deps): update dependency @edx/paragon to v20.44.0

* chore: add paragon messages

* build: update lock file

* build: update lock file

* build: update lock file

* build: update lock file

* build: update lock file

* build: update lock file

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-07-10 13:59:46 +05:00
renovate[bot]
c74cf52e38 fix(deps): update dependency @edx/frontend-component-footer to v12.1.1 2023-07-10 02:43:35 -04:00
renovate[bot]
5c9b448b14 fix(deps): update dependency core-js to v3.31.1 2023-07-10 00:10:06 -04:00
renovate[bot]
4e95495dcc chore(deps): update dependency @edx/frontend-build to v12.8.60 2023-07-10 00:09:47 -04:00
renovate[bot]
82f86f5fbe fix(deps): update dependency @edx/frontend-component-header to v4.2.3 2023-07-03 10:56:56 +00:00
Awais Ansari
f737d6e158 Merge pull request #814 from openedx/aansari/INF-914
feat: load more courses button in notification courses list
2023-07-03 13:53:09 +05:00
renovate[bot]
f9feb94668 chore(deps): update dependency @edx/frontend-build to v12.8.57 2023-07-03 08:34:25 +00:00
Awais Ansari
112afa7e51 test: add load more courses test case 2023-06-26 19:34:05 +05:00
Awais Ansari
1694ea38ab feat: load more courses button in notification courses list 2023-06-26 18:46:06 +05:00
renovate[bot]
0f3b7caa0f fix(deps): update dependency reselect to v4.1.8 2023-06-19 21:29:42 +00:00
renovate[bot]
a85e1e1e15 fix(deps): update dependency redux-thunk to v2.4.2 2023-06-19 18:42:13 +00:00
renovate[bot]
55d00027a9 fix(deps): update dependency @edx/frontend-component-header to v4.1.0 2023-06-19 12:25:45 +00:00
renovate[bot]
bf012619a6 chore(deps): update dependency @edx/frontend-build to v12.8.54 2023-06-19 06:33:44 +00:00
renovate[bot]
f47795bf40 fix(deps): update dependency @edx/paragon to v20.44.0 (#806)
* fix(deps): update dependency @edx/paragon to v20.44.0

* fix: update snapshots

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: mashal-m <mashal.malik@arbisoft.com>
2023-06-13 19:50:46 +05:00
renovate[bot]
848e7a2f85 fix(deps): update dependency redux-saga to v1.2.3 2023-06-12 14:12:51 +00:00
renovate[bot]
ecc6138833 fix(deps): update dependency core-js to v3.31.0 2023-06-12 11:30:46 +00:00
Muhammad Adeel Tajamul
d6600eb876 test: added tests for notification preferences (#802) 2023-06-12 13:00:52 +05:00
renovate[bot]
bc237de755 chore(deps): update dependency @edx/frontend-build to v12.8.51 2023-06-12 07:38:35 +00:00
Jenkins
89abb51734 chore(i18n): update translations 2023-06-11 17:03:54 -04:00
Muhammad Adeel Tajamul
be8570edac feat: integrate notification preferences api (#794) 2023-06-06 11:04:23 +05:00
renovate[bot]
09cce6802d fix(deps): update dependency @edx/frontend-platform to v4.5.1 2023-06-05 17:17:50 +00:00
renovate[bot]
c164dd7dcf chore(deps): update dependency @edx/frontend-build to v12.8.40 2023-06-05 12:29:18 +00:00
renovate[bot]
00435fb27d fix(deps): update dependency @edx/frontend-platform to v4.5.0 2023-05-29 09:00:15 +00:00
renovate[bot]
0ebaa0b991 chore(deps): update dependency @edx/frontend-build to v12.8.38 2023-05-29 07:54:53 +00:00
Jenkins
0d7b529233 chore(i18n): update translations 2023-05-28 17:03:50 -04:00
renovate[bot]
2c2f7e8e98 fix(deps): update dependency qs to v6.11.2 2023-05-22 12:54:19 +00:00
renovate[bot]
768b8a2417 fix(deps): update dependency @edx/frontend-component-header to v4.0.3 2023-05-22 08:40:26 +00:00
Muhammad Adeel Tajamul
192714629c feat: added notification preference ui (#784) 2023-05-22 06:17:51 +05:00
edX requirements bot
f2f761e8db chore: update browserslist DB (#787)
Co-authored-by: abdullahwaheed <abdullahwaheed@users.noreply.github.com>
2023-05-18 18:37:57 +05:00
renovate[bot]
410aa14b28 fix(deps): update dependency core-js to v3.30.2 2023-05-15 14:10:07 +00:00
renovate[bot]
e857293414 fix(deps): update dependency @edx/paragon to v20.36.0 2023-05-15 10:49:43 +00:00
edX requirements bot
492f911930 chore: update browserslist DB (#775)
Co-authored-by: abdullahwaheed <abdullahwaheed@users.noreply.github.com>
2023-05-11 14:10:36 +05:00
renovate[bot]
0002d84a6c fix(deps): update dependency @edx/frontend-component-header to v4.0.2 2023-05-08 17:20:01 +00:00
renovate[bot]
9b49b38496 chore(deps): update dependency @edx/frontend-build to v12.8.27 2023-05-08 08:37:54 +00:00
renovate[bot]
7cf9294e09 chore(deps): update dependency @edx/browserslist-config to v1.2.0 2023-05-01 12:58:51 +00:00
renovate[bot]
129f73aa4f chore(deps): update dependency @edx/frontend-build to v12.8.16 2023-05-01 08:20:20 +00:00
Adolfo R. Brandes
7bdb93784b Merge pull request #779 from raccoongang/sagirov/tCRIL_GA-58
[FC-0014] update frontend-platform version to v4.2.0
2023-04-26 19:19:15 -03:00
Sagirov Eugeniy
b32c001fec chore: update frontend-platform version to v4.2.0 2023-04-21 14:21:48 +03:00
Omar Al-Ithawi
5f134d7b99 feat: use atlas in make pull_translations (#774)
Changes:
 - Bump frontend-platform to bring intl-imports.js script
 - Move all i18n imports into `src/i18n/index.js` so intl-imports.js can
   override it with latest translations
 - Add `atlas` into `make pull_translations` when `OPENEDX_ATLAS_PULL`
   environment variable is set.
 - Fixed lint rules for frontend-platform@4.1.0

Refs: [FC-0012 project](https://openedx.atlassian.net/l/cp/XGS0iCcQ) implementing Translation Infrastructure OEP-58.
2023-04-20 12:26:47 -04:00
Bilal Qamar
81b0823632 refactor: reverted platform to v2.6 to avoid dependency issues (#778) 2023-04-20 10:52:46 +05:00
renovate[bot]
95e3af7487 chore(deps): update dependency @edx/frontend-build to v12.8.10 (#776)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-17 14:13:28 +05:00
renovate[bot]
77047bab2a fix(deps): update dependency long to v5.2.3 (#777)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-04-17 14:12:55 +05:00
Jenkins
c1cf6d65de chore(i18n): update translations 2023-04-13 08:44:36 -04:00
renovate[bot]
f626fc2f89 fix(deps): update dependency @edx/frontend-component-footer to v11.7.2 2023-04-11 21:35:12 +00:00
renovate[bot]
f56b1b4530 fix(deps): update dependency @edx/frontend-component-header to v3.7.2 2023-04-11 05:33:19 +00:00
Bilal Qamar
1c423c4b6c feat: upgraded to node v18, added .nvmrc and updated workflows (#759)
* feat: upgraded to node v18, added .nvmrc and updated workflows

* refactor: updated workflow

* refactor: upgraded frontend-platform to v4

* build: updated frontend-build, frontend-platform, component-footer & component-header packages
2023-04-10 12:55:48 +05:00
edX requirements bot
7715b143d2 chore: update browserslist DB (#764)
Co-authored-by: abdullahwaheed <abdullahwaheed@users.noreply.github.com>
2023-04-10 11:03:12 +05:00
renovate[bot]
b200417903 fix(deps): update dependency @edx/frontend-component-header to v3.7.0 2023-04-03 12:43:34 +00:00
renovate[bot]
138d80d57d fix(deps): update dependency @edx/frontend-component-footer to v11.7.0 2023-04-03 10:44:28 +00:00
renovate[bot]
07a33447ad chore(deps): update dependency @edx/frontend-build to v12.7.0 2023-03-27 19:56:09 +00:00
renovate[bot]
4d11d28f96 fix(deps): update dependency @edx/frontend-component-header to v3.6.5 2023-03-27 12:09:12 +00:00
renovate[bot]
e61dc12eab chore(deps): update dependency @edx/frontend-build to v12.6.2 2023-03-24 10:46:08 +00:00
edX requirements bot
2c268d906c chore: update browserslist DB (#756)
Co-authored-by: abdullahwaheed <abdullahwaheed@users.noreply.github.com>
2023-03-22 17:23:44 +05:00
renovate[bot]
0acaebe067 fix(deps): update dependency qs to v6.11.1 2023-03-20 10:36:09 +00:00
renovate[bot]
9ea109f705 fix(deps): update dependency @edx/paragon to v20.28.5 2023-03-20 07:13:38 +00:00
Jenkins
be2964562f chore(i18n): update translations 2023-03-19 17:03:40 -04:00
Yoiber
6d7bf1b878 chore(i18n): add more languages (#747)
* chore(i18n): add more languages

* chore(i18n): Pylint fixes
2023-03-09 18:45:20 +05:00
renovate[bot]
3a7c963f3c fix(deps): update dependency @edx/paragon to v20.28.4 2023-03-07 01:06:59 +00:00
renovate[bot]
3728928a6d fix(deps): update dependency @edx/frontend-component-header to v3.6.4 2023-03-06 12:45:10 +00:00
Mashal Malik
596eeee59d refactor: remove unused tranisfex v2 url (#755) 2023-03-03 17:51:17 +05:00
Eugene Dyudyunov
ae2f8a384f feat: make password reset support URL configurable (#745)
* feat: make password reset support URL configurable

Replace the hardcoded `support.edx.org` value with the one from the env vars.

* fix: linting error
2023-03-02 11:46:32 +05:00
Sarina Canelake
ffe0989969 Merge pull request #750 from openedx/update-readme
docs: Remove old maintaining team from README
2023-02-28 09:56:10 -05:00
renovate[bot]
53eeb26e28 fix(deps): update dependency redux to v4.2.1 2023-02-27 16:05:10 +00:00
Feanil Patel
0325d2a7f6 Update standard workflow files. (#751)
* build: Creating a missing workflow file `self-assign-issue.yml`.

The .github/workflows/self-assign-issue.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.

* build: Creating a missing workflow file `add-remove-label-on-comment.yml`.

The .github/workflows/add-remove-label-on-comment.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.

* build: Updating a missing workflow file `add-depr-ticket-to-depr-board.yml`.

The .github/workflows/add-depr-ticket-to-depr-board.yml workflow is missing or needs an update to stay in
sync with the current standard for this workflow as defined in the
`.github` repo of the `openedx` GitHub org.
2023-02-27 13:46:43 +05:00
edX requirements bot
b83d568dfb chore: update browserslist DB (#752)
Co-authored-by: abdullahwaheed <abdullahwaheed@users.noreply.github.com>
2023-02-27 11:35:54 +05:00
edX requirements bot
2f839b3362 chore: update browserslist DB (#744)
Co-authored-by: abdullahwaheed <abdullahwaheed@users.noreply.github.com>
2023-02-24 13:30:19 -08:00
Sarina Canelake
bfc655d6ab docs: Remove old maintaining team from README 2023-02-22 18:35:02 -05:00
renovate[bot]
bd63ea9484 fix(deps): update dependency @edx/frontend-component-header to v3.6.3 2023-02-20 19:04:27 +00:00
renovate[bot]
6c825625fb fix(deps): update dependency @edx/frontend-component-footer to v11.6.3 2023-02-20 13:02:24 +00:00
Jenkins
be59c2da90 chore(i18n): update translations 2023-02-12 16:03:37 -05:00
edX requirements bot
8a4e6730a8 chore: update browserslist DB (#741)
Co-authored-by: abdullahwaheed <abdullahwaheed@users.noreply.github.com>
2023-02-07 18:47:58 +05:00
Jenkins
6053475c6f chore(i18n): update translations 2023-02-05 16:03:36 -05:00
edX requirements bot
ed1cfc28aa chore: update browserslist DB (#728)
Co-authored-by: abdullahwaheed <abdullahwaheed@users.noreply.github.com>
2023-02-01 14:29:59 +05:00
dependabot[bot]
1637aea0fa chore(deps): bump cookiejar from 2.1.3 to 2.1.4 (#737)
Bumps [cookiejar](https://github.com/bmeck/node-cookiejar) from 2.1.3 to 2.1.4.
- [Release notes](https://github.com/bmeck/node-cookiejar/releases)
- [Commits](https://github.com/bmeck/node-cookiejar/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-01 14:15:38 +05:00
Emad Rad
3c5aa05b48 Feature: Persian language Support (#666) 2023-02-01 11:36:29 +05:00
renovate[bot]
5a1dc5a992 fix(deps): update dependency form-urlencoded to v6 (#478)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2023-01-31 14:27:54 +05:00
dependabot[bot]
ccbf8201a0 chore(deps): bump json5 from 1.0.1 to 1.0.2 (#727)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-31 14:12:51 +05:00
Jenkins
34be9f681a chore(i18n): update translations 2023-01-29 16:03:37 -05:00
Bilal Qamar
cfca229500 chore: update dependency @edx/frontend-build to v12.4.19 (#738) 2023-01-26 11:50:43 +05:00
renovate[bot]
d2396a8162 fix(deps): update dependency @edx/frontend-component-footer to v11.6.1 2023-01-23 13:52:01 +00:00
renovate[bot]
b630514906 chore(deps): update dependency @edx/frontend-build to v12.4.16 2023-01-23 09:53:56 +00:00
Jenkins
f8dbbf7757 chore(i18n): update translations 2023-01-22 16:08:39 -05:00
renovate[bot]
8eb26db72d fix(deps): update dependency redux to v4.2.0 2023-01-16 14:06:10 +00:00
renovate[bot]
499cbeaa62 fix(deps): update dependency @edx/paragon to v20.27.0 2023-01-16 09:11:35 +00:00
renovate[bot]
4b103161b9 fix(deps): update dependency qs to v6.11.0 2023-01-09 14:37:08 +00:00
renovate[bot]
fb8e4829c3 fix(deps): update dependency @edx/paragon to v20.26.3 2023-01-09 10:31:51 +00:00
renovate[bot]
4a81220aa3 fix(deps): update dependency @edx/brand to v1.2.0 2023-01-09 07:56:11 +00:00
renovate[bot]
85a0d751c0 fix(deps): update dependency prop-types to v15.8.1 2023-01-02 12:32:44 +00:00
renovate[bot]
859195be94 fix(deps): update dependency core-js to v3.27.1 2023-01-02 09:25:12 +00:00
renovate[bot]
8499002072 fix(deps): update dependency @edx/paragon to v20.25.0 2022-12-26 11:13:49 +00:00
renovate[bot]
6e85a2e1d9 fix(deps): update dependency @edx/frontend-component-header to v3.6.0 2022-12-26 08:37:21 +00:00
Jenkins
e4b4197a9a chore(i18n): update translations 2022-12-25 16:08:48 -05:00
renovate[bot]
60f34bf2ae chore(deps): update dependency @edx/frontend-build to v12.4.15 2022-12-22 00:44:44 +00:00
Muhammad Abdullah Waheed
63afc7d7be chore: auto update browserslist shared workflow (#720)
* chore: used a shared script to update broswerslist DB

* refactor: added required secrets param
2022-12-20 17:36:37 +05:00
renovate[bot]
0aec952fd2 fix(deps): update dependency @edx/frontend-component-footer to v11.6.0 2022-12-19 12:52:40 +00:00
renovate[bot]
f8b8374058 fix(deps): update dependency @edx/paragon to v20.21.5 2022-12-19 09:47:00 +00:00
Shahroz Ahmad
d62bb11a9e fix: added language codes to support affected languages (#708)
* fix: added language codes to support affected languages

* fix: temporary fix to unbreak Banner component on unexpected languages
2022-12-15 15:45:36 +05:00
renovate[bot]
af39ecbc5f fix(deps): update dependency @edx/paragon to v20.21.2 2022-12-12 09:24:49 +00:00
Jenkins
6c2330fc6c chore(i18n): update translations 2022-12-11 16:03:30 -05:00
Bilal Qamar
109da5fa38 feat: updated paragon & frontend-build version (#696)
* refactor: updated frontend-build version

* feat: updated paragon to v20

* refactor: updated snapshots
2022-12-09 14:56:55 +05:00
renovate[bot]
81ac063cf8 fix(deps): update dependency @edx/frontend-component-header to v3.5.0 2022-12-05 13:18:08 +00:00
renovate[bot]
50f2442512 fix(deps): update dependency @edx/frontend-component-footer to v11.5.2 2022-12-05 09:46:55 +00:00
Jenkins
be28f398d6 chore(i18n): update translations 2022-12-04 16:03:31 -05:00
alangsto
cd47862c1d Merge pull request #702 from openedx/alangsto/remove_dob_defaults
fix: prevent users from submitting DOB without update
2022-12-02 15:02:05 -05:00
Alie Langston
847e3e05b0 fix: prevent users from submitting DOB without update 2022-12-02 14:51:23 -05:00
Mashal Malik
e347b3fb23 refactor: migrate off modal paragon depreciation components (#654)
* refactor:  worked on modal paragon depreciation component and changed them into latest paragon modals

* refactor:  migrate off  modal paragon depreciation components

* fix: fix eslint and commit message

* fix: units tests were not working, its fixed

* test: add unit tests in modal

* test: add unit tests in id verfication modal

* refactor: convert test cases from enzyme to react testing library

* refactor: remove empty file
2022-12-01 11:51:33 +05:00
Abdullah Waheed
a6a1d94d92 refactor: updated renovate config to auto update minor and patch versions of edx dependencies 2022-11-30 13:25:15 +00:00
edX requirements bot
e54de547f6 fix: -t flag added in pull translation command (#695) 2022-11-30 16:46:54 +05:00
renovate[bot]
574cfb91c2 fix(deps): update dependency jslib-html5-camera-photo to v3.3.4 2022-11-28 12:07:44 +00:00
renovate[bot]
5bdf0a4978 chore(deps): update dependency @edx/frontend-build to v12.4.0 2022-11-28 08:40:57 +00:00
Jenkins
cd8dee1731 chore(i18n): update translations 2022-11-27 16:03:29 -05:00
Andrew Shultz
0e7d2c048d Merge pull request #694 from openedx/ashultz0/more-coppa-rewording
chore: trivial changes to compliance form text
2022-11-22 12:52:58 -05:00
Andy Shultz
79c4a14f6f chore: trivial changes to compliance form text 2022-11-22 10:06:56 -05:00
Muhammad Abdullah Waheed
de8977347a Merge pull request #692 from openedx/bilalqamar95/dependabot-vulnerabilities
refactor: bumped minimatch, recursive-readdir & loader-utils
2022-11-22 16:01:00 +05:00
renovate[bot]
783f78a9ef fix(deps): update dependency core-js to v3.26.1 2022-11-21 13:28:16 +00:00
Bilal Qamar
c1fa6efb30 refactor: bumped minimatch, recursive-readdir & loader-utils 2022-11-21 16:09:38 +05:00
renovate[bot]
b3cd370d8e fix(deps): update dependency regenerator-runtime to v0.13.11 2022-11-21 10:20:41 +00:00
renovate[bot]
8cb416d142 fix(deps): update dependency @edx/frontend-component-header to v3.4.1 2022-11-15 01:10:31 +00:00
renovate[bot]
dbd4faf558 fix(deps): update dependency @edx/frontend-component-footer to v11.5.1 2022-11-14 22:15:50 +00:00
Jenkins
92c8f17e2a chore(i18n): update translations 2022-11-13 16:03:26 -05:00
Muhammad Abdullah Waheed
c858966035 Supported Transifex languages in Makefile (#641)
* feat: added new translations in Makefile and updated all the translations

* refactor: updated i18n index file to fix language errors
2022-11-08 18:42:27 +05:00
Muhammad Abdullah Waheed
547d55a31f Paragon form component deprecations (#612)
* refactor: removed deprecated paragon components from CoachingToggle and used alternatives

* refactor: removed deprecated paragon components from ConfirmationModal and used alternatives

* refactor: removed deprecations from EditableField and created separate component for SelectField

* refactor: updated DemographicsSection to use new select component

* refactor: removed deprecations from EmailField and used alternatives

* refactor: removed deprecated Input from CoachingConsentForm

* refactor: removed deprecated Input from DemographicsSection

* refactor: removed deprecated Input from SummaryPanel component

* refactor: removed deprecated CheckBox and used Form.CheckBox

* refactor: fixed unit tests

* refactor: changes based on PR reviews

* fix: linting issue
2022-11-08 18:42:09 +05:00
renovate[bot]
f78420511e fix(deps): update dependency react-router-hash-link to v2 (#262)
* fix(deps): update dependency react-router-hash-link to v2

* refactor: update snapshot tests

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: mashal-m <mashal.malik@arbisoft.com>
2022-11-08 13:23:24 +05:00
renovate[bot]
e3ad6e6e54 fix(deps): update dependency form-urlencoded to v4.5.1 (#133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-11-08 11:45:54 +05:00
renovate[bot]
c6ffc51a5d fix(deps): update dependency @tensorflow/tfjs-converter to v3.21.0 (#408)
* fix(deps): update dependency @tensorflow/tfjs-converter to v3.21.0

* fix: updated package-lock to resolve dependency tree issue

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Bilal Qamar <59555732+BilalQamar95@users.noreply.github.com>
Co-authored-by: Abdullah Waheed <abdullah.waheed@arbisoft.com>
2022-11-08 10:47:14 +05:00
renovate[bot]
9b28469afd fix(deps): update dependency @tensorflow/tfjs-core to v3.21.0 (#409)
* fix(deps): update dependency @tensorflow/tfjs-core to v3.21.0

* fix: updated package-lock to resolve dependency tree issue

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Bilal Qamar <59555732+BilalQamar95@users.noreply.github.com>
Co-authored-by: Abdullah Waheed <abdullah.waheed@arbisoft.com>
2022-11-08 10:17:16 +05:00
renovate[bot]
2bb1388a45 fix(deps): update dependency @fortawesome/react-fontawesome to v0.2.0 2022-11-07 15:15:03 +00:00
renovate[bot]
bf0d1379f6 fix(deps): update dependency long to v5.2.1 2022-11-07 13:56:08 +00:00
renovate[bot]
0d9bc5988d fix(deps): update dependency @edx/frontend-component-footer to v11.4.1 2022-11-07 10:56:46 +00:00
renovate[bot]
3a2075de9c fix(deps): update dependency @edx/frontend-component-footer to v11.3.1 (#672)
* fix(deps): update dependency @edx/frontend-component-footer to v11.3.1

* fix: updated package-lock to resolve dependency tree issue

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Bilal Qamar <59555732+BilalQamar95@users.noreply.github.com>
Co-authored-by: Abdullah Waheed <abdullah.waheed@arbisoft.com>
2022-11-07 12:30:53 +05:00
renovate[bot]
ef6239a807 chore(deps): update dependency enzyme-adapter-react-16 to v1.15.7 2022-11-07 07:19:43 +00:00
renovate[bot]
8cb9df6d89 fix(deps): update dependency @edx/paragon to v19.25.3 2022-10-31 13:28:35 +00:00
renovate[bot]
fa856b54bf fix(deps): update dependency @edx/frontend-component-header to v3.3.0 2022-10-31 10:16:07 +00:00
Muhammad Abdullah Waheed
e9f6acff27 Merge pull request #673 from openedx/abdullahwaheed/eslint-issues-fix
fix: migrated functions to arrow functions to fix linting
2022-10-24 18:02:37 +05:00
Abdullah Waheed
dc26da9cc1 fix: migrated functions to arrow functions to fix linting 2022-10-24 15:53:58 +05:00
renovate[bot]
9dd05761fd chore(deps): update dependency @edx/frontend-build to v12.3.0 2022-10-24 08:26:58 +00:00
renovate[bot]
0d5b86df57 fix(deps): update dependency regenerator-runtime to v0.13.10 2022-10-17 08:57:06 +00:00
renovate[bot]
d8eb6e9da5 fix(deps): update dependency react-redux to v7.2.9 2022-10-17 08:49:23 +00:00
renovate[bot]
d280062fe3 chore(deps): update dependency @edx/frontend-build to v12.0.6 2022-10-10 07:24:33 +00:00
renovate[bot]
f0366a98f4 chore(deps): update dependency @edx/browserslist-config to v1.1.1 2022-10-10 07:16:08 +00:00
Muhammad Abdullah Waheed
9d0577fd93 fix: removed env constants and used them directly (#657) 2022-10-04 18:23:56 +05:00
Muhammad Abdullah Waheed
dba70b557c Automate Browserlist DB Update (#642)
* feat: added cron github action to auto update brwoserlist DB periodically

* feat: added workflow_dispatch for manual testing

* fix: fixed error

* fix: fixed indentation issue

* fix: linting issue
2022-10-04 17:55:30 +05:00
Jenkins
f6840bc202 chore(i18n): update translations 2022-10-02 17:05:49 -04:00
renovate[bot]
1af5f0d179 fix(deps): update dependency classnames to v2.3.2 2022-09-26 12:40:22 +00:00
renovate[bot]
b3a91470f8 fix(deps): update dependency @edx/frontend-component-header to v3.2.1 2022-09-26 12:32:19 +00:00
Jenkins
9b18588e26 chore(i18n): update translations 2022-09-25 17:04:29 -04:00
renovate[bot]
3d2e7dc8a4 fix(deps): update dependency @edx/frontend-component-footer to v11.2.1 2022-09-19 12:29:38 +00:00
renovate[bot]
41f1552f9b chore(deps): update dependency @edx/frontend-build to v12.0.5 2022-09-19 12:22:14 +00:00
Jenkins
ffa8a36ce6 chore(i18n): update translations 2022-09-18 17:01:34 -04:00
alangsto
3505e645e3 Merge pull request #647 from openedx/alangsto/update_dob_language
fix: update dob form language
2022-09-14 16:15:01 -04:00
Alie Langston
88db6084df fix: update dob form language 2022-09-14 15:59:58 -04:00
Sarina Canelake
27c8ab28f3 Merge pull request #643 from openedx/tcril/fix-gh-org-url
Fix github url strings (org edx -> openedx)
2022-09-14 10:19:22 -04:00
renovate[bot]
e01b595a0c fix(deps): update dependency @edx/frontend-platform to v2.6.2 2022-09-12 11:37:36 +00:00
renovate[bot]
0d351f5173 chore(deps): update dependency @edx/frontend-build to v12.0.4 2022-09-12 11:29:56 +00:00
Jenkins
03dbfb5670 chore(i18n): update translations 2022-09-11 17:01:42 -04:00
Sarina Canelake
1e17fc9e32 fix: update path to .github workflows to read from openedx org 2022-09-10 18:05:35 -04:00
Sarina Canelake
da912f1e35 fix: fix github url strings (org edx -> openedx) 2022-09-07 08:57:12 -04:00
renovate[bot]
fb0f832832 fix(deps): update dependency @edx/frontend-component-header to v3.2.0 2022-09-05 18:05:19 +00:00
renovate[bot]
870dd631bb fix(deps): update dependency @edx/frontend-component-footer to v11.2.0 2022-09-05 17:57:37 +00:00
Jenkins
5b9f251e93 chore(i18n): update translations 2022-09-04 17:01:50 -04:00
renovate[bot]
98c14bbd3b chore(deps): update dependency @testing-library/jest-dom to v5.16.5 2022-08-29 12:14:47 +00:00
renovate[bot]
00ce8b927f fix(deps): update dependency react-transition-group to v4.4.5 2022-08-29 12:06:54 +00:00
renovate[bot]
ab215cd909 fix(deps): update dependency react-redux to v7.2.8 2022-08-22 11:04:54 +00:00
renovate[bot]
51e7773207 chore(deps): pin dependencies 2022-08-22 10:57:13 +00:00
Jenkins
6f5a1a8aa9 chore(i18n): update translations 2022-08-21 17:02:10 -04:00
Bilal Qamar
deb48fb9d2 Merge pull request #625 from openedx/bilalqamar95/revert-eslint-fragment-change
Reverted changes made to satisfy jsx-no-useless-fragment rule
2022-08-19 18:46:58 +05:00
Bilal Qamar
20b451afb6 refactor: reverted changes made to satisfy jsx-no-useless-fragment rule 2022-08-19 17:25:48 +05:00
alangsto
ad6f812974 Merge pull request #623 from openedx/alangsto/update_dob
feat: add DOB month and year field
2022-08-18 13:59:26 -04:00
Alie Langston
9573516b37 feat: add DOB month and year field 2022-08-18 13:40:30 -04:00
Bilal Qamar
f0d6a92ab2 Upgraded frontend-build version to v12 (#613)
* refactor: upgraded frontend-build version to v12

* refactor: updated frontend-build

* refactor: resolved eslint issues

* refactor: updated package-lock

* refactor: resolved eslint issues after master branch merge
2022-08-16 10:57:43 -04:00
renovate[bot]
dc4d4031e9 fix(deps): update dependency @edx/frontend-component-header to v3.1.3 2022-08-15 09:43:03 +00:00
renovate[bot]
84a5c7aaf1 fix(deps): update dependency @edx/frontend-component-footer to v11.1.2 2022-08-15 09:29:22 +00:00
edx-semantic-release
6ad666342d chore(i18n): update translations 2022-08-14 17:02:20 -04:00
Diana Olarte
1252498872 feat: allow runtime configuration (#603) 2022-08-09 13:44:05 -04:00
renovate[bot]
f9d04e4dd4 fix(deps): update dependency @edx/frontend-platform to v1.15.6 2022-08-08 12:23:08 +00:00
renovate[bot]
c96b9bb77d chore(deps): update dependency @testing-library/react to v12.1.5 2022-08-08 12:13:06 +00:00
edx-semantic-release
f3c672c5ae chore(i18n): update translations 2022-08-07 17:02:31 -04:00
renovate[bot]
63c396c03a chore(deps): update dependency node-forge to 1.3.0 [security] (#567)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2022-07-28 11:33:38 +05:00
Maman Khan
0981857062 fix: removed depreciated codecov package (#608) 2022-07-26 13:27:58 +05:00
Maman Khan
0527c73529 Update packages to remove security vulnerabilities (#605)
* fix: updated vulnerable packages

* fix: fixed failed tests after package update

* fix: linting issues failing ci tests

* fix: package lock update

* fix: snapshot updated to UTC

* fix: missing dependency 'long'
2022-07-26 13:05:31 +05:00
renovate[bot]
5c5dbc369b chore(deps): update dependency lodash to 4.17.21 [security] (#550)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2022-07-26 11:51:21 +05:00
Maman Khan
196719963f Merge pull request #607 from openedx/maman/add-browserslist-config
build: use shared browserslist configuration
2022-06-29 15:55:16 +05:00
maman
92ee4dfbb9 fix: removed es5 ci check 2022-06-13 18:11:05 +05:00
maman
9d0b3524cb feat: made browserslist setting configurable 2022-06-13 17:48:29 +05:00
edX requirements bot
c39fd332b6 chore!: Dropped support for Node 12 (#594) 2022-05-24 13:45:40 +05:00
Kshitij Sobti
04d515f554 feat: remove usage of scss variables (#601)
Remove usage of scss variables so that the MFE doesn't need scss variables at build-time.
2022-05-16 15:41:48 -04:00
David Joy
f425e9b94f build: adding nvmrc, bumping paragon to latest (#602)
Adding .nvmrc is a nicety.  Bumping paragon to latest is necessary to ensure compatiblity with the edX override header (frontend-component-header-edx).
2022-05-09 16:15:50 -04:00
edX requirements bot
7ec147fe6f feat: Add package-lock file version check (#599)
* feat: Add package-lock file version check

* fix: update name

Co-authored-by: Jawayria <39649635+Jawayria@users.noreply.github.com>
2022-05-09 19:19:39 +05:00
edx-semantic-release
7a8ae85b72 chore(i18n): update translations 2022-04-10 17:08:04 -04:00
Mohammad Ahtasham ul Hassan
0f8f5a1e9a fix: update packagelock (#596) 2022-04-08 15:05:24 +05:00
Usama Sadiq
757a9ac033 Merge pull request #593 from openedx/update-transifex-pull-translations-command
build: update pull_translations command
2022-04-04 14:29:45 +05:00
UsamaSadiq
9efc8d1290 build: update pull_translations command 2022-04-04 14:24:04 +05:00
Usama Sadiq
5f53270148 Merge pull request #591 from openedx/jenkins/transifex-client-migration-bb6390f
fix: transifex migration to new client
2022-04-04 14:06:38 +05:00
Ali Adnan
9716495951 Merge pull request #590 from openedx/aadnan/migrate-reactifex
feat: migrate translations to reactifex
2022-03-18 14:01:24 +05:00
edX requirements bot
f9b29948e7 fix: transifex migration to new client 2022-03-17 08:48:14 -04:00
aliadnan
bededb3912 feat: migrate translations to reactifex 2022-03-17 15:36:53 +05:00
David Joy
bb6390f9ae build: Delete CODEOWNERS (#589)
The community-engineering team no longer exists, so we don't want this CODEOWNERS file anymore.
2022-03-15 14:13:18 -04:00
Jawayria
d2300d2dfd feat: Added support for Node 16 (#588)
* feat: Added support for Node 16

* fix: run both npm 6 and 8
2022-03-15 19:15:11 +05:00
Renovate Bot
a501407907 fix(deps): update dependency qs to v6.10.3 2022-03-14 11:35:58 +00:00
Renovate Bot
88e63cd390 chore(deps): update dependency @testing-library/react to v12.1.4 2022-03-14 11:19:33 +00:00
Sarina Canelake
eaebe6980b Merge DEPR automation workflow
Add DEPR workflow automation
2022-02-24 15:21:56 -05:00
Sarina Canelake
d6efba63ca build: add DEPR workflow automation 2022-02-23 14:37:30 -05:00
edX Transifex Bot
257e425fd9 chore(i18n): update translations 2022-02-20 16:06:24 -05:00
Bianca Severino
02e3364874 Merge pull request #580 from openedx/bseverino/dashboard-language
[MST-1372] Remove dashboard reference from IDV submission confirmation
2022-02-16 14:21:07 -05:00
Bianca Severino
9ae74708fb fix: remove dashboard reference from IDV submitted 2022-02-16 11:40:58 -05:00
alangsto
e946e377c6 Merge pull request #579 from openedx/alangsto/remove_pending_language
fix: remove alert and update language if verified name comes from proctored exam attempt
2022-02-16 09:18:59 -05:00
Alie Langston
5dbe649b2c fix: remove alert if verified name comes from proctored exam attempt 2022-02-16 09:06:19 -05:00
Bianca Severino
2792902975 Merge pull request #577 from openedx/bseverino/idv-camera-language
[MST-1269] Change language referring to government ID
2022-02-14 15:11:57 -05:00
Bianca Severino
6f643070ea fix: change language referring to government ID 2022-02-14 11:45:54 -05:00
edX Transifex Bot
ce946f56b2 chore(i18n): update translations 2022-02-06 16:06:00 -05:00
Bianca Severino
de1e67f68d Merge pull request #573 from openedx/bseverino/remove-idv-experiment
[MST-869] Remove unused IDV experiment paths
2022-02-02 11:13:32 -05:00
Bianca Severino
79001bccd8 feat: remove unused IDV experiment paths 2022-01-27 14:21:43 -05:00
Bianca Severino
963884cc4c Merge pull request #570 from openedx/bseverino/idv-camera-focus
[MST-1075] Prevent focus jump when taking photo
2022-01-24 13:48:53 -05:00
Bianca Severino
e43c1bcc9e fix: prevent focus jump when taking photo 2022-01-20 17:09:15 -05:00
edX Transifex Bot
32cc2c7835 chore(i18n): update translations 2021-12-26 16:14:50 -05:00
Waheed Ahmed
5c39c279f3 Merge pull request #562 from edx/waheed/update-support-link
chore: update help center link for email confirmation
2021-12-07 21:23:40 +05:00
Waheed Ahmed
12bca9b771 chore: update help center link for email confirmation 2021-12-07 19:57:59 +05:00
Renovate Bot
5e10c2bc18 fix(deps): update dependency core-js to v3.19.3 2021-12-06 10:24:35 +00:00
Renovate Bot
f2fe22b8f7 fix(deps): update dependency core-js to v3.19.1 2021-11-29 08:52:52 +00:00
Renovate Bot
d5601a21fd chore(deps): update dependency @testing-library/jest-dom to v5.15.1 2021-11-29 08:30:03 +00:00
edX Transifex Bot
f3bd7a8589 chore(i18n): update translations 2021-11-29 02:04:09 +05:00
Renovate Bot
7643bbd6ba fix(deps): update dependency @edx/frontend-platform to v1.14.1 2021-11-22 12:34:37 +00:00
Renovate Bot
fd1044b531 chore(deps): update dependency es-check to v6.1.1 2021-11-22 12:11:24 +00:00
Uzair Rasheed
f25e5db422 Merge pull request #555 from edx/modify-message-text
refactor: modify activation text message
2021-11-22 13:02:58 +05:00
uzairr
0d569a060b refactor: modify activation text message
alter text of the activation message which appears when an inactive
user tries to delete its profile

VAN-778
2021-11-22 11:47:29 +05:00
Renovate Bot
350016cbe0 chore(deps): update dependency @testing-library/jest-dom to v5.15.0 2021-11-15 10:47:31 +00:00
Renovate Bot
1619dade50 chore(deps): update dependency @edx/frontend-build to v8.1.6 2021-11-15 10:22:11 +00:00
Renovate Bot
dafc34f535 chore(deps): update dependency @edx/frontend-build to v8.1.3 2021-11-08 11:11:25 +00:00
Renovate Bot
9bbab2620c fix(deps): update dependency redux to v4.1.2 2021-11-08 10:43:47 +00:00
edX Transifex Bot
81bef65cc2 chore(i18n): update translations 2021-11-08 02:03:29 +05:00
Michael Roytman
a18daecf8a Merge pull request #541 from edx/MST-1130-remove-verified-name-waffle-flag
feat: Remove Use of Verified Name Enabled Flag
2021-11-01 12:55:52 -04:00
Renovate Bot
449e4c0253 fix(deps): update dependency react-redux to v7.2.6 2021-11-01 10:23:31 +00:00
Renovate Bot
a66ba187ae fix(deps): update dependency @fortawesome/react-fontawesome to v0.1.16 2021-11-01 10:03:19 +00:00
edX Transifex Bot
2d710f7060 chore(i18n): update translations 2021-11-01 02:03:49 +05:00
michaelroytman
e109e5018e feat: Remove Use of Verified Name Enabled Flag
The VERIFIED_NAME_FLAG was added as part https://github.com/edx/edx-name-affirmation/pull/12, [MST-801](https://openedx.atlassian.net/browse/MST-801) in order to control the release of the Verified Name project. It was used for a phased roll out by percentage of users.

The release reached a percentage of 50% before it was observed that, due to the way percentage roll out works in django-waffle, the code to create or update VerifiedName records was not working properly. The code was written such that any change to a SoftwareSecurePhotoVerification model instance sent a signal, which was received and handled by the Name Affirmation application. If the VERIFIED_NAME_FLAG was on for the requesting user, a Celery task was launched from the Name Affirmation application to perform the creation of or update to the appropriate VerifiedName model instances based on the verify_student application signal. However, we observed that when SoftwareSecurePhotoVerification records were moved into the "created" or "ready" status, a Celery task in Name Affirmation was created, but when SoftwareSecurePhotoVerification records were moved into the "submitted" status, the corresponding Celery task in Name Affirmation was not created. This caused VerifiedName records to stay in the "pending" state.

The django-waffle waffle flag used by the edx-toggle library implements percentage rollout by setting a cookie in a learner's browser session to assign them to the enabled or disabled group.
It turns out that the code that submits a SoftwareSecurePhotoVerification record, which moves it into the "submitted" state, happens as part of a Celery task in the verify_student application in the edx-platform. Therefore, we believe that because there is no request object in a Celery task, the edx-toggle code is defaulting to the case where there is no request object. In this case, the code checks whether the flag is enabled for everyone when determining whether the flag is enabled. Because of the percentage rollout (i.e. waffle flag not enabled for everyone), the Celery task in Name Affirmation is not created. This behavior was confirmed by logging added as part of https://github.com/edx/edx-name-affirmation/pull/62.

We have determined that we do not need the waffle flag, as we are comfortable that enabling the waffle flag for everyone will fix the issue and are comfortable releasing the feature to all users. For this reason, we are removing references to the flag.

[MST-1130](https://openedx.atlassian.net/browse/MST-1130)
2021-10-29 11:06:55 -04:00
Waheed Ahmed
1444831833 Merge pull request #540 from edx/waheed/VAN-762
feat: remove `primary/elementary` option from education
2021-10-28 14:13:08 +05:00
Waheed Ahmed
4b4f29ae19 feat: remove primary/elementary option from education
If COPPA compliance feature flag is enabled, remove the `primary/elementary` option
from the level of education dropdown field on edit but keep the value showing for
the users who have already selected it.

VAN-762
2021-10-28 12:34:46 +05:00
Bianca Severino
cbc4123e78 Merge pull request #539 from edx/bseverino/name-change-input
[MST-1074] Remove radio buttons from ID name check if verified name is enabled
2021-10-27 09:00:38 -04:00
Bianca Severino
2c896f77d4 feat: remove radio buttons from ID name check if verified name is enabled 2021-10-26 13:19:14 -04:00
Renovate Bot
112ddf80e6 chore(deps): update dependency @edx/frontend-build to v8.0.6 2021-10-25 09:13:08 +00:00
Renovate Bot
6f2a69acc1 fix(deps): pin dependency lodash.camelcase to 4.3.0 2021-10-25 08:52:11 +00:00
edX Transifex Bot
d2c83b82f7 chore(i18n): update translations 2021-10-25 02:03:33 +05:00
Simon Chen
03501a8125 Merge pull request #535 from edx/schen/fix_access_block
fix: allow learners to access IDV by default
2021-10-21 11:09:16 -04:00
Simon Chen
6e17214476 fix: allow learners to access IDV by default 2021-10-21 10:58:04 -04:00
Michael Roytman
2c6cec7f8c Merge pull request #527 from edx/bseverino/idv-redirect
[MST-1104] Redirect user to original location after IDV
2021-10-20 14:33:29 -04:00
Michael Roytman
f76797cade Merge pull request #526 from edx/mroytman/MST-1099-fix-loading-issues
[MST-1099] Fix Flickering By Removing Unnecessary Re-Renders
2021-10-20 14:29:02 -04:00
Attiya Ishaque
59325bd412 Merge pull request #531 from edx/attiya/VAN-751/put_year_of_birth_behind_flag
feat: [VAN-751] Put user's year of birth behind the feature flag
2021-10-20 15:45:11 +05:00
attiyaIshaque
65a6bc5002 feat: [VAN-751] Put user's year of birth behind the feature flag 2021-10-20 14:56:55 +05:00
edX Transifex Bot
eff28d8b47 chore(i18n): update translations 2021-10-18 02:04:24 +05:00
Bianca Severino
51b758a18f Merge pull request #532 from edx/bseverino/modal-scroll
fix: remove scrollbars from name affirmation modals
2021-10-14 13:54:58 -04:00
Bianca Severino
30bd145bdd fix: remove scrollbars from name affirmation modals 2021-10-14 12:28:53 -04:00
Renovate Bot
1f15802bc6 fix(deps): update dependency core-js to v3.18.2 2021-10-11 05:26:13 +00:00
edX Transifex Bot
5ba476a570 chore(i18n): update translations 2021-10-11 02:09:21 +05:00
Bianca Severino
dfb13c4286 fix: convert duplicate useEffects to custom hook 2021-10-08 11:49:06 -04:00
Bianca Severino
aada46f6eb fix: redirect user to original location after IDV 2021-10-08 10:26:56 -04:00
Ned Batchelder
8c57140640 build: use the organization commitlint check 2021-10-07 13:57:41 -04:00
michaelroytman
9e967ba1ea perf: Fix Flickering By Removing Unnecessary Re-Renders
The use of useState and useEffect hooks in the IDVerificationContextProvider and VerificationContextProvider that were unnecessary was triggering many extra re-renders and inappropriate renders, causing a UI flicker across multiple re-renders.
2021-10-06 16:28:09 -04:00
Bianca Severino
8239209cd7 Merge pull request #525 from edx/bseverino/name-change-pending-message
[MST-1106] Display correct messages for name changes
2021-10-06 11:24:37 -04:00
Bianca Severino
65bb042443 fix: display correct messages for name changes
Removes confusing wording where alerts and help text would state that
the changed name would be used on certificates once approved,
even if it wasn't the name that was selected to be used on certificates.
2021-10-06 10:26:20 -04:00
Michael Roytman
4dd7529799 Merge pull request #524 from edx/mroytman/MST-1099-verified-name-inconsistent-panel
[MST-1099] Fix Flickering in IDV Panel Loading and Inconsistent Panel Loading
2021-10-05 10:15:51 -04:00
michaelroytman
e6684f5048 fix: Fix Flickering in IDV Panel Loading and Inconsistent Panel Loading
[MST-1099](https://openedx.atlassian.net/browse/MST-1099)

Due to a bug in the way the canVerify React state is being set, when the verified name feature is enabled, the code inconsistently loads either the AccessBlocked panel or the ReviewRequirementsPanel when the learner should be allowed to verify. If the learner is ineligible to complete IDV due to their IDV history (e.g. they already have an approved IDV), and the verified name feature is enabled, the learner should be able to verify and should not see the AccessBlocked panel. This code change fixes this bug.

This code change also fixes flickering that occurs when IDV is loading. This code change also includes some copy fixes.
2021-10-04 15:06:41 -04:00
Michael Roytman
e276d6a5c4 Merge pull request #520 from edx/mroytman/MST-1065-IDV-UI-and-copy-changes-verified-name
MST-1065: Add Copy and UI Treatment for Name Affirmation When Enabled
2021-10-04 13:16:37 -04:00
Renovate Bot
36f1d1dbfb fix(deps): update dependency formdata-polyfill to v4.0.10 2021-10-04 07:26:23 +00:00
Renovate Bot
6c324e85fc chore(deps): update dependency @testing-library/react to v12.1.2 2021-10-04 07:07:31 +00:00
edX Transifex Bot
16b5d066ff chore(i18n): update translations 2021-10-04 02:04:23 +05:00
michaelroytman
6e45abbe8b feat: Add Copy and UI Treatment for Name Affirmation When Enabled
[MST-1065](https://openedx.atlassian.net/browse/MST-1065)

When the Name Affirmation feature is enabled, a few panels in the IDV workflow should have different copy to reflect the changes associated with Name Affirmation. This is because entering a new during the IDV flow (GetNameIdPanel) will no longer modify the learner's "Account Name". Furthermore, "Account Name" is an ambiguous term; it's not clear that it refers to the learner's full name on their profile. These copy changes make the panel more generic and remove details about name changes. The SummaryPanel also has a minor copy change to change "Account Name" to "Name".

These code changes also include some UI changes.
2021-10-01 14:35:14 -04:00
Bianca Severino
b3e6335396 Merge pull request #519 from edx/bseverino/idv-failure-message
[MST-1091] Update IDV failure message to be more generic
2021-10-01 10:10:22 -04:00
Bianca Severino
08a2da5459 fix: update IDV failure message to be more generic 2021-09-30 17:34:41 -04:00
Bianca Severino
094361c689 Merge pull request #518 from edx/bseverino/modal-trigger-bug
[MST-1089] Only display name change modal when backend requires verification
2021-09-30 16:48:23 -04:00
Bianca Severino
b7b33ef597 fix: only display name change modal when backend requires verification
Fixes an issue where the name change modal would trigger for any error
related to the "full name" field, rather than just when the name change
requires ID verification.
2021-09-30 16:26:49 -04:00
alangsto
afa808ff5d Merge pull request #516 from edx/alangsto/update_dismissable_alert
fix: approved alert should appear for subsequent approved attempts
2021-09-29 14:36:02 -04:00
Alie Langston
5279f2a9c9 fix: approved alert should appear for subsequent approved attempts 2021-09-29 14:00:30 -04:00
Michael Roytman
4cc00fd7e3 Merge pull request #514 from edx/mroytman/MST-1073-pending-verified-name-display
fix: Display Last Approved Verified Name When Most Recent Verified Name is Pending
2021-09-29 13:03:08 -04:00
alangsto
8815626411 Merge pull request #515 from edx/alangsto/fix_certificate_checkbox
fix: Checkbox incorrectly tied to profile name
2021-09-29 12:57:51 -04:00
Alie Langston
db319b6cdf fix: Checkbox incorrectly tied to profile name
Fixed a bug that incorrectly rendered the certificate preference checkbox upon each page render. The bug caused the 1) the checkboxes to both be selected at the same time, 2) the checkbox to always be selected for the profile name, and 3) wouldn't allow the proper UI changes upon saving a verified name. The checkbox selection works correctly now, and the save button for the verified name field also works as expected.
2021-09-29 09:53:05 -04:00
michaelroytman
50edcb1c50 fix: Display Last Approved Verified Name When Most Recent Verified Name is Pending
When a learner's most recent Verified Name is in the pending state, we should display their most recent approved Verified Name. If such a Verified Name does not exist, do not display any Verified Name. In either case, do not disable or gray out either the Full name or Verified name fields.

This change is being made because we do not want to prevent a learner from editing the Verified name field if their most recent entry is pending. An entry remains in the pending state when a learner has created a Verified name by changing their name on the Account Settings page but has not followed through with submitting their IDV.
2021-09-28 13:07:46 -04:00
David Joy
d6519bc825 fix: update deps and modernize, fix favicon (#513) 2021-09-27 19:50:16 -04:00
Bianca Severino
b54aeb9446 Merge pull request #512 from edx/bseverino/idv-language
[MST-797] Update IDV instructions
2021-09-27 17:01:11 -04:00
Zachary Hancock
bb1bd6e648 fix: null check mostRecentVerifiedName 2021-09-27 14:33:46 -04:00
Bianca Severino
7df9f92dd8 fix: update IDV instructions
Update IDV instructions to be more explicit about
the need for a photo identification card, and provide
a few more examples.
2021-09-27 14:23:36 -04:00
Zach Hancock
3627915985 fix: null check mostRecentVerifiedName 2021-09-27 14:19:51 -04:00
Zachary Hancock
9fe1a04a0a feat: enter name change flow when submitting a new verified name 2021-09-27 11:33:30 -04:00
Zach Hancock
7455821500 fix: missing null check 2021-09-27 10:12:15 -04:00
Renovate Bot
817980be00 fix(deps): update dependency formdata-polyfill to v4.0.8 2021-09-27 08:56:10 +00:00
Michael Roytman
7d4e31f69d Merge pull request #508 from edx/mroytman/MST-1059-verified-name-not-sent-in-IDV
feat: send nameOnAccount as full_name with all IDV submissions when learner has no name change and verified name feature is enabled
2021-09-24 16:16:08 -04:00
Zach Hancock
34dde09ccc style: lint 2021-09-24 14:37:56 -04:00
Zach Hancock
aee4e44f8c feat: submit new verified name 2021-09-24 14:20:00 -04:00
michaelroytman
7381cfd3b6 feat: send nameOnAccount as full_name with all IDV submissions when learner has no name change and verified name feature is enabled
MST-1059: https://openedx.atlassian.net/browse/MST-1059

This code change changes how the full_name field is included in the form data sent during IDV submission.

Before, full_name was only included in the form data when the learner entered a different name in the GetNameIdPanel than what was displayed to them as the default (i.e. their full name on their profile). If a full_name was provided in the request, the server would use the supplied full_name when creating the IDV record and would update the learner's full name on their profile accordingly. If a full_name was not provided in the request, then the server would fall back to the current full name on their profile when creating the IDV record and no change to the full name on their profile would occur.

With the introduction of the Verified Name feature, things have changed. Assuming the feature is enabled, the name displayed to the learner is either the most recent pending or approved verified name or their full name on their profile if the former does not exist. This means that it is no longer correct for the server to create an IDV record using the current full name on the learner's profile when full_name is not provided, which, again, occurs if the learner does not change the name submitted with IDV. This is because, if the learner has a pending or approved verified name, that should be prioritzed over the full name on their profile.

This code change sends the full_name field whether or not the learner has modified it in the IDV flow, if the verified name feature is enabled. This allows the server to create a verified name with whatever value is submitted to it through IDV. The reason we only do this if the feature is enabled is that, when the feature is off, the server will change the learner's profile name to this value, as described above. If we send the idPhotoName on all requests, even ones where the learner does not change the idPhotoName, then the server will record that the full name on the learner's profile has a requested change, even if the name is the same. Their name may not change, but this will pollute the history by being logged as a requested change.
2021-09-24 12:22:19 -04:00
Michael Roytman
1d0bd3986c Merge pull request #505 from edx/mroytman/MST-1016-verified-name-in-account-name-check
feat: include pending or approved verified names in the name field if they exist
2021-09-24 12:03:07 -04:00
michaelroytman
1c0dc36907 feat: include pending or approved verified names in the name field if they exist
MST-1016: https://openedx.atlassian.net/browse/MST-1016

If a learner has a pending or approved verified name, the most recent should take precedence over the profile name on the GetNameIdPanel during the "Account Name Check". If the learner has no pending or approved verified names, then the learner's profile name should be used instead.
2021-09-23 11:44:51 -04:00
Bianca Severino
ac0ab9daea Merge pull request #504 from edx/bseverino/pending-name
[MST-1057] Display pending name when in "submitted" state
2021-09-21 11:43:08 -04:00
Bianca Severino
0d45d17cd3 fix: display pending name when in submitted state 2021-09-21 10:18:49 -04:00
Renovate Bot
a6d265b885 chore(deps): update dependency @testing-library/react to v12.1.0 2021-09-20 08:18:04 +00:00
Renovate Bot
3508bc6c34 fix(deps): update dependency @edx/frontend-platform to v1.12.7 2021-09-20 08:00:12 +00:00
edX Transifex Bot
28de621fc7 chore(i18n): update translations 2021-09-20 02:04:23 +05:00
Bianca Severino
e6df5e77ae Merge pull request #486 from edx/bseverino/change-name
[MST-803] Add name change modal
2021-09-16 10:09:47 -04:00
Bianca Severino
8bc5c1fae8 feat: add name change modal 2021-09-15 14:15:43 -04:00
Bianca Severino
6e48c9d2d1 Merge pull request #497 from edx/bseverino/certificate-name
feat: add certificate preference to name fields
2021-09-15 13:37:23 -04:00
Andrew Shultz
d3469d648f Merge pull request #500 from edx/ashultz0/idv_redo_ok
feat: if verified name is on, we can redo ID verification
2021-09-15 11:03:56 -04:00
Bianca Severino
cc65ffc96f feat: add certificate preference to name fields
Adds a checkbox under "Name" and "Verified Name" fields,
signifying which name the user prefers to display on their
certificates. When checking the checkbox, the user can save
this choice along with their name. When unchecking the check-
box, a modal appears, prompting the user to choose a name
to display on their certificate.
2021-09-15 11:01:20 -04:00
Andy Shultz
5640fb95c2 fix: use correct initial state type 2021-09-15 09:36:09 -04:00
Andy Shultz
7bbb889258 feat: if verified name is on, we can redo ID verification
The verified_name endpoint provides is_enabled only it a name exists,
so we have to request the enabled flag separately.

MST-1026
2021-09-13 13:17:14 -04:00
Renovate Bot
53b59231cb fix(deps): update dependency @edx/frontend-platform to v1.12.6 2021-09-13 10:16:42 +00:00
Renovate Bot
8fb25fd89b fix(deps): update dependency formdata-polyfill to v4.0.7 2021-09-13 09:59:52 +00:00
Bianca Severino
15d2bf60f9 Merge pull request #493 from edx/mroytman/MST-954-failure-name-change-additional-changes
fix: Fix Broken Editing of Verified Name Field
2021-09-08 10:57:46 -04:00
Renovate Bot
135826bc52 fix(deps): update react-router monorepo 2021-09-06 08:39:07 +00:00
Renovate Bot
d7251e6aec fix(deps): update dependency react-redux to v7.2.5 2021-09-06 08:18:30 +00:00
edX Transifex Bot
0b0846fb00 fix(i18n): update translations 2021-09-06 02:04:24 +05:00
michaelroytman
b1cd1b1995 fix: Fix Broken Editing of Verified Name Field
This code change fixes a bug where the verified name field was not editable. This was due to the fact that the verified name was not wired up to the Redux store, so draft verified names were not stored in the Redux store.
2021-09-03 11:44:14 -04:00
alangsto
50c468857a Merge pull request #492 from edx/alangsto/pending_alert
feat: add alert and correct messaging for submitted verified name
2021-08-31 15:47:07 -04:00
Alie Langston
c940d3463c feat: add alert and correct messaging for pending verified name
MST-954 (https://openedx.atlassian.net/browse/MST-954)
2021-08-31 15:35:31 -04:00
Michael Roytman
376deba866 Merge pull request #491 from edx/mroytman/MST-954-failure-name-change-display
feat: Change Which VerifiedName Record is Displayed When Most Recent VerifiedName is Denied
2021-08-31 13:52:43 -04:00
michaelroytman
37d0e6e0fb feat: Change Which VerifiedName Record is Displayed When Most Recent VerifiedName is Denied
If a learner requests a profile name change or verified name change that requires ID verification, their ID verification submission may be rejected. This code change changes the verified name that is displayed in this case. If a learner's most recent verified name record has been denied, and the learner has a previously approved verified name, display that. If If a learner's most recent verified name record has been denied, and the learner either has no previously verified name records or no previous verified name records at all, do not show a verified name field.

It also refactors the code to better support UI treatments for other verified name statuses, like pending and submitted.
MST-954 (https://openedx.atlassian.net/browse/MST-954)
2021-08-30 17:05:08 -04:00
Michael Roytman
9261711d4a Merge pull request #490 from edx/mroytman/MST-954-failure-name-change-alert
feat: Add Dismissible Alert Informing Learner of Failed Name Verification
2021-08-30 17:04:24 -04:00
michaelroytman
3e42d42ad7 feat: Add Dismissible Alert Informing Learner of Failed Name Verification
If a learner requests a profile name change or verified name change that requires ID verification, their ID verification submission may be rejected. This code change adds a dismissible error alert to the Account Settings page informing the learner of the failure and redirecting them to a help article about ID verification failures. The local storage key used to determine whether to show the alert or not uses the created time of the name verification object to ensure that subsequent ID verification failures still trigger this alert, even for the same name.

MST-954 (https://openedx.atlassian.net/browse/MST-954)
2021-08-30 13:18:02 -04:00
Simon Chen
7de4edc002 Merge pull request #487 from edx/schen/pending_name
feat: Make VerifiedName not editable and show proper icon when the Name is in pending status
2021-08-30 12:58:39 -04:00
Simon Chen
2936498b02 feat: Make VerifiedName not editable and show proper icon when the Name is in pending status 2021-08-30 11:50:45 -04:00
Renovate Bot
44d26c444b chore(deps): update dependency husky to v7.0.2 2021-08-30 09:28:00 +00:00
Michael Roytman
b1c1c6502d Merge pull request #485 from edx/mroytman/MST-956-pending-name-change-alert
feat: Replace use of verified_name API with verified_name_history API.
2021-08-27 13:50:04 -04:00
michaelroytman
21dda3f25b feat: Replace use of verified_name API with verified_name_history API.
This replaces the use of the verified_name API ({LMS_BASE}/api/edx_name_affirmation/v1/verified_name) with the verified_name_history API ({LMS_BASE}/api/edx_name_affirmation/v1/verified_name/history). The verified_name API returns the most recent verified name for a learner. However, we need access to the entire learner's verified name history in order to view verified name records with other statuses (i.e. pending, submitted, denied) as well as to be able to display previously verified names in the event that the most recent verified name record is denied.

MST-954 (https://openedx.atlassian.net/browse/MST-954)
2021-08-27 12:24:21 -04:00
Andrew Shultz
f87b5040a3 Merge pull request #484 from edx/ashultz0/verified-status
switch account status page to use verified name status
2021-08-26 11:38:52 -04:00
Andy Shultz
0dc1df07d4 feat: gate render verified name success message outside of function
this frees the function from doing logic we've already done elsewhere
2021-08-26 11:13:43 -04:00
Andy Shultz
efa682092f feat: specify shape of verified name object
at least the fields that we are interested in
2021-08-26 11:09:33 -04:00
Andy Shultz
0d9e6f8b87 feat: do not smear verified name fields into general fields
smooshing an unattached field name status into the general set of
fields is a recipie for future bugs
2021-08-26 10:08:28 -04:00
Andy Shultz
26d2b50859 feat: update verified name display to look at status not is_verified bool
note that there is no way to trigger pending display currently without
code manipulation because the verified name endpoint in use only
returns approved names
2021-08-26 10:07:13 -04:00
Simon Chen
3eb63cd624 Merge pull request #482 from edx/schen/fix_name_error
fix: Should not display error message when the account name matches the ID name
2021-08-23 11:06:55 -04:00
Simon Chen
ba0774c5c4 fix: Should not display error message when the account name matches the ID name
MST-990, we should fix the problem where the ID name error message still show up with Account name filled in or Verified name filled in.
2021-08-23 11:03:19 -04:00
Simon Chen
ac47d0b180 Merge pull request #481 from edx/renovate/formdata-polyfill-4.x
fix(deps): update dependency formdata-polyfill to v4
2021-08-23 09:26:25 -04:00
Simon Chen
02038b8ac9 Merge pull request #471 from edx/renovate/edx-frontend-build-8.x
chore(deps): update dependency @edx/frontend-build to v8
2021-08-23 09:16:02 -04:00
Simon Chen
458f9f7e3d Merge pull request #477 from edx/renovate/es-check-6.x
chore(deps): update dependency es-check to v6
2021-08-23 09:15:10 -04:00
Simon Chen
c7d9c270f9 Merge pull request #479 from edx/schen/idv_use_verified_name
feat: Update IDV workflow to use the VerifiedName
2021-08-23 09:13:12 -04:00
Renovate Bot
d0ecbbfb8a fix(deps): update dependency formdata-polyfill to v4 2021-08-23 08:24:31 +00:00
Renovate Bot
22db0d9202 chore(deps): update dependency es-check to v6 2021-08-23 08:19:01 +00:00
Renovate Bot
34a142f55f chore(deps): update dependency @edx/frontend-build to v8 2021-08-23 08:17:25 +00:00
Renovate Bot
6e00915f98 fix(deps): update dependency @edx/frontend-platform to v1.12.4 2021-08-23 08:09:49 +00:00
Simon Chen
4cfa1707de feat: Update IDV workflow to use the VerifiedName instead of the profile name
If the Verified Name feature is turned on, and the user do have a VerifiedName, use that name on the Account Name Check page of the IDV flow instead of account profile name
2021-08-20 11:08:43 -04:00
Renovate Bot
a721887886 fix(deps): update dependency @edx/frontend-platform to v1.12.3 2021-08-16 06:59:33 +00:00
Renovate Bot
9cdbb93bf3 fix(deps): update dependency @edx/frontend-component-footer to v10.1.6 2021-08-16 06:41:37 +00:00
stvn
f9e7519e26 merge(#454): renovate/actions-setup-node-2.x
commits
=======
- chore(deps): update actions/setup-node action to v2
2021-08-10 10:08:50 -07:00
Renovate Bot
ff8d5a4d09 chore(deps): update actions/setup-node action to v2 2021-08-10 07:29:09 +00:00
stvn
f14c71c4fb merge(#470): renovate/codecov-codecov-action-2.x
commits
=======
- chore(deps): update codecov/codecov-action action to v2
2021-08-09 21:41:29 -07:00
Renovate Bot
43caac8430 chore(deps): update codecov/codecov-action action to v2 2021-08-10 00:38:25 +00:00
stvn
ee1ecb8ab9 merge(#354): adzuci/update-owner-to-tnl
commits
=======
- docs: remove owner from openedx.yml
2021-08-09 16:40:36 -07:00
Adam Blackwell
020aa84986 docs: remove owner from openedx.yml 2021-08-09 16:27:51 -07:00
Renovate Bot
24459daf6d fix(deps): update font awesome 2021-08-09 10:31:52 +00:00
Renovate Bot
587533703e fix(deps): update dependency redux to v4.1.1 2021-08-09 10:14:52 +00:00
Renovate Bot
866746d1c6 fix(deps): update dependency @edx/frontend-platform to v1.11.3 2021-08-02 19:45:00 +00:00
Albert (AJ) St. Aubin
4c618a55c0 Merge pull request #472 from edx/aj/fix_demographics
fix: Demographics section will now show when enabled
2021-08-02 14:42:50 -04:00
Albert (AJ) St. Aubin
c9f6cf708e fix: Demographics section will now show when enabled 2021-08-02 14:29:39 -04:00
edX Transifex Bot
5f314ee65f fix(i18n): update translations 2021-08-02 02:04:23 +05:00
Michael Roytman
7e35b23b36 Merge pull request #466 from edx/mroytman/MST-800-verified-name-field
[MST-800] Add Verified Name field and success alert to the Account Settings page
2021-07-30 11:02:57 -04:00
Michael Roytman
842bd11d89 feat: Add Verified Name field and success alert to the Account Settings page
MST-800: https://openedx.atlassian.net/browse/MST-800

This code change adds a Verified Name editable field to the Account Settings page. When the verified name API in the LMS returns that a learner has a verified name, and that the name affirmation feature flag is turned on, the Account Settings page displays the Verified Name field. If the learner's name is verified (i.e. it is not pending verification), then a green checkmark is displayed to the right of the field label.

The first time the learner visits the Account Settings page after verification, a dismissible alert is displayed indicating that their vreified name was verified. Whether the alert has been dismissed is stored in localStorage, and the alert no longer reappears after it is dismissed.
2021-07-30 10:16:40 -04:00
Renovate Bot
b1e11dfb36 chore(deps): update dependency @edx/frontend-build to v7.1.0 2021-07-26 07:27:17 +00:00
Renovate Bot
aa57b69924 chore(deps): update dependency codecov to v3.8.3 2021-07-26 07:15:09 +00:00
stvn
e7769b37e9 merge(#461): renovate/husky-7.x
commits
=======
- chore(deps): update dependency husky to v7
2021-07-21 11:32:53 -07:00
Renovate Bot
fcc7b26c28 chore(deps): update dependency husky to v7 2021-07-19 07:55:01 +00:00
Renovate Bot
16d844528d fix(deps): update dependency jslib-html5-camera-photo to v3.1.8 2021-07-19 07:49:54 +00:00
Renovate Bot
223234f623 chore(deps): update dependency @edx/frontend-build to v7.0.6 2021-07-19 07:37:33 +00:00
Renovate Bot
d8a1c0ca8c fix(deps): update dependency @edx/paragon to v16.1.0 2021-07-12 07:08:14 +00:00
Renovate Bot
1da8f630eb fix(deps): update dependency @edx/frontend-platform to v1.11.1 2021-07-12 06:55:19 +00:00
Renovate Bot
228eec0afa chore(deps): update dependency @edx/frontend-build to v7.0.3 2021-07-12 06:40:54 +00:00
David Joy
cf62b4b82c fix: upgrade frontend-build to v7 and paragon to v16 (#457)
* build: bumping to Paragon 16.x

Test snapshots needed to be updated because the Hyperlink external link icon is now SVG instead of font-awesome, and because it added some new classes.

* build: bump frontend-build to v7
2021-07-07 16:10:09 -04:00
renovate[bot]
0184c1fa25 chore(deps): update dependency @edx/frontend-build to v6 (#455)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-07-06 14:44:31 -04:00
Renovate Bot
8ed103b2ad chore(deps): update dependency @testing-library/react to v12 2021-07-05 06:59:43 +00:00
Renovate Bot
84a9de44a5 chore(deps): update dependency es-check to v5.2.4 2021-06-28 07:27:56 +00:00
Renovate Bot
84df0a0b3e fix(deps): update dependency redux to v4.1.0 2021-06-21 07:21:17 +00:00
Renovate Bot
a3917ae550 fix(deps): update dependency qs to v6.10.1 2021-06-21 07:09:01 +00:00
Renovate Bot
bfd6a07a2c fix(deps): update dependency memoize-one to v5.2.1 2021-06-21 06:55:37 +00:00
Renovate Bot
1df570989b fix(deps): update dependency classnames to v2.3.1 2021-06-21 06:40:54 +00:00
stvn
b7e433876e merge(#396): renovate/testing-library-react-11.x
commits
=======
- chore(deps): update dependency @testing-library/react to v11
2021-06-17 02:48:11 -07:00
Renovate Bot
1669d577f6 chore(deps): update dependency @testing-library/react to v11 2021-06-15 09:09:28 +00:00
Renovate Bot
d1ca7decce fix(deps): update dependency @edx/paragon to v13.17.5 2021-06-15 09:04:04 +00:00
Renovate Bot
79a43ae713 fix(deps): update dependency @edx/frontend-component-footer to v10.1.5 2021-06-15 08:49:54 +00:00
Renovate Bot
9d0b315714 fix(deps): update dependency @edx/frontend-component-header to v2.3.0 2021-06-14 23:34:42 +00:00
Renovate Bot
4b2bc11378 fix(deps): update dependency @edx/frontend-platform to v1.11.0 2021-06-14 07:10:19 +00:00
Renovate Bot
fc7ce6b91e chore(deps): update dependency @testing-library/jest-dom to v5.14.1 2021-06-14 06:51:50 +00:00
Renovate Bot
39a25fe5bc fix(deps): update font awesome 2021-06-08 07:57:01 +00:00
Renovate Bot
307cb1541b fix(deps): update dependency redux-devtools-extension to v2.13.9 2021-06-07 07:02:48 +00:00
Renovate Bot
03e026ce4e fix(deps): update reactrouter monorepo to v5.2.0 2021-06-05 07:06:40 +00:00
Renovate Bot
a29876aff0 fix(deps): update dependency react-redux to v7.2.4 2021-06-05 06:48:36 +00:00
Renovate Bot
ab77246015 fix(deps): update dependency react-transition-group to v4.4.2 2021-06-05 06:31:19 +00:00
stvn
572b05e7f1 merge(#444): build/renovate
commits
=======
- build(renovate): fix json syntax
2021-06-04 23:05:09 -07:00
stvn
3e4de47ba6 build(renovate): fix json syntax 2021-06-04 22:09:16 -07:00
stvn
6c6cedd422 merge(#442): build/renovate
commits
=======
- build(renovate): be more selective about automerging devDependencies
2021-06-04 13:20:35 -07:00
stvn
43694921ca build(renovate): be more selective about automerging devDependencies
to avoid bumping the major version, besides linters and testers.
2021-06-04 13:15:46 -07:00
Renovate Bot
06ded1e66e fix(deps): update react monorepo to v16.14.0 2021-06-04 02:03:50 +00:00
Renovate Bot
aba1bb3382 fix(deps): update dependency @tensorflow/tfjs-core to v1.7.4 2021-06-04 00:13:07 +00:00
Renovate Bot
d67b880028 fix(deps): update dependency @tensorflow/tfjs-converter to v1.7.4 2021-06-03 21:48:51 +00:00
Renovate Bot
29692add53 chore(deps): update dependency enzyme to v3.11.0 2021-06-03 19:45:17 +00:00
Renovate Bot
7f53bf32ca chore(deps): update dependency codecov to v3.8.2 2021-06-03 19:18:50 +00:00
Renovate Bot
add22d9756 fix(deps): update dependency bowser to v2.11.0 2021-06-03 18:50:57 +00:00
Renovate Bot
bef9bf76fd chore(deps): update dependency husky to v3.1.0 2021-06-03 18:22:21 +00:00
Renovate Bot
bf6b2fb8b8 chore(deps): update dependency @edx/frontend-build to v5.6.14 2021-06-03 17:52:24 +00:00
Renovate Bot
a77cd6d91a chore(deps): update dependency es-check to v5.2.3 2021-06-03 17:21:04 +00:00
stvn
9b3f222191 merge(#441): build/renovate
commits
=======
- build(renovate): remove linters/testers automerge presets
- build(renovate): automerge lockFileMaintenence
- build(renovate): use enableVulnerabilityAlerts preset
- build(renovate): allow unscheduled updates
2021-06-03 09:40:38 -07:00
stvn
90f2ed8393 merge(#440): build/remove-travis
commits
=======
- build(ci): run ci tests via matrix
- build(ci): always run `npm ci` via Makefile
- build(ci): convert travis-ci to github actions
2021-06-03 09:38:51 -07:00
stvn
95c53ad380 build(renovate): remove linters/testers automerge presets
on the assumption that our blanket policy for `devDependencies` will overlap.
2021-06-02 13:47:16 -07:00
stvn
3023cd3d55 build(renovate): automerge lockFileMaintenence 2021-06-02 12:33:07 -07:00
stvn
50f85674b1 build(renovate): use enableVulnerabilityAlerts preset 2021-06-02 12:33:06 -07:00
stvn
9437ee36f3 build(renovate): allow unscheduled updates
so we don't need to wait until next week to see changes.
2021-06-02 12:32:57 -07:00
stvn
463944012c merge(#438): build/renovate
commits
=======
- build(renovate): automerge devDependencies
- build(renovate): use preset to schedule weekly
- build(renovate): use preset to rebaseStalePrs
2021-06-02 11:40:56 -07:00
stvn
2d297aa7be build(renovate): automerge devDependencies
tickets
=======
- Fixes CENG-109
- Fixes stvstnfrd/openedx-meta#157
2021-06-02 11:33:17 -07:00
stvn
1ab5901d24 build(ci): run ci tests via matrix 2021-06-02 11:28:11 -07:00
stvn
ba678d92f7 build(ci): always run npm ci via Makefile 2021-06-02 11:28:11 -07:00
stvn
884651a702 build(ci): convert travis-ci to github actions
tickets
=======
- Fixes CENG-108
- Fixes stvstnfrd/openedx-meta#156
2021-06-02 11:26:53 -07:00
stvn
a152c631da build(renovate): use preset to schedule weekly 2021-06-01 23:36:43 -07:00
stvn
fbe91ce7e4 build(renovate): use preset to rebaseStalePrs 2021-06-01 23:33:07 -07:00
stvn
13681c1360 merge: stvn/own/code 2021-05-26 13:42:09 -07:00
stvn
43435f8ff3 build: add CODEOWNERS; edx/community-engineering
Background
==========
As part of our Squad-based ownership, we should stay on top of what
happens in these repositories. However, due to the number of
repositories (and subsequently pull requests) across the edX ecosystem,
it is challenging to stay on top of notifications, separating the
'signal' from the 'noise'. Email filters can go a long way to taming
Inbox notifications, but this is manual and requires maintenance as
Squad ownership changes. It also fails to account for Github-specific behavior.

Proposal
========
By leveraging Github support for `CODEOWNERS` files [1],
we can ensure that our team is at least CCed explicitly, here,
in the form a requested review. This request is just that, a request,
not a requirement; we are not enacting any new merge requirements
at this time.

- [1] https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
2021-05-26 12:14:32 -07:00
David Joy
830fb05819 docs: update README with environment variable details. (#436)
* build: defaulting coaching and demographics to false

Given that coaching and demographics are private, internal features to edx.org, we default them to false.

* docs: documenting custom MFE environment variables

Also adjusting the group to tag while I’m in here, and linking directly to the common env variables in read the docs.

* docs: improve formatting and fix sentence fragment in SUPPORT_URL
2021-05-26 13:41:24 -04:00
David Joy
f62bd5ad76 chore: let renovate be more liberal about what it merges (#435)
This repo was allowing both patch/minor updates of Paragon, but I've opened that up a bit with this PR.  This is based on similar config we've used in other repositories.

It loses the "stability days" that were here, but I don't think we need that for patch/minor updates.
2021-05-18 16:40:15 -04:00
edX Transifex Bot
e2f9edd623 fix(i18n): update translations 2021-05-10 02:11:44 +05:00
renovate[bot]
0b63613736 fix(deps): update dependency @edx/frontend-platform to v1.9.5 (#190)
Co-authored-by: Renovate Bot <bot@renovateapp.com>
2021-05-07 14:06:41 -04:00
David Joy
940828ff18 fix: Resolving i18n violations, hard-coded edx.org-specific strings (#433)
* fix: removing hard-coded edX from DemographicsSection messages

* fix: removing hard-coded “edX” strings from IDV messages

* fix: updating ThirdPartyAuth message description to remove hard-coded “edX” references.

* fix: replacing hard-coded “edX” strings with SITE_NAME in AccountSettingsPage

* fix: conditionalizing edx-specific strings in ConfirmationModal

If the SITE_NAME is ‘edX’, then edx.org-specific strings will be used.  Otherwise, more general, Open edX-appropriate strings will be used.

* fix: conditionalizing edX-specific strings in delete account components.

If the SITE_NAME is ‘edX’, then edx.org-specific strings will be used.  Otherwise, more general, Open edX-appropriate strings will be used.

* fix: replacing hard-coded ‘edX’ strings with SITE_NAME in ReviewRequirementsPanel

I missed a few because the messages were re-used.

* fix: review feedback, improving messages

- Removing unnecessary {siteName} references
- Improving some message descriptions
2021-05-07 14:00:21 -04:00
David Joy
296f68f7dd fix: remove hard-coded edX from page title (#431)
This uses variable substitution to insert SITE_NAME into index.html, rather than hard-coding “edX” into the file.  It also updates the .env files to use “localhost” as the default SITE_NAME.
2021-05-03 15:52:55 -04:00
Sarina Canelake
e59ada660c Merge pull request #427 from edx/sarina/update-README
update README
2021-04-16 15:45:58 -04:00
alangsto
c123daacd6 Merge pull request #430 from edx/alangsto/add_photo_mode_to_submit
Add additional post parameters to track photo modes
2021-04-15 13:36:19 -04:00
Alie Langston
7440cd367f Add additional post parameters to track photo modes 2021-04-15 10:50:22 -04:00
sarina
1d639c4a57 doc: Update README
- New, clearer installation instructions
- Additional details regarding MFE domain & roadmap
2021-04-14 12:29:41 -04:00
alangsto
6db789d6ac Merge pull request #429 from edx/alangsto/add_event_tracking
MST IDV Experiment Event Tracking
2021-04-13 07:40:35 -04:00
Alie Langston
4bbff91ad7 MST IDV Experiment
Add event tracking for initial choice in photo mode, and additional events when a user toggles between modes on the photo capture pages
2021-04-12 17:23:54 -04:00
edX Transifex Bot
24e32cd0c5 fix(i18n): update translations 2021-04-11 17:11:05 -04:00
186 changed files with 32641 additions and 25108 deletions

59
.env
View File

@@ -1,25 +1,36 @@
ACCESS_TOKEN_COOKIE_NAME=null
BASE_URL=null
CREDENTIALS_BASE_URL=null
CSRF_TOKEN_API_PATH=null
ECOMMERCE_BASE_URL=null
LANGUAGE_PREFERENCE_COOKIE_NAME=null
LMS_BASE_URL=null
DEMOGRAPHICS_BASE_URL=null
LOGIN_URL=null
LOGOUT_URL=null
MARKETING_SITE_BASE_URL=null
NODE_ENV=null
ORDER_HISTORY_URL=null
REFRESH_ACCESS_TOKEN_ENDPOINT=null
SEGMENT_KEY=null
SITE_NAME=null
SUPPORT_URL=null
USER_INFO_COOKIE_NAME=null
LOGO_URL=''
LOGO_TRADEMARK_URL=''
LOGO_WHITE_URL=''
ACCESS_TOKEN_COOKIE_NAME=''
BASE_URL=''
COACHING_ENABLED=''
CREDENTIALS_BASE_URL=''
CSRF_TOKEN_API_PATH=''
DEMOGRAPHICS_BASE_URL=''
DISCOVERY_API_BASE_URL=''
ECOMMERCE_BASE_URL=''
ENABLE_DEMOGRAPHICS_COLLECTION=''
FAVICON_URL=''
PUBLISHER_BASE_URL=
STUDIO_BASE_URL=
DISCOVERY_API_BASE_URL=
LANGUAGE_PREFERENCE_COOKIE_NAME=''
LMS_BASE_URL=''
LOGIN_URL=''
LOGO_TRADEMARK_URL=''
LOGO_URL=''
LOGO_WHITE_URL=''
LOGOUT_URL=''
MARKETING_SITE_BASE_URL=''
NODE_ENV=''
ORDER_HISTORY_URL=''
PUBLISHER_BASE_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT=''
SEGMENT_KEY=''
SITE_NAME=''
STUDIO_BASE_URL=''
SUPPORT_URL=''
USER_INFO_COOKIE_NAME=''
ENABLE_COPPA_COMPLIANCE=''
ENABLE_ACCOUNT_DELETION=''
ENABLE_DOB_UPDATE=''
MARKETING_EMAILS_OPT_IN=''
APP_ID=
MFE_CONFIG_API_URL=
PASSWORD_RESET_SUPPORT_LINK=''
LEARNER_FEEDBACK_URL=''
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://support.edx.org/hc/en-us/articles/207206067'

View File

@@ -1,29 +1,37 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1997'
COACHING_ENABLED=''
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
DISCOVERY_API_BASE_URL=''
ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_DEMOGRAPHICS_COLLECTION=''
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGOUT_URL='http://localhost:18000/logout'
MARKETING_SITE_BASE_URL='http://localhost:5335'
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
NODE_ENV='development'
ORDER_HISTORY_URL='localhost:1996/orders'
ORDER_HISTORY_URL='http://localhost:1996/orders'
PORT=1997
PUBLISHER_BASE_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
SEGMENT_KEY=''
SITE_NAME=localhost
STUDIO_BASE_URL=''
SUPPORT_URL='http://localhost:18000/support'
USER_INFO_COOKIE_NAME='edx-user-info'
# Temporary, Remove this once we are ready to release the feature.
COACHING_ENABLED=true
ENABLE_DEMOGRAPHICS_COLLECTION=true
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
PUBLISHER_BASE_URL=
STUDIO_BASE_URL=
DISCOVERY_API_BASE_URL=
ENABLE_COPPA_COMPLIANCE=''
ENABLE_ACCOUNT_DELETION=''
ENABLE_DOB_UPDATE=''
MARKETING_EMAILS_OPT_IN=''
APP_ID=
MFE_CONFIG_API_URL=
PASSWORD_RESET_SUPPORT_LINK='mailto:support@example.com'
LEARNER_FEEDBACK_URL=''
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://support.edx.org/hc/en-us/articles/207206067'

View File

@@ -1,27 +1,35 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1997'
COACHING_ENABLED=''
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
DISCOVERY_API_BASE_URL=''
ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_DEMOGRAPHICS_COLLECTION=''
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
LOGOUT_URL='http://localhost:18000/logout'
MARKETING_SITE_BASE_URL='http://localhost:5335'
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
NODE_ENV=null
ORDER_HISTORY_URL='localhost:1996/orders'
NODE_ENV=''
ORDER_HISTORY_URL='http://localhost:1996/orders'
PUBLISHER_BASE_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
SEGMENT_KEY=''
SITE_NAME=localhost
STUDIO_BASE_URL=''
SUPPORT_URL='http://localhost:18000/support'
USER_INFO_COOKIE_NAME='edx-user-info'
COACHING_ENABLED=''
ENABLE_DEMOGRAPHICS_COLLECTION=''
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
PUBLISHER_BASE_URL=
STUDIO_BASE_URL=
DISCOVERY_API_BASE_URL=
ENABLE_COPPA_COMPLIANCE=''
ENABLE_ACCOUNT_DELETION=''
ENABLE_DOB_UPDATE=''
MARKETING_EMAILS_OPT_IN=''
APP_ID=
MFE_CONFIG_API_URL=
LEARNER_FEEDBACK_URL=''
SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT='https://support.edx.org/hc/en-us/articles/207206067'

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('eslint');

View File

@@ -0,0 +1,19 @@
# Run the workflow that adds new tickets that are either:
# - labelled "DEPR"
# - title starts with "[DEPR]"
# - body starts with "Proposal Date" (this is the first template field)
# to the org-wide DEPR project board
name: Add newly created DEPR issues to the DEPR project board
on:
issues:
types: [opened]
jobs:
routeissue:
uses: openedx/.github/.github/workflows/add-depr-ticket-to-depr-board.yml@master
secrets:
GITHUB_APP_ID: ${{ secrets.GRAPHQL_AUTH_APP_ID }}
GITHUB_APP_PRIVATE_KEY: ${{ secrets.GRAPHQL_AUTH_APP_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_ISSUE_BOT_TOKEN }}

View File

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

29
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,29 @@
name: ci
on:
push:
branches:
- master
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
npm-test:
- i18n_extract
- lint
- test
steps:
- uses: actions/checkout@v2
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- uses: actions/setup-node@v2
with:
node-version: ${{ env.NODE_VER }}
- run: make requirements
- run: make test NPM_TESTS=build
- run: make test NPM_TESTS=${{ matrix.npm-test }}
- name: upload coverage
uses: codecov/codecov-action@v3
with:
fail_ci_if_error: false

10
.github/workflows/commitlint.yml vendored Normal file
View File

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

View File

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

12
.github/workflows/self-assign-issue.yml vendored Normal file
View File

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

View File

@@ -0,0 +1,12 @@
name: Update Browserslist DB
on:
schedule:
- cron: '0 0 * * 1'
workflow_dispatch:
jobs:
update-browserslist:
uses: openedx/.github/.github/workflows/update-browserslist-db.yml@master
secrets:
requirements_bot_github_token: ${{ secrets.requirements_bot_github_token }}

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18

View File

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

View File

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

46
Makefile Executable file → Normal file
View File

@@ -1,17 +1,28 @@
export TRANSIFEX_RESOURCE = frontend-app-account
transifex_resource = frontend-app-account
transifex_langs = "ar,fr,es_419,zh_CN"
transifex_langs = "ar,de,es_419,fa_IR,fr,fr_CA,hi,it,pt,ru,uk,zh_CN,it_IT,pt_PT,de_DE"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json
tx_url1 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/translation/en/strings/
tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transifex_resource)/source/
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
transifex_temp = ./temp/babel-plugin-formatjs
requirements:
npm install
NPM_TESTS=build i18n_extract lint test
.PHONY: test
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
.PHONY: test.npm.*
test.npm.%: validate-no-uncommitted-package-lock-changes
test -d node_modules || $(MAKE) requirements
npm run $(*)
.PHONY: requirements
requirements: ## install ci requirements
npm ci
i18n.extract:
# Pulling display strings from .jsx files into .json files...
@@ -34,15 +45,30 @@ push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/reactifex/bash_scripts/get_hashed_strings.sh $(tx_url1)
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/reactifex/bash_scripts/put_comments.sh $(tx_url2)
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --language=$(transifex_langs)
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull --filter=$(transifex_langs) \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-app-account/src/i18n/messages:frontend-app-account
$(intl_imports) paragon frontend-component-header frontend-component-footer frontend-app-account
endif
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:

View File

@@ -1,48 +1,125 @@
|Build Status| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
|ci-badge| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
frontend-app-account
====================
This is a micro-frontend application responsible for the display and updating of a user's account information. Please tag **@edx/arch-team** on any PRs or issues.
Introduction
------------
Development
-----------
This is a micro-frontend application responsible for the display and updating of a user's account information.
Start Devstack
^^^^^^^^^^^^^^
What is the domain of this MFE?
To use this application `devstack <https://github.com/edx/devstack>`__ must be running and you must be logged into it.
In this MFE: Private user settings UIs. Public facing profile is in a `separate MFE (Profile) <https://github.com/openedx/frontend-app-profile>`_
- Start devstack
- Log in (http://localhost:18000/login)
- Account settings page
Start the development server
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Demographics collection
In this project, install requirements and start the development server by running:
- IDV (Identity Verification)
.. code:: bash
Installation
------------
npm install
npm start # The server will run on port 1997
This MFE is bundled with `Devstack <https://github.com/openedx/devstack>`_, see the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ section for setup instructions.
Once the dev server is up visit http://localhost:1997.
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
Configuration and Deployment
----------------------------
2. Start up Devstack, if it's not already started.
This MFE is configured via node environment variables supplied at build time. See the .env file for the list of required environment variables. Example build syntax with a single environment variable:
3. Log in to Devstack (http://localhost:18000/login )
4. Within this project, install requirements and start the development server:
.. code-block::
npm install
npm start # The server will run on port 1997
5. Once the dev server is up, visit http://localhost:1997 to access the MFE
.. image:: ./docs/images/localhost_preview.png
Environment Variables/Setup Notes
---------------------------------
This MFE is configured via environment variables supplied at build time. All micro-frontends have a shared set of required environment variables, as documented in the Open edX Developer Guide under `Required Environment Variables <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
The account settings micro-frontend also supports the following additional variable:
``SUPPORT_URL``
Example: ``https://support.example.com``
The fully-qualified URL to the support page in the target environment.
``PASSWORD_RESET_SUPPORT_LINK``
Examples:
- ``https://support.edx.org/hc/en-us/articles/206212088-What-if-I-did-not-receive-a-password-reset-message-``
- ``mailto:support@example.com``
The fully-qualified URL to the support page or email to request the support from in the target environment.
``ENABLE_ACCOUNT_DELETION``
Example: ``'false'`` | ``''`` (empty strings are true)
Enable the account deletion option, defaults to true.
To disable account deletion set ``ENABLE_ACCOUNT_DELETION`` to ``'false'`` (string), otherwise it will default to true.
edX-specific Environment Variables
**********************************
Furthermore, there are several edX-specific environment variables that enable integrations with closed-source services private to the edX organization, and are unsupported in Open edX. Enabling these environment variables will result in undefined behavior in Open edX installations:
``COACHING_ENABLED``
Example: ``true`` | ``''`` (empty strings are falsy)
Enables support for a section of the micro-frontend that helps users arrange for coaching sessions. Integrates with a private coaching plugin and is only used by edx.org.
``ENABLE_DEMOGRAPHICS_COLLECTION``
Example: ``true`` | ``''`` (empty strings are falsy)
Enables support for a section of the account settings page where a user can enter demographics information. Integrates with a private demographics service and is only used by edx.org.
``DEMOGRAPHICS_BASE_URL``
Example: ``https://demographics.example.com``
Required only if ``ENABLE_DEMOGRAPHICS_COLLECTION`` is true. The fully-qualified URL to the private demographics service in the target environment.
Example build syntax with a single environment variable:
.. code:: bash
NODE_ENV=development ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' npm run build
For more information see the document: `Micro-frontend applications in Open
edX <https://github.com/edx/edx-developer-docs/blob/5191e800bf16cf42f25c58c58f983bdaf7f9305d/docs/micro-frontends-in-open-edx.rst>`__.
edX <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html#required-environment-variables>`__.
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-account.svg?branch=master
:target: https://travis-ci.com/edx/frontend-app-account
Known Issues
------------
None
Development Roadmap
-------------------
We don't have anything planned for the core of the MFE (the account settings page) - this MFE is currently in maintenance mode.
There may be a replacement for IDV coming down the pipe, so that may be DEPRed.
In the future, it's possible that demographics could be modeled as a plugin rather than being hard-coded into this MFE.
==============================
.. |ci-badge| image:: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml/badge.svg
:target: https://github.com/openedx/edx-developer-docs/actions/workflows/ci.yml
:alt: Continuous Integration
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-account
:target: https://codecov.io/gh/edx/frontend-app-account
.. |npm_version| image:: https://img.shields.io/npm/v/@edx/frontend-app-account.svg

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

View File

@@ -1,7 +1,7 @@
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', {
setupFiles: [
setupFilesAfterEnv: [
'<rootDir>/src/setupTest.js',
],
});

View File

@@ -3,5 +3,4 @@
nick: acct
oeps: {}
owner: edx/arch-team
openedx-release: {ref: master}

41713
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,50 +6,48 @@
"license": "AGPL-3.0",
"repository": {
"type": "git",
"url": "git+https://github.com/edx/frontend-app-account.git"
"url": "git+https://github.com/openedx/frontend-app-account.git"
},
"scripts": {
"build": "fedx-scripts webpack",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"is-es5": "es-check es5 ./dist/*.js",
"i18n_extract": "fedx-scripts formatjs extract",
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress",
"test": "fedx-scripts jest --coverage --passWithNoTests"
"test": "TZ=UTC fedx-scripts jest --coverage --passWithNoTests"
},
"bugs": {
"url": "https://github.com/edx/frontend-app-account/issues"
"url": "https://github.com/openedx/frontend-app-account/issues"
},
"homepage": "https://github.com/edx/frontend-app-account#readme",
"homepage": "https://github.com/openedx/frontend-app-account#readme",
"publishConfig": {
"access": "public"
},
"browserslist": [
"last 2 versions",
"ie 11"
"extends @edx/browserslist-config"
],
"dependencies": {
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-footer": "10.1.4",
"@edx/frontend-component-header": "2.2.4",
"@edx/frontend-platform": "1.8.4",
"@edx/paragon": "13.1.2",
"@fortawesome/fontawesome-svg-core": "1.2.34",
"@fortawesome/free-brands-svg-icons": "5.8.2",
"@fortawesome/free-regular-svg-icons": "5.7.2",
"@fortawesome/free-solid-svg-icons": "5.8.2",
"@fortawesome/react-fontawesome": "0.1.14",
"@edx/brand": "npm:@edx/brand-openedx@1.2.0",
"@edx/frontend-component-footer": "12.2.1",
"@edx/frontend-component-header": "4.6.1",
"@edx/frontend-platform": "5.4.0",
"@edx/paragon": "20.46.2",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"@tensorflow-models/blazeface": "0.0.7",
"@tensorflow/tfjs-converter": "1.6.1",
"@tensorflow/tfjs-core": "1.6.1",
"babel-polyfill": "6.26.0",
"bowser": "2.10.0",
"classnames": "2.2.6",
"@tensorflow/tfjs-converter": "3.21.0",
"@tensorflow/tfjs-core": "3.21.0",
"bowser": "2.11.0",
"classnames": "2.3.2",
"core-js": "3.33.0",
"font-awesome": "4.7.0",
"form-urlencoded": "4.0.1",
"formdata-polyfill": "3.0.20",
"history": "4.10.1",
"jslib-html5-camera-photo": "3.1.6",
"form-urlencoded": "6.1.4",
"formdata-polyfill": "4.0.10",
"jslib-html5-camera-photo": "3.3.4",
"lodash.camelcase": "4.3.0",
"lodash.debounce": "4.0.8",
"lodash.findindex": "4.6.0",
"lodash.get": "4.4.2",
@@ -58,36 +56,38 @@
"lodash.omit": "4.5.0",
"lodash.pick": "4.4.0",
"lodash.pickby": "4.6.0",
"memoize-one": "5.1.1",
"newrelic": "5.13.1",
"prop-types": "15.7.2",
"qs": "6.9.6",
"react": "16.10.2",
"react-dom": "16.10.2",
"react-redux": "7.1.3",
"react-router": "5.1.2",
"react-router-dom": "5.1.2",
"react-router-hash-link": "1.2.2",
"lodash.snakecase": "4.1.1",
"long": "5.2.3",
"memoize-one": "5.2.1",
"prop-types": "15.8.1",
"qs": "6.11.2",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "6.14.2",
"react-router-dom": "6.14.2",
"react-router-hash-link": "2.4.3",
"react-scrollspy": "3.4.3",
"react-transition-group": "4.3.0",
"redux": "4.0.5",
"redux-devtools-extension": "2.13.8",
"react-transition-group": "4.4.5",
"redux": "4.2.1",
"redux-devtools-extension": "2.13.9",
"redux-logger": "3.0.6",
"redux-saga": "1.1.3",
"redux-thunk": "2.3.0",
"reselect": "4.0.0",
"redux-saga": "1.2.3",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.0",
"reselect": "4.1.8",
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@edx/frontend-build": "5.6.9",
"@testing-library/jest-dom": "5.11.9",
"@testing-library/react": "10.4.9",
"codecov": "3.7.2",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.6",
"es-check": "5.0.0",
"husky": "3.0.9",
"react-test-renderer": "16.8.6",
"@edx/browserslist-config": "1.2.0",
"@edx/frontend-build": "13.0.0",
"@edx/reactifex": "1.1.0",
"@testing-library/jest-dom": "5.17.0",
"@testing-library/react": "12.1.5",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
"enzyme": "3.11.0",
"react-test-renderer": "17.0.2",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.4"
}

View File

@@ -1,17 +1,148 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en-us">
<head>
<title>Account | edX</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=webpackConfig.output.publicPath%>favicon.ico" type="image/x-icon" />
<% if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
></script>
<% } %>
<title>Account | <%= process.env.SITE_NAME %></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"
/>
<% if (process.env.OPTIMIZELY_PROJECT_ID) { %>
<script src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"></script>
<% } %>
</head>
<body>
<!-- begin usabilla live embed code -->
<script defer type="text/javascript">
window.lightningjs ||
(function (n) {
var e = "lightningjs";
function t(e, t) {
var r, i, a, o, d, c;
return (
t && (t += (/\?/.test(t) ? "&" : "?") + "lv=1"),
n[e] ||
((r = window),
(i = document),
(a = e),
(o = i.location.protocol),
(d = "load"),
(c = 0),
(function () {
n[a] = function () {
var t = arguments,
i = this,
o = ++c,
d = (i && i != r && i.id) || 0;
function s() {
return (s.id = o), n[a].apply(s, arguments);
}
return (
(e.s = e.s || []).push([o, d, t]),
(s.then = function (n, t, r) {
var i = (e.fh[o] = e.fh[o] || []),
a = (e.eh[o] = e.eh[o] || []),
d = (e.ph[o] = e.ph[o] || []);
return (
n && i.push(n), t && a.push(t), r && d.push(r), s
);
}),
s
);
};
var e = (n[a]._ = {});
function s() {
e.P(d), (e.w = 1), n[a]("_load");
}
(e.fh = {}),
(e.eh = {}),
(e.ph = {}),
(e.l = t
? t.replace(/^\/\//, ("https:" == o ? o : "http:") + "//")
: t),
(e.p = { 0: +new Date() }),
(e.P = function (n) {
e.p[n] = new Date() - e.p[0];
}),
e.w && s(),
r.addEventListener
? r.addEventListener(d, s, !1)
: r.attachEvent("onload", s);
var l = function () {
function n() {
return [
"<!DOCTYPE ",
o,
"><",
o,
"><head></head><",
t,
"><",
r,
' src="',
e.l,
'"></',
r,
"></",
t,
"></",
o,
">",
].join("");
}
var t = "body",
r = "script",
o = "html",
d = i[t];
if (!d) return setTimeout(l, 100);
e.P(1);
var c,
s = i.createElement("div"),
h = s.appendChild(i.createElement("div")),
u = i.createElement("iframe");
(s.style.display = "none"),
(d.insertBefore(s, d.firstChild).id = "lightningjs-" + a),
(u.frameBorder = "0"),
(u.id = "lightningjs-frame-" + a),
/MSIE[ ]+6/.test(navigator.userAgent) &&
(u.src = "javascript:false"),
(u.allowTransparency = "true"),
h.appendChild(u);
try {
u.contentWindow.document.open();
} catch (n) {
(e.domain = i.domain),
(c =
"javascript:var d=document.open();d.domain='" +
i.domain +
"';"),
(u.src = c + "void(0);");
}
try {
var p = u.contentWindow.document;
p.write(n()), p.close();
} catch (e) {
u.src =
c +
'd.write("' +
n().replace(/"/g, String.fromCharCode(92) + '"') +
'");d.close();';
}
e.P(2);
};
e.l && l();
})()),
(n[e].lv = "1"),
n[e]
);
}
var r = (window.lightningjs = t(e));
(r.require = t), (r.modules = n);
})({});
</script>
<!-- end usabilla live embed code -->
<div id="root"></div>
</body>
</html>

View File

@@ -1,17 +1,33 @@
{
"extends": [
"config:base"
"config:base",
"schedule:weekly",
":automergeLinters",
":automergeMinor",
":automergeTesters",
":enableVulnerabilityAlerts",
":rebaseStalePrs",
":semanticCommits",
":updateNotScheduled"
],
"patch": {
"automerge": true
},
"rebaseStalePrs": true,
"packageRules": [
{
"matchPackagePatterns": ["^@edx/paragon"],
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
"automerge": true,
"stabilityDays": 3
"matchDepTypes": [
"devDependencies"
],
"matchUpdateTypes": [
"lockFileMaintenance",
"minor",
"patch",
"pin"
],
"automerge": true
},
{
"matchPackagePatterns": ["@edx", "@openedx"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}
]
],
"timezone": "America/New_York"
}

View File

@@ -1,5 +1,5 @@
import { AppContext } from '@edx/frontend-platform/react';
import { getConfig, history, getQueryParameters } from '@edx/frontend-platform';
import { getConfig, getQueryParameters } from '@edx/frontend-platform';
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
@@ -13,43 +13,51 @@ import {
getCountryList,
getLanguageList,
} from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import {
Button, Hyperlink, Icon, Alert,
} from '@edx/paragon';
import { CheckCircle, Error, WarningFilled } from '@edx/paragon/icons';
import messages from './AccountSettingsPage.messages';
import { fetchSettings, saveSettings, updateDraft } from './data/actions';
import {
fetchSettings,
saveMultipleSettings,
saveSettings,
updateDraft,
beginNameChange,
} from './data/actions';
import { accountSettingsPageSelector } from './data/selectors';
import PageLoading from './PageLoading';
import Alert from './Alert';
import JumpNav from './JumpNav';
import DeleteAccount from './delete-account';
import EditableField from './EditableField';
import EditableSelectField from './EditableSelectField';
import ResetPassword from './reset-password';
import NameChange from './name-change';
import ThirdPartyAuth from './third-party-auth';
import BetaLanguageBanner from './BetaLanguageBanner';
import EmailField from './EmailField';
import OneTimeDismissibleAlert from './OneTimeDismissibleAlert';
import DOBModal from './DOBForm';
import {
YEAR_OF_BIRTH_OPTIONS,
EDUCATION_LEVELS,
GENDER_OPTIONS,
COUNTRY_WITH_STATES,
COPPA_COMPLIANCE_YEAR,
getStatesList,
} from './data/constants';
import { fetchSiteLanguages } from './site-language';
import CoachingToggle from './coaching/CoachingToggle';
import DemographicsSection from './demographics/DemographicsSection';
import { fetchCourseList } from '../notification-preferences/data/thunks';
import { withLocation, withNavigate } from './hoc';
class AccountSettingsPage extends React.Component {
constructor(props, context) {
super(props, context);
// If there is a "duplicate_provider" query parameter, that's the backend's
// way of telling us that the provider account the user tried to link is already linked
// to another Open edX account. We use this to display a message to that effect, and remove the
// parameter from the URL.
const duplicateTpaProvider = getQueryParameters().duplicate_provider;
if (duplicateTpaProvider !== undefined) {
history.replace(history.location.pathname);
}
this.state = {
duplicateTpaProvider,
};
@@ -66,8 +74,9 @@ class AccountSettingsPage extends React.Component {
}
componentDidMount() {
this.props.fetchCourseList();
this.props.fetchSettings();
this.props.fetchSiteLanguages();
this.props.fetchSiteLanguages(this.props.navigate);
sendTrackingLogEvent('edx.user.settings.viewed', {
page: 'account',
visibility: null,
@@ -144,6 +153,34 @@ class AccountSettingsPage extends React.Component {
this.props.saveSettings(formId, values);
};
handleSubmitProfileName = (formId, values) => {
if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) {
this.props.saveMultipleSettings([
{
formId,
commitValues: values,
},
{
formId: 'useVerifiedNameForCerts',
commitValues: this.props.formValues.useVerifiedNameForCerts,
},
], formId);
} else {
this.props.saveSettings(formId, values);
}
};
handleSubmitVerifiedName = (formId, values) => {
if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) {
this.props.saveSettings('useVerifiedNameForCerts', this.props.formValues.useVerifiedNameForCerts);
}
if (values !== this.props.committedValues?.verified_name) {
this.props.beginNameChange(formId);
} else {
this.props.saveSettings(formId, values);
}
};
isEditable(fieldName) {
return !this.props.staticFields.includes(fieldName);
}
@@ -159,15 +196,22 @@ class AccountSettingsPage extends React.Component {
return null;
}
// If there is a "duplicate_provider" query parameter, that's the backend's
// way of telling us that the provider account the user tried to link is already linked
// to another user account on the platform. We use this to display a message to that effect,
// and remove the parameter from the URL.
this.props.navigate(this.props.location, { replace: true });
return (
<div>
<Alert className="alert alert-danger" role="alert">
<Alert variant="danger">
<FormattedMessage
id="account.settings.message.duplicate.tpa.provider"
defaultMessage="The {provider} account you selected is already linked to another edX account."
description="alert message informing the user that the third-party account they attempted to link is already linked to another edX account"
defaultMessage="The {provider} account you selected is already linked to another {siteName} account."
description="alert message informing the user that the third-party account they attempted to link is already linked to another account"
values={{
provider: <b>{this.state.duplicateTpaProvider}</b>,
siteName: getConfig().SITE_NAME,
}}
/>
</Alert>
@@ -182,7 +226,7 @@ class AccountSettingsPage extends React.Component {
return (
<div>
<Alert className="alert alert-primary" role="alert">
<Alert variant="info">
<FormattedMessage
id="account.settings.message.managed.settings"
defaultMessage="Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help."
@@ -205,6 +249,170 @@ class AccountSettingsPage extends React.Component {
);
}
renderFullNameHelpText = (status, proctoredExamId) => {
if (!this.props.verifiedNameHistory) {
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text']);
}
let messageString = 'account.settings.field.full.name.help.text';
if (status === 'submitted') {
messageString += '.submitted';
if (proctoredExamId) {
messageString += '.proctored';
}
} else {
messageString += '.default';
}
if (!this.props.committedValues.useVerifiedNameForCerts) {
messageString += '.certificate';
}
return this.props.intl.formatMessage(messages[messageString]);
};
renderVerifiedNameSuccessMessage = (verifiedName, created) => {
const dateValue = new Date(created).valueOf();
const id = `dismissedVerifiedNameSuccessMessage-${verifiedName}-${dateValue}`;
return (
<OneTimeDismissibleAlert
id={id}
variant="success"
icon={CheckCircle}
header={this.props.intl.formatMessage(messages['account.settings.field.name.verified.success.message.header'])}
body={this.props.intl.formatMessage(messages['account.settings.field.name.verified.success.message'])}
/>
);
};
renderVerifiedNameFailureMessage = (verifiedName, created) => {
const dateValue = new Date(created).valueOf();
const id = `dismissedVerifiedNameFailureMessage-${verifiedName}-${dateValue}`;
return (
<OneTimeDismissibleAlert
id={id}
variant="danger"
icon={Error}
header={this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message.header'])}
body={
(
<>
<div className="d-flex flex-row">
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message'])}
</div>
<div className="d-flex flex-row-reverse mt-3">
<Button
variant="primary"
href="https://support.edx.org/hc/en-us/articles/360004381594-Why-was-my-ID-verification-denied"
>
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message.help.link'])}
</Button>{' '}
</div>
</>
)
}
/>
);
};
renderVerifiedNameSubmittedMessage = (willCertNameChange) => (
<Alert
variant="warning"
icon={WarningFilled}
>
<Alert.Heading>
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.submitted.message.header'])}
</Alert.Heading>
<p>
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.submitted.message'])}{' '}
{
willCertNameChange
&& this.props.intl.formatMessage(messages['account.settings.field.name.verified.submitted.message.certificate'])
}
</p>
</Alert>
);
renderVerifiedNameMessage = verifiedNameRecord => {
const {
created,
status,
profile_name: profileName,
verified_name: verifiedName,
proctored_exam_attempt_id: proctoredExamId,
} = verifiedNameRecord;
let willCertNameChange = false;
if (
(
// User submitted a profile name change, and uses their profile name on certificates
this.props.committedValues.name !== profileName
&& !this.props.committedValues.useVerifiedNameForCerts
)
|| (
// User submitted a verified name change, and uses their verified name on certificates
this.props.committedValues.name === profileName
&& this.props.committedValues.useVerifiedNameForCerts
)
) {
willCertNameChange = true;
}
if (proctoredExamId) {
return null;
}
switch (status) {
case 'approved':
return this.renderVerifiedNameSuccessMessage(verifiedName, created);
case 'denied':
return this.renderVerifiedNameFailureMessage(verifiedName, created);
case 'submitted':
return this.renderVerifiedNameSubmittedMessage(willCertNameChange);
default:
return null;
}
};
renderVerifiedNameIcon = (status) => {
switch (status) {
case 'approved':
return (<Icon src={CheckCircle} className="ml-1" style={{ height: '18px', width: '18px', color: 'green' }} />);
case 'submitted':
return (<Icon src={WarningFilled} className="ml-1" style={{ height: '18px', width: '18px', color: 'yellow' }} />);
default:
return null;
}
};
renderVerifiedNameHelpText = (status, proctoredExamId) => {
let messageStr = 'account.settings.field.name.verified.help.text';
// add additional string based on status
if (status === 'approved') {
messageStr += '.verified';
} else if (status === 'submitted') {
messageStr += '.submitted';
} else {
return null;
}
// add additional string if verified name came from a proctored exam attempt
if (proctoredExamId) {
messageStr += '.proctored';
}
// add additional string based on certificate name use
if (this.props.committedValues.useVerifiedNameForCerts) {
messageStr += '.certificate';
}
return this.props.intl.formatMessage(messages[messageStr]);
};
renderEmptyStaticFieldMessage() {
if (this.isManagedProfile()) {
return this.props.intl.formatMessage(messages['account.settings.static.field.empty'], {
@@ -214,6 +422,13 @@ class AccountSettingsPage extends React.Component {
return this.props.intl.formatMessage(messages['account.settings.static.field.empty.no.admin']);
}
renderNameChangeModal() {
if (this.props.nameChangeModal && this.props.nameChangeModal.formId) {
return <NameChange targetFormId={this.props.nameChangeModal.formId} />;
}
return null;
}
renderSecondaryEmailField(editableFieldProps) {
if (!this.props.formValues.secondary_email_enabled) {
return null;
@@ -260,6 +475,8 @@ class AccountSettingsPage extends React.Component {
// Show State field only if the country is US (could include Canada later)
const showState = this.props.formValues.country === COUNTRY_WITH_STATES;
const { verifiedName } = this.props;
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
this.props.timeZoneOptions,
this.props.countryTimeZoneOptions,
@@ -268,38 +485,112 @@ class AccountSettingsPage extends React.Component {
const hasLinkedTPA = findIndex(this.props.tpaProviders, provider => provider.connected) >= 0;
// if user is under 13 and does not have cookie set
const shouldUpdateDOB = (
getConfig().ENABLE_COPPA_COMPLIANCE
&& getConfig().ENABLE_DOB_UPDATE
&& this.props.formValues.year_of_birth.toString() >= COPPA_COMPLIANCE_YEAR.toString()
&& !localStorage.getItem('submittedDOB')
);
return (
<>
<div className="account-section" id="basic-information" ref={this.navLinkRefs['#basic-information']}>
<h2 className="section-heading">
{ shouldUpdateDOB
&& (
<DOBModal
{...editableFieldProps}
/>
)}
<div className="account-section pt-3 mb-5" id="basic-information" ref={this.navLinkRefs['#basic-information']}>
{
this.props.mostRecentVerifiedName
&& this.renderVerifiedNameMessage(this.props.mostRecentVerifiedName)
}
{localStorage.getItem('submittedDOB')
&& (
<OneTimeDismissibleAlert
id="updated-dob"
variant="success"
icon={CheckCircle}
header={this.props.intl.formatMessage(messages['account.settings.field.dob.form.success'])}
body=""
/>
)}
<h2 className="section-heading h4 mb-3">
{this.props.intl.formatMessage(messages['account.settings.section.account.information'])}
</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.account.information.description'])}</p>
{this.renderManagedProfileMessage()}
{this.renderNameChangeModal()}
<EditableField
name="username"
type="text"
value={this.props.formValues.username}
label={this.props.intl.formatMessage(messages['account.settings.field.username'])}
helpText={this.props.intl.formatMessage(messages['account.settings.field.username.help.text'])}
helpText={this.props.intl.formatMessage(
messages['account.settings.field.username.help.text'],
{ siteName: getConfig().SITE_NAME },
)}
isEditable={false}
{...editableFieldProps}
/>
<EditableField
name="name"
type="text"
value={this.props.formValues.name}
value={
verifiedName?.status === 'submitted'
&& this.props.formValues.pending_name_change
? this.props.formValues.pending_name_change
: this.props.formValues.name
}
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
emptyLabel={
this.isEditable('name')
? this.props.intl.formatMessage(messages['account.settings.field.full.name.empty'])
: this.renderEmptyStaticFieldMessage()
}
helpText={this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])}
isEditable={this.isEditable('name')}
{...editableFieldProps}
helpText={
verifiedName
? this.renderFullNameHelpText(verifiedName.status, verifiedName.proctored_exam_attempt_id)
: this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])
}
isEditable={
verifiedName
? this.isEditable('verifiedName') && this.isEditable('name')
: this.isEditable('name')
}
isGrayedOut={
verifiedName && !this.isEditable('verifiedName')
}
onChange={this.handleEditableFieldChange}
onSubmit={this.handleSubmitProfileName}
/>
{verifiedName
&& (
<EditableField
name="verified_name"
type="text"
value={this.props.formValues.verified_name}
label={
(
<div className="d-flex">
{this.props.intl.formatMessage(messages['account.settings.field.name.verified'])}
{
this.renderVerifiedNameIcon(verifiedName.status)
}
</div>
)
}
helpText={this.renderVerifiedNameHelpText(verifiedName.status, verifiedName.proctored_exam_attempt_id)}
isEditable={this.isEditable('verifiedName')}
isGrayedOut={!this.isEditable('verifiedName')}
onChange={this.handleEditableFieldChange}
onSubmit={this.handleSubmitVerifiedName}
/>
)}
<EmailField
name="email"
label={this.props.intl.formatMessage(messages['account.settings.field.email'])}
@@ -310,22 +601,28 @@ class AccountSettingsPage extends React.Component {
}
value={this.props.formValues.email}
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
helpText={this.props.intl.formatMessage(messages['account.settings.field.email.help.text'])}
helpText={this.props.intl.formatMessage(
messages['account.settings.field.email.help.text'],
{ siteName: getConfig().SITE_NAME },
)}
isEditable={this.isEditable('email')}
{...editableFieldProps}
/>
{this.renderSecondaryEmailField(editableFieldProps)}
<ResetPassword email={this.props.formValues.email} />
<EditableField
name="year_of_birth"
type="select"
label={this.props.intl.formatMessage(messages['account.settings.field.dob'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.dob.empty'])}
value={this.props.formValues.year_of_birth}
options={yearOfBirthOptions}
{...editableFieldProps}
/>
<EditableField
{(!getConfig().ENABLE_COPPA_COMPLIANCE)
&& (
<EditableSelectField
name="year_of_birth"
type="select"
label={this.props.intl.formatMessage(messages['account.settings.field.dob'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.dob.empty'])}
value={this.props.formValues.year_of_birth}
options={yearOfBirthOptions}
{...editableFieldProps}
/>
)}
<EditableSelectField
name="country"
type="select"
value={this.props.formValues.country}
@@ -341,7 +638,7 @@ class AccountSettingsPage extends React.Component {
/>
{showState
&& (
<EditableField
<EditableSelectField
name="state"
type="select"
value={this.props.formValues.state}
@@ -358,21 +655,23 @@ class AccountSettingsPage extends React.Component {
)}
</div>
<div className="account-section" id="profile-information" ref={this.navLinkRefs['#profile-information']}>
<h2 className="section-heading">
<div className="account-section pt-3 mb-5" id="profile-information" ref={this.navLinkRefs['#profile-information']}>
<h2 className="section-heading h4 mb-3">
{this.props.intl.formatMessage(messages['account.settings.section.profile.information'])}
</h2>
<EditableField
<EditableSelectField
name="level_of_education"
type="select"
value={this.props.formValues.level_of_education}
options={educationLevelOptions}
options={getConfig().ENABLE_COPPA_COMPLIANCE
? educationLevelOptions.filter(option => option.value !== 'el')
: educationLevelOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.education'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.education.empty'])}
{...editableFieldProps}
/>
<EditableField
<EditableSelectField
name="gender"
type="select"
value={this.props.formValues.gender}
@@ -381,7 +680,7 @@ class AccountSettingsPage extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.gender.empty'])}
{...editableFieldProps}
/>
<EditableField
<EditableSelectField
name="language_proficiencies"
type="select"
value={this.props.formValues.language_proficiencies}
@@ -401,11 +700,16 @@ class AccountSettingsPage extends React.Component {
)}
</div>
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && this.renderDemographicsSection()}
<div className="account-section" id="social-media">
<h2 className="section-heading">
<div className="account-section pt-3 mb-5" id="social-media">
<h2 className="section-heading h4 mb-3">
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.social.media.description'])}</p>
<p>
{this.props.intl.formatMessage(
messages['account.settings.section.social.media.description'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<EditableField
name="social_link_linkedin"
@@ -433,13 +737,13 @@ class AccountSettingsPage extends React.Component {
/>
</div>
<div className="account-section" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
<h2 className="section-heading">
<div className="account-section pt-3 mb-5" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
<h2 className="section-heading h4 mb-3">
{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}
</h2>
<BetaLanguageBanner />
<EditableField
<EditableSelectField
name="siteLanguage"
type="select"
options={this.props.siteLanguageOptions}
@@ -448,7 +752,7 @@ class AccountSettingsPage extends React.Component {
helpText={this.props.intl.formatMessage(messages['account.settings.field.site.language.help.text'])}
{...editableFieldProps}
/>
<EditableField
<EditableSelectField
name="time_zone"
type="select"
value={this.props.formValues.time_zone}
@@ -464,18 +768,26 @@ class AccountSettingsPage extends React.Component {
/>
</div>
<div className="account-section" id="linked-accounts" ref={this.navLinkRefs['#linked-accounts']}>
<h2 className="section-heading">{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts'])}</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts.description'])}</p>
<div className="account-section pt-3 mb-5" id="linked-accounts" ref={this.navLinkRefs['#linked-accounts']}>
<h2 className="section-heading h4 mb-3">{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts'])}</h2>
<p>
{this.props.intl.formatMessage(
messages['account.settings.section.linked.accounts.description'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<ThirdPartyAuth />
</div>
<div className="account-section" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
<DeleteAccount
isVerifiedAccount={this.props.isActive}
hasLinkedTPA={hasLinkedTPA}
/>
</div>
{getConfig().ENABLE_ACCOUNT_DELETION
&& (
<div className="account-section pt-3 mb-5" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
<DeleteAccount
isVerifiedAccount={this.props.isActive}
hasLinkedTPA={hasLinkedTPA}
/>
</div>
)}
</>
);
@@ -512,12 +824,12 @@ class AccountSettingsPage extends React.Component {
</h1>
<div>
<div className="row">
<div className="col-md-3">
<div className="col-md-2">
<JumpNav
displayDemographicsLink={this.props.formValues.shouldDisplayDemographicsSection}
/>
</div>
<div className="col-md-9">
<div className="col-md-10">
{loading ? this.renderLoading() : null}
{loaded ? this.renderContent() : null}
{loadingError ? this.renderError() : null}
@@ -549,6 +861,7 @@ AccountSettingsPage.propTypes = {
level_of_education: PropTypes.string,
gender: PropTypes.string,
language_proficiencies: PropTypes.string,
pending_name_change: PropTypes.string,
phone_number: PropTypes.string,
social_link_linkedin: PropTypes.string,
social_link_facebook: PropTypes.string,
@@ -561,7 +874,18 @@ AccountSettingsPage.propTypes = {
}),
state: PropTypes.string,
shouldDisplayDemographicsSection: PropTypes.bool,
useVerifiedNameForCerts: PropTypes.bool.isRequired,
verified_name: PropTypes.string,
}).isRequired,
committedValues: PropTypes.shape({
name: PropTypes.string,
useVerifiedNameForCerts: PropTypes.bool,
verified_name: PropTypes.string,
}),
drafts: PropTypes.shape({}),
formErrors: PropTypes.shape({
name: PropTypes.string,
}),
siteLanguage: PropTypes.shape({
previousValue: PropTypes.string,
draft: PropTypes.string,
@@ -585,15 +909,48 @@ AccountSettingsPage.propTypes = {
})),
fetchSiteLanguages: PropTypes.func.isRequired,
updateDraft: PropTypes.func.isRequired,
saveMultipleSettings: PropTypes.func.isRequired,
saveSettings: PropTypes.func.isRequired,
fetchSettings: PropTypes.func.isRequired,
tpaProviders: PropTypes.arrayOf(PropTypes.object),
beginNameChange: PropTypes.func.isRequired,
fetchCourseList: PropTypes.func.isRequired,
tpaProviders: PropTypes.arrayOf(PropTypes.shape({
connected: PropTypes.bool,
})),
nameChangeModal: PropTypes.shape({
formId: PropTypes.string,
}),
verifiedName: PropTypes.shape({
verified_name: PropTypes.string,
status: PropTypes.string,
proctored_exam_attempt_id: PropTypes.number,
}),
mostRecentVerifiedName: PropTypes.shape({
verified_name: PropTypes.string,
status: PropTypes.string,
proctored_exam_attempt_id: PropTypes.number,
}),
verifiedNameHistory: PropTypes.arrayOf(
PropTypes.shape({
verified_name: PropTypes.string,
status: PropTypes.string,
proctored_exam_attempt_id: PropTypes.number,
}),
),
navigate: PropTypes.func.isRequired,
location: PropTypes.string.isRequired,
};
AccountSettingsPage.defaultProps = {
loading: false,
loaded: false,
loadingError: null,
committedValues: {
useVerifiedNameForCerts: false,
verified_name: null,
},
drafts: {},
formErrors: {},
siteLanguage: null,
siteLanguageOptions: [],
timeZoneOptions: [],
@@ -603,11 +960,18 @@ AccountSettingsPage.defaultProps = {
tpaProviders: [],
isActive: true,
secondary_email_enabled: false,
nameChangeModal: {},
verifiedName: null,
mostRecentVerifiedName: {},
verifiedNameHistory: [],
};
export default connect(accountSettingsPageSelector, {
export default withLocation(withNavigate(connect(accountSettingsPageSelector, {
fetchCourseList,
fetchSettings,
saveSettings,
saveMultipleSettings,
updateDraft,
fetchSiteLanguages,
})(injectIntl(AccountSettingsPage));
beginNameChange,
})(injectIntl(AccountSettingsPage))));

View File

@@ -63,7 +63,7 @@ const messages = defineMessages({
},
'account.settings.section.linked.accounts.description': {
id: 'account.settings.section.linked.accounts.description',
defaultMessage: 'You can link your identity accounts to simplify signing in to edX.',
defaultMessage: 'You can link your identity accounts to simplify signing in to {siteName}.',
description: 'The linked accounts section heading description.',
},
'account.settings.field.username': {
@@ -73,7 +73,7 @@ const messages = defineMessages({
},
'account.settings.field.username.help.text': {
id: 'account.settings.field.username.help.text',
defaultMessage: 'The name that identifies you on edX. You cannot change your username.',
defaultMessage: 'The name that identifies you on {siteName}. You cannot change your username.',
description: 'Help text for the account settings username field.',
},
'account.settings.field.full.name': {
@@ -91,6 +91,126 @@ const messages = defineMessages({
defaultMessage: 'The name that is used for ID verification and that appears on your certificates.',
description: 'Help text for the account settings name field.',
},
'account.settings.field.full.name.help.text.default': {
id: 'account.settings.field.full.name.help.text.default',
defaultMessage: 'The name that appears on your public profile.',
description: 'Help text for the account settings name field.',
},
'account.settings.field.full.name.help.text.default.certificate': {
id: 'account.settings.field.full.name.help.text.default.certificate',
defaultMessage: 'This name is selected to appear on your certificates and public-facing records.',
description: 'Help text for the account settings name field.',
},
'account.settings.field.name.verified': {
id: 'account.settings.field.name.verified',
defaultMessage: 'Verified name',
description: 'Label for account settings verified name field.',
},
'account.settings.field.name.verified.help.text.verified': {
id: 'account.settings.field.name.verified.help.text.verified',
defaultMessage: 'This name has been verified by photo ID.',
description: 'Help text for the account settings verified name field when the name is verified.',
},
'account.settings.field.name.verified.help.text.verified.proctored': {
id: 'account.settings.field.name.verified.help.text.verified.proctored',
defaultMessage: 'This name has been verified by proctoring.',
description: 'Help text for the account settings verified name field when the name is verified through proctoring.',
},
'account.settings.field.name.verified.help.text.verified.certificate': {
id: 'account.settings.field.name.verified.help.text.verified.certificate',
defaultMessage: 'This name has been verified by photo ID, and is selected to appear on your certificates and public-facing records.',
description: 'Help text for the account settings verified name field when the name is selected for certificates.',
},
'account.settings.field.name.verified.help.text.verified.proctored.certificate': {
id: 'account.settings.field.name.verified.help.text.verified.proctored.certificate',
defaultMessage: 'This name has been verified by proctoring, and is selected to appear on your certificates and public-facing records.',
description: 'Help text for the account settings verified name field when the name is selected for certificates, and the name is verified through proctoring.',
},
'account.settings.field.name.verified.help.text.submitted': {
id: 'account.settings.field.name.verified.help.text.submitted',
defaultMessage: 'Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.',
description: 'Help text for the account settings verified name field when a verified name has been submitted.',
},
'account.settings.field.name.verified.help.text.submitted.proctored': {
id: 'account.settings.field.name.verified.help.text.submitted.proctored',
defaultMessage: 'Your proctored exam has been submitted. Verified name cannot be changed at this time. Please check back in 2-5 days.',
description: 'Help text for the account settings verified name field when a verified name has been submitted through proctoring.',
},
'account.settings.field.name.verified.help.text.submitted.certificate': {
id: 'account.settings.field.name.verified.help.text.submitted.certificate',
defaultMessage: 'When identity verification is successful, this name will appear on your certificates and public-facing records. Verified name cannot be changed at this time.',
description: 'Help text for the account settings verified name field when a verified name has been submitted and will appear on certificates.',
},
'account.settings.field.name.verified.help.text.submitted.proctored.certificate': {
id: 'account.settings.field.name.verified.help.text.submitted.proctored.certificate',
defaultMessage: 'Once your proctored exam passes review, this name will appear on your certificate and public-facing records. Verified Name cannot be changed at this time.',
description: 'Help text for the account settings verified name field when a verified name has been submitted through proctoring and will appear on certificates.',
},
'account.settings.field.name.verified.verification.alert': {
id: 'account.settings.field.name.verified.verification.help',
defaultMessage: 'Enter your name as it appears on your unexpired student, work, or government-issued identification card.',
description: 'Form label instructing the user to enter the name on their ID.',
},
'account.settings.field.full.name.help.text.submitted': {
id: 'account.settings.field.full.name.help.text.submitted',
defaultMessage: 'Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.',
description: 'Help text for the account settings full name field when a verified name has been submitted.',
},
'account.settings.field.full.name.help.text.submitted.proctored': {
id: 'account.settings.field.full.name.help.text.submitted.proctored',
defaultMessage: 'Your proctored exam has been submitted. Full name cannot be changed at this time. Please check back in 2-5 days.',
description: 'Help text for the account settings full name field when a verified name has been submitted through proctoring.',
},
'account.settings.field.full.name.help.text.submitted.certificate': {
id: 'account.settings.field.full.name.help.text.submitted.certificate',
defaultMessage: 'When identity verification is successful, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.',
description: 'Help text for the account settings full name field when a full name has been submitted and will appear on certificates.',
},
'account.settings.field.full.name.help.text.submitted.proctored.certificate': {
id: 'account.settings.field.full.name.help.text.submitted.proctored.certificate',
defaultMessage: 'Once your proctored exam passes review, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.',
description: 'Help text for the account settings full name field when a full name has been submitted and will appear on certificates.',
},
'account.settings.field.name.verified.success.message': {
id: 'account.settings.field.name.verified.success.message',
defaultMessage: 'Your identity verification request has successfully completed. You now have the option of selecting which name you prefer to appear on your certificates and public-records.',
description: 'The body of the success alert indicating that a user\'s name has been verified',
},
'account.settings.field.name.verified.success.message.header': {
id: 'account.settings.field.name.verified.success.message.header',
defaultMessage: 'Your name change request is complete!',
description: 'The header of the success alert indicating that a user\'s name has been verified',
},
'account.settings.field.name.verified.failure.message': {
id: 'account.settings.field.name.verified.failure.message',
defaultMessage: 'Your most recent identity verification attempt did not pass. Related account settings have been restored.',
description: 'The body of the failure alert indicating that a user\'s name was not able to be verified',
},
'account.settings.field.name.verified.failure.message.header': {
id: 'account.settings.field.name.verified.failure.message.header',
defaultMessage: 'We were not able to verify your identity.',
description: 'The header of the failure alert indicating that a user\'s name was not able to be verified',
},
'account.settings.field.name.verified.failure.message.help.link': {
id: 'account.settings.field.name.verified.failure.message.help.link',
defaultMessage: 'Learn more about ID verification',
description: 'The text of the button displayed when a user\'s name was not able to be verified, intended to direct the user to a help article about ID verification.',
},
'account.settings.field.name.verified.submitted.message': {
id: 'account.settings.field.name.verified.submitted.message',
defaultMessage: 'Your identity verification request has been submitted and usually takes between 24 and 48 hours to complete.',
description: 'The body of the submitted alert indicating that a user\'s name has been submitted for verification',
},
'account.settings.field.name.verified.submitted.message.certificate': {
id: 'account.settings.field.name.verified.submitted.message.certificate',
defaultMessage: 'When your request is approved, your updated name will appear on all associated certificates and public-facing records.',
description: 'The body of the submitted alert indicating that a user\'s name will be updated on certificates.',
},
'account.settings.field.name.verified.submitted.message.header': {
id: 'account.settings.field.name.verified.submitted.message.header',
defaultMessage: 'Your name change request is almost complete!',
description: 'The header of the submitted alert indicating that a user\'s name has been submitted for verification',
},
'account.settings.field.email': {
id: 'account.settings.field.email',
defaultMessage: 'Email address (Sign in)',
@@ -108,7 +228,7 @@ const messages = defineMessages({
},
'account.settings.field.email.help.text': {
id: 'account.settings.field.email.help.text',
defaultMessage: 'You receive messages from edX and course teams at this address.',
defaultMessage: 'You receive messages from {siteName} and course teams at this address.',
description: 'Help text for the account settings email field.',
},
'account.settings.field.secondary.email': {
@@ -146,6 +266,56 @@ const messages = defineMessages({
defaultMessage: 'Select a year of birth',
description: 'Option for empty value on account settings year of birth field.',
},
'account.settings.field.dob.month': {
id: 'account.settings.field.dob.month',
defaultMessage: 'Month',
description: 'Label for account settings month of birth field.',
},
'account.settings.field.dob.year': {
id: 'account.settings.field.dob.year',
defaultMessage: 'Year',
description: 'Label for account settings year of birth field.',
},
'account.settings.field.dob.month.default': {
id: 'account.settings.field.month.year.default',
defaultMessage: 'Select month',
description: 'Default label for account settings month of birth field.',
},
'account.settings.field.dob.year.default': {
id: 'account.settings.field.dob.year.default',
defaultMessage: 'Select year',
description: 'Default label for account settings year of birth field.',
},
'account.settings.field.dob.form.button': {
id: 'account.settings.field.dob.form.button',
defaultMessage: 'Please confirm your date of birth',
description: 'Message to prompt user to enter dob',
},
'account.settings.field.dob.form.title': {
id: 'account.settings.field.dob.form.title',
defaultMessage: 'Enter your birth month and year',
description: 'Title of DOB form',
},
'account.settings.field.dob.form.help.text': {
id: 'account.settings.field.dob.form.help.text',
defaultMessage: 'We ask for birth month and year information to help us comply with our legal obligations.',
description: 'Help text for DOB form',
},
'account.settings.field.dob.form.success': {
id: 'account.settings.field.dob.form.success',
defaultMessage: 'Thank you for entering your information.',
description: 'Title of banner when date of birth is successfully entered',
},
'account.settings.field.month_of_birth.options.empty': {
id: 'account.settings.field.month_of_birth.options.empty',
defaultMessage: 'Select a month of birth',
description: 'Option for empty value on account settings month of birth field.',
},
'account.settingsfield.dob.error.general': {
id: 'account.settingsfield.dob.error.general',
defaultMessage: 'A technical error occurred. Please try again.',
description: 'Generic error message.',
},
'account.settings.field.country': {
id: 'account.settings.field.country',
defaultMessage: 'Country',
@@ -331,7 +501,7 @@ const messages = defineMessages({
},
'account.settings.section.social.media.description': {
id: 'account.settings.section.social.media.description',
defaultMessage: 'Optionally, link your personal accounts to the social media icons on your edX profile.',
defaultMessage: 'Optionally, link your personal accounts to the social media icons on your {siteName} profile.',
description: 'Section subheader for social media links settings',
},
'account.settings.field.social.platform.name.linkedin': {
@@ -395,6 +565,11 @@ const messages = defineMessages({
defaultMessage: 'No value set.',
description: 'The placeholder for an empty but uneditable field when there is no administrator',
},
'notification.preferences.notifications.label': {
id: 'notification.preferences.notifications.label',
defaultMessage: 'Notifications',
description: 'Label for Notifications',
},
});
export default messages;

View File

@@ -2,18 +2,16 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
function Alert(props) {
return (
<div className={classNames('alert d-flex align-items-start', props.className)}>
<div>
{props.icon}
</div>
<div>
{props.children}
</div>
const Alert = (props) => (
<div className={classNames('alert d-flex align-items-start', props.className)}>
<div>
{props.icon}
</div>
);
}
<div>
{props.children}
</div>
</div>
);
Alert.propTypes = {
className: PropTypes.string,

View File

@@ -49,6 +49,9 @@ class BetaLanguageBanner extends React.Component {
render() {
const savedLanguage = this.getSiteLanguageEntry(this.context.locale);
if (!savedLanguage) {
return null;
}
const isSavedLanguageReleased = savedLanguage.released === true;
const noPreviousLanguageSet = this.props.siteLanguage.previousValue === null;
if (isSavedLanguageReleased || noPreviousLanguageSet) {

View File

@@ -0,0 +1,162 @@
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Form, StatefulButton, ModalDialog, ActionRow, useToggle, Button,
} from '@edx/paragon';
import React, { useCallback, useEffect, useState } from 'react';
import { connect, useDispatch } from 'react-redux';
import messages from './AccountSettingsPage.messages';
import { YEAR_OF_BIRTH_OPTIONS } from './data/constants';
import { editableFieldSelector } from './data/selectors';
import { saveSettingsReset } from './data/actions';
const DOBModal = (props) => {
const {
saveState,
error,
onSubmit,
intl,
} = props;
const dispatch = useDispatch();
// eslint-disable-next-line no-unused-vars
const [isOpen, open, close, toggle] = useToggle(true, {});
const [monthValue, setMonthValue] = useState('');
const [yearValue, setYearValue] = useState('');
const handleChange = (e) => {
e.preventDefault();
if (e.target.name === 'month') {
setMonthValue(e.target.value);
} else if (e.target.name === 'year') {
setYearValue(e.target.value);
}
};
const handleSubmit = (e) => {
e.preventDefault();
const data = monthValue !== '' && yearValue !== '' ? [{ field_name: 'DOB', field_value: `${yearValue}-${monthValue}` }] : [];
onSubmit('extended_profile', data);
};
const handleComplete = useCallback(() => {
localStorage.setItem('submittedDOB', 'true');
close();
dispatch(saveSettingsReset());
}, [dispatch, close]);
const handleClose = useCallback(() => {
close();
dispatch(saveSettingsReset());
}, [dispatch, close]);
function renderErrors() {
if (saveState === 'error' || error) {
return (
<Form.Control.Feedback type="invalid" key="general-error">
{intl.formatMessage(messages['account.settingsfield.dob.error.general'])}
</Form.Control.Feedback>
);
}
return null;
}
useEffect(() => {
if (saveState === 'complete' && isOpen) {
handleComplete();
}
}, [handleComplete, saveState, isOpen, monthValue, yearValue]);
return (
<>
<Button variant="primary" onClick={open}>
{intl.formatMessage(messages['account.settings.field.dob.form.button'])}
</Button>
<ModalDialog
title={intl.formatMessage(messages['account.settings.field.dob.form.title'])}
isOpen={isOpen}
onClose={handleClose}
hasCloseButton={false}
variant="default"
>
<form onSubmit={handleSubmit}>
<ModalDialog.Header>
<ModalDialog.Title>
{intl.formatMessage(messages['account.settings.field.dob.form.title'])}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body className="overflow-hidden" style={{ padding: '1.5rem' }}>
<p>{intl.formatMessage(messages['account.settings.field.dob.form.help.text'])}</p>
<Form.Group>
<Form.Label>
{intl.formatMessage(messages['account.settings.field.dob.month'])}
</Form.Label>
<Form.Control
as="select"
name="month"
onChange={handleChange}
>
<option value="">{intl.formatMessage(messages['account.settings.field.dob.month.default'])}</option>
{[...Array(12).keys()].map(month => (
<option key={month + 1} value={month + 1}>{month + 1}</option>
))}
</Form.Control>
</Form.Group>
<Form.Group>
<Form.Label>
{intl.formatMessage(messages['account.settings.field.dob.year'])}
</Form.Label>
<Form.Control
as="select"
name="year"
onChange={handleChange}
>
<option value="">{intl.formatMessage(messages['account.settings.field.dob.year.default'])}</option>
{YEAR_OF_BIRTH_OPTIONS.map(year => (
<option key={year.value} value={year.value}>{year.label}</option>
))}
</Form.Control>
</Form.Group>
{renderErrors()}
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
Cancel
</ModalDialog.CloseButton>
<StatefulButton
type="submit"
state={!(monthValue && yearValue) ? 'unedited' : saveState}
labels={{
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
}}
disabledStates={['unedited']}
/>
</ActionRow>
</ModalDialog.Footer>
</form>
</ModalDialog>
</>
);
};
DOBModal.propTypes = {
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
error: PropTypes.string,
onSubmit: PropTypes.func.isRequired,
intl: intlShape.isRequired,
};
DOBModal.defaultProps = {
saveState: undefined,
error: undefined,
};
export default connect(editableFieldSelector)(injectIntl(DOBModal));

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Button, Input, StatefulButton, ValidationFormGroup,
Button, Form, StatefulButton,
} from '@edx/paragon';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -16,8 +16,9 @@ import {
closeForm,
} from './data/actions';
import { editableFieldSelector } from './data/selectors';
import CertificatePreference from './certificate-preference/CertificatePreference';
function EditableField(props) {
const EditableField = (props) => {
const {
name,
label,
@@ -25,7 +26,6 @@ function EditableField(props) {
type,
value,
userSuppliedValue,
options,
saveState,
error,
confirmationMessageDefinition,
@@ -37,6 +37,7 @@ function EditableField(props) {
onChange,
isEditing,
isEditable,
isGrayedOut,
intl,
...others
} = props;
@@ -72,15 +73,6 @@ function EditableField(props) {
}
let finalValue = rawValue;
if (options) {
// Use == instead of === to prevent issues when HTML casts numbers as strings
// eslint-disable-next-line eqeqeq
const selectedOption = options.find(option => option.value == rawValue);
if (selectedOption) {
finalValue = selectedOption.label;
}
}
if (userSuppliedValue) {
finalValue += `: ${userSuppliedValue}`;
}
@@ -102,54 +94,56 @@ function EditableField(props) {
expression={isEditing ? 'editing' : 'default'}
cases={{
editing: (
<form onSubmit={handleSubmit}>
<ValidationFormGroup
for={id}
invalid={error != null}
invalidMessage={error}
helpText={helpText}
>
<label className="h6 d-block" htmlFor={id}>{label}</label>
<Input
data-hj-suppress
name={name}
id={id}
type={type}
value={value}
onChange={handleChange}
options={options}
{...others}
/>
<>{others.children}</>
</ValidationFormGroup>
<p>
<StatefulButton
type="submit"
className="mr-2"
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
}}
onClick={(e) => {
// Swallow clicks if the state is pending.
// We do this instead of disabling the button to prevent
// it from losing focus (disabled elements cannot have focus).
// Disabling it would causes upstream issues in focus management.
// Swallowing the onSubmit event on the form would be better, but
// we would have to add that logic for every field given our
// current structure of the application.
if (saveState === 'pending') { e.preventDefault(); }
}}
disabledStates={[]}
/>
<Button
variant="outline-primary"
onClick={handleCancel}
<>
<form onSubmit={handleSubmit}>
<Form.Group
controlId={id}
isInvalid={error != null}
>
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
</Button>
</p>
</form>
<Form.Label size="sm" className="h6 d-block" htmlFor={id}>{label}</Form.Label>
<Form.Control
data-hj-suppress
name={name}
id={id}
type={type}
value={value}
onChange={handleChange}
{...others}
/>
{!!helpText && <Form.Text>{helpText}</Form.Text>}
{error != null && <Form.Control.Feedback hasIcon={false}>{error}</Form.Control.Feedback>}
{others.children}
</Form.Group>
<p>
<StatefulButton
type="submit"
className="mr-2"
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
}}
onClick={(e) => {
// Swallow clicks if the state is pending.
// We do this instead of disabling the button to prevent
// it from losing focus (disabled elements cannot have focus).
// Disabling it would causes upstream issues in focus management.
// Swallowing the onSubmit event on the form would be better, but
// we would have to add that logic for every field given our
// current structure of the application.
if (saveState === 'pending') { e.preventDefault(); }
}}
disabledStates={[]}
/>
<Button
variant="outline-primary"
onClick={handleCancel}
>
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
</Button>
</p>
</form>
{['name', 'verified_name'].includes(name) && <CertificatePreference fieldName={name} />}
</>
),
default: (
<div className="form-group">
@@ -161,26 +155,22 @@ function EditableField(props) {
</Button>
) : null}
</div>
<p data-hj-suppress>{renderValue(value)}</p>
<p data-hj-suppress className={isGrayedOut ? 'grayed-out' : null}>{renderValue(value)}</p>
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
</div>
),
}}
/>
);
}
};
EditableField.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]),
emptyLabel: PropTypes.node,
type: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
userSuppliedValue: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
})),
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
error: PropTypes.string,
confirmationMessageDefinition: PropTypes.shape({
@@ -196,12 +186,12 @@ EditableField.propTypes = {
onChange: PropTypes.func.isRequired,
isEditing: PropTypes.bool,
isEditable: PropTypes.bool,
isGrayedOut: PropTypes.bool,
intl: intlShape.isRequired,
};
EditableField.defaultProps = {
value: undefined,
options: undefined,
saveState: undefined,
label: undefined,
emptyLabel: undefined,
@@ -211,6 +201,7 @@ EditableField.defaultProps = {
helpText: undefined,
isEditing: false,
isEditable: true,
isGrayedOut: false,
userSuppliedValue: undefined,
};

View File

@@ -0,0 +1,232 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
Button, Form, StatefulButton,
} from '@edx/paragon';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import SwitchContent from './SwitchContent';
import messages from './AccountSettingsPage.messages';
import {
openForm,
closeForm,
} from './data/actions';
import { editableFieldSelector } from './data/selectors';
import CertificatePreference from './certificate-preference/CertificatePreference';
const EditableSelectField = (props) => {
const {
name,
label,
emptyLabel,
type,
value,
userSuppliedValue,
options,
saveState,
error,
confirmationMessageDefinition,
confirmationValue,
helpText,
onEdit,
onCancel,
onSubmit,
onChange,
isEditing,
isEditable,
isGrayedOut,
intl,
...others
} = props;
const id = `field-${name}`;
const handleSubmit = (e) => {
e.preventDefault();
onSubmit(name, new FormData(e.target).get(name));
};
const handleChange = (e) => {
onChange(name, e.target.value);
};
const handleEdit = () => {
onEdit(name);
};
const handleCancel = () => {
onCancel(name);
};
const renderEmptyLabel = () => {
if (isEditable) {
return <Button variant="link" onClick={handleEdit} className="p-0">{emptyLabel}</Button>;
}
return <span className="text-muted">{emptyLabel}</span>;
};
const renderValue = (rawValue) => {
if (!rawValue) {
return renderEmptyLabel();
}
let finalValue = rawValue;
if (options) {
// Use == instead of === to prevent issues when HTML casts numbers as strings
// eslint-disable-next-line eqeqeq
const selectedOption = options.find(option => option.value == rawValue);
if (selectedOption) {
finalValue = selectedOption.label;
}
}
if (userSuppliedValue) {
finalValue += `: ${userSuppliedValue}`;
}
return finalValue;
};
const renderConfirmationMessage = () => {
if (!confirmationMessageDefinition || !confirmationValue) {
return null;
}
return intl.formatMessage(confirmationMessageDefinition, {
value: confirmationValue,
});
};
const selectOptions = options.map(option => (
<option value={option.value} key={`${option.value}-${option.label}`}>{option.label}</option>
));
return (
<SwitchContent
expression={isEditing ? 'editing' : 'default'}
cases={{
editing: (
<>
<form onSubmit={handleSubmit}>
<Form.Group
controlId={id}
isInvalid={error != null}
>
<Form.Label size="sm" className="h6 d-block" htmlFor={id}>{label}</Form.Label>
<Form.Control
data-hj-suppress
name={name}
id={id}
type={type}
as={type}
value={value}
onChange={handleChange}
{...others}
>
{options.length > 0 && selectOptions}
</Form.Control>
{!!helpText && <Form.Text>{helpText}</Form.Text>}
{error != null && <Form.Control.Feedback>{error}</Form.Control.Feedback>}
{others.children}
</Form.Group>
<p>
<StatefulButton
type="submit"
className="mr-2"
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
}}
onClick={(e) => {
// Swallow clicks if the state is pending.
// We do this instead of disabling the button to prevent
// it from losing focus (disabled elements cannot have focus).
// Disabling it would causes upstream issues in focus management.
// Swallowing the onSubmit event on the form would be better, but
// we would have to add that logic for every field given our
// current structure of the application.
if (saveState === 'pending') { e.preventDefault(); }
}}
disabledStates={[]}
/>
<Button
variant="outline-primary"
onClick={handleCancel}
>
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
</Button>
</p>
</form>
{['name', 'verified_name'].includes(name) && <CertificatePreference fieldName={name} />}
</>
),
default: (
<div className="form-group">
<div className="d-flex align-items-start">
<h6 aria-level="3">{label}</h6>
{isEditable ? (
<Button variant="link" onClick={handleEdit} className="ml-3">
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />{intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
</Button>
) : null}
</div>
<p data-hj-suppress className={isGrayedOut ? 'grayed-out' : null}>{renderValue(value)}</p>
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
</div>
),
}}
/>
);
};
EditableSelectField.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]),
emptyLabel: PropTypes.node,
type: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
userSuppliedValue: PropTypes.string,
options: PropTypes.arrayOf(PropTypes.shape({
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
})),
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
error: PropTypes.string,
confirmationMessageDefinition: PropTypes.shape({
id: PropTypes.string.isRequired,
defaultMessage: PropTypes.string.isRequired,
description: PropTypes.string,
}),
confirmationValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
helpText: PropTypes.node,
onEdit: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
isEditing: PropTypes.bool,
isEditable: PropTypes.bool,
isGrayedOut: PropTypes.bool,
intl: intlShape.isRequired,
};
EditableSelectField.defaultProps = {
value: undefined,
options: [],
saveState: undefined,
label: undefined,
emptyLabel: undefined,
error: undefined,
confirmationMessageDefinition: undefined,
confirmationValue: undefined,
helpText: undefined,
isEditing: false,
isEditable: true,
isGrayedOut: false,
userSuppliedValue: undefined,
};
export default connect(editableFieldSelector, {
onEdit: openForm,
onCancel: closeForm,
})(injectIntl(EditableSelectField));

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import {
Button, StatefulButton, Input, ValidationFormGroup,
Button, StatefulButton, Form,
} from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
@@ -18,7 +18,7 @@ import {
} from './data/actions';
import { editableFieldSelector } from './data/selectors';
function EmailField(props) {
const EmailField = (props) => {
const {
name,
label,
@@ -106,14 +106,12 @@ function EmailField(props) {
cases={{
editing: (
<form onSubmit={handleSubmit}>
<ValidationFormGroup
for={id}
invalid={error != null}
invalidMessage={error}
helpText={helpText}
<Form.Group
controlId={id}
isInvalid={error != null}
>
<label className="h6 d-block" htmlFor={id}>{label}</label>
<Input
<Form.Label className="h6 d-block" htmlFor={id}>{label}</Form.Label>
<Form.Control
data-hj-suppress
name={name}
id={id}
@@ -121,7 +119,9 @@ function EmailField(props) {
value={value}
onChange={handleChange}
/>
</ValidationFormGroup>
{!!helpText && <Form.Text>{helpText}</Form.Text>}
{error != null && <Form.Control.Feedback hasIcon={false}>{error}</Form.Control.Feedback>}
</Form.Group>
<p>
<StatefulButton
type="submit"
@@ -169,7 +169,7 @@ function EmailField(props) {
}}
/>
);
}
};
EmailField.propTypes = {
name: PropTypes.string.isRequired,

View File

@@ -1,15 +1,26 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { breakpoints, useWindowSize, Icon } from '@edx/paragon';
import { OpenInNew } from '@edx/paragon/icons';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import { useSelector } from 'react-redux';
import { NavHashLink } from 'react-router-hash-link';
import Scrollspy from 'react-scrollspy';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import messages from './AccountSettingsPage.messages';
import { selectShowPreferences } from '../notification-preferences/data/selectors';
const JumpNav = ({
intl,
displayDemographicsLink,
}) => {
const stickToTop = useWindowSize().width > breakpoints.small.minWidth;
const showPreferences = useSelector(selectShowPreferences());
function JumpNav({ intl, displayDemographicsLink }) {
return (
<div className="jump-nav">
<div className={classNames('jump-nav px-2.25', { 'jump-nav-sm position-sticky pt-3': stickToTop })}>
<Scrollspy
items={[
'basic-information',
@@ -56,15 +67,33 @@ function JumpNav({ intl, displayDemographicsLink }) {
{intl.formatMessage(messages['account.settings.section.linked.accounts'])}
</NavHashLink>
</li>
<li>
<NavHashLink to="#delete-account">
{intl.formatMessage(messages['account.settings.jump.nav.delete.account'])}
</NavHashLink>
</li>
{getConfig().ENABLE_ACCOUNT_DELETION
&& (
<li>
<NavHashLink to="#delete-account">
{intl.formatMessage(messages['account.settings.jump.nav.delete.account'])}
</NavHashLink>
</li>
)}
</Scrollspy>
{showPreferences && (
<>
<hr />
<Scrollspy
className="list-unstyled"
>
<li>
<Link to="/notifications" target="_blank" rel="noopener noreferrer">
<span>{intl.formatMessage(messages['notification.preferences.notifications.label'])}</span>
<Icon className="d-inline-block align-bottom ml-1" src={OpenInNew} />
</Link>
</li>
</Scrollspy>
</>
)}
</div>
);
}
};
JumpNav.propTypes = {
intl: intlShape.isRequired,

View File

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

View File

@@ -0,0 +1,45 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Alert } from '@edx/paragon';
const OneTimeDismissibleAlert = (props) => {
const [dismissed, setDismissed] = useState(localStorage.getItem(props.id) !== 'true');
const onClose = () => {
localStorage.setItem(props.id, 'true');
setDismissed(false);
};
return (
<Alert
variant={props.variant}
dismissible
icon={props.icon}
onClose={onClose}
show={dismissed}
>
<Alert.Heading>{props.header}</Alert.Heading>
<p>
{props.body}
</p>
</Alert>
);
};
OneTimeDismissibleAlert.propTypes = {
id: PropTypes.string.isRequired,
variant: PropTypes.string,
icon: PropTypes.func,
header: PropTypes.string,
body: PropTypes.string,
};
OneTimeDismissibleAlert.defaultProps = {
variant: 'success',
icon: undefined,
header: undefined,
body: undefined,
};
export default OneTimeDismissibleAlert;

View File

@@ -22,7 +22,7 @@ const onChildExit = (htmlNode) => {
}
};
function SwitchContent({ expression, cases, className }) {
const SwitchContent = ({ expression, cases, className }) => {
const getContent = (caseKey) => {
if (cases[caseKey]) {
if (typeof cases[caseKey] === 'string') {
@@ -48,7 +48,7 @@ function SwitchContent({ expression, cases, className }) {
{getContent(expression)}
</TransitionReplace>
);
}
};
SwitchContent.propTypes = {
expression: PropTypes.string,

View File

@@ -14,12 +14,11 @@
display: inline-block;
}
.jump-nav-sm {
top: 1rem;
}
.jump-nav {
@media (min-width: map-get($grid-breakpoints, "sm")) {
padding-top: 1rem;
position: sticky;
top: 1rem;
}
li {
margin-bottom: .5rem;
@@ -30,16 +29,6 @@
}
}
.section-heading {
@extend .h4;
margin-bottom: map-get($spacers, 3);
}
.account-section {
// These properties together will shift the hashlink position
margin-bottom: map-get($spacers, 5);
padding-top: 1rem;
}
.custom-switch {
padding: 0;
@@ -50,4 +39,8 @@
line-height: 1.6rem;
}
}
.grayed-out{
opacity: 0.6; /* Real browsers */
filter: alpha(opacity = 60); /* MSIE */
}
}

View File

@@ -0,0 +1,173 @@
import React, { useState, useEffect } from 'react';
import { connect, useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import {
ActionRow,
Form,
ModalDialog,
StatefulButton,
} from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
closeForm,
resetDrafts,
saveSettings,
updateDraft,
} from '../data/actions';
import { certPreferenceSelector } from '../data/selectors';
import commonMessages from '../AccountSettingsPage.messages';
import messages from './messages';
const CertificatePreference = ({
intl,
fieldName,
originalFullName,
originalVerifiedName,
saveState,
useVerifiedNameForCerts,
}) => {
const dispatch = useDispatch();
const [checked, setChecked] = useState(false);
const [modalIsOpen, setModalIsOpen] = useState(false);
const formId = 'useVerifiedNameForCerts';
const handleCheckboxChange = () => {
if (!checked) {
if (fieldName === 'verified_name') {
dispatch(updateDraft(formId, true));
} else {
dispatch(updateDraft(formId, false));
}
} else {
setModalIsOpen(true);
}
};
const handleCancel = () => {
setModalIsOpen(false);
dispatch(resetDrafts());
};
const handleModalChange = (e) => {
if (e.target.value === 'fullName') {
dispatch(updateDraft(formId, false));
} else {
dispatch(updateDraft(formId, true));
}
};
const handleSubmit = (e) => {
e.preventDefault();
if (saveState === 'pending') {
return;
}
dispatch(saveSettings(formId, useVerifiedNameForCerts));
};
useEffect(() => {
if (originalVerifiedName) {
if (fieldName === 'verified_name') {
setChecked(useVerifiedNameForCerts);
} else {
setChecked(!useVerifiedNameForCerts);
}
}
}, [originalVerifiedName, fieldName, useVerifiedNameForCerts]);
useEffect(() => {
if (originalVerifiedName) {
if (modalIsOpen && saveState === 'complete') {
setModalIsOpen(false);
dispatch(closeForm(fieldName));
}
}
}, [dispatch, originalVerifiedName, fieldName, modalIsOpen, saveState]);
// If the user doesn't have an approved verified name, do not display this component
return originalVerifiedName ? (
<>
<Form.Checkbox className="mt-1 mb-4" checked={checked} onChange={handleCheckboxChange}>
{intl.formatMessage(messages['account.settings.field.name.checkbox.certificate.select'])}
</Form.Checkbox>
<ModalDialog
title={intl.formatMessage(messages['account.settings.field.name.modal.certificate.title'])}
isOpen={modalIsOpen}
onClose={handleCancel}
size="lg"
hasCloseButton
isFullscreenOnMobile
>
<Form onSubmit={handleSubmit}>
<ModalDialog.Header>
<ModalDialog.Title>
{intl.formatMessage(messages['account.settings.field.name.modal.certificate.title'])}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body className="overflow-hidden">
<Form.Group className="mb-4">
<Form.Label>
{intl.formatMessage(messages['account.settings.field.name.modal.certificate.select'])}
</Form.Label>
<Form.RadioSet
name={formId}
value={useVerifiedNameForCerts ? 'verifiedName' : 'fullName'}
onChange={handleModalChange}
>
<Form.Radio value="fullName">
{originalFullName}{' '}
({intl.formatMessage(messages['account.settings.field.name.modal.certificate.option.full'])})
</Form.Radio>
<Form.Radio value="verifiedName">
{originalVerifiedName}{' '}
({intl.formatMessage(messages['account.settings.field.name.modal.certificate.option.verified'])})
</Form.Radio>
</Form.RadioSet>
</Form.Group>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="outline-primary" disabled={saveState === 'pending'}>
{intl.formatMessage(commonMessages['account.settings.editable.field.action.cancel'])}
</ModalDialog.CloseButton>
<StatefulButton
type="submit"
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.field.name.modal.certificate.button.choose']),
}}
disabledStates={[]}
/>
</ActionRow>
</ModalDialog.Footer>
</Form>
</ModalDialog>
</>
) : null;
};
CertificatePreference.propTypes = {
intl: intlShape.isRequired,
fieldName: PropTypes.string.isRequired,
originalFullName: PropTypes.string,
originalVerifiedName: PropTypes.string,
saveState: PropTypes.string,
useVerifiedNameForCerts: PropTypes.bool,
};
CertificatePreference.defaultProps = {
originalFullName: '',
originalVerifiedName: '',
saveState: null,
useVerifiedNameForCerts: false,
};
export default connect(certPreferenceSelector)(injectIntl(CertificatePreference));

View File

@@ -0,0 +1,22 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { handleRequestError } from '../../data/utils';
// eslint-disable-next-line import/prefer-default-export
export async function postVerifiedNameConfig(username, commitValues) {
const requestConfig = { headers: { Accept: 'application/json' } };
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name/config`;
const { useVerifiedNameForCerts } = commitValues;
const postValues = {
username,
use_verified_name_for_certs: useVerifiedNameForCerts,
};
const { data } = await getAuthenticatedHttpClient()
.post(requestUrl, postValues, requestConfig)
.catch(error => handleRequestError(error));
return data;
}

View File

@@ -0,0 +1,36 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'account.settings.field.name.checkbox.certificate.select': {
id: 'account.settings.field.name.certificate.select',
defaultMessage: 'If checked, this name will appear on your certificates and public-facing records.',
description: 'Label for checkbox describing that the selected name will appear on the users certificates.',
},
'account.settings.field.name.modal.certificate.title': {
id: 'account.settings.field.name.modal.certificate.title',
defaultMessage: 'Choose a preferred name for certificates and public-facing records',
description: 'Title instructing the user to choose a preferred name.',
},
'account.settings.field.name.modal.certificate.select': {
id: 'account.settings.field.name.modal.certificate.select',
defaultMessage: 'Select a name',
description: 'Label instructing the user to select a name.',
},
'account.settings.field.name.modal.certificate.option.full': {
id: 'account.settings.field.name.modal.certificate.option.full',
defaultMessage: 'Full Name',
description: 'Option representing the users full name.',
},
'account.settings.field.name.modal.certificate.option.verified': {
id: 'account.settings.field.name.modal.certificate.option.verified',
defaultMessage: 'Verified Name',
description: 'Option representing the users verified name.',
},
'account.settings.field.name.modal.certificate.button.choose': {
id: 'account.settings.field.name.modal.certificate.button.choose',
defaultMessage: 'Choose name',
description: 'Button to confirm the users name choice.',
},
});
export default messages;

View File

@@ -0,0 +1,171 @@
/* eslint-disable no-import-assign */
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
fireEvent,
render,
screen,
} from '@testing-library/react';
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
ReactDOM.createPortal = node => node;
import CertificatePreference from '../CertificatePreference'; // eslint-disable-line import/first
const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}));
jest.mock('@edx/frontend-platform/auth');
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ certPreferenceSelector: () => ({}) })));
const IntlCertificatePreference = injectIntl(CertificatePreference);
const mockStore = configureStore();
describe('NameChange', () => {
let props = {};
let store = {};
const formId = 'useVerifiedNameForCerts';
const updateDraft = 'UPDATE_DRAFT';
const labelText = 'If checked, this name will appear on your certificates and public-facing records.';
const reduxWrapper = children => (
<Router>
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
</Router>
);
beforeEach(() => {
store = mockStore();
props = {
fieldName: 'name',
originalFullName: 'Ed X',
originalVerifiedName: 'edX Verified',
saveState: null,
useVerifiedNameForCerts: false,
intl: {},
};
auth.getAuthenticatedHttpClient = jest.fn(() => ({
patch: async () => ({
data: { status: 200 },
catch: () => {},
}),
}));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
});
afterEach(() => jest.clearAllMocks());
it('does not render if there is no verified name', () => {
props = {
...props,
originalVerifiedName: '',
};
const wrapper = render(reduxWrapper(<IntlCertificatePreference {...props} />));
expect(wrapper).toMatchSnapshot();
});
it('does not trigger modal when checking empty checkbox, and updates draft immediately', () => {
props = {
...props,
useVerifiedNameForCerts: true,
};
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
expect(checkbox.checked).toEqual(false);
fireEvent.click(checkbox);
expect(screen.queryByRole('radiogroup')).toBeNull();
expect(mockDispatch).toHaveBeenCalledWith({
payload: { name: formId, value: false },
type: updateDraft,
});
});
it('triggers modal when attempting to uncheck checkbox', () => {
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
expect(checkbox.checked).toEqual(true);
fireEvent.click(checkbox);
expect(mockDispatch).not.toHaveBeenCalled();
screen.getByRole('radiogroup');
});
it('updates draft when changing radio value', () => {
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
fireEvent.click(checkbox);
const fullNameOption = screen.getByLabelText('Ed X (Full Name)');
const verifiedNameOption = screen.getByLabelText('edX Verified (Verified Name)');
expect(fullNameOption.checked).toEqual(true);
expect(verifiedNameOption.checked).toEqual(false);
fireEvent.click(verifiedNameOption);
expect(mockDispatch).toHaveBeenCalledWith({
payload: { name: formId, value: true },
type: updateDraft,
});
});
it('clears draft on cancel', () => {
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
fireEvent.click(checkbox);
const cancelButton = screen.getByText('Cancel');
fireEvent.click(cancelButton);
expect(mockDispatch).toHaveBeenCalledWith({ type: 'RESET_DRAFTS' });
expect(screen.queryByRole('radiogroup')).toBeNull();
});
it('submits', () => {
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
fireEvent.click(checkbox);
const submitButton = screen.getByText('Choose name');
fireEvent.click(submitButton);
expect(mockDispatch).toHaveBeenCalledWith({
payload: { formId, commitValues: false },
type: 'ACCOUNT_SETTINGS__SAVE_SETTINGS',
});
});
it('checks box for verified name', () => {
props = {
...props,
fieldName: 'verified_name',
useVerifiedNameForCerts: true,
};
render(reduxWrapper(<IntlCertificatePreference {...props} />));
const checkbox = screen.getByLabelText(labelText);
expect(checkbox.checked).toEqual(true);
});
});

View File

@@ -0,0 +1,62 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NameChange does not render if there is no verified name 1`] = `
Object {
"asFragment": [Function],
"baseElement": <body>
<div />
</body>,
"container": <div />,
"debug": [Function],
"findAllByAltText": [Function],
"findAllByDisplayValue": [Function],
"findAllByLabelText": [Function],
"findAllByPlaceholderText": [Function],
"findAllByRole": [Function],
"findAllByTestId": [Function],
"findAllByText": [Function],
"findAllByTitle": [Function],
"findByAltText": [Function],
"findByDisplayValue": [Function],
"findByLabelText": [Function],
"findByPlaceholderText": [Function],
"findByRole": [Function],
"findByTestId": [Function],
"findByText": [Function],
"findByTitle": [Function],
"getAllByAltText": [Function],
"getAllByDisplayValue": [Function],
"getAllByLabelText": [Function],
"getAllByPlaceholderText": [Function],
"getAllByRole": [Function],
"getAllByTestId": [Function],
"getAllByText": [Function],
"getAllByTitle": [Function],
"getByAltText": [Function],
"getByDisplayValue": [Function],
"getByLabelText": [Function],
"getByPlaceholderText": [Function],
"getByRole": [Function],
"getByTestId": [Function],
"getByText": [Function],
"getByTitle": [Function],
"queryAllByAltText": [Function],
"queryAllByDisplayValue": [Function],
"queryAllByLabelText": [Function],
"queryAllByPlaceholderText": [Function],
"queryAllByRole": [Function],
"queryAllByTestId": [Function],
"queryAllByText": [Function],
"queryAllByTitle": [Function],
"queryByAltText": [Function],
"queryByDisplayValue": [Function],
"queryByLabelText": [Function],
"queryByPlaceholderText": [Function],
"queryByRole": [Function],
"queryByTestId": [Function],
"queryByText": [Function],
"queryByTitle": [Function],
"rerender": [Function],
"unmount": [Function],
}
`;

View File

@@ -17,13 +17,9 @@ import LogoSVG from '../../logo.svg';
import { fetchSettings } from '../data/actions';
import { coachingConsentPageSelector } from '../data/selectors';
const Logo = ({ src, alt, ...attributes }) => (
<>
<img src={src} alt={alt} {...attributes} />
</>
);
const Logo = ({ src, alt, ...attributes }) => <img src={src} alt={alt} {...attributes} />;
const SuccessMessage = props => (
const SuccessMessage = (props) => (
<div className="col-12 col-lg-6 shadow-lg mx-auto mt-4 p-5">
<FontAwesomeIcon className="text-success" icon={faCheck} size="5x" />
<div className="h3">{props.header}</div>
@@ -36,6 +32,7 @@ const SuccessMessage = props => (
const AutoRedirect = (props) => {
window.location.href = props.redirectUrl;
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
};
@@ -71,6 +68,23 @@ class CoachingConsent extends React.Component {
this.props.fetchSettings();
}
handleSubmit(e) {
e.preventDefault();
const fullName = e.target.fullName.value;
const phoneNumber = e.target.phoneNumber.value;
const body = {
coaching_consent: true,
consent_form_seen: true,
phone_number: phoneNumber,
full_name: fullName,
};
this.setState({
formErrors: {},
formSubmitted: true,
declineSubmitted: false,
}, () => this.patchUsingCoachingConsentForm(body));
}
sanitizeForwardingUrl(url) {
// Redirect to root of MFE if invalid next param is sent
return url && url.startsWith(getConfig().LMS_BASE_URL) ? url : `${getConfig().LMS_BASE_URL}/dashboard/`;
@@ -99,23 +113,6 @@ class CoachingConsent extends React.Component {
}
}
handleSubmit(e) {
e.preventDefault();
const fullName = e.target.fullName.value;
const phoneNumber = e.target.phoneNumber.value;
const body = {
coaching_consent: true,
consent_form_seen: true,
phone_number: phoneNumber,
full_name: fullName,
};
this.setState({
formErrors: {},
formSubmitted: true,
declineSubmitted: false,
}, () => this.patchUsingCoachingConsentForm(body));
}
declineCoaching(e) {
e.preventDefault();
const body = {
@@ -160,6 +157,7 @@ class CoachingConsent extends React.Component {
case VIEWS.DECLINED:
return <AutoRedirect redirectUrl={this.state.redirectUrl} />;
default:
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
}
@@ -253,12 +251,12 @@ CoachingConsent.propTypes = {
}),
}).isRequired,
formErrors: PropTypes.shape({
coaching: PropTypes.object,
coaching: PropTypes.shape({}),
}).isRequired,
confirmationValues: PropTypes.shape({
coaching: PropTypes.object,
name: PropTypes.object,
phone_number: PropTypes.object,
coaching: PropTypes.shape({}),
name: PropTypes.shape({}),
phone_number: PropTypes.shape({}),
}).isRequired,
fetchSettings: PropTypes.func.isRequired,
profileDataManager: PropTypes.string,

View File

@@ -1,30 +1,28 @@
import React from 'react';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Input, Button, Hyperlink } from '@edx/paragon';
import { Form, Button, Hyperlink } from '@edx/paragon';
import PropTypes from 'prop-types';
import Alert from '../Alert';
import messages from './CoachingConsent.messages';
const ErrorMessage = props => (
<div className="alert-warning mb-2">{props.message}</div>
);
const ErrorMessage = (props) => <div className="alert-warning mb-2">{props.message}</div>;
const ManagedProfileAlert = ({ profileDataManager }) => (
<Alert className="alert alert-primary" role="alert">
<FormattedMessage
id="account.settings.coaching.managed.alert"
defaultMessage="Your name is managed by {managerTitle}. Contact your administrator for help."
description="alert message informing the user their account data is managed by a third party"
description="Alert message informing the user their account data is managed by a third party"
values={{
managerTitle: <b>{profileDataManager}</b>,
}}
/>
</Alert>
);
const CoachingForm = props => (
const CoachingForm = (props) => (
<div className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg">
<h2 className="h2">
{props.intl.formatMessage(messages['account.settings.coaching.consent.welcome.header'])}
@@ -34,13 +32,13 @@ const CoachingForm = props => (
<form onSubmit={props.onSubmit}>
<div className="py-3">
{!!props.profileDataManager && (
<ManagedProfileAlert profileDataManager={props.profileDataManager} />
<ManagedProfileAlert profileDataManager={props.profileDataManager} />
)}
<ErrorMessage message={props.formErrors.full_name} />
<label className="h6" htmlFor="fullName">
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}
</label>
<Input
<Form.Control
type="text"
name="full-name"
id="fullName"
@@ -53,7 +51,7 @@ const CoachingForm = props => (
<label className="h6" htmlFor="phoneNumber">
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}
</label>
<Input
<Form.Control
type="text"
name="phone_number"
id="phoneNumber"

View File

@@ -2,13 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { ValidationFormGroup, Input } from '@edx/paragon';
import { Form } from '@edx/paragon';
import messages from './CoachingToggle.messages';
import { editableFieldSelector } from '../data/selectors';
import { saveSettings, updateDraft, saveMultipleSettings } from '../data/actions';
import EditableField from '../EditableField';
const CoachingToggle = props => (
const CoachingToggle = (props) => (
<>
<EditableField
name="phone_number"
@@ -37,20 +37,18 @@ const CoachingToggle = props => (
return props.saveSettings('phone_number', props.phone_number);
}}
/>
<ValidationFormGroup
for="coachingConsent"
helpText={props.intl.formatMessage(messages['account.settings.field.coaching_consent.tooltip'])}
invalid={!!props.error}
invalidMessage={props.intl.formatMessage(messages['account.settings.field.coaching_consent.error'])}
<Form.Group
isInvalid={!!props.error}
className="custom-control custom-switch"
>
<Input
<Form.Switch
name={props.name}
className="custom-control-input"
disabled={props.saveState === 'pending'}
type="checkbox"
id="coachingConsent"
checked={props.coaching.coaching_consent}
helperText={props.intl.formatMessage(messages['account.settings.field.coaching_consent.tooltip'])}
value={props.coaching.coaching_consent}
onChange={async (e) => {
const { name } = e.target;
@@ -58,14 +56,21 @@ const CoachingToggle = props => (
const { user, eligible_for_coaching } = props.coaching;
const value = {
user,
// eslint-disable-next-line camelcase
eligible_for_coaching,
coaching_consent: e.target.checked,
};
props.saveSettings(name, value);
}}
/>
<label className="custom-control-label" htmlFor="coachingConsent">{props.intl.formatMessage(messages['account.settings.field.coaching_consent'])}</label>
</ValidationFormGroup>
>
{props.intl.formatMessage(messages['account.settings.field.coaching_consent'])}
</Form.Switch>
{!!props.error && (
<Form.Control.Feedback>
{props.intl.formatMessage(messages['account.settings.field.coaching_consent.error'])}
</Form.Control.Feedback>
)}
</Form.Group>
</>
);

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-import-assign */
import React from 'react';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';

View File

@@ -34,13 +34,11 @@ exports[`CoachingConsent disables name field on enterprise user 1`] = `
>
<div />
<div>
<span>
Your name is managed by
<b>
test person
</b>
. Contact your administrator for help.
</span>
Your name is managed by
<b>
test person
</b>
. Contact your administrator for help.
</div>
</div>
<div
@@ -54,14 +52,20 @@ exports[`CoachingConsent disables name field on enterprise user 1`] = `
>
Please confirm your name
</label>
<input
className="form-control"
defaultValue="edx edx"
disabled={true}
id="fullName"
name="full-name"
type="text"
/>
<div
className="pgn__form-control-decorator-group"
>
<input
className="has-value form-control"
defaultValue="edx edx"
disabled={true}
id="fullName"
name="full-name"
onBlur={[Function]}
onChange={[Function]}
type="text"
/>
</div>
</div>
<div
className="py-3"
@@ -77,13 +81,19 @@ exports[`CoachingConsent disables name field on enterprise user 1`] = `
>
Enter your mobile number
</label>
<input
className="form-control"
defaultValue="1234567890"
id="phoneNumber"
name="phone_number"
type="text"
/>
<div
className="pgn__form-control-decorator-group"
>
<input
className="has-value form-control"
defaultValue="1234567890"
id="phoneNumber"
name="phone_number"
onBlur={[Function]}
onChange={[Function]}
type="text"
/>
</div>
</div>
<div
className=" py-3"
@@ -114,7 +124,7 @@ exports[`CoachingConsent disables name field on enterprise user 1`] = `
className="mt-3"
>
<a
className="mt-3 text-dark btn-link small"
className="pgn__hyperlink default-link standalone-link mt-3 text-dark btn-link small"
href="http://localhost:18000/dashboard/"
onClick={[Function]}
target="_self"
@@ -203,14 +213,20 @@ exports[`CoachingConsent should render 1`] = `
>
Please confirm your name
</label>
<input
className="form-control"
defaultValue="edx edx"
disabled={false}
id="fullName"
name="full-name"
type="text"
/>
<div
className="pgn__form-control-decorator-group"
>
<input
className="has-value form-control"
defaultValue="edx edx"
disabled={false}
id="fullName"
name="full-name"
onBlur={[Function]}
onChange={[Function]}
type="text"
/>
</div>
</div>
<div
className="py-3"
@@ -226,13 +242,19 @@ exports[`CoachingConsent should render 1`] = `
>
Enter your mobile number
</label>
<input
className="form-control"
defaultValue="1234567890"
id="phoneNumber"
name="phone_number"
type="text"
/>
<div
className="pgn__form-control-decorator-group"
>
<input
className="has-value form-control"
defaultValue="1234567890"
id="phoneNumber"
name="phone_number"
onBlur={[Function]}
onChange={[Function]}
type="text"
/>
</div>
</div>
<div
className=" py-3"
@@ -263,7 +285,7 @@ exports[`CoachingConsent should render 1`] = `
className="mt-3"
>
<a
className="mt-3 text-dark btn-link small"
className="pgn__hyperlink default-link standalone-link mt-3 text-dark btn-link small"
href="http://localhost:18000/dashboard/"
onClick={[Function]}
target="_self"

View File

@@ -9,6 +9,7 @@ export const OPEN_FORM = 'OPEN_FORM';
export const CLOSE_FORM = 'CLOSE_FORM';
export const UPDATE_DRAFT = 'UPDATE_DRAFT';
export const RESET_DRAFTS = 'RESET_DRAFTS';
export const BEGIN_NAME_CHANGE = 'BEGIN_NAME_CHANGE';
// FETCH SETTINGS ACTIONS
@@ -25,6 +26,7 @@ export const fetchSettingsSuccess = ({
thirdPartyAuthProviders,
profileDataManager,
timeZones,
verifiedNameHistory,
}) => ({
type: FETCH_SETTINGS.SUCCESS,
payload: {
@@ -32,6 +34,7 @@ export const fetchSettingsSuccess = ({
thirdPartyAuthProviders,
profileDataManager,
timeZones,
verifiedNameHistory,
},
});
@@ -68,6 +71,10 @@ export const resetDrafts = () => ({
type: RESET_DRAFTS,
});
export const beginNameChange = (formId) => ({
type: BEGIN_NAME_CHANGE,
payload: { formId },
});
// SAVE SETTINGS ACTIONS
export const saveSettings = (formId, commitValues) => ({

View File

@@ -10,6 +10,11 @@ export const YEAR_OF_BIRTH_OPTIONS = (() => {
return years.reverse();
})();
export const COPPA_COMPLIANCE_YEAR = (() => {
const currentYear = new Date().getFullYear();
return currentYear - 13;
})();
export const EDUCATION_LEVELS = [
'',
'p',

View File

@@ -8,11 +8,13 @@ import {
UPDATE_DRAFT,
RESET_DRAFTS,
SAVE_MULTIPLE_SETTINGS,
BEGIN_NAME_CHANGE,
} from './actions';
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from '../delete-account';
import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from '../site-language';
import { reducer as resetPasswordReducer, RESET_PASSWORD } from '../reset-password';
import { reducer as nameChangeReducer, REQUEST_NAME_CHANGE } from '../name-change';
import { reducer as thirdPartyAuthReducer, DISCONNECT_AUTH } from '../third-party-auth';
export const defaultState = {
@@ -31,10 +33,15 @@ export const defaultState = {
deleteAccount: deleteAccountReducer(),
siteLanguage: siteLanguageReducer(),
resetPassword: resetPasswordReducer(),
nameChange: nameChangeReducer(),
thirdPartyAuth: thirdPartyAuthReducer(),
nameChangeModal: false,
verifiedName: null,
mostRecentVerifiedName: {},
verifiedNameHistory: {},
};
const reducer = (state = defaultState, action) => {
const reducer = (state = defaultState, action = {}) => {
let dispatcherIsOpenForm;
switch (action.type) {
@@ -56,6 +63,7 @@ const reducer = (state = defaultState, action) => {
loading: false,
loaded: true,
loadingError: null,
verifiedNameHistory: action.payload.verifiedNameHistory,
};
case FETCH_SETTINGS.FAILURE:
return {
@@ -89,6 +97,7 @@ const reducer = (state = defaultState, action) => {
saveState: null,
errors: {},
drafts: {},
nameChangeModal: false,
};
}
return state;
@@ -106,6 +115,15 @@ const reducer = (state = defaultState, action) => {
drafts: {},
};
case BEGIN_NAME_CHANGE:
return {
...state,
saveState: 'error',
nameChangeModal: {
formId: action.payload.formId,
},
};
case SAVE_SETTINGS.BEGIN:
return {
...state,
@@ -119,7 +137,6 @@ const reducer = (state = defaultState, action) => {
values: { ...state.values, ...action.payload.values },
errors: {},
confirmationValues: {
...state.confirmationValues,
...action.payload.confirmationValues,
},
@@ -198,6 +215,15 @@ const reducer = (state = defaultState, action) => {
resetPassword: resetPasswordReducer(state.resetPassword, action),
};
case REQUEST_NAME_CHANGE.BEGIN:
case REQUEST_NAME_CHANGE.SUCCESS:
case REQUEST_NAME_CHANGE.FAILURE:
case REQUEST_NAME_CHANGE.RESET:
return {
...state,
nameChange: nameChangeReducer(state.nameChange, action),
};
case DISCONNECT_AUTH.BEGIN:
case DISCONNECT_AUTH.SUCCESS:
case DISCONNECT_AUTH.FAILURE:

View File

@@ -25,11 +25,13 @@ import {
saveMultipleSettingsBegin,
saveMultipleSettingsSuccess,
saveMultipleSettingsFailure,
beginNameChange,
} from './actions';
// Sub-modules
import { saga as deleteAccountSaga } from '../delete-account';
import { saga as resetPasswordSaga } from '../reset-password';
import { saga as nameChangeSaga } from '../name-change';
import {
saga as siteLanguageSaga,
patchPreferences,
@@ -38,7 +40,12 @@ import {
import { saga as thirdPartyAuthSaga } from '../third-party-auth';
// Services
import { getSettings, patchSettings, getTimeZones } from './service';
import {
getSettings,
patchSettings,
getTimeZones,
getVerifiedNameHistory,
} from './service';
export function* handleFetchSettings() {
try {
@@ -54,6 +61,8 @@ export function* handleFetchSettings() {
userId,
);
const verifiedNameHistory = yield call(getVerifiedNameHistory);
if (values.country) { yield put(fetchTimeZones(values.country)); }
yield put(fetchSettingsSuccess({
@@ -61,6 +70,7 @@ export function* handleFetchSettings() {
thirdPartyAuthProviders,
profileDataManager,
timeZones,
verifiedNameHistory,
}));
} catch (e) {
yield put(fetchSettingsFailure(e.message));
@@ -98,6 +108,9 @@ export function* handleSaveSettings(action) {
yield put(closeForm(action.payload.formId));
} catch (e) {
if (e.fieldErrors) {
if (e.fieldErrors.name?.includes('verification')) {
yield put(beginNameChange('name'));
}
yield put(saveSettingsFailure({ fieldErrors: e.fieldErrors }));
} else {
yield put(saveSettingsFailure(e.message));
@@ -126,6 +139,9 @@ export function* handleSaveMultipleSettings(action) {
}
} catch (e) {
if (e.fieldErrors) {
if (e.fieldErrors.name?.includes('verification')) {
yield put(beginNameChange('name'));
}
yield put(saveMultipleSettingsFailure({ fieldErrors: e.fieldErrors }));
} else {
yield put(saveMultipleSettingsFailure(e.message));
@@ -148,6 +164,7 @@ export default function* saga() {
deleteAccountSaga(),
siteLanguageSaga(),
resetPasswordSaga(),
nameChangeSaga(),
thirdPartyAuthSaga(),
]);
}

View File

@@ -1,5 +1,6 @@
import { createSelector, createStructuredSelector } from 'reselect';
import { siteLanguageListSelector, siteLanguageOptionsSelector } from '../site-language';
import { compareVerifiedNamesByCreatedDate } from '../../utils';
export const storeName = 'accountSettings';
@@ -7,9 +8,74 @@ export const accountSettingsSelector = state => ({ ...state[storeName] });
const editableFieldNameSelector = (state, props) => props.name;
const verifiedNameSettingsSelector = createSelector(
accountSettingsSelector,
accountSettings => ({
history: accountSettings.verifiedNameHistory.results,
useVerifiedNameForCerts: accountSettings?.verifiedNameHistory.use_verified_name_for_certs,
}),
);
const sortedVerifiedNameHistorySelector = createSelector(
verifiedNameSettingsSelector,
verifiedNameSettings => {
const { history } = verifiedNameSettings;
if (Array.isArray(history)) {
return history.sort(compareVerifiedNamesByCreatedDate);
}
return [];
},
);
const mostRecentVerifiedNameSelector = createSelector(
sortedVerifiedNameHistorySelector,
sortedHistory => (sortedHistory.length > 0 ? sortedHistory[0] : null),
);
const mostRecentApprovedVerifiedNameValueSelector = createSelector(
sortedVerifiedNameHistorySelector,
mostRecentVerifiedNameSelector,
(sortedHistory, mostRecentVerifiedName) => {
const approvedVerifiedNames = sortedHistory.filter(name => name.status === 'approved');
const approvedVerifiedName = approvedVerifiedNames.length > 0 ? approvedVerifiedNames[0] : null;
let verifiedName = null;
switch (mostRecentVerifiedName && mostRecentVerifiedName.status) {
case 'approved':
case 'denied':
case 'pending':
verifiedName = approvedVerifiedName;
break;
case 'submitted':
verifiedName = mostRecentVerifiedName;
break;
default:
verifiedName = null;
}
return verifiedName;
},
);
const valuesSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.values,
mostRecentApprovedVerifiedNameValueSelector,
(accountSettings, mostRecentApprovedVerifiedNameValue) => {
let useVerifiedNameForCerts = (
accountSettings.verifiedNameHistory?.use_verified_name_for_certs || false
);
if (Object.keys(accountSettings.confirmationValues).includes('useVerifiedNameForCerts')) {
useVerifiedNameForCerts = accountSettings.confirmationValues.useVerifiedNameForCerts;
}
return {
...accountSettings.values,
verified_name: mostRecentApprovedVerifiedNameValue?.verified_name,
useVerifiedNameForCerts,
};
},
);
const draftsSelector = createSelector(
@@ -50,6 +116,11 @@ const errorSelector = createSelector(
accountSettings => accountSettings.errors,
);
const nameChangeModalSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.nameChangeModal,
);
const saveStateSelector = createSelector(
accountSettingsSelector,
accountSettings => accountSettings.saveState,
@@ -69,7 +140,18 @@ export const profileDataManagerSelector = createSelector(
export const staticFieldsSelector = createSelector(
accountSettingsSelector,
accountSettings => (accountSettings.profileDataManager ? ['name', 'email', 'country'] : []),
mostRecentVerifiedNameSelector,
(accountSettings, verifiedName) => {
const staticFields = [];
if (accountSettings.profileDataManager) {
staticFields.push('name', 'email', 'country');
}
if (verifiedName && ['submitted'].includes(verifiedName.status)) {
staticFields.push('verifiedName');
}
return staticFields;
},
);
/**
@@ -85,7 +167,11 @@ const formValuesSelector = createSelector(
(values, drafts) => {
const formValues = {};
Object.entries(values).forEach(([name, value]) => {
formValues[name] = chooseFormValue(drafts[name], value) || '';
if (typeof value === 'boolean') {
formValues[name] = chooseFormValue(drafts[name], value);
} else {
formValues[name] = chooseFormValue(drafts[name], value) || '';
}
});
return formValues;
},
@@ -93,7 +179,7 @@ const formValuesSelector = createSelector(
const transformTimeZonesToOptions = timeZoneArr => timeZoneArr
.map(({ time_zone, description }) => ({ // eslint-disable-line camelcase
value: time_zone, label: description,
value: time_zone, label: description, // eslint-disable-line camelcase
}));
const timeZonesSelector = createSelector(
@@ -130,21 +216,35 @@ export const accountSettingsPageSelector = createSelector(
siteLanguageOptionsSelector,
siteLanguageSelector,
formValuesSelector,
valuesSelector,
draftsSelector,
errorSelector,
profileDataManagerSelector,
staticFieldsSelector,
timeZonesSelector,
countryTimeZonesSelector,
activeAccountSelector,
nameChangeModalSelector,
mostRecentApprovedVerifiedNameValueSelector,
mostRecentVerifiedNameSelector,
sortedVerifiedNameHistorySelector,
(
accountSettings,
siteLanguageOptions,
siteLanguage,
formValues,
committedValues,
drafts,
formErrors,
profileDataManager,
staticFields,
timeZoneOptions,
countryTimeZoneOptions,
activeAccount,
nameChangeModal,
verifiedName,
mostRecentVerifiedName,
verifiedNameHistory,
) => ({
siteLanguageOptions,
siteLanguage,
@@ -155,9 +255,37 @@ export const accountSettingsPageSelector = createSelector(
countryTimeZoneOptions,
isActive: activeAccount,
formValues,
committedValues,
drafts,
formErrors,
profileDataManager,
staticFields,
tpaProviders: accountSettings.thirdPartyAuth.providers,
nameChangeModal,
verifiedName,
mostRecentVerifiedName,
verifiedNameHistory,
}),
);
export const certPreferenceSelector = createSelector(
valuesSelector,
formValuesSelector,
mostRecentApprovedVerifiedNameValueSelector,
saveStateSelector,
errorSelector,
(
committedValues,
formValues,
mostRecentApprovedVerifiedNameValue,
saveState,
errors,
) => ({
originalFullName: committedValues?.name || '',
originalVerifiedName: mostRecentApprovedVerifiedNameValue?.verified_name || '',
useVerifiedNameForCerts: formValues.useVerifiedNameForCerts || false,
saveState,
formErrors: errors,
}),
);
@@ -204,3 +332,12 @@ export const demographicsSectionSelector = createSelector(
formErrors: errors,
}),
);
export const nameChangeSelector = createSelector(
accountSettingsSelector,
formValuesSelector,
(accountSettings, formValues) => ({
...accountSettings.nameChange,
formValues,
}),
);

View File

@@ -7,6 +7,7 @@ import isEmpty from 'lodash.isempty';
import { handleRequestError, unpackFieldErrors } from './utils';
import { getThirdPartyAuthProviders } from '../third-party-auth';
import { postVerifiedNameConfig } from '../certificate-preference/data/service';
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
@@ -176,12 +177,57 @@ export async function shouldDisplayDemographicsQuestions() {
return false;
}
export async function getVerifiedName() {
let data;
const client = getAuthenticatedHttpClient();
try {
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name`;
({ data } = await client.get(requestUrl));
} catch (error) {
return {};
}
return data;
}
export async function getVerifiedNameHistory() {
let data;
const client = getAuthenticatedHttpClient();
try {
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name/history`;
({ data } = await client.get(requestUrl));
} catch (error) {
return {};
}
return data;
}
export async function postVerifiedName(data) {
const requestConfig = { headers: { Accept: 'application/json' } };
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name`;
await getAuthenticatedHttpClient()
.post(requestUrl, data, requestConfig)
.catch(error => handleRequestError(error));
}
/**
* A single function to GET everything considered a setting.
* Currently encapsulates Account, Preferences, Coaching, ThirdPartyAuth, and Demographics
*/
export async function getSettings(username, userRoles, userId) {
const results = await Promise.all([
const [
account,
preferences,
thirdPartyAuthProviders,
profileDataManager,
timeZones,
coaching,
shouldDisplayDemographicsQuestionsResponse,
demographics,
demographicsOptions,
] = await Promise.all([
getAccount(username),
getPreferences(username),
getThirdPartyAuthProviders(),
@@ -194,15 +240,15 @@ export async function getSettings(username, userRoles, userId) {
]);
return {
...results[0],
...results[1],
thirdPartyAuthProviders: results[2],
profileDataManager: results[3],
timeZones: results[4],
coaching: results[5],
shouldDisplayDemographicsSection: results[6],
...results[7], // demographics
demographicsOptions: results[8],
...account,
...preferences,
thirdPartyAuthProviders,
profileDataManager,
timeZones,
coaching,
shouldDisplayDemographicsSection: shouldDisplayDemographicsQuestionsResponse,
...demographics,
demographicsOptions,
};
}
@@ -217,11 +263,19 @@ export async function patchSettings(username, commitValues, userId) {
const preferenceKeys = ['time_zone'];
const coachingKeys = ['coaching'];
const demographicsKeys = DEMOGRAPHICS_FIELDS;
const certificateKeys = ['useVerifiedNameForCerts'];
const isDemographicsKey = (value, key) => key.includes('demographics');
const accountCommitValues = omit(commitValues, preferenceKeys, coachingKeys, demographicsKeys);
const accountCommitValues = omit(
commitValues,
preferenceKeys,
coachingKeys,
demographicsKeys,
certificateKeys,
);
const preferenceCommitValues = pick(commitValues, preferenceKeys);
const coachingCommitValues = pick(commitValues, coachingKeys);
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
const certCommitValues = pick(commitValues, certificateKeys);
const patchRequests = [];
if (!isEmpty(accountCommitValues)) {
@@ -236,6 +290,9 @@ export async function patchSettings(username, commitValues, userId) {
if (!isEmpty(demographicsCommitValues)) {
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
}
if (!isEmpty(certCommitValues)) {
patchRequests.push(postVerifiedNameConfig(username, certCommitValues));
}
const results = await Promise.all(patchRequests);
// Assigns in order of requests. Preference keys

View File

@@ -1,8 +1,7 @@
import { put } from 'redux-saga/effects';
import { logError } from '@edx/frontend-platform/logging';
import { history } from '@edx/frontend-platform';
export default function* handleFailure(error, failureAction = null, failureRedirectPath = null) {
export default function* handleFailure(error, navigate, failureAction = null, failureRedirectPath = null) {
if (error.fieldErrors && failureAction !== null) {
yield put(failureAction({ fieldErrors: error.fieldErrors }));
}
@@ -11,6 +10,6 @@ export default function* handleFailure(error, failureAction = null, failureRedir
yield put(failureAction(error.message));
}
if (failureRedirectPath !== null) {
history.push(failureRedirectPath);
navigate(failureRedirectPath);
}
}

View File

@@ -6,6 +6,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Hyperlink } from '@edx/paragon';
// Messages
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
// Components
@@ -22,13 +23,16 @@ const BeforeProceedingBanner = (props) => {
<FormattedMessage
id="account.settings.delete.account.before.proceeding"
defaultMessage="Before proceeding, please {actionLink}."
description="Error that appears if you are trying to delete your edX account, but something about your account needs attention first. The actionLink will be instructions, such as 'unlink your Facebook account'."
description="Error that appears if you are trying to delete your account, but something about your account needs attention first. The actionLink will be instructions, such as 'unlink your Facebook account'."
values={{
actionLink: (
actionLink: supportArticleUrl ? (
<Hyperlink destination={supportArticleUrl}>
{intl.formatMessage(messages[instructionMessageId])}
</Hyperlink>
) : (
intl.formatMessage(messages[instructionMessageId])
),
siteName: getConfig().SITE_NAME,
}}
/>
</Alert>

View File

@@ -0,0 +1,48 @@
import React from 'react';
import ReactDOM from 'react-dom';
import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl, createIntl } from '@edx/frontend-platform/i18n';
ReactDOM.createPortal = node => node;
import BeforeProceedingBanner from './BeforeProceedingBanner'; // eslint-disable-line import/first
const IntlBeforeProceedingBanner = injectIntl(BeforeProceedingBanner);
describe('BeforeProceedingBanner', () => {
it('should match the snapshot if SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT does not have a support link', () => {
const props = {
instructionMessageId: 'account.settings.delete.account.please.unlink',
intl: createIntl({ locale: 'en' }),
supportArticleUrl: '',
};
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlBeforeProceedingBanner
{...props}
/>
</IntlProvider>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('should match the snapshot when SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT has a support link', () => {
const props = {
instructionMessageId: 'account.settings.delete.account.please.unlink',
intl: createIntl({ locale: 'en' }),
supportArticleUrl: 'http://test-support.edx',
};
const tree = renderer
.create((
<IntlProvider locale="en">
<IntlBeforeProceedingBanner
{...props}
/>
</IntlProvider>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -2,12 +2,13 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
Button, Input, Modal, ValidationFormGroup,
AlertModal,
Button, Input, ValidationFormGroup, ActionRow,
} from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
import Alert from '../Alert';
import PrintingInstructions from './PrintingInstructions';
@@ -65,52 +66,66 @@ export class ConfirmationModal extends Component {
const open = ['confirming', 'pending', 'failed'].includes(status);
const passwordFieldId = 'passwordFieldId';
const invalidMessage = messages[this.getShortErrorMessageId(errorType)];
// TODO: We lack a good way of providing custom language for a particular site. This is a hack
// to allow edx.org to fulfill its business requirements.
const deleteAccountModalText2MessageKey = getConfig().SITE_NAME === 'edX'
? 'account.settings.delete.account.modal.text.2.edX'
: 'account.settings.delete.account.modal.text.2';
return (
<Modal
open={open}
<AlertModal
isOpen={open}
title={intl.formatMessage(messages['account.settings.delete.account.modal.header'])}
body={(
<div>
{this.renderError()}
<Alert
className="alert-warning mt-n2"
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
>
<h6>
{intl.formatMessage(messages['account.settings.delete.account.modal.text.1'])}
</h6>
<p>{intl.formatMessage(messages['account.settings.delete.account.modal.text.2'])}</p>
<p>
<PrintingInstructions />
</p>
</Alert>
<ValidationFormGroup
for={passwordFieldId}
invalid={errorType !== null}
invalidMessage={intl.formatMessage(invalidMessage)}
>
<label className="d-block" htmlFor={passwordFieldId}>
{intl.formatMessage(messages['account.settings.delete.account.modal.enter.password'])}
</label>
<Input
name="password"
id={passwordFieldId}
type="password"
value={password}
onChange={onChange}
/>
</ValidationFormGroup>
</div>
)}
buttons={[
<Button variant="danger" onClick={onSubmit}>
{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])}
</Button>,
]}
closeText={intl.formatMessage(messages['account.settings.delete.account.modal.confirm.cancel'])}
renderHeaderCloseButton={false}
onClose={onCancel}
/>
footerNode={(
<ActionRow>
<Button variant="link" onClick={onCancel}>Cancel</Button>
<Button variant="danger" onClick={onSubmit}>Yes, Delete</Button>
</ActionRow>
)}
>
<div className="p-3">
{this.renderError()}
<Alert
className="alert-warning mt-n2"
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
>
<h6>
{intl.formatMessage(
messages['account.settings.delete.account.modal.text.1'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{intl.formatMessage(
messages[deleteAccountModalText2MessageKey],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
<PrintingInstructions />
</p>
</Alert>
<ValidationFormGroup
for={passwordFieldId}
invalid={errorType !== null}
invalidMessage={intl.formatMessage(invalidMessage)}
>
<label className="d-block" htmlFor={passwordFieldId}>
{intl.formatMessage(messages['account.settings.delete.account.modal.enter.password'])}
</label>
<Input
name="password"
id={passwordFieldId}
type="password"
value={password}
onChange={onChange}
/>
</ValidationFormGroup>
</div>
</AlertModal>
);
}
}

View File

@@ -59,20 +59,44 @@ export class DeleteAccount extends React.Component {
hasLinkedTPA, isVerifiedAccount, status, errorType, intl,
} = this.props;
const canDelete = isVerifiedAccount && !hasLinkedTPA;
const supportArticleUrl = process.env.SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT;
// TODO: We lack a good way of providing custom language for a particular site. This is a hack
// to allow edx.org to fulfill its business requirements.
const deleteAccountText2MessageKey = getConfig().SITE_NAME === 'edX'
? 'account.settings.delete.account.text.2.edX'
: 'account.settings.delete.account.text.2';
const optInInstructionMessageId = getConfig().MARKETING_EMAILS_OPT_IN
? 'account.settings.delete.account.please.confirm'
: 'account.settings.delete.account.please.activate';
return (
<div>
<h2 className="section-heading">
<h2 className="section-heading h4 mb-3">
{intl.formatMessage(messages['account.settings.delete.account.header'])}
</h2>
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
<p>{intl.formatMessage(messages['account.settings.delete.account.text.1'])}</p>
<p>{intl.formatMessage(messages['account.settings.delete.account.text.2'])}</p>
<p>
{intl.formatMessage(
messages['account.settings.delete.account.text.1'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
{intl.formatMessage(
messages[deleteAccountText2MessageKey],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
<PrintingInstructions />
</p>
<p className="text-danger h6">
{intl.formatMessage(messages['account.settings.delete.account.text.warning'])}
{intl.formatMessage(
messages['account.settings.delete.account.text.warning'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
<Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
@@ -91,15 +115,15 @@ export class DeleteAccount extends React.Component {
{isVerifiedAccount ? null : (
<BeforeProceedingBanner
instructionMessageId="account.settings.delete.account.please.activate"
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
instructionMessageId={optInInstructionMessageId}
supportArticleUrl="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email-"
/>
)}
{hasLinkedTPA ? (
<BeforeProceedingBanner
instructionMessageId="account.settings.delete.account.please.unlink"
supportArticleUrl="https://support.edx.org/hc/en-us/articles/207206067"
supportArticleUrl={supportArticleUrl}
/>
) : null}

View File

@@ -1,10 +1,15 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React from 'react';
import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
// Testing the modals separately, they just clutter up the snapshots if included here.
jest.mock('./ConfirmationModal');
jest.mock('./SuccessModal');
jest.mock('./ConfirmationModal', () => function ConfirmationModalMock() {
return <></>;
});
jest.mock('./SuccessModal', () => function SuccessModalMock() {
return <></>;
});
import { DeleteAccount } from './DeleteAccount'; // eslint-disable-line import/first
@@ -37,6 +42,7 @@ describe('DeleteAccount', () => {
</IntlProvider>
))
.toJSON();
expect(tree).toMatchSnapshot();
});

View File

@@ -2,22 +2,39 @@ import React from 'react';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
const PrintingInstructions = (props) => {
const actionLink = (
<Hyperlink
destination="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
// TODO: What would a generic version of this link look like? Should
// CERTIFICATE_SHARING_HELP_URL really be a configuration variable? In the meantime,
// We've removed the link from the default message.
destination="https://support.edx.org/hc/en-us/sections/115004173027-Receive-and-Share-edX-Certificates"
>
{props.intl.formatMessage(messages['account.settings.delete.account.text.3.link'])}
</Hyperlink>
);
// TODO: We lack a good way of providing custom language for a particular site. This is a hack
// to allow edx.org to mention MicroMasters certificates to fulfill its business requirements.
if (getConfig().SITE_NAME === 'edX') {
return (
<FormattedMessage
id="account.settings.delete.account.text.3.edX"
defaultMessage="You may also lose access to verified certificates and other program credentials like MicroMasters certificates. You can make a copy of these for your records before proceeding with deletion. {actionLink}."
description="A message in the user account deletion area warning users that deleting their account will prevent them from accessing their certificates. 'actionLink' is a HTML link with a full sentence that describes how to print a certificate."
values={{ actionLink }}
/>
);
}
return (
<FormattedMessage
id="account.settings.delete.account.text.3"
defaultMessage="You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}."
description="A message in the user account deletion area"
defaultMessage="You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion."
description="A message in the user account deletion area warning users that deleting their account will prevent them from accessing their certificates."
values={{ actionLink }}
/>
);

View File

@@ -1,27 +1,31 @@
import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Modal } from '@edx/paragon';
import { ModalLayer, ModalCloseButton } from '@edx/paragon';
import messages from './messages';
export const SuccessModal = (props) => {
const { status, intl, onClose } = props;
return (
<Modal
open={status === 'deleted'}
title={intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
body={(
<div>
<ModalLayer isOpen={status === 'deleted'} onClose={onClose}>
<div className="mw-sm p-5 bg-white mx-auto my-3">
<h3>
{intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
</h3>
<div className="p-3">
<p className="h6">
{intl.formatMessage(messages['account.settings.delete.account.modal.after.text'])}
</p>
</div>
)}
closeText={intl.formatMessage(messages['account.settings.delete.account.modal.after.button'])}
renderHeaderCloseButton={false}
onClose={onClose}
/>
<p>
<ModalCloseButton className="float-right" variant="link">Close</ModalCloseButton>
</p>
</div>
</ModalLayer>
);
};

View File

@@ -0,0 +1,68 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BeforeProceedingBanner should match the snapshot if SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT does not have a support link 1`] = `
<div
className="alert d-flex align-items-start alert-warning mt-n2"
>
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
data-icon="exclamation-triangle"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
fill="currentColor"
style={Object {}}
/>
</svg>
</div>
<div>
Before proceeding, please unlink all social media accounts.
</div>
</div>
`;
exports[`BeforeProceedingBanner should match the snapshot when SUPPORT_URL_TO_UNLINK_SOCIAL_MEDIA_ACCOUNT has a support link 1`] = `
<div
className="alert d-flex align-items-start alert-warning mt-n2"
>
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
data-icon="exclamation-triangle"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
fill="currentColor"
style={Object {}}
/>
</svg>
</div>
<div>
Before proceeding, please
<a
className="pgn__hyperlink default-link standalone-link"
href="http://test-support.edx"
onClick={[Function]}
target="_self"
>
unlink all social media accounts
</a>
.
</div>
</div>
`;

View File

@@ -1,262 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfirmationModal should match default closed confirmation modal snapshot 1`] = `
Array [
<div
className="fade"
role="presentation"
/>,
<div
className="modal fade"
role="presentation"
>
<div
aria-labelledby="id1"
aria-modal={true}
className=""
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id1"
>
Are you sure?
</h2>
</div>
<div
className="modal-body"
>
<div>
<div
className="alert d-flex align-items-start alert-warning mt-n2"
>
<div>
<svg
aria-hidden="true"
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
data-icon="exclamation-triangle"
data-prefix="fas"
focusable="false"
role="img"
style={Object {}}
viewBox="0 0 576 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
fill="currentColor"
style={Object {}}
/>
</svg>
</div>
<div>
<h6>
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
</h6>
<p>
If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
</p>
<p>
<span>
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
<a
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
onClick={[Function]}
target="_self"
>
follow the instructions for printing or downloading a certificate
</a>
.
</span>
</p>
</div>
</div>
<div
className="form-group"
>
<label
className="d-block"
htmlFor="passwordFieldId"
>
If you still wish to continue and delete your account, please enter your account password:
</label>
<input
aria-describedby=""
className="form-control"
id="passwordFieldId"
name="password"
onChange={[MockFunction]}
type="password"
value="fluffy bunnies"
/>
<strong
className="invalid-feedback"
id="passwordFieldId-invalid-feedback"
>
Unable to delete account
</strong>
</div>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Cancel
</button>
<button
className="btn btn-danger"
disabled={false}
onClick={[MockFunction]}
type="button"
>
Yes, Delete
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
]
`;
exports[`ConfirmationModal should match default closed confirmation modal snapshot 1`] = `null`;
exports[`ConfirmationModal should match empty password confirmation modal snapshot 1`] = `
Array [
<div
className="modal-backdrop show"
role="presentation"
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>,
<div
className="modal show d-block"
role="presentation"
className="pgn__modal-layer"
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
>
<div
aria-labelledby="id3"
aria-modal={true}
className="modal-dialog"
role="dialog"
className="pgn__modal-content-container"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
className="pgn__modal-backdrop"
onClick={[MockFunction]}
onKeyDown={[MockFunction]}
/>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={1}
/>
<div
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
aria-label="Are you sure?"
className="pgn__modal pgn__modal-md pgn__modal-default pgn__alert-modal"
role="dialog"
>
<div
className="modal-content"
className="pgn__modal-header"
>
<div
className="modal-header"
<h2
className="pgn__modal-title"
>
<h2
className="modal-title"
id="id3"
Are you sure?
</h2>
</div>
<div
className="pgn__modal-body pgn__modal-body-scroll-top pgn__modal-body-scroll-bottom"
>
<div />
<div
className="pgn__modal-body-content"
>
<div
className="p-3"
>
Are you sure?
</h2>
</div>
<div
className="modal-body"
>
<div>
<div
className="alert d-flex align-items-start alert-danger mt-n2"
>
@@ -314,23 +119,13 @@ Array [
</div>
<div>
<h6>
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
</h6>
<p>
If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
If you proceed, you will be unable to use this account to take courses on localhost.
</p>
<p>
<span>
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
<a
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
onClick={[Function]}
target="_self"
>
follow the instructions for printing or downloading a certificate
</a>
.
</span>
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
</p>
</div>
</div>
@@ -361,13 +156,18 @@ Array [
</div>
</div>
</div>
<div />
</div>
<div
className="pgn__modal-footer"
>
<div
className="modal-footer"
className="pgn__action-row"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
onClick={[MockFunction]}
type="button"
>
Cancel
@@ -383,99 +183,84 @@ Array [
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>
</div>
</div>,
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>,
]
`;
exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
Array [
<div
className="modal-backdrop show"
role="presentation"
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>,
<div
className="modal show d-block"
role="presentation"
className="pgn__modal-layer"
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
aria-labelledby="id2"
aria-modal={true}
className="modal-dialog"
role="dialog"
className="pgn__modal-content-container"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
className="pgn__modal-backdrop"
onClick={[MockFunction]}
onKeyDown={[MockFunction]}
/>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={1}
/>
<div
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
aria-label="Are you sure?"
className="pgn__modal pgn__modal-md pgn__modal-default pgn__alert-modal"
role="dialog"
>
<div
className="modal-content"
className="pgn__modal-header"
>
<div
className="modal-header"
<h2
className="pgn__modal-title"
>
<h2
className="modal-title"
id="id2"
Are you sure?
</h2>
</div>
<div
className="pgn__modal-body pgn__modal-body-scroll-top pgn__modal-body-scroll-bottom"
>
<div />
<div
className="pgn__modal-body-content"
>
<div
className="p-3"
>
Are you sure?
</h2>
</div>
<div
className="modal-body"
>
<div>
<div
className="alert d-flex align-items-start alert-warning mt-n2"
>
@@ -500,23 +285,13 @@ Array [
</div>
<div>
<h6>
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
</h6>
<p>
If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
If you proceed, you will be unable to use this account to take courses on localhost.
</p>
<p>
<span>
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
<a
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
onClick={[Function]}
target="_self"
>
follow the instructions for printing or downloading a certificate
</a>
.
</span>
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
</p>
</div>
</div>
@@ -547,13 +322,18 @@ Array [
</div>
</div>
</div>
<div />
</div>
<div
className="pgn__modal-footer"
>
<div
className="modal-footer"
className="pgn__action-row"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
onClick={[MockFunction]}
type="button"
>
Cancel
@@ -569,22 +349,22 @@ Array [
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>
</div>
</div>,
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>,
]
`;

View File

@@ -3,7 +3,7 @@
exports[`DeleteAccount should match default section snapshot 1`] = `
<div>
<h2
className="section-heading"
className="section-heading h4 mb-3"
>
Delete My Account
</h2>
@@ -11,31 +11,22 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
We're sorry to see you go!
</p>
<p>
Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
Please note: Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
</p>
<p>
Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
Once your account is deleted, you cannot use it to take courses on localhost.
</p>
<p>
<span>
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
<a
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
onClick={[Function]}
target="_self"
>
follow the instructions for printing or downloading a certificate
</a>
.
</span>
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
</p>
<p
className="text-danger h6"
>
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on localhost.
</p>
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
onClick={[Function]}
target="_self"
@@ -59,7 +50,7 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
exports[`DeleteAccount should match unverified account section snapshot 1`] = `
<div>
<h2
className="section-heading"
className="section-heading h4 mb-3"
>
Delete My Account
</h2>
@@ -67,31 +58,22 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
We're sorry to see you go!
</p>
<p>
Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
Please note: Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
</p>
<p>
Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
Once your account is deleted, you cannot use it to take courses on localhost.
</p>
<p>
<span>
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
<a
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
onClick={[Function]}
target="_self"
>
follow the instructions for printing or downloading a certificate
</a>
.
</span>
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
</p>
<p
className="text-danger h6"
>
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on localhost.
</p>
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
onClick={[Function]}
target="_self"
@@ -132,17 +114,16 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
</svg>
</div>
<div>
<span>
Before proceeding, please
<a
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
onClick={[Function]}
target="_self"
>
activate your account
</a>
.
</span>
Before proceeding, please
<a
className="pgn__hyperlink default-link standalone-link"
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-confirm-my-email-"
onClick={[Function]}
target="_self"
>
activate your account
</a>
.
</div>
</div>
</div>
@@ -151,7 +132,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
exports[`DeleteAccount should match unverified account section snapshot 2`] = `
<div>
<h2
className="section-heading"
className="section-heading h4 mb-3"
>
Delete My Account
</h2>
@@ -159,31 +140,22 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
We're sorry to see you go!
</p>
<p>
Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
Please note: Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
</p>
<p>
Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
Once your account is deleted, you cannot use it to take courses on localhost.
</p>
<p>
<span>
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
<a
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
onClick={[Function]}
target="_self"
>
follow the instructions for printing or downloading a certificate
</a>
.
</span>
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
</p>
<p
className="text-danger h6"
>
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on localhost.
</p>
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
onClick={[Function]}
target="_self"
@@ -224,17 +196,16 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
</svg>
</div>
<div>
<span>
Before proceeding, please
<a
href="https://support.edx.org/hc/en-us/articles/207206067"
onClick={[Function]}
target="_self"
>
unlink all social media accounts
</a>
.
</span>
Before proceeding, please
<a
className="pgn__hyperlink default-link standalone-link"
href="https://support.edx.org/hc/en-us/articles/207206067"
onClick={[Function]}
target="_self"
>
unlink all social media accounts
</a>
.
</div>
</div>
</div>

View File

@@ -1,563 +1,89 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SuccessModal should match default closed success modal snapshot 1`] = `
Array [
<div
className="fade"
role="presentation"
/>,
<div
className="modal fade"
role="presentation"
>
<div
aria-labelledby="id1"
aria-modal={true}
className=""
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id1"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
]
`;
exports[`SuccessModal should match default closed success modal snapshot 1`] = `null`;
exports[`SuccessModal should match default closed success modal snapshot 2`] = `
Array [
<div
className="fade"
role="presentation"
/>,
<div
className="modal fade"
role="presentation"
>
<div
aria-labelledby="id2"
aria-modal={true}
className=""
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id2"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
]
`;
exports[`SuccessModal should match default closed success modal snapshot 2`] = `null`;
exports[`SuccessModal should match default closed success modal snapshot 3`] = `
Array [
<div
className="fade"
role="presentation"
/>,
<div
className="modal fade"
role="presentation"
>
<div
aria-labelledby="id3"
aria-modal={true}
className=""
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id3"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
]
`;
exports[`SuccessModal should match default closed success modal snapshot 3`] = `null`;
exports[`SuccessModal should match default closed success modal snapshot 4`] = `
Array [
<div
className="fade"
role="presentation"
/>,
<div
className="modal fade"
role="presentation"
>
<div
aria-labelledby="id4"
aria-modal={true}
className=""
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id4"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
]
`;
exports[`SuccessModal should match default closed success modal snapshot 4`] = `null`;
exports[`SuccessModal should match open success modal snapshot 1`] = `
Array [
<div
className="modal-backdrop show"
role="presentation"
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>,
<div
className="modal show d-block"
role="presentation"
className="pgn__modal-layer"
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
aria-labelledby="id5"
aria-modal={true}
className="modal-dialog"
role="dialog"
className="pgn__modal-content-container"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
className="pgn__modal-backdrop"
onClick={[MockFunction]}
onKeyDown={[MockFunction]}
/>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={1}
/>
<div
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
className="mw-sm p-5 bg-white mx-auto my-3"
>
<h3>
We're sorry to see you go! Your account will be deleted shortly.
</h3>
<div
className="modal-content"
className="p-3"
>
<div
className="modal-header"
<p
className="h6"
>
<h2
className="modal-title"
id="id5"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
<p>
<button
className="pgn__modal-close-button float-right btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</p>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>
</div>
</div>,
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>,
]
`;

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './DeleteAccount';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';

View File

@@ -13,22 +13,27 @@ const messages = defineMessages({
},
'account.settings.delete.account.text.1': {
id: 'account.settings.delete.account.text.1',
defaultMessage: 'Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.',
defaultMessage: 'Please note: Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.',
description: 'A message in the user account deletion area',
},
'account.settings.delete.account.text.2': {
id: 'account.settings.delete.account.text.2',
defaultMessage: 'Once your account is deleted, you cannot use it to take courses on {siteName}.',
description: 'A message in the user account deletion area',
},
'account.settings.delete.account.text.2.edX': {
id: 'account.settings.delete.account.text.2.edX',
defaultMessage: 'Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employers or universitys system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.',
description: 'A message in the user account deletion area',
},
'account.settings.delete.account.text.3.link': {
id: 'account.settings.delete.account.text.3.link',
defaultMessage: 'follow the instructions for printing or downloading a certificate',
description: 'This text will be a link to a technical support page; it will go in the phrase If you want to make a copy of these for your records, ______ .',
defaultMessage: 'Follow these instructions for printing or downloading a certificate',
description: 'This text is a link to a technical support page where users can learn how to print or download their certificates.',
},
'account.settings.delete.account.text.warning': {
id: 'account.settings.delete.account.text.warning',
defaultMessage: 'Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.',
defaultMessage: 'Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on {siteName}.',
description: 'A message in the user account deletion area',
},
'account.settings.delete.account.text.change.instead': {
@@ -39,13 +44,18 @@ const messages = defineMessages({
'account.settings.delete.account.button': {
id: 'account.settings.delete.account.button',
defaultMessage: 'Delete My Account',
description: 'Button label to permanently delete your edX account',
description: 'Button label to permanently delete your platform account',
},
'account.settings.delete.account.please.activate': {
id: 'account.settings.delete.account.please.activate',
defaultMessage: 'activate your account',
description: 'This is the text on a link that goes to the support page. It is part of this sentence: Before proceeding, please activate your account.',
},
'account.settings.delete.account.please.confirm': {
id: 'account.settings.delete.account.please.confirm',
defaultMessage: 'confirm your account',
description: 'This is the text on a link that goes to the support page. It is part of this sentence: Before proceeding, please confirm your account.',
},
'account.settings.delete.account.please.unlink': {
id: 'account.settings.delete.account.please.unlink',
defaultMessage: 'unlink all social media accounts',
@@ -58,11 +68,16 @@ const messages = defineMessages({
},
'account.settings.delete.account.modal.text.1': {
id: 'account.settings.delete.account.modal.text.1',
defaultMessage: 'You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.',
defaultMessage: 'You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.',
description: 'Messaging in the dialog asking user to confirm that they want to delete their entire account',
},
'account.settings.delete.account.modal.text.2': {
id: 'account.settings.delete.account.modal.text.2',
defaultMessage: 'If you proceed, you will be unable to use this account to take courses on {siteName}.',
description: 'Messaging in the dialog asking user to confirm that they want to delete their entire account',
},
'account.settings.delete.account.modal.text.2.edX': {
id: 'account.settings.delete.account.modal.text.2.edX',
defaultMessage: 'If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer\'s or university\'s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.',
description: 'Messaging in the dialog asking user to confirm that they want to delete their entire account',
},

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { CheckBox } from '@edx/paragon';
import { Form } from '@edx/paragon';
import { DECLINED } from '../data/constants';
const Checkboxes = (props) => {
@@ -14,7 +14,7 @@ const Checkboxes = (props) => {
const [selected, setSelected] = useState(values);
useEffect(() => {
onChange(id, selected);
}, [selected]);
}, [id, onChange, selected]);
const handleToggle = (value, option) => {
// If the user checked 'declined', uncheck all other options
@@ -40,16 +40,17 @@ const Checkboxes = (props) => {
const isChecked = selected.includes(option.value);
return (
<div key={option.value} className="checkboxOption">
<CheckBox
<Form.Checkbox
type="checkbox"
id={option.value}
name={option.value}
value={option.value}
checked={isChecked}
autoFocus={isFirst}
label={option.label}
onChange={(value) => handleToggle(value, option.value)}
/>
onChange={(event) => handleToggle(event.target.checked, option.value)}
>
{option.label}
</Form.Checkbox>
</div>
);
});

View File

@@ -5,7 +5,7 @@ import {
intlShape,
} from '@edx/frontend-platform/i18n';
import { Hyperlink, Input } from '@edx/paragon';
import { Hyperlink, Form } from '@edx/paragon';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
@@ -13,7 +13,7 @@ import get from 'lodash.get';
import isEmpty from 'lodash.isempty';
import memoize from 'memoize-one';
import { demographicsSectionSelector } from '../data/selectors';
import EditableField from '../EditableField';
import EditableSelectField from '../EditableSelectField';
import Checkboxes from './Checkboxes';
import Alert from '../Alert';
import { saveMultipleSettings, updateDraft } from '../data/actions';
@@ -75,7 +75,7 @@ class DemographicsSection extends React.Component {
const matchingOption = demographicsEthnicityOptions.filter(option => option.value === e)[0];
return matchingOption && matchingOption.label;
}).join(', ');
}
};
handleEditableFieldChange = (name, value) => {
this.props.updateDraft(name, value);
@@ -162,8 +162,8 @@ class DemographicsSection extends React.Component {
const showWorkStatusDescribe = this.props.formValues.demographics_work_status === OTHER;
return (
<div className="account-section" id="demographics-information" ref={this.props.forwardRef}>
<h2 className="section-heading">
<div className="account-section pt-3 mb-5" id="demographics-information" ref={this.props.forwardRef}>
<h2 className="section-heading h4 mb-3">
{this.props.intl.formatMessage(messages['account.settings.section.demographics.information'])}
</h2>
<p>
@@ -172,7 +172,12 @@ class DemographicsSection extends React.Component {
target="_blank"
rel="noopener noreferrer"
>
{this.props.intl.formatMessage(messages['account.settings.section.demographics.why'])}
{this.props.intl.formatMessage(
messages['account.settings.section.demographics.why'],
{
siteName: getConfig().SITE_NAME,
},
)}
</Hyperlink>
</p>
{this.renderDemographicsServiceIssueWarning()}
@@ -183,7 +188,7 @@ class DemographicsSection extends React.Component {
*/}
{this.hasRetrievedDemographicsOptions() && (
<div id="demographics-fields">
<EditableField
<EditableSelectField
name="demographics_gender"
type="select"
value={this.props.formValues.demographics_gender}
@@ -194,7 +199,7 @@ class DemographicsSection extends React.Component {
{...editableFieldProps}
>
{showSelfDescribe && (
<Input
<Form.Control
name="demographics_gender_description"
id="field-demographics_gender_description"
type="text"
@@ -205,8 +210,8 @@ class DemographicsSection extends React.Component {
className="mt-1"
/>
)}
</EditableField>
<EditableField
</EditableSelectField>
<EditableSelectField
name="demographics_user_ethnicity"
type="select"
hidden
@@ -221,8 +226,8 @@ class DemographicsSection extends React.Component {
values={this.props.formValues.demographics_user_ethnicity}
{...editableFieldProps}
/>
</EditableField>
<EditableField
</EditableSelectField>
<EditableSelectField
name="demographics_income"
type="select"
value={this.props.formValues.demographics_income}
@@ -231,7 +236,7 @@ class DemographicsSection extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.income.empty'])}
{...editableFieldProps}
/>
<EditableField
<EditableSelectField
name="demographics_military_history"
type="select"
value={this.props.formValues.demographics_military_history}
@@ -240,7 +245,7 @@ class DemographicsSection extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.military_history.empty'])}
{...editableFieldProps}
/>
<EditableField
<EditableSelectField
name="demographics_learner_education_level"
type="select"
value={this.props.formValues.demographics_learner_education_level}
@@ -249,7 +254,7 @@ class DemographicsSection extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.learner_education_level.empty'])}
{...editableFieldProps}
/>
<EditableField
<EditableSelectField
name="demographics_parent_education_level"
type="select"
value={this.props.formValues.demographics_parent_education_level}
@@ -258,7 +263,7 @@ class DemographicsSection extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.parent_education_level.empty'])}
{...editableFieldProps}
/>
<EditableField
<EditableSelectField
name="demographics_work_status"
type="select"
value={this.props.formValues.demographics_work_status}
@@ -271,7 +276,7 @@ class DemographicsSection extends React.Component {
{...editableFieldProps}
>
{showWorkStatusDescribe && (
<Input
<Form.Control
name="demographics_work_status_description"
id="field-demographics_work_status_description"
type="text"
@@ -282,8 +287,8 @@ class DemographicsSection extends React.Component {
className="mt-1"
/>
)}
</EditableField>
<EditableField
</EditableSelectField>
<EditableSelectField
name="demographics_current_work_sector"
type="select"
value={this.props.formValues.demographics_current_work_sector}
@@ -292,7 +297,7 @@ class DemographicsSection extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.current_work_sector.empty'])}
{...editableFieldProps}
/>
<EditableField
<EditableSelectField
name="demographics_future_work_sector"
type="select"
value={this.props.formValues.demographics_future_work_sector}
@@ -312,7 +317,7 @@ DemographicsSection.propTypes = {
intl: intlShape.isRequired,
formValues: PropTypes.shape({
demographics_gender: PropTypes.string,
demographics_user_ethnicity: PropTypes.array,
demographics_user_ethnicity: PropTypes.shape([]),
demographics_income: PropTypes.string,
demographics_military_history: PropTypes.string,
demographics_learner_education_level: PropTypes.string,
@@ -322,11 +327,11 @@ DemographicsSection.propTypes = {
demographics_future_work_sector: PropTypes.string,
demographics_work_status_description: PropTypes.string,
demographics_gender_description: PropTypes.string,
demographicsOptions: PropTypes.object,
demographicsOptions: PropTypes.shape({}),
}).isRequired,
drafts: PropTypes.shape({
demographics_gender: PropTypes.string,
demographics_user_ethnicity: PropTypes.array,
demographics_user_ethnicity: PropTypes.shape([]),
demographics_income: PropTypes.string,
demographics_military_history: PropTypes.string,
demographics_learner_education_level: PropTypes.string,
@@ -336,7 +341,7 @@ DemographicsSection.propTypes = {
demographics_future_work_sector: PropTypes.string,
demographics_work_status_description: PropTypes.string,
demographics_gender_description: PropTypes.string,
demographicsOptions: PropTypes.object,
demographicsOptions: PropTypes.shape({}),
}).isRequired,
formErrors: PropTypes.shape({
demographicsError: PropTypes.string,

View File

@@ -162,7 +162,7 @@ const messages = defineMessages({
/* Legal copy link text */
'account.settings.section.demographics.why': {
id: 'account.settings.section.demographics.why',
defaultMessage: 'Why does edX collect this information?',
defaultMessage: 'Why does {siteName} collect this information?',
description: 'Link text for a link to external legal text',
},
});

View File

@@ -1,3 +1,4 @@
/* eslint-disable no-import-assign */
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';

View File

@@ -2,30 +2,57 @@
exports[`DemographicsSection should render 1`] = `
<div
className="account-section"
className="account-section pt-3 mb-5"
id="demographics-information"
>
<h2
className="section-heading"
className="section-heading h4 mb-3"
>
Optional Information
</h2>
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
rel="noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
<span>
Why does localhost collect this information?
<span
className="pgn__hyperlink__external-icon"
title="Opens in a new tab"
>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
fill="none"
focusable={false}
height={24}
role="img"
viewBox="0 0 24 24"
width={24}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
<span
className="sr-only"
>
in a new tab
</span>
</span>
</span>
</a>
</p>
@@ -85,6 +112,7 @@ exports[`DemographicsSection should render 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -148,6 +176,7 @@ exports[`DemographicsSection should render 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
<button
@@ -218,6 +247,7 @@ exports[`DemographicsSection should render 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -281,6 +311,7 @@ exports[`DemographicsSection should render 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -344,6 +375,7 @@ exports[`DemographicsSection should render 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -407,6 +439,7 @@ exports[`DemographicsSection should render 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -470,6 +503,7 @@ exports[`DemographicsSection should render 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -533,6 +567,7 @@ exports[`DemographicsSection should render 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -596,6 +631,7 @@ exports[`DemographicsSection should render 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -612,30 +648,57 @@ exports[`DemographicsSection should render 1`] = `
exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
<div
className="account-section"
className="account-section pt-3 mb-5"
id="demographics-information"
>
<h2
className="section-heading"
className="section-heading h4 mb-3"
>
Optional Information
</h2>
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
rel="noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
<span>
Why does localhost collect this information?
<span
className="pgn__hyperlink__external-icon"
title="Opens in a new tab"
>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
fill="none"
focusable={false}
height={24}
role="img"
viewBox="0 0 24 24"
width={24}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
<span
className="sr-only"
>
in a new tab
</span>
</span>
</span>
</a>
</p>
@@ -647,9 +710,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
>
<div />
<div>
<span>
An error occurred attempting to retrieve or save your account information. Please try again later.
</span>
An error occurred attempting to retrieve or save your account information. Please try again later.
</div>
</div>
</div>
@@ -709,6 +770,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -772,6 +834,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
<button
@@ -842,6 +905,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -905,6 +969,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -968,6 +1033,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1031,6 +1097,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1094,6 +1161,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1157,6 +1225,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1220,6 +1289,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1236,30 +1306,57 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
exports[`DemographicsSection should render an Alert when demographicsOptions props are empty 1`] = `
<div
className="account-section"
className="account-section pt-3 mb-5"
id="demographics-information"
>
<h2
className="section-heading"
className="section-heading h4 mb-3"
>
Optional Information
</h2>
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
rel="noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
<span>
Why does localhost collect this information?
<span
className="pgn__hyperlink__external-icon"
title="Opens in a new tab"
>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
fill="none"
focusable={false}
height={24}
role="img"
viewBox="0 0 24 24"
width={24}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
<span
className="sr-only"
>
in a new tab
</span>
</span>
</span>
</a>
</p>
@@ -1271,9 +1368,7 @@ exports[`DemographicsSection should render an Alert when demographicsOptions pro
>
<div />
<div>
<span>
An error occurred attempting to retrieve or save your account information. Please try again later.
</span>
An error occurred attempting to retrieve or save your account information. Please try again later.
</div>
</div>
</div>
@@ -1282,30 +1377,57 @@ exports[`DemographicsSection should render an Alert when demographicsOptions pro
exports[`DemographicsSection should render ethnicity correctly when multiple options are selected 1`] = `
<div
className="account-section"
className="account-section pt-3 mb-5"
id="demographics-information"
>
<h2
className="section-heading"
className="section-heading h4 mb-3"
>
Optional Information
</h2>
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
rel="noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
<span>
Why does localhost collect this information?
<span
className="pgn__hyperlink__external-icon"
title="Opens in a new tab"
>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
fill="none"
focusable={false}
height={24}
role="img"
viewBox="0 0 24 24"
width={24}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
<span
className="sr-only"
>
in a new tab
</span>
</span>
</span>
</a>
</p>
@@ -1365,6 +1487,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1428,6 +1551,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Hispanic, Latin, or Spanish origin, White
@@ -1491,6 +1615,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1554,6 +1679,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1617,6 +1743,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1680,6 +1807,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1743,6 +1871,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1806,6 +1935,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1869,6 +1999,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -1885,30 +2016,57 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
exports[`DemographicsSection should render ethnicity text correctly 1`] = `
<div
className="account-section"
className="account-section pt-3 mb-5"
id="demographics-information"
>
<h2
className="section-heading"
className="section-heading h4 mb-3"
>
Optional Information
</h2>
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
rel="noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
<span>
Why does localhost collect this information?
<span
className="pgn__hyperlink__external-icon"
title="Opens in a new tab"
>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
fill="none"
focusable={false}
height={24}
role="img"
viewBox="0 0 24 24"
width={24}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
<span
className="sr-only"
>
in a new tab
</span>
</span>
</span>
</a>
</p>
@@ -1968,6 +2126,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2031,6 +2190,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Asian
@@ -2094,6 +2254,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2157,6 +2318,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2220,6 +2382,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2283,6 +2446,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2346,6 +2510,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2409,6 +2574,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2472,6 +2638,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2488,30 +2655,57 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
exports[`DemographicsSection should set user input correctly when user provides answers to work_status question 1`] = `
<div
className="account-section"
className="account-section pt-3 mb-5"
id="demographics-information"
>
<h2
className="section-heading"
className="section-heading h4 mb-3"
>
Optional Information
</h2>
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
rel="noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
<span>
Why does localhost collect this information?
<span
className="pgn__hyperlink__external-icon"
title="Opens in a new tab"
>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
fill="none"
focusable={false}
height={24}
role="img"
viewBox="0 0 24 24"
width={24}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
<span
className="sr-only"
>
in a new tab
</span>
</span>
</span>
</a>
</p>
@@ -2571,6 +2765,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2634,6 +2829,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
<button
@@ -2704,6 +2900,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2767,6 +2964,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2830,6 +3028,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2893,6 +3092,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -2956,6 +3156,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Other: test
@@ -3019,6 +3220,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -3082,6 +3284,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -3098,30 +3301,57 @@ exports[`DemographicsSection should set user input correctly when user provides
exports[`DemographicsSection should set user input correctly when user provides gender self-description 1`] = `
<div
className="account-section"
className="account-section pt-3 mb-5"
id="demographics-information"
>
<h2
className="section-heading"
className="section-heading h4 mb-3"
>
Optional Information
</h2>
<p>
<a
className="pgn__hyperlink default-link standalone-link"
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
rel="noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
<span>
Why does localhost collect this information?
<span
className="pgn__hyperlink__external-icon"
title="Opens in a new tab"
>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
className="pgn__icon"
style={
Object {
"height": "1em",
"width": "1em",
}
}
>
<svg
aria-hidden={true}
fill="none"
focusable={false}
height={24}
role="img"
viewBox="0 0 24 24"
width={24}
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 19H5V5h7V3H3v18h18v-9h-2v7ZM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7Z"
fill="currentColor"
/>
</svg>
<span
className="sr-only"
>
in a new tab
</span>
</span>
</span>
</a>
</p>
@@ -3181,6 +3411,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer to self describe: test
@@ -3244,6 +3475,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
<button
@@ -3314,6 +3546,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -3377,6 +3610,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -3440,6 +3674,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -3503,6 +3738,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -3566,6 +3802,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -3629,6 +3866,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond
@@ -3692,6 +3930,7 @@ exports[`DemographicsSection should set user input correctly when user provides
</button>
</div>
<p
className={null}
data-hj-suppress={true}
>
Prefer not to respond

View File

@@ -0,0 +1,19 @@
import React from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
export const withNavigate = Component => {
const WrappedComponent = props => {
const navigate = useNavigate();
return <Component {...props} navigate={navigate} />;
};
return WrappedComponent;
};
export const withLocation = Component => {
const WrappedComponent = props => {
const location = useLocation();
return <Component {...props} location={location.pathname} />;
};
return WrappedComponent;
};

View File

@@ -0,0 +1,38 @@
import React from 'react';
import { mount } from 'enzyme';
import { withLocation, withNavigate } from './hoc';
const mockedNavigator = jest.fn();
jest.mock('react-router-dom', () => ({
useNavigate: () => mockedNavigator,
useLocation: () => ({
pathname: '/current-location',
}),
}));
// eslint-disable-next-line react/prop-types
const MockComponent = ({ navigate, location }) => (
// eslint-disable-next-line react/button-has-type, react/prop-types
<button id="btn" onClick={() => navigate('/some-route')}>{location}</button>
);
const WrappedComponent = withNavigate(withLocation(MockComponent));
test('Provide Navigation to Component', () => {
const wrapper = mount(
<WrappedComponent />,
);
const btn = wrapper.find('#btn');
btn.simulate('click');
expect(mockedNavigator).toHaveBeenCalledWith('/some-route');
});
test('Provide Location Pathname to Component', () => {
const wrapper = mount(
<WrappedComponent />,
);
expect(wrapper.find('#btn').text()).toContain('/current-location');
});

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './AccountSettingsPage';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';

View File

@@ -0,0 +1,203 @@
import React, { useCallback, useEffect, useState } from 'react';
import { connect, useDispatch } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Alert,
Button,
Col,
Form,
ModalDialog,
StatefulButton,
} from '@edx/paragon';
import { closeForm, saveSettingsReset } from '../data/actions';
import { nameChangeSelector } from '../data/selectors';
import { requestNameChange, requestNameChangeFailure, requestNameChangeReset } from './data/actions';
import messages from './messages';
const NameChangeModal = ({
targetFormId,
errors,
formValues,
intl,
saveState,
}) => {
const dispatch = useDispatch();
const navigate = useNavigate();
const { username } = getAuthenticatedUser();
const [verifiedNameInput, setVerifiedNameInput] = useState(formValues.verified_name || '');
const [confirmedWarning, setConfirmedWarning] = useState(false);
const resetLocalState = useCallback(() => {
setConfirmedWarning(false);
dispatch(requestNameChangeReset());
}, [dispatch]);
const handleChange = (e) => {
setVerifiedNameInput(e.target.value);
};
const handleClose = useCallback(() => {
resetLocalState();
dispatch(closeForm(targetFormId));
dispatch(saveSettingsReset());
}, [dispatch, resetLocalState, targetFormId]);
const handleSubmit = (e) => {
e.preventDefault();
if (saveState === 'pending') {
return;
}
if (!verifiedNameInput) {
dispatch(requestNameChangeFailure({
verified_name: intl.formatMessage(messages['account.settings.name.change.error.valid.name']),
}));
} else {
const draftProfileName = targetFormId === 'name' ? formValues.name : null;
dispatch(requestNameChange(username, draftProfileName, verifiedNameInput));
}
};
useEffect(() => {
if (saveState === 'complete') {
handleClose();
navigate(`/id-verification?next=${encodeURIComponent('account/settings')}`);
}
}, [handleClose, navigate, saveState]);
function renderErrors() {
if (Object.keys(errors).length > 0) {
return (
<>
{Object.entries(errors).map(([key, value]) => (
<Form.Control.Feedback type="invalid" key={key}>
{
key === 'general_error'
? intl.formatMessage(messages['account.settings.name.change.error.general'])
: value
}
</Form.Control.Feedback>
))}
</>
);
}
return null;
}
function renderTitle() {
if (!confirmedWarning) {
return intl.formatMessage(messages['account.settings.name.change.title.id']);
}
return intl.formatMessage(messages['account.settings.name.change.title.begin']);
}
function renderBody() {
if (!confirmedWarning) {
return (
<Alert variant="warning">
<p>
{intl.formatMessage(messages['account.settings.name.change.warning.one'])}
</p>
<p>
{intl.formatMessage(messages['account.settings.name.change.warning.two'])}
</p>
</Alert>
);
}
return (
<Form.Group as={Col} isInvalid={Object.keys(errors).length > 0}>
<Form.Label>
{intl.formatMessage(messages['account.settings.name.change.id.name.label'])}
</Form.Label>
<Form.Control
type="text"
name="verifiedName"
placeholder={intl.formatMessage(messages['account.settings.name.change.id.name.placeholder'])}
value={verifiedNameInput}
onChange={handleChange}
/>
{renderErrors()}
</Form.Group>
);
}
function renderContinueButton() {
if (!confirmedWarning) {
return (
<Button variant="primary" onClick={() => setConfirmedWarning(true)}>
{intl.formatMessage(messages['account.settings.name.change.continue'])}
</Button>
);
}
return (
<StatefulButton
type="submit"
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.name.change.continue']),
}}
disabledStates={[]}
/>
);
}
return (
<ModalDialog
title={renderTitle()}
isOpen
hasCloseButton={false}
onClose={handleClose}
>
<Form onSubmit={handleSubmit}>
<ModalDialog.Header>
<ModalDialog.Title>
{renderTitle()}
</ModalDialog.Title>
</ModalDialog.Header>
<ModalDialog.Body className="mb-3 overflow-hidden">
{renderBody()}
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton variant="tertiary">
{intl.formatMessage(messages['account.settings.name.change.cancel'])}
</ModalDialog.CloseButton>
{renderContinueButton()}
</ActionRow>
</ModalDialog.Footer>
</Form>
</ModalDialog>
);
};
NameChangeModal.propTypes = {
targetFormId: PropTypes.string.isRequired,
errors: PropTypes.shape({}).isRequired,
formValues: PropTypes.shape({
name: PropTypes.string,
verified_name: PropTypes.string,
}).isRequired,
saveState: PropTypes.string,
intl: intlShape.isRequired,
};
NameChangeModal.defaultProps = {
saveState: null,
};
export default connect(nameChangeSelector)(injectIntl(NameChangeModal));

View File

@@ -0,0 +1,25 @@
import { AsyncActionType } from '../../data/utils';
export const REQUEST_NAME_CHANGE = new AsyncActionType('ACCOUNT_SETTINGS', 'REQUEST_NAME_CHANGE');
export const requestNameChange = (username, profileName, verifiedName) => ({
type: REQUEST_NAME_CHANGE.BASE,
payload: { username, profileName, verifiedName },
});
export const requestNameChangeBegin = () => ({
type: REQUEST_NAME_CHANGE.BEGIN,
});
export const requestNameChangeSuccess = () => ({
type: REQUEST_NAME_CHANGE.SUCCESS,
});
export const requestNameChangeFailure = errors => ({
type: REQUEST_NAME_CHANGE.FAILURE,
payload: { errors },
});
export const requestNameChangeReset = () => ({
type: REQUEST_NAME_CHANGE.RESET,
});

View File

@@ -0,0 +1,44 @@
import { REQUEST_NAME_CHANGE } from './actions';
export const defaultState = {
saveState: null,
errors: {},
};
const reducer = (state = defaultState, action = null) => {
if (action !== null) {
switch (action.type) {
case REQUEST_NAME_CHANGE.BEGIN:
return {
...state,
saveState: 'pending',
errors: {},
};
case REQUEST_NAME_CHANGE.SUCCESS:
return {
...state,
saveState: 'complete',
};
case REQUEST_NAME_CHANGE.FAILURE:
return {
...state,
saveState: 'error',
errors: action.payload.errors || { general_error: 'A technical error occurred. Please try again.' },
};
case REQUEST_NAME_CHANGE.RESET:
return {
...state,
saveState: null,
errors: {},
};
default:
}
}
return state;
};
export default reducer;

View File

@@ -0,0 +1,40 @@
import { put, call, takeEvery } from 'redux-saga/effects';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { postVerifiedName } from '../../data/service';
import {
REQUEST_NAME_CHANGE,
requestNameChangeBegin,
requestNameChangeSuccess,
requestNameChangeFailure,
} from './actions';
import { postNameChange } from './service';
export function* handleRequestNameChange(action) {
let { name: profileName } = getAuthenticatedUser();
try {
yield put(requestNameChangeBegin());
if (action.payload.profileName) {
yield call(postNameChange, action.payload.profileName);
profileName = action.payload.profileName;
}
yield call(postVerifiedName, {
username: action.payload.username,
verified_name: action.payload.verifiedName,
profile_name: profileName,
});
yield put(requestNameChangeSuccess());
} catch (err) {
if (err.customAttributes?.httpErrorResponseData) {
yield put(requestNameChangeFailure(JSON.parse(err.customAttributes.httpErrorResponseData)));
} else {
yield put(requestNameChangeFailure());
}
}
}
export default function* saga() {
yield takeEvery(REQUEST_NAME_CHANGE.BASE, handleRequestNameChange);
}

View File

@@ -0,0 +1,17 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { handleRequestError } from '../../data/utils';
// eslint-disable-next-line import/prefer-default-export
export async function postNameChange(name) {
// Requests a pending name change, rather than saving the account name immediately
const requestConfig = { headers: { Accept: 'application/json' } };
const requestUrl = `${getConfig().LMS_BASE_URL}/api/user/v1/accounts/name_change/`;
const { data } = await getAuthenticatedHttpClient()
.post(requestUrl, { name }, requestConfig)
.catch(error => handleRequestError(error));
return data;
}

View File

@@ -0,0 +1,5 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './NameChange';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { REQUEST_NAME_CHANGE } from './data/actions';

View File

@@ -0,0 +1,56 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'account.settings.name.change.title.id': {
id: 'account.settings.name.change.title.id',
defaultMessage: 'This name change requires identity verification',
description: 'Inform the user that changing their name requires identity verification',
},
'account.settings.name.change.title.begin': {
id: 'account.settings.name.change.title.begin',
defaultMessage: 'Before we begin',
description: 'Title before beginning the ID verification process',
},
'account.settings.name.change.warning.one': {
id: 'account.settings.name.change.warning.one',
defaultMessage: 'Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.',
description: 'Warning informing the user that a name change will update the name on all of their certificates.',
},
'account.settings.name.change.warning.two': {
id: 'account.settings.name.change.warning.two',
defaultMessage: 'This action cannot be undone without verifying your identity.',
description: 'Warning informing the user that a name change cannot be undone without ID verification.',
},
'account.settings.name.change.id.name.label': {
id: 'account.settings.name.change.id.name.label',
defaultMessage: 'Enter your name as it appears on your unexpired student, work, or government-issued identification card.',
description: 'Form label instructing the user to enter the name on their ID.',
},
'account.settings.name.change.id.name.placeholder': {
id: 'account.settings.name.change.id.name.placeholder',
defaultMessage: 'Enter the name on your photo ID',
description: 'Form label instructing the user to enter the name on their ID.',
},
'account.settings.name.change.error.valid.name': {
id: 'account.settings.name.change.error.valid.name',
defaultMessage: 'Please enter a valid name.',
description: 'Error that appears when the user doesnt enter a valid name.',
},
'account.settings.name.change.error.general': {
id: 'account.settings.name.change.error.general',
defaultMessage: 'A technical error occurred. Please try again.',
description: 'Generic error message.',
},
'account.settings.name.change.continue': {
id: 'account.settings.name.change.continue',
defaultMessage: 'Continue',
description: 'Continue button.',
},
'account.settings.name.change.cancel': {
id: 'account.settings.name.change.cancel',
defaultMessage: 'Cancel',
description: 'Cancel button.',
},
});
export default messages;

View File

@@ -0,0 +1,170 @@
/* eslint-disable no-import-assign */
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
fireEvent,
render,
screen,
} from '@testing-library/react';
import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
ReactDOM.createPortal = node => node;
import NameChange from '../NameChange'; // eslint-disable-line import/first
const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}));
jest.mock('@edx/frontend-platform/auth');
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ nameChangeSelector: () => ({}) })));
const IntlNameChange = injectIntl(NameChange);
const mockStore = configureStore();
describe('NameChange', () => {
let props = {};
let store = {};
const reduxWrapper = children => (
<Router>
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
</Router>
);
beforeEach(() => {
store = mockStore();
props = {
targetFormId: 'test_form',
errors: {},
formValues: {
name: 'edx edx',
verified_name: 'edX Verified',
},
saveState: null,
intl: {},
};
auth.getAuthenticatedHttpClient = jest.fn(() => ({
patch: async () => ({
data: { status: 200 },
catch: () => {},
}),
}));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'edx' }));
});
afterEach(() => jest.clearAllMocks());
it('renders populated input after clicking continue if verified_name in form data', async () => {
const getInput = () => screen.queryByPlaceholderText('Enter the name on your photo ID');
render(reduxWrapper(<IntlNameChange {...props} />));
expect(getInput()).toBeNull();
const continueButton = screen.getByText('Continue');
fireEvent.click(continueButton);
expect(getInput().value).toBe('edX Verified');
});
it('renders empty input after clicking continue if verified_name not in form data', async () => {
const getInput = () => screen.queryByPlaceholderText('Enter the name on your photo ID');
const formProps = {
...props,
formValues: {
name: 'edx edx',
},
};
render(reduxWrapper(<IntlNameChange {...formProps} />));
const continueButton = screen.getByText('Continue');
fireEvent.click(continueButton);
expect(getInput().value).toBe('');
});
it('dispatches verifiedName on submit if targetForm is not "name"', async () => {
const dispatchData = {
payload: {
profileName: null,
username: 'edx',
verifiedName: 'Verified Name',
},
type: 'ACCOUNT_SETTINGS__REQUEST_NAME_CHANGE',
};
render(reduxWrapper(<IntlNameChange {...props} />));
const continueButton = screen.getByText('Continue');
fireEvent.click(continueButton);
const input = screen.getByPlaceholderText('Enter the name on your photo ID');
fireEvent.change(input, { target: { value: 'Verified Name' } });
const submitButton = screen.getByText('Continue');
fireEvent.click(submitButton);
expect(mockDispatch).toHaveBeenCalledWith(dispatchData);
});
it('dispatches both profileName and verifiedName on submit if the targetForm is "name"', async () => {
const dispatchData = {
payload: {
profileName: 'edx edx',
username: 'edx',
verifiedName: 'Verified Name',
},
type: 'ACCOUNT_SETTINGS__REQUEST_NAME_CHANGE',
};
const formProps = {
...props,
targetFormId: 'name',
};
render(reduxWrapper(<IntlNameChange {...formProps} />));
const continueButton = screen.getByText('Continue');
fireEvent.click(continueButton);
const input = screen.getByPlaceholderText('Enter the name on your photo ID');
fireEvent.change(input, { target: { value: 'Verified Name' } });
const submitButton = screen.getByText('Continue');
fireEvent.click(submitButton);
expect(mockDispatch).toHaveBeenCalledWith(dispatchData);
});
it('does not dispatch action while pending', async () => {
props.saveState = 'pending';
render(reduxWrapper(<IntlNameChange {...props} />));
const continueButton = screen.getByText('Continue');
fireEvent.click(continueButton);
const input = screen.getByPlaceholderText('Enter the name on your photo ID');
fireEvent.change(input, { target: { value: 'Verified Name' } });
const submitButton = screen.getByText('Continue');
fireEvent.click(submitButton);
expect(mockDispatch).not.toHaveBeenCalled();
});
it('routes to IDV when name change request is successful', async () => {
props.saveState = 'complete';
render(reduxWrapper(<IntlNameChange {...props} />));
expect(window.location.pathname).toEqual('/id-verification');
});
});

View File

@@ -1,5 +1,6 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -12,7 +13,7 @@ const ConfirmationAlert = (props) => {
const technicalSupportLink = (
<Hyperlink
destination="https://support.edx.org/hc/en-us/articles/206212088-What-if-I-did-not-receive-a-password-reset-message-"
destination={getConfig().PASSWORD_RESET_SUPPORT_LINK}
>
<FormattedMessage
id="account.settings.editable.field.password.reset.button.confirmation.support.link"

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './ResetPassword';
export { default as reducer } from './data/reducers';
export { RESET_PASSWORD } from './data/actions';

View File

@@ -2,8 +2,9 @@ import { AsyncActionType } from '../data/utils';
export const FETCH_SITE_LANGUAGES = new AsyncActionType('SITE_LANGUAGE', 'FETCH_SITE_LANGUAGES');
export const fetchSiteLanguages = () => ({
export const fetchSiteLanguages = handleNavigation => ({
type: FETCH_SITE_LANGUAGES.BASE,
payload: { handleNavigation },
});
export const fetchSiteLanguagesBegin = () => ({

View File

@@ -19,6 +19,11 @@ const siteLanguageList = [
name: 'Español (Latinoamérica)',
released: true,
},
{
code: 'fa-ir',
name: 'فارسی',
released: true,
},
{
code: 'fr',
name: 'Français',
@@ -69,6 +74,31 @@ const siteLanguageList = [
name: '中文 (简体)',
released: true,
},
{
code: 'pt',
name: 'Português',
released: true,
},
{
code: 'it',
name: 'Italian',
released: true,
},
{
code: 'de',
name: 'German',
released: true,
},
{
code: 'hi',
name: 'Hindi',
released: true,
},
{
code: 'fr-ca',
name: 'French (CA)',
released: true,
},
];
export default siteLanguageList;

View File

@@ -10,13 +10,13 @@ import {
import { getSiteLanguageList } from './service';
import { handleFailure } from '../data/utils';
function* handleFetchSiteLanguages() {
function* handleFetchSiteLanguages(action) {
try {
yield put(fetchSiteLanguagesBegin());
const siteLanguageList = yield call(getSiteLanguageList);
yield put(fetchSiteLanguagesSuccess(siteLanguageList));
} catch (e) {
yield call(handleFailure, e, fetchSiteLanguagesFailure);
yield call(handleFailure, e, action.payload.handleNavigation, fetchSiteLanguagesFailure);
}
}

View File

@@ -1,45 +1,65 @@
import React from 'react';
import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { initializeMockApp, mergeConfig, setConfig } from '@edx/frontend-platform';
import { BrowserRouter as Router } from 'react-router-dom';
import { mergeConfig, setConfig } from '@edx/frontend-platform';
import JumpNav from '../JumpNav';
import configureStore from '../../data/configureStore';
const IntlJumpNav = injectIntl(JumpNav);
describe('JumpNav', () => {
mergeConfig({
ENABLE_DEMOGRAPHICS_COLLECTION: false,
ENABLE_ACCOUNT_DELETION: true,
});
let props = {};
let store;
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
props = {
intl: {},
displayDemographicsLink: false,
};
store = configureStore({
notificationPreferences: {
showPreferences: false,
},
});
});
it('should not render Optional Information link', () => {
it('should not render Optional Information or delete account link', () => {
setConfig({
ENABLE_ACCOUNT_DELETION: false,
});
const tree = renderer.create((
// Had to wrap the following in a router or I will receive an error stating:
// "Invariant failed: You should not use <NavLink> outside a <Router>"
<Router>
<IntlProvider locale="en">
<IntlProvider locale="en">
<AppProvider store={store}>
<IntlJumpNav {...props} />
</IntlProvider>
</Router>
</AppProvider>
</IntlProvider>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('should render Optional Information link', () => {
it('should render Optional Information and delete account link', () => {
setConfig({
ENABLE_DEMOGRAPHICS_COLLECTION: true,
ENABLE_ACCOUNT_DELETION: true,
});
props = {
@@ -48,12 +68,11 @@ describe('JumpNav', () => {
};
const tree = renderer.create((
// Same as previous test
<Router>
<IntlProvider locale="en">
<IntlProvider locale="en">
<AppProvider store={store}>
<IntlJumpNav {...props} />
</IntlProvider>
</Router>
</AppProvider>
</IntlProvider>
))
.toJSON();

View File

@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`JumpNav should not render Optional Information link 1`] = `
exports[`JumpNav should not render Optional Information or delete account link 1`] = `
<div
className="jump-nav"
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
>
<ul
className="list-unstyled"
@@ -15,8 +15,8 @@ exports[`JumpNav should not render Optional Information link 1`] = `
aria-current="page"
className="active"
href="/#basic-information"
isActive={[Function]}
onClick={[Function]}
style={Object {}}
>
Account Information
</a>
@@ -28,8 +28,8 @@ exports[`JumpNav should not render Optional Information link 1`] = `
aria-current="page"
className="active"
href="/#profile-information"
isActive={[Function]}
onClick={[Function]}
style={Object {}}
>
Profile Information
</a>
@@ -41,8 +41,8 @@ exports[`JumpNav should not render Optional Information link 1`] = `
aria-current="page"
className="active"
href="/#social-media"
isActive={[Function]}
onClick={[Function]}
style={Object {}}
>
Social Media Links
</a>
@@ -54,8 +54,8 @@ exports[`JumpNav should not render Optional Information link 1`] = `
aria-current="page"
className="active"
href="/#site-preferences"
isActive={[Function]}
onClick={[Function]}
style={Object {}}
>
Site Preferences
</a>
@@ -67,32 +67,19 @@ exports[`JumpNav should not render Optional Information link 1`] = `
aria-current="page"
className="active"
href="/#linked-accounts"
isActive={[Function]}
onClick={[Function]}
style={Object {}}
>
Linked Accounts
</a>
</li>
<li
className=""
>
<a
aria-current="page"
className="active"
href="/#delete-account"
onClick={[Function]}
style={Object {}}
>
Delete My Account
</a>
</li>
</ul>
</div>
`;
exports[`JumpNav should render Optional Information link 1`] = `
exports[`JumpNav should render Optional Information and delete account link 1`] = `
<div
className="jump-nav"
className="jump-nav px-2.25 jump-nav-sm position-sticky pt-3"
>
<ul
className="list-unstyled"
@@ -105,8 +92,8 @@ exports[`JumpNav should render Optional Information link 1`] = `
aria-current="page"
className="active"
href="/#basic-information"
isActive={[Function]}
onClick={[Function]}
style={Object {}}
>
Account Information
</a>
@@ -118,8 +105,8 @@ exports[`JumpNav should render Optional Information link 1`] = `
aria-current="page"
className="active"
href="/#profile-information"
isActive={[Function]}
onClick={[Function]}
style={Object {}}
>
Profile Information
</a>
@@ -131,8 +118,8 @@ exports[`JumpNav should render Optional Information link 1`] = `
aria-current="page"
className="active"
href="/#demographics-information"
isActive={[Function]}
onClick={[Function]}
style={Object {}}
>
Optional Information
</a>
@@ -144,8 +131,8 @@ exports[`JumpNav should render Optional Information link 1`] = `
aria-current="page"
className="active"
href="/#social-media"
isActive={[Function]}
onClick={[Function]}
style={Object {}}
>
Social Media Links
</a>
@@ -157,8 +144,8 @@ exports[`JumpNav should render Optional Information link 1`] = `
aria-current="page"
className="active"
href="/#site-preferences"
isActive={[Function]}
onClick={[Function]}
style={Object {}}
>
Site Preferences
</a>
@@ -170,8 +157,8 @@ exports[`JumpNav should render Optional Information link 1`] = `
aria-current="page"
className="active"
href="/#linked-accounts"
isActive={[Function]}
onClick={[Function]}
style={Object {}}
>
Linked Accounts
</a>
@@ -183,8 +170,8 @@ exports[`JumpNav should render Optional Information link 1`] = `
aria-current="page"
className="active"
href="/#delete-account"
isActive={[Function]}
onClick={[Function]}
style={Object {}}
>
Delete My Account
</a>

View File

@@ -16,7 +16,7 @@ class ThirdPartyAuth extends Component {
}
const disconnectUrl = e.currentTarget.getAttribute('data-disconnect-url');
this.props.disconnectAuth(disconnectUrl, providerId);
}
};
renderUnconnectedProvider(url, name) {
return (
@@ -100,7 +100,7 @@ class ThirdPartyAuth extends Component {
<FormattedMessage
id="account.settings.sso.no.providers"
defaultMessage="No accounts can be linked at this time."
description="Displayed when no third party accounts are available to link an edX account to"
description="Displayed when no third-party accounts are available for the user to link to their account on the platform."
/>
);
}

View File

@@ -1,3 +1,4 @@
// eslint-disable-next-line no-restricted-exports
export { default } from './ThirdPartyAuth';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

4
src/constants.js Normal file
View File

@@ -0,0 +1,4 @@
export const IDLE_STATUS = 'idle';
export const LOADING_STATUS = 'loading';
export const SUCCESS_STATUS = 'success';
export const FAILURE_STATUS = 'failure';

View File

@@ -4,8 +4,10 @@ import {
reducer as accountSettingsReducer,
storeName as accountSettingsStoreName,
} from '../account-settings';
import notificationPreferencesReducer from '../notification-preferences/data/reducers';
const createRootReducer = () => combineReducers({
[accountSettingsStoreName]: accountSettingsReducer,
notificationPreferences: notificationPreferencesReducer,
});
export default createRootReducer;

21
src/head/Head.jsx Normal file
View File

@@ -0,0 +1,21 @@
import React from 'react';
import { Helmet } from 'react-helmet';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import messages from './messages';
const Head = ({ intl }) => (
<Helmet>
<title>
{intl.formatMessage(messages['account.page.title'], { siteName: getConfig().SITE_NAME })}
</title>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
);
Head.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(Head);

17
src/head/Head.test.jsx Normal file
View File

@@ -0,0 +1,17 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet';
import { mount } from 'enzyme';
import { getConfig } from '@edx/frontend-platform';
import Head from './Head';
describe('Head', () => {
const props = {};
it('should match render title tag and fivicon with the site configuration values', () => {
mount(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
const helmet = Helmet.peek();
expect(helmet.title).toEqual(`Account | ${getConfig().SITE_NAME}`);
expect(helmet.linkTags[0].rel).toEqual('shortcut icon');
expect(helmet.linkTags[0].href).toEqual(getConfig().FAVICON_URL);
});
});

11
src/head/messages.js Normal file
View File

@@ -0,0 +1,11 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'account.page.title': {
id: 'account.page.title',
defaultMessage: 'Account | {siteName}',
description: 'Title tag',
},
});
export default messages;

68
src/hooks.js Normal file
View File

@@ -0,0 +1,68 @@
import { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { logError } from '@edx/frontend-platform/logging';
import {
IDLE_STATUS, LOADING_STATUS, SUCCESS_STATUS, FAILURE_STATUS,
} from './constants';
// eslint-disable-next-line import/prefer-default-export
export function useAsyncCall(asyncFunc) {
// React doesn't batch setStates call in async useEffect hooks,
// so we use a combined object here to ensure that users
// re-render once.
const [data, setData] = useState({ status: IDLE_STATUS });
useEffect(
() => {
(async () => {
setData(currData => ({ ...currData, status: LOADING_STATUS }));
const response = await asyncFunc();
if (Object.keys(response).length === 0) {
setData(currData => ({ ...currData, status: FAILURE_STATUS, data: response }));
} else {
setData(currData => ({ ...currData, status: SUCCESS_STATUS, data: response }));
}
})();
},
[asyncFunc],
);
return data;
}
// Redirect the user to their original location based on session storage
export function useRedirect() {
const [redirect, setRedirect] = useState({
location: 'dashboard',
text: 'id.verification.return.dashboard',
});
useEffect(() => {
if (sessionStorage.getItem('courseId')) {
setRedirect({
location: `courses/${sessionStorage.getItem('courseId')}`,
text: 'id.verification.return.course',
});
} else if (sessionStorage.getItem('next')) {
setRedirect({
location: sessionStorage.getItem('next'),
text: 'id.verification.return.generic',
});
}
}, []);
return redirect;
}
export function useFeedbackWrapper() {
useEffect(() => {
try {
// eslint-disable-next-line no-undef
window.usabilla_live = lightningjs?.require('usabilla_live', getConfig().LEARNER_FEEDBACK_URL);
} catch (error) {
logError('Error loading usabilla_live', error);
}
}, []);
}

View File

@@ -1,32 +1,44 @@
import { messages as headerMessages } from '@edx/frontend-component-header';
import { messages as footerMessages } from '@edx/frontend-component-footer';
import { messages as paragonMessages } from '@edx/paragon';
import arMessages from './messages/ar.json';
import caMessages from './messages/ca.json';
// no need to import en messages-- they are in the defaultMessage field
import deMessages from './messages/de.json';
import es419Messages from './messages/es_419.json';
import faIRMessages from './messages/fa_IR.json';
import frMessages from './messages/fr.json';
import zhcnMessages from './messages/zh_CN.json';
import heMessages from './messages/he.json';
import idMessages from './messages/id.json';
import kokrMessages from './messages/ko_kr.json';
import plMessages from './messages/pl.json';
import ptbrMessages from './messages/pt_br.json';
import frCAMessages from './messages/fr_CA.json';
import hiMessages from './messages/hi.json';
import itMessages from './messages/it.json';
import ptMessages from './messages/pt.json';
import ruMessages from './messages/ru.json';
import thMessages from './messages/th.json';
import ukMessages from './messages/uk.json';
import zhcnMessages from './messages/zh_CN.json';
import dedeCAMessages from './messages/de_DE.json';
import ititCAMessages from './messages/it_IT.json';
import ptptCAMessages from './messages/pt_PT.json';
// no need to import en messages-- they are in the defaultMessage field
const messages = {
const appMessages = {
ar: arMessages,
'es-419': es419Messages,
'fa-ir': faIRMessages,
fr: frMessages,
'zh-cn': zhcnMessages,
ca: caMessages,
he: heMessages,
id: idMessages,
'ko-kr': kokrMessages,
pl: plMessages,
'pt-br': ptbrMessages,
pt: ptMessages,
it: itMessages,
de: deMessages,
hi: hiMessages,
'fr-ca': frCAMessages,
ru: ruMessages,
th: thMessages,
uk: ukMessages,
'de-de': dedeCAMessages,
'it-it': ititCAMessages,
'pt-pt': ptptCAMessages,
};
export default messages;
export default [
headerMessages,
paragonMessages,
footerMessages,
appMessages,
];

View File

@@ -1,55 +1,89 @@
{
"account.settings.message.duplicate.tpa.provider": "إنّ حساب {provider} الذي اخترتَه مرتبط مسبقًا بحساب آخر في edX. ",
"account.settings.message.managed.settings": "تتم إدارة إعدادات الملف الشخصي بواسطة {ManagerTitle}. اتصل بالمسؤول أو {support} للحصول على المساعدة.",
"account.settings.message.duplicate.tpa.provider": "حساب {provider} الذي حددته موصول من قبل بحساب آخر على {siteName}.",
"account.settings.message.managed.settings": "إعدادات ملفك الشخصي يديرها {ManagerTitle}. اتصل بمديرك أو ب{support} للحصول على المساعدة.",
"account.settings.message.managed.settings.support": "الدعم",
"account.settings.page.heading": "إعدادات الحساب",
"account.settings.loading.message": "جاري التحميل...",
"account.settings.loading.message": "التحميل جارٍ...",
"account.settings.loading.error": "خطأ: {error}",
"account.settings.banner.beta.language": "لقد قمت بتعيين لغتك الى {beta_language}، والتي لم تتم ترجمتها بالكامل حاليًا. يمكنك مساعدتنا في ترجمة هذه اللغة بالكامل من خلال الانضمام إلى مجتمع Transifex وإضافة ترجمات من الإنجليزية للمتعلمين الذين يتحدثون {beta_language}.",
"account.settings.banner.beta.language.action.switch.back": "العودة لـِ {previous_language}",
"account.settings.banner.beta.language.action.help.translate": "المساعدة في الترجمة إلى {beta_language}",
"account.settings.banner.beta.language": "لقد قمت بضبط لغتك على {beta_language}، وهي حاليا غير مترجمة بالكامل. يمكنك مساعدتنا في إتمام ترجمة هذه اللغة من خلال الانضمام إلى مجتمع Transifex وإضافة ترجمات من الإنجليزية للمتعلمين الذين يتحدثون {beta_language}.",
"account.settings.banner.beta.language.action.switch.back": "العودة إلى {previous_language}",
"account.settings.banner.beta.language.action.help.translate": "ساعد في الترجمة إلى {beta_language}",
"account.settings.section.account.information": "معلومات الحساب",
"account.settings.section.account.information.description": "تتضمن هذه الإعدادات معلومات أساسية عن حسابك.",
"account.settings.section.profile.information": "معلومات الملف الشخصي",
"account.settings.section.demographics.information": "معلومات اختيارية",
"account.settings.section.site.preferences": "تفضيلات الموقع",
"account.settings.section.linked.accounts": "الحسابات المرتبطة",
"account.settings.section.linked.accounts.description": "يمكنك ربط حساباتك الشخصية لتسهيل عملية تسجيل دخولك إلى edX.",
"account.settings.section.linked.accounts": "الحسابات الموصولة",
"account.settings.section.linked.accounts.description": "يمكنك وصل حسابات هويتك لتبسيط تسجيل دخولك إلى {siteName}.",
"account.settings.field.username": "اسم المستخدم",
"account.settings.field.username.help.text": "اسم المستخدم الخاص بك الذي يميزك في edX. لا يمكنك تغيير اسم المستخدم الخاص بك لاحقاً.",
"account.settings.field.username.help.text": "الاسم الذي يعرّفك على {siteName}. لا يمكنك تغيير اسم المستخدم الخاص بك.",
"account.settings.field.full.name": "الاسم الكامل",
"account.settings.field.full.name.empty": "إضافة اسم",
"account.settings.field.full.name.help.text": "الاسم المستخدم للتحقق من هويتك والذي سوف يظهر على الشهادات الخاصة بك.",
"account.settings.field.email": "البريد الالكتروني (الدخول)",
"account.settings.field.full.name.empty": "إضافة الاسم",
"account.settings.field.full.name.help.text": "الاسم المستعمل للتحقق من هويتك والذي يظهر في شهاداتك.",
"account.settings.field.full.name.help.text.default": "الاسم الذي يظهر في ملفك الشخصي العامّ.",
"account.settings.field.full.name.help.text.default.certificate": "سيظهر هذا الاسم في شهاداتك و سجلّاتك العامّة.",
"account.settings.field.name.verified": "اسم متحقَّق منه",
"account.settings.field.name.verified.help.text.verified": "تم التحقق من هذا الاسم عن طريق بطاقة تعريف ذات صورة.",
"account.settings.field.name.verified.help.text.verified.proctored": "تم التحقق من هذا الاسم عن طريق المراقبة.",
"account.settings.field.name.verified.help.text.verified.certificate": "تم التحقق من هذا الاسم عن طريق بطاقة هوية ذات صورة، وتم اختياره للظهور في الشهادات والسجلات العامة.",
"account.settings.field.name.verified.help.text.verified.proctored.certificate": "تم التحقق من هذا الاسم من خلال المراقبة، وتم تحديده للظهور في الشهادات والسجلات العامة.",
"account.settings.field.name.verified.help.text.submitted": "تم تسليم طلب التحقق. في العادة يستغرق هذا 48 ساعة أو أقل. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت.",
"account.settings.field.name.verified.help.text.submitted.proctored": "تم تسليم امتحانك المراقَب. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت. رجاءً أعد التفقد بعد 2-5 أيام.",
"account.settings.field.name.verified.help.text.submitted.certificate": "عند نجاح التحقق من الهوية، سيظهر هذا الاسم على شهاداتك و سجلاتك العامة. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت.",
"account.settings.field.name.verified.help.text.submitted.proctored.certificate": "بمجرد اجتياز امتحانك المراقب للمراجعة، سيظهر هذا الاسم في شهادتك و سجلاتك العامة. لا يمكن تغيير الاسم المتحقَّق منه في هذا الوقت.",
"account.settings.field.name.verified.verification.help": "أدخل اسمك كما يظهر في بطاقة تعريف الطالب أو الموظف الخاصة بك، أو بطاقة تعريفك الصادرة عن الحكومة.",
"account.settings.field.full.name.help.text.submitted": "تم تسليم طلب التحقق. يستغرق هذا عادةً 48 ساعة أو أقل. لا يمكن تغيير الاسم الكامل في هذا الوقت.",
"account.settings.field.full.name.help.text.submitted.proctored": "تم تسليم امتحانك المراقب. لا يمكن تغيير الاسم الكامل في هذا الوقت. يرجى التحقق مرة أخرى خلال 2-5 أيام.",
"account.settings.field.full.name.help.text.submitted.certificate": "عند نجاح التحقق من الهوية، سيظهر هذا الاسم على الشهادات والسجلات العامة. لا يمكن تغيير الاسم الكامل في هذا الوقت.",
"account.settings.field.full.name.help.text.submitted.proctored.certificate": "بمجرد اجتياز امتحانك المراقب للمراجعة، سيظهر هذا الاسم في شهاداتك وسجلاتك العامة. لا يمكن تغيير الاسم الكامل في هذا الوقت.",
"account.settings.field.name.verified.success.message": "اكتمل طلب التحقق من هويتك بنجاح. لديك الآن خيار تحديد الاسم الذي تفضل ظهوره على شهاداتك وسجلاتك العامة.",
"account.settings.field.name.verified.success.message.header": "اكتمل طلب تغيير اسمك!",
"account.settings.field.name.verified.failure.message": "لم تنجح آخر محاولاتك للتحقق من هويتك. تم إرجاع إعدادات الحساب ذات الصلة إلى وضعها السابق.",
"account.settings.field.name.verified.failure.message.header": "لم نستطع التحقق من هويتك.",
"account.settings.field.name.verified.failure.message.help.link": "اعرف المزيد عن التحقق من الهوية.",
"account.settings.field.name.verified.submitted.message": "تم تسليم طلب التحقق من هويتك و عادة ما يستغرق إكماله ما بين 24 و 48 ساعة.",
"account.settings.field.name.verified.submitted.message.certificate": "عند الموافقة على طلبك، سيظهر اسمك المحدَّث على جميع الشهادات السجلات العامة ذات الصلة.",
"account.settings.field.name.verified.submitted.message.header": "طلب تغيير اسمك يكاد يكتمل!",
"account.settings.field.email": "البريد الالكتروني (تسجيل الدخول)",
"account.settings.field.email.empty": "إضافة عنوان البريد الإلكتروني",
"account.settings.field.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر فوق الرابط في الرسالة لتحديث عنوان بريدك الإلكتروني.",
"account.settings.field.email.help.text": "أنت تتلقى رسائل من edX وفرق المساق على هذا العنوان.",
"account.settings.field.secondary.email": "عنوان البريد الإلكتروني للاسترداد",
"account.settings.field.secondary.email.empty": "إضافة عنوان البريد الإلكتروني للاسترداد",
"account.settings.field.secondary.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر فوق الرابط في الرسالة لتحديث عنوان بريد الاسترداد الإلكتروني.",
"account.settings.field.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر على الرابط في الرسالة لتحديث عنوان بريدك الإلكتروني.",
"account.settings.field.email.help.text": "على هذا العنوان تصلك الرسائل من {siteName} و من فرق المساقات.",
"account.settings.field.secondary.email": "عنوان بريد الاستعادة الإلكتروني.",
"account.settings.field.secondary.email.empty": "إضافة عنوان بريد إلكتروني لاستعادة حسابك",
"account.settings.field.secondary.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر على الرابط في الرسالة لتحديث عنوان بريد الاستعادة الإلكتروني الخاص بك.",
"account.settings.email.field.confirmation.header": "تعليق عملية التأكيد",
"account.settings.field.dob": "سنة الميلاد",
"account.settings.field.dob.empty": "إضافة سنة الميلاد",
"account.settings.field.year_of_birth.options.empty": "اختر سنة ميلاد",
"account.settings.field.country": "الدولة",
"account.settings.field.year_of_birth.options.empty": "تحديد سنة الميلاد",
"account.settings.field.dob.month": "الشهر",
"account.settings.field.dob.year": "السنة",
"account.settings.field.month.year.default": "اختر الشهر",
"account.settings.field.dob.year.default": "اختر السنة",
"account.settings.field.dob.form.button": "يرجى تأكيد تاريخ ميلادك",
"account.settings.field.dob.form.title": "أدخل شهر وسنة ميلادك",
"account.settings.field.dob.form.help.text": "نطلب معلومات شهر وسنة الميلاد لمساعدتنا على الامتثال لالتزاماتنا القانونية.",
"account.settings.field.dob.form.success": "شكرا لك على إدخال المعلومات الخاصة بك.",
"account.settings.field.month_of_birth.options.empty": "تحديد شهر الميلاد",
"account.settingsfield.dob.error.general": "حدث خطأ تقني. رجاءً حاول مجددا.",
"account.settings.field.country": "البلد",
"account.settings.field.country.empty": "إضافة البلد ",
"account.settings.field.country.options.empty": "اختر دولة",
"account.settings.field.state": "الحالة",
"account.settings.field.state.empty": "إضافة منطقة",
"account.settings.field.state.options.empty": "اختر منطقة",
"account.settings.field.country.options.empty": "اختر البلد",
"account.settings.field.state": "المنطقة / الولاية / المحافظة",
"account.settings.field.state.empty": "إضافة المنطقة / الولاية / المحافظة",
"account.settings.field.state.options.empty": "حدد المنطقة",
"account.settings.field.site.language": "لغة الموقع",
"account.settings.field.site.language.help.text": "اللغة المستخدمة في كافة أقسام هذا الموقع. يتوفّر هذا الموقع حاليًا بعدد محدود من اللغات.",
"account.settings.field.education": "المستوى التعليمي",
"account.settings.field.education.empty": "إضافة المستوى التعليمي",
"account.settings.field.education.levels.empty": "اختر المستوى التعليمي",
"account.settings.field.education.levels.empty": "اختر مستوى تعليميًّا",
"account.settings.field.education.levels.p": "دكتوراه",
"account.settings.field.education.levels.m": "ماجستير أو شهادة مهنيّة",
"account.settings.field.education.levels.b": "بكالوريوس",
"account.settings.field.education.levels.a": "زمالة",
"account.settings.field.education.levels.hs": "شهادة الثانوية العامة",
"account.settings.field.education.levels.jhs": "شهادة الثانوية الصغرى/الإعدادية/المرحلة المتوسّطة",
"account.settings.field.education.levels.el": "شهادة المدرسة الابتدائية",
"account.settings.field.education.levels.none": "لا يوجد تعليم رسمي",
"account.settings.field.education.levels.m": "ماجستير / ماستر أو شهادة مهنيّة",
"account.settings.field.education.levels.b": "بكالوريوس / ليسانس",
"account.settings.field.education.levels.a": "درجة الزمالة / دبلوم الدراسات الجامعية",
"account.settings.field.education.levels.hs": "الثانوية العامة / البكالوريا",
"account.settings.field.education.levels.jhs": "المدرسة الإعدادية / المتوسطة",
"account.settings.field.education.levels.el": "المدرسة الابتدائية / الأساسية",
"account.settings.field.education.levels.none": ون تعليم رسمي",
"account.settings.field.education.levels.o": "نوع آخر من التعليم",
"account.settings.field.gender": "الجنس",
"account.settings.field.gender.empty": "إضافة الجنس",
@@ -62,163 +96,185 @@
"account.settings.field.language_proficiencies.options.empty": "اختر لغة",
"account.settings.field.time.zone": "المنطقة الزمنية",
"account.settings.field.time.zone.empty": "ضبط المنطقة الزمنية",
"account.settings.field.time.zone.description": "حدد المنطقة الزمنية لعرض تواريخ المساق. إذا لم تحدد منطقة زمنية، فسيتم عرض تواريخ المساق، بما في ذلك المواعيد النهائية للواجب في المنطقة الزمنية المحلية في المتصفح.",
"account.settings.field.time.zone.default": "الافتراضي (منطقة التوقيت المحلي)",
"account.settings.field.time.zone.description": "حدد المنطقة الزمنية لعرض تواريخ المساقات. إذا لم تحدد منطقة زمنية، فسيتم عرض تواريخ المساق، بما في ذلك المواعيد النهائية للواجبات بالتوقيت المحلّي للمتصفح.",
"account.settings.field.time.zone.default": "الافتراضي (المنطقة الزمنية المحلية)",
"account.settings.field.time.zone.all": "جميع المناطق الزمنية",
"account.settings.field.time.zone.country": "المنطقة الزمنية للدولة",
"account.settings.section.social.media": "روابط منصات التواصل الإجتماعي",
"account.settings.section.social.media.description": "اختياريا، قم بربط حساباتك الشخصية بأيقونات منصات التواصل الاجتماعي في ملف التعريف الخاص بك.",
"account.settings.field.time.zone.country": "المناطق الزمنية للبلدان",
"account.settings.section.social.media": "روابط التواصل الإجتماعي",
"account.settings.section.social.media.description": "اختياريًا، اربط حساباتك الشخصية بأيقونات التواصل الاجتماعي في ملفك على {siteName}.",
"account.settings.field.social.platform.name.linkedin": "لينكد إن",
"account.settings.field.social.platform.name.linkedin.empty": "إضافة عنوان ملف لينكد إن ",
"account.settings.jump.nav.delete.account": "احذف حسابي",
"account.settings.field.social.platform.name.linkedin.empty": "إضافة سيرة لينكد إن",
"account.settings.jump.nav.delete.account": "حذف حسابي",
"account.settings.field.social.platform.name.twitter": "تويتر",
"account.settings.field.social.platform.name.twitter.empty": "إضافة عنوان صفحة تويتر",
"account.settings.field.social.platform.name.twitter.empty": "إضافة صفحة تويتر",
"account.settings.field.social.platform.name.facebook": "فيسبوك",
"account.settings.field.social.platform.name.facebook.empty": "إضافة عنوان صفحة فيسبوك",
"account.settings.field.social.platform.name.facebook.empty": "إضافة حساب فيسبوك",
"account.settings.editable.field.action.save": "حفظ",
"account.settings.editable.field.action.cancel": "إلغاء",
"account.settings.editable.field.action.edit": حرير",
"account.settings.static.field.empty": م يتم تحديد قيمة، فضلًا اتصل بمدير {enterprise} لتعيين بعض التغييرات.",
"account.settings.static.field.empty.no.admin": م يتم تحديد قيمة",
"account.settings.coaching.consent.welcome.header": "لنبدأ",
"account.settings.editable.field.action.edit": عديل",
"account.settings.static.field.empty": ا قيمة محددة. رجاءً اتصل بمديرك في {enterprise} ليقوم بالتعديلات.",
"account.settings.static.field.empty.no.admin": ا قيمة محددة.",
"notification.preferences.notifications.label": "Notifications",
"account.settings.field.name.certificate.select": "في حال التأشير، سيظهر هذا الاسم في شهاداتك و سجلاتك العامة.",
"account.settings.field.name.modal.certificate.title": "اختر اسمًا مفضلًا للشهادات والسجلات العامة",
"account.settings.field.name.modal.certificate.select": "اختر اسمًا",
"account.settings.field.name.modal.certificate.option.full": "الاسم الكامل",
"account.settings.field.name.modal.certificate.option.verified": "اسم متحقَّق منه",
"account.settings.field.name.modal.certificate.button.choose": "اختيار الاسم",
"account.settings.coaching.consent.welcome.header": "لنبدأ.",
"account.settings.coaching.consent.welcome.subheader": "نحن هنا لأجلك من البداية حتى النهاية",
"account.settings.coaching.consent.description": "تتضمن برامج البكالوريوس التدريب الذي يركز على مهنتك وتعليمك وكيفية تحقيق نتائج مبهرة من خلال التواصل الشخصي مع خبراء متمرسين. إذا كنت مهتمًا، فقدّم المعلومات أدناه وانقر فوق \"إرسال\"، وسيتصل بك شريكنا في التدريب عبر البريد الإلكتروني و/أو الرسائل النصية لمساعدتك على المضي قدمًا. تنطبق الشروط والأحكام.*",
"account.settings.coaching.consent.text-messaging.disclaimer": "* يتم تضمين خدمات التدريب بدون أي تكلفة إضافية للمتعلمين الذين لديهم أرقام هواتف أمريكية. يتضمن التدريب رسائل نصية متكررة. قد تنطبق أسعار الرسائل والبيانات. إيقاف النص لإلغاء الاشتراك.",
"account.settings.coaching.consent.accept-coaching": "سجّل للاستفادة من خدمات التدريب",
"account.settings.coaching.consent.decline-coaching": "أفضّل عدم الاتصال بخدمات التدريب المجانية",
"account.settings.coaching.consent.label.name": "يرجى تأكيد الاسم",
"account.settings.coaching.consent.label.phone-number": "فضلًا أدخل رقم الهاتف ",
"account.settings.coaching.consent.success.header": "تمت العملية بنجاح",
"account.settings.coaching.consent.success.message": "لقد اشتركت في التدريب. يمكنك توقع استلام رسالة عبر البريد الإلكتروني أو الرسائل القصيرة في الأيام المقبلة.",
"account.settings.coaching.consent.description": "تتضمن برامج MicroBachelors مرافقة تركز على مهنتك و تعليمك و كيفية تحقيقك للنتائج، و ذلك من خلال التواصل الفردي مع خبير متمرس. إن كنت مهتمًا، فيرجى تزويدنا بالمعلومات أدناه و النقر على \"إرسال\"، و سيتصل بك شريكنا في المرافقة عبر البريد الإلكتروني و/أو الرسائل النصية لمساعدتك على المضي قدمًا. تنطبق الشروط والأحكام.*",
"account.settings.coaching.consent.text-messaging.disclaimer": "* خدمات المرافقة مشمولة دون تكاليف إضافية للمتعلمين الذين لديهم أرقام هواتف أمريكية. تتضمن المرافقة رسائل نصية دورية. قد تنطبق أسعار على الرسائل والبيانات. أرسل STOP للانسحاب.",
"account.settings.coaching.consent.accept-coaching": "سجّل للاستفادة من خدمات المرافقة",
"account.settings.coaching.consent.decline-coaching": "أفضّل ألا يٌتصَل بي بخصوص خدمات المرافقة المجانية",
"account.settings.coaching.consent.label.name": "رجاءً أكّد اسمك",
"account.settings.coaching.consent.label.phone-number": "أدخل رقم هاتفك الجوّال",
"account.settings.coaching.consent.success.header": "نجحت العملية!",
"account.settings.coaching.consent.success.message": "أنت الآن مشترك في المرافقة. ترقّب رسالة عبر البريد الإلكتروني أو خدمة الرسائل القصيرة في في الأيام المقبلة.",
"account.settings.coaching.consent.success.continue": "البدء في مساقي",
"account.settings.coaching.managed.support": "الدعم",
"account.settings.coaching.managed.alert": "تتم إدارة اسمك بواسطة {ManagerTitle}. اتصل بالمسؤول للحصول على المساعدة.",
"account.settings.coaching.managed.alert": "اسمك يديره {ManagerTitle}. اتصل بمديرك للحصول على المساعدة.",
"account.settings.field.phone_number": "رقم الهاتف",
"account.settings.field.phone_number.empty": "إضافة رقم الهاتف",
"account.settings.field.coaching_consent": "اتفاقية التدريب",
"account.settings.field.coaching_consent.tooltip": "تتضمن برامج البكالوريوس التدريب القائم على الرسائل النصية الذي يساعدك على إقران التجارب التعليمية مع أهدافك المهنية من خلال النصائح الشخصية. يتم تضمين خدمات التدريب بدون أي تكلفة إضافية، وهي متوفرة للمتعلمين الذين لديهم أرقام هواتف نقالة أمريكية. تنطبق أسعار المراسلة القياسية. أرسل \"قف\" في أي وقت لإلغاء الرسائل.",
"account.settings.field.coaching_consent.error": "مطلوب رقم هاتف أمريكي صالح للدخول في التدريب",
"account.settings.field.phone_number.empty": "إضافة رقم هاتف",
"account.settings.field.coaching_consent": "الموافقة على المرافقة",
"account.settings.field.coaching_consent.tooltip": "تتضمن برامج MicroBachelors مرافقة قائمة على الرسائل النصية، تساعدك على إقران تجاربك التعلّمية مع أهدافك المهنية من خلال النصائح الفردية. خدمات المرافقة مشمولة دون تكاليف إضافية، وهي متوفرة للمتعلمين الذين لديهم أرقام هواتف جوالة أمريكية. تنطبق أسعار المراسلة القياسية. أرسل 'STOP' في أي وقت للانسحاب من الرسائل.",
"account.settings.field.coaching_consent.error": "مطلوب رقم هاتف أمريكي صحيح للتسجيل في المرافقة",
"account.settings.delete.account.before.proceeding": "قبل المتابعة، يرجى {actionLink}.",
"account.settings.delete.account.header": "احذف حسابي",
"account.settings.delete.account.header": "حذف حسابي",
"account.settings.delete.account.subheader": "نأسف لذهابك!",
"account.settings.delete.account.text.1": "يرجى ملاحظة: إن حذف حسابك والبيانات الشخصية قرار نهائي ولا يمكن التراجع عنه. لن تتمكن edX من استعادة حسابك أو البيانات التي تم حذفها.",
"account.settings.delete.account.text.2": "بمجرد حذف حسابك ، لا يمكنك استخدامه لأخذ دورات تدريبية على تطبيق edX أو edx.org أو أي موقع آخر تستضيفه edX. وهذا يشمل الوصول إلى edx.org من نظام صاحب العمل أو الجامعة والوصول إلى المواقع الخاصة التي تقدمها MIT Open Learning و Wharton Executive Education و Harvard Medical School.",
"account.settings.delete.account.text.3.link": "اتبع الإرشادات التالية لطباعة أو تحميل شهادة.",
"account.settings.delete.account.text.warning": "تحذير: حذف الحساب قرار نهائي. يرجى قراءة ما سبق بعناية قبل المتابعة. هذا إجراء لا رجعة فيه، ولن تتمكن بعد الآن من استخدام البريد الإلكتروني نفسه على edX.",
"account.settings.delete.account.text.change.instead": "هل تريد تغيير البريد الإلكتروني أو الاسم أو كلمة المرور بدلاً من ذلك؟",
"account.settings.delete.account.button": "احذف حسابي",
"account.settings.delete.account.please.activate": "تنشيط حسابك",
"account.settings.delete.account.please.unlink": "إلغاء ربط جميع حسابات التواصل الاجتماعي",
"account.settings.delete.account.text.1": "ترجى الملاحظة: إن حذف حسابك وبياناتك الشخصية له أثر دائم و لا يمكن التراجع عنه. لن يكون بمقدور {siteName} استعادة حسابك ولا البيانات التي يتم حذفها.",
"account.settings.delete.account.text.2": "بمجرد حذف حسابك، فإنك لن تستطيع استخدامه لمتابعة المساقات على {siteName}.",
"account.settings.delete.account.text.2.edX": "بمجرد حذف حسابك، فإنك لن تستطيع استخدامه لمتابعة المساقات على تطبيق edX ولا edx.org ولا أي موقع آخر تستضيفه edX. وهذا يشمل الوصول إلى edx.org من نظام صاحب العمل أو الجامعة و الوصول إلى المواقع الخاصة التي تقدمها MIT Open Learning و Wharton Executive Education و Harvard Medical School.",
"account.settings.delete.account.text.3.link": "اتّبع هذه التعليمات لطباعة أو تحميل شهادة",
"account.settings.delete.account.text.warning": "تحذير: حذف الحساب أثره دائم. يرجى قراءة ما ورد أعلاه بعناية قبل المتابعة. هذا إجراء غير رجعي، و لن تتمكن بعده من استخدام نفس البريد الإلكتروني على {siteName}.",
"account.settings.delete.account.text.change.instead": "هل تريد بدلاً من ذلك تغيير بريدك الإلكتروني أو اسمك أو كلمة المرور الخاصة بك؟",
"account.settings.delete.account.button": "حذف حسابي",
"account.settings.delete.account.please.activate": "تفعيل حسابك",
"account.settings.delete.account.please.confirm": "تأكيد حسابك",
"account.settings.delete.account.please.unlink": "فصل جميع حسابات التواصل الاجتماعي",
"account.settings.delete.account.modal.header": "هل أنت متأكد؟",
"account.settings.delete.account.modal.text.1": "لقد حددت \"حذف حسابي\". إن حذف حسابك وبياناتك الشخصية قرار نهائي لا يمكن التراجع عنه. لن تتمكن edX من استعادة حسابك أو البيانات التي تم حذفها.",
"account.settings.delete.account.modal.text.2": "عند المتابعة، فلن تتمكن من استخدام هذا الحساب للحصول على مساقات على تطبيق edX أو edx.org أو أي موقع آخر يستضيفه edX. وهذا يشمل الوصول إلى edx.org من نظام صاحب العمل أو الجامعة والوصول إلى المواقع الخاصة التي يقدمها معهد ماساتشوستس للتكنولوجيا التعليم المفتوح، وارتون التعليم التنفيذي، وكلية هارفارد الطبية.",
"account.settings.delete.account.modal.enter.password": "إذا كنت لا تزال ترغب في المتابعة وحذف حسابك ، فيرجى إدخال كلمة مرور حسابك:",
"account.settings.delete.account.modal.confirm.delete": "تعم، أحذف",
"account.settings.delete.account.modal.confirm.cancel": "إلغاء",
"account.settings.delete.account.error.unable.to.delete": "تعذر حذف الحساب",
"account.settings.delete.account.modal.text.1": "لقد اخترت \"حذف حسابي\". إن حذف حسابك وبياناتك الشخصية ذو أثر دائم لا يمكن التراجع عنه. لن يكون بمقدور {siteName} استعادة حسابك و لا البيانات التي حذفت.",
"account.settings.delete.account.modal.text.2": "إن واصلت، فلن تستطيع استخدام هذا الحساب لمتابعة المساقات على {siteName}.",
"account.settings.delete.account.modal.text.2.edX": "إن واصلت، فلن تستطيع استخدام هذا الحساب لمتابعة المساقات على على تطبيق edX و لا edx.org و لا أي موقع آخر تستضيفه edX. وهذا يشمل الوصول إلى edx.org من نظام صاحب العمل أو الجامعة و الوصول إلى المواقع الخاصة التي يقدمها MIT Open Learning و Wharton Executive Education و Harvard Medical School.",
"account.settings.delete.account.modal.enter.password": "إن كنت لا تزال ترغب في المتابعة و حذف حسابك، فيرجى إدخال كلمة المرور:",
"account.settings.delete.account.modal.confirm.delete": "تعم، احذف",
"account.settings.delete.account.modal.confirm.cancel": "لا",
"account.settings.delete.account.error.unable.to.delete": "لم نستطع حذف الحساب",
"account.settings.delete.account.error.no.password": "كلمة المرور مطلوبة",
"account.settings.delete.account.error.invalid.password": "كلمة المرور المدخلة غير صحيحة",
"account.settings.delete.account.error.unable.to.delete.details": "عذراً ، حدث خطأ أثناء محاولة معالجة طلبك. الرجاء معاودة المحاولة في وقت لاحق.",
"account.settings.delete.account.modal.after.header": "نأسف لذهابك! سيتم حذف حسابك قريبا.",
"account.settings.delete.account.modal.after.text": "قد يستغرق حذف الحساب ، بما في ذلك الإزالة من قوائم البريد الإلكتروني ، بضعة أسابيع حتى تتم معالجته بالكامل من خلال نظامنا. إذا كنت ترغب في إلغاء الاشتراك في رسائل البريد الإلكتروني قبل ذلك الحين ، يرجى إلغاء الاشتراك من تذييل أي بريد إلكتروني.",
"account.settings.delete.account.error.invalid.password": "كلمة المرور غير صحيحة",
"account.settings.delete.account.error.unable.to.delete.details": "عذراً، حدث خطأ أثناء محاولة معالجة طلبك. رجاءً أعد المحاولة لاحقًا.",
"account.settings.delete.account.modal.after.header": "نأسف لذهابك! سيُحذف حسابك في ظرف وجيز.",
"account.settings.delete.account.modal.after.text": "حذف الحساب، بما في ذلك من إزالة من القوائم البريدية، إجراء قد يستغرق بضعة أسابيع حتى يكتمل عبر نظامنا. إن كنت تريد قبل ذلك الحين إيقاف تلقي البريد الإلكتروني، فيرجى إلغاء الاشتراك من تذييل أي بريد إلكتروني.",
"account.settings.delete.account.modal.after.button": "إغلاق ",
"account.settings.delete.account.text.3": "قد تفقد أيضًا إمكانية الوصول إلى الشهادات التي تم التحقق منها وبيانات اعتماد البرامج الأخرى مثل شهادات MicroMasters. إذا أردت عمل نسخة من هذه السجلات قبل متابعة الحذف، {actionLink}.",
"account.settings.message.demographics.service.issue": "حدث خطأ أثناء محاولة استرداد معلومات حسابك أو حفظها. يرجى المحاولة مرة أخرى لاحقًا.",
"account.settings.delete.account.text.3.edX": "قد تفقد كذلك إمكانية الوصول إلى الشهادات الموثقة و كذا مؤهلات البرامج الأخرى مثل شهادات MicroMasters. يمكنك الاحتفاظ بنسخة عنها لديك قبل المواصلة إلى الحذف. {actionLink}.",
"account.settings.delete.account.text.3": "قد تفقد كذلك إمكانية الوصول إلى الشهادات الموثقة و كذا مؤهلات البرامج الأخرى. يمكنك الاحتفاظ بنسخة عنها لديك قبل المواصلة إلى الحذف.",
"account.settings.message.demographics.service.issue": "حدث خطأ أثناء محاولة استخراج أو حفظ معلومات حسابك. رجاءً أعد المحاولة لاحقًا.",
"account.settings.field.demographics.gender": "هوية الجنس",
"account.settings.field.demographics.gender.empty": "إضافة هوية الجنس",
"account.settings.field.demographics.gender.options.empty": "حدد هوية الجنس",
"account.settings.field.demographics.gender_description": "وصف هوية الجنس",
"account.settings.field.demographics.gender_description.empty": "أدخل وصفًا",
"account.settings.field.demographics.gender_description.empty": "أدخل الوصف",
"account.settings.field.demographics.ethnicity": "هوية العرق/ الأصل",
"account.settings.field.demographics.ethnicity.empty": "إضافة هوية العرق/الأصل",
"account.settings.field.demographics.ethnicity.options.empty": "فضلًا اختر جميع ما ينطبق",
"account.settings.field.demographics.income": "الدخل المادي الأسري",
"account.settings.field.demographics.income.empty": "إضافة الدخل المادي الأسري",
"account.settings.field.demographics.income.options.empty": "حدد نطاق الدخل المادي للأسرة",
"account.settings.field.demographics.military_history": "حالة الخدمة العسكرية في الولايات المتحدة الأمريكية",
"account.settings.field.demographics.military_history.empty": "إضافة حالة الخدمة العسكرية",
"account.settings.field.demographics.military_history.options.empty": "اختر حالة الخدمة العسكرية",
"account.settings.field.demographics.learner_education_level": ؤهلك التعليمي",
"account.settings.field.demographics.learner_education_level.empty": "إضافة مؤهلك التعليمي",
"account.settings.field.demographics.parent_education_level": ؤهل الوالدين/الآوصياء التعليمي",
"account.settings.field.demographics.parent_education_level.empty": "إضافة المؤهل التعليمي",
"account.settings.field.demographics.education_level.options.empty": "حدد موهلاً تعليميًا",
"account.settings.field.demographics.ethnicity.empty": "إضافة هوية العرق / الأصل",
"account.settings.field.demographics.ethnicity.options.empty": "اختر كل ما ينطبق",
"account.settings.field.demographics.income": "دخل الأسرة",
"account.settings.field.demographics.income.empty": "إضافة دخل الأسرة",
"account.settings.field.demographics.income.options.empty": "حدد نطاقًا لدخل الأسرة",
"account.settings.field.demographics.military_history": "الوضعية إزاء الخدمة العسكرية في الولايات المتحدة",
"account.settings.field.demographics.military_history.empty": "إضافة الوضعية إزاء الخدمة العسكرية",
"account.settings.field.demographics.military_history.options.empty": "اختر الوضعية إزاء الخدمة العسكرية",
"account.settings.field.demographics.learner_education_level": ستواك التعليمي",
"account.settings.field.demographics.learner_education_level.empty": "إضافة المستوى التعليمي",
"account.settings.field.demographics.parent_education_level": ستوى الوالدين/الأولياء التعليمي",
"account.settings.field.demographics.parent_education_level.empty": "إضافة المستوى التعليمي",
"account.settings.field.demographics.education_level.options.empty": "حدد المستوى التعليمي",
"account.settings.field.demographics.work_status": "الحالة الوظيفية",
"account.settings.field.demographics.work_status.empty": "إضافة الحالة الوظيفية",
"account.settings.field.demographics.work_status.options.empty": "فضلًا حدد حالتك الوظيفية",
"account.settings.field.demographics.work_status.options.empty": "حدد الحالة الوظيفية",
"account.settings.field.demographics.work_status_description": "وصف الحالة الوظيفية",
"account.settings.field.demographics.work_status_description.empty": "أدخل وصفًا",
"account.settings.field.demographics.work_status_description.empty": "أدخل الوصف",
"account.settings.field.demographics.current_work_sector": "مجال العمل الحالي",
"account.settings.field.demographics.current_work_sector.empty": "إضافة مجال العمل",
"account.settings.field.demographics.future_work_sector": "مجال العمل المستقبلي",
"account.settings.field.demographics.future_work_sector.empty": "إضافة مجال العمل",
"account.settings.field.demographics.work_sector.options.empty": "حدد مجال العمل",
"account.settings.section.demographics.why": "لماذا تجمع edX هذه المعلومات؟",
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في نص الرابط. الرجاء التحقق من الرابط والمحاولة مجددا.",
"account.settings.section.demographics.why": "ما هي غاية {siteName} من جمع هذه المعلومات؟",
"account.settings.name.change.title.id": غيير الاسم هذا يتطلب التحقق من الهوية",
"account.settings.name.change.title.begin": "قبل أن نبدأ",
"account.settings.name.change.warning.one": "تحذير: يقوم هذا الإجراء بتحديث الاسم الذي يظهر على جميع الشهادات التي تم الحصول عليها على هذا الحساب في الماضي و أي شهادات تحصل عليها حاليا أو مستقبلاً.",
"account.settings.name.change.warning.two": "لا يمكن التراجع عن هذا الإجراء دون التحقق من هويتك.",
"account.settings.name.change.id.name.label": "أدخل اسمك كما يظهر في بطاقة تعريف الطالب أو العمل أو بطاقة الهوية الصادرة عن الحكومة.",
"account.settings.name.change.id.name.placeholder": "أدخل الاسم الموجود في بطاقة تعريفك ذات الصورة.",
"account.settings.name.change.error.valid.name": "رجاءً أدخل اسما صحيحا.",
"account.settings.name.change.error.general": "حدث خطأ تقني. رجاءً حاول مجددًا.",
"account.settings.name.change.continue": "مواصلة",
"account.settings.name.change.cancel": "إلغاء",
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجددًا.",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "الدعم الفني",
"account.settings.editable.field.password.reset.button.confirmation": "لقد أرسلنا رسالة إلى {email}. انقر فوق الرابط في الرسالة لإعادة تعيين كلمة المرور. إذا لم يتم استلام الرسالة؟ اتصل بـ {technicalSupportLink}.",
"account.settings.editable.field.password.reset.button": "تغيير كلمة المرور",
"account.settings.editable.field.password.reset.button.forbidden": "طلبك السابق قيد التقدم، يرجى إعادة المحاولة بعد لحظات قليلة.",
"account.settings.editable.field.password.reset.button.confirmation": "لقد أرسلنا رسالة إلى {email}. انقر على الرابط في الرسالة لإعادة ضبط كلمة المرور الخاصة بك. لم تصلك الرسالة؟ اتصل بـ{technicalSupportLink}.",
"account.settings.editable.field.password.reset.button": "إعادة ضبط كلمة المرور",
"account.settings.editable.field.password.reset.button.forbidden": "طلبك السابق في تقدّم، رجاءً حاول مجددًا بعد لحظات قليلة.",
"account.settings.editable.field.password.reset.label": "كلمة المرور",
"account.settings.sso.link.account": "تسجيل الدخول كـ {name}",
"account.settings.sso.account.connected": ربوط",
"account.settings.sso.account.disconnect.error": "حدثت مشكلة أثناء قطع اتصال هذا الحساب، اتصل بالدعم عند استمرار المشكلة.",
"account.settings.sso.unlink.account": "إلغاء ربط حساب {name} ",
"account.settings.sso.no.providers": "لا يمكن ربط أية حسابات حاليًا",
"id.verification.access.blocked.denied": "لا يمكنك التحقق من هويتك في الوقت الحالي. إذا لم تقم بعد بتنشيط حسابك، فيرجى التحقق من مجلد البريد المهمل للحصول على رسالة التفعيل من {email}.",
"account.settings.sso.link.account": "تسجيل الدخول باستخدام {name}",
"account.settings.sso.account.connected": وصول",
"account.settings.sso.account.disconnect.error": "حدثت مشكلة أثناء فصل هذا الحساب، اتصل بالدعم إن استمرت المشكلة.",
"account.settings.sso.unlink.account": "فصل حساب {name}",
"account.settings.sso.no.providers": "لا يمكن وصل أي حسابات في الوقت الراهن.",
"account.page.title": "الحساب | {siteName}",
"id.verification.access.blocked.denied": "لا يمكننا التحقق من هويتك في الوقت الراهن. إن لم تكن قد فعّلت حسابك بعد، فيرجى تفقد مجلد الرسائل غير المرغوب فيها بحثًا عن بريد التفعيل الإلكتروني من {email}.",
"id.verification.next": "التالي",
"id.verification.support": "support",
"id.verification.example.card.alt": "مثال على بطاقة هوية صحيحة بالاسم الكامل وصورة.",
"id.verification.requirements.title": "متطلبات التحقق من الصورة",
"id.verification.requirements.description": "يجب عليك اتباع الآتي لإكمال عملية التحقق الإلكتروني من هويتك:",
"id.verification.support": "الدعم",
"id.verification.example.card.alt": "مثال بطاقة تعريف صحيحة بالاسم الكامل والصورة.",
"id.verification.requirements.title": "متطلبات التحقق باستخدام الصورة",
"id.verification.requirements.description": "لإكمال التحقق بالصورة، ستحتاج ما يلي:",
"id.verification.requirements.card.device.title": "جهاز مزود بكاميرا",
"id.verification.requirements.card.device.allow": "موافق",
"id.verification.requirements.card.id.title": "صورة التحقق من الشخصية.",
"id.verification.requirements.card.id.text": "تحتاج إلى بطاقة هوية صحيحة للتحقق تحوي اسمك الكامل وصورتك.",
"id.verification.privacy.title": "بيانات الخصوصية.",
"id.verification.privacy.need.photo.question": "لماذا تحتاج edX إلى صورتي؟ ",
"id.verification.privacy.need.photo.answer": "نستخدم صور التحقق الخاصة بك لتأكيد هويتك والتأكد من صحة شهادتك.",
"id.verification.privacy.do.with.photo.question": "ما الذي تفعله edX بهذه الصورة؟",
"id.verification.privacy.do.with.photo.answer": "سنقوم بتشفير صورتك بأمان وإرسالها لخدمة التحقق للمراجعة . لن يتم حفظ صورتك ومعلوماتك أو عرضها في أي مكان على edX بعد اكتمال عملية التحقق.",
"id.verification.requirements.card.device.allow": "السماح",
"id.verification.requirements.card.id.title": "بطاقة تعريف بها صورة",
"id.verification.requirements.card.id.text": "تحتاج إلى بطاقة تعريف صالحة تحتوي اسمك الكامل وصورتك، كرخصة القيادة أو جواز السفر مثلاً.",
"id.verification.privacy.title": "معلومات الخصوصية",
"id.verification.privacy.need.photo.question": "ما حاجة {siteName} لصورتي؟",
"id.verification.privacy.need.photo.answer": "نستخدم صور التحقق الخاصة بك لتأكيد هويتك ولضمان صحة شهادتك.",
"id.verification.privacy.do.with.photo.question": "ماذا يُفعَل بهذه الصورة في {siteName}؟",
"id.verification.privacy.do.with.photo.answer": "نقوم بتشفير صورتك بشكل آمن وإرسالها إلى خدمة الترخيص الخاصة بنا للمراجعة. صورتك و معلوماتك لا تُحفَظ و لا تظهر في أي مكان على {siteName} بعد اكتمال عملية التحقق.",
"id.verification.access.blocked.title": "التحقق من الهوية",
"id.verification.access.blocked.enrollment": "أنت الآن ملتحق بمساق يتطلب التحقق من الهوية.",
"id.verification.access.blocked.pending": "لقد قمت بالفعل بإرسال معلومات التحقق الخاصة بك. ستصلك رسالة على لوحة المعلومات عند اكتمال عملية التحقق (عادةً خلال 5 أيام).",
"id.verification.access.blocked.enrollment": "أنت حاليا غير ملتحق بأي مساق يتطلب التحقق من الهوية.",
"id.verification.access.blocked.pending": "لقد سلّمت من قبل معلومات التحقق الخاصة بك. ستصلك رسالة على لوحة المعلومات الخاصة بك عند اكتمال عملية التحقق (عادةً في ظرف 5 أيام).",
"id.verification.photo.take": "التقاط صورة ",
"id.verification.photo.retake": "Retake Photo?",
"id.verification.photo.enable.detection": مكين خاصية التعرف على الوجه",
"id.verification.photo.enable.detection.portrait.help.text": "عند تحديد هذا الخيار، فسيظهر مربع حول وجهك. يمكن رؤية وجهك بوضوح إذا كان المربع المحيط به أزرق. إذا لم يكن وجهك في وضع جيد أو إذا لم يكن قابلاً للاكتشاف، فسيكون المربع باللون الأحمر.",
"id.verification.photo.enable.detection.id.help.text": "عند تحديد هذا الخيار، فسيظهر مربع حول صورة وجهك في بطاقة الهوية. يمكن رؤية وجهك بوضوح إذا كان المربع المحيط به أزرق. إذا لم يكن وجهك في وضع جيد أو إذا لم يكن قابلاً للاكتشاف، فسيكون المربع باللون الأحمر.",
"id.verification.photo.feedback.correct": "وضع الوجه جيد.",
"id.verification.photo.feedback.two.faces": "تم تحديد أكثر من وجه.",
"id.verification.photo.feedback.no.faces": "لم يتم تحديد أي وجه.",
"id.verification.photo.feedback.top.left": "وضع خاطئ. أعلى اليسار.",
"id.verification.photo.feedback.top.center": "وضع خاطئ. أعلى الوسط.",
"id.verification.photo.feedback.top.right": "وضع خاطئ. أعلى اليمين.",
"id.verification.photo.feedback.center.left": "وضع خاطئ. وسط اليسار.",
"id.verification.photo.feedback.center.center": "وضع خاطئ. قريب جدًا من الكاميرا.",
"id.verification.photo.feedback.center.right": "وضع خاطئ. وسط اليمين.",
"id.verification.photo.feedback.bottom.left": "وضع خاطئ. أسفل اليسار.",
"id.verification.photo.feedback.bottom.center": "وضع خاطئ. أسفل الوسط.",
"id.verification.photo.feedback.bottom.right": "وضع خاطئ. أسفل اليمين.",
"id.verification.photo.retake": "إعادة التقاط الصورة؟",
"id.verification.photo.enable.detection": فعيل اكتشاف الوجوه",
"id.verification.photo.enable.detection.portrait.help.text": "في حال التأشير، سيبرز مربع حول وجهك. يمكن رؤية وجهك بوضوح إن كان المربع المحيط به أزرق اللون. أما إن كان وجهك في وضع غير جيد أو غير قابل للاكتشاف، فسيكون المربع أحمر اللون.",
"id.verification.photo.enable.detection.id.help.text": "في حال التأشير، سيظهر مربع حول صورة وجهك في بطاقة الهوية. يمكن رؤية وجهك بوضوح إن كان المربع المحيط به أزرق اللون. أما إن كان وجهك في وضع غير جيد أو غير قابل للاكتشاف، فسيكون المربع أحمر اللون.",
"id.verification.photo.feedback.correct": "موضع الوجه جيد.",
"id.verification.photo.feedback.two.faces": "تم اكتشاف أكثر من وجه واحد.",
"id.verification.photo.feedback.no.faces": "لم يتم اكتشاف أي وجه.",
"id.verification.photo.feedback.top.left": "الموضع خاطئ. أعلى اليسار.",
"id.verification.photo.feedback.top.center": "الموضع خاطئ. أعلى الوسط.",
"id.verification.photo.feedback.top.right": "الموضع خاطئ. أعلى اليمين.",
"id.verification.photo.feedback.center.left": "الموضع خاطئ. وسط اليسار.",
"id.verification.photo.feedback.center.center": "الموضع خاطئ. قريب جدًا من الكاميرا.",
"id.verification.photo.feedback.center.right": "الموضع خاطئ. وسط اليمين.",
"id.verification.photo.feedback.bottom.left": "الموضع خاطئ. أسفل اليسار.",
"id.verification.photo.feedback.bottom.center": "الموضع خاطئ. أسفل الوسط.",
"id.verification.photo.feedback.bottom.right": "الموضع خاطئ. أسفل اليمين.",
"id.verification.camera.access.title": "صلاحيات الكاميرا",
"id.verification.camera.access.title.success": "تمكين الوصول للكاميرا",
"id.verification.camera.access.title.success": "الوصول للكاميرا ممكن",
"id.verification.camera.access.title.failed": "تعذّر الوصول للكاميرا.",
"id.verification.camera.access.click.allow": "فضلًا تأكد من اختيار الأمر \"السماح\"",
"id.verification.camera.access.enable": مكين الكاميرا",
"id.verification.camera.access.problems": "هل تواجه أية مشكلة؟",
"id.verification.camera.access.skip": "قم بتخطي ملفات الصور وتحميلها بدلاً من ذلك",
"id.verification.camera.access.success": "يبدو أن الكاميرا تعمل وجاهزة.",
"id.verification.camera.access.failure": "يبدو أننا غير قادرين على الوصول إلى الكاميرا. ستحتاج إلى تحميل ملفات الصور الخاصة بك و معرّف الصور الخاص بك.",
"id.verification.camera.access.failure.temporary": "يبدو أننا غير قادرين على الوصول إلى الكاميرا. يرجى التحقق من أن كاميرا الويب متصلة ومن أنك سمحت للمتصفح بالوصول إليها.",
"id.verification.camera.access.failure.temporary.chrome": "تمكين الوصول للكاميرا في متصفّح كروم: ",
"id.verification.camera.access.failure.temporary.chrome.step1": "فتح متصفّح كروم.",
"id.verification.camera.access.click.allow": "رجاءً تأكد من النقر على \"السماح\"",
"id.verification.camera.access.enable": فعيل الكاميرا",
"id.verification.camera.access.problems": "لديك مشاكل؟",
"id.verification.camera.access.skip": "تَخَطَّ و قم برفع ملفات صور بدلاً من ذلك.",
"id.verification.camera.access.success": "يبدو أن كاميرا جهازك جاهزة و تعمل.",
"id.verification.camera.access.failure": "يبدو أننا غير قادرين على الوصول إلى كاميرا جهازك. ستحتاج لرفع ملف صورتك و صورة بطاقة هويتك.",
"id.verification.camera.access.failure.temporary": "يبدو أننا غير قادرين على الوصول إلى الكاميرا. رجاءً تحقق من أن كاميرا جهازك متصلة ومن أنك قد سمحت للمتصفح بالوصول إليها.",
"id.verification.camera.access.failure.temporary.chrome": "لتفعيل الوصول للكاميرا في متصفح كروم:",
"id.verification.camera.access.failure.temporary.chrome.step1": "افتح كروم.",
"id.verification.camera.access.failure.temporary.chrome.step2": "توجه إلى المزيد > الإعدادات.",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "لنظام ويندوز: Alt+F أو Alt+E أو F10 متبوعاً بـ SPACEBAR",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "لنظام ماك: Command+,",
"id.verification.camera.access.failure.temporary.chrome.step3": "ضمن علامة التبويب \"الخصوصية والأمان\"، حدد \"إعدادات الموقع\" ثم \"الكاميرا\".",
"id.verification.camera.access.failure.temporary.chrome.step3": "تحت علامة التبويب \"الخصوصية والأمان\"، اختر \"إعدادات الموقع\" ثم \"الكاميرا\".",
"id.verification.camera.access.failure.temporary.chrome.step4": "ضمن \"قائمة الحظر\"، ابحث عن \"edx.org\" وحدده.",
"id.verification.camera.access.failure.temporary.chrome.step5": "في القسم \"الصلاحيات\"، قم بتحديث صلاحيات الكاميرا إلى \"السماح\".",
"id.verification.camera.access.failure.temporary.ie11": "تمكين الوصول للكاميرا في متصفح انترنت إكسبلورر:",
"id.verification.camera.access.failure.temporary.chrome.step5": "في قسم \"الصلاحيات\"، قم بتحديث صلاحيات الكاميرا إلى \"السماح\".",
"id.verification.camera.access.failure.temporary.ie11": "لتفعيل الوصول للكاميرا في متصفح انترنت إكسبلورر:",
"id.verification.camera.access.failure.temporary.ie11.step1": "افتح إدارة إعدادات Flash Player عن طريق الانتقال إلى إعدادات Windows > لوحة التحكم > Flash Player.",
"id.verification.camera.access.failure.temporary.ie11.step2": "حدد علامة التبويب \"Camera and Mic\" (الكاميرا والميكروفون)، ثم حدد الزر \"Camera and Microphone Settings by Site\" (إعدادات الكاميرا والميكروفون حسب الموقع).",
"id.verification.camera.access.failure.temporary.ie11.step3": "اختر \"edx.org\" من قائمة مواقع ويب وقم بتغيير الصلاحيات من خلال تحديد \"السماح\" في القائمة المنسدلة.",
@@ -232,87 +288,85 @@
"id.verification.camera.access.failure.temporary.firefox.step7": "اختر \"حفظ التغييرات.\"",
"id.verification.camera.access.failure.temporary.safari": "تمكين الوصول للكاميرا في متصفّح سفاري: ",
"id.verification.camera.access.failure.temporary.safari.step1": "فتح متصفّح سفاري.",
"id.verification.camera.access.failure.temporary.safari.step2": "انقر فوق قائمة تطبيق سفاري، ثم حدد \"Preferences\" (التفضيلات). يمكنك أيضاً استخدام الأمرCommand+ كاختصار للوحة المفاتيح",
"id.verification.camera.access.failure.temporary.safari.step3": "حدد علامة التبويب \"مواقع ويب\" ثم حدد \"كاميرا\".",
"id.verification.camera.access.failure.temporary.safari.step2": "انقر فوق قائمة تطبيق سفاري، ثم حدد \"Preferences\" (التفضيلات). يمكنك أيضاً استخدام الأمرCommand+, كاختصار للوحة المفاتيح",
"id.verification.camera.access.failure.temporary.safari.step3": "حدد علامة التبويب \"مواقع الويب\" ثم حدد \"كاميرا\".",
"id.verification.camera.access.failure.temporary.safari.step4": "حدد \"edx.org\" وقم بتغيير صلاحيات الكاميرا إلى \"السماح\".",
"id.verification.camera.access.failure.unsupported": "It looks like your browser does not support camera access.",
"id.verification.camera.access.failure.unsupported.chrome.explanation": "The Chrome browser currently does not support camera access on iOS devices, such as iPhones and iPads.",
"id.verification.camera.access.failure.unsupported.instructions": "Please use another browser to complete Identity Verification.",
"id.verification.photo.tips.title": "تلميحات مفيدة للصورة",
"id.verification.photo.tips.description": "بعد ذلك، سنحتاج منك التقاط صورة لوجهك. يرجى مراجعة التلميحات المفيدة أدناه.",
"id.verification.photo.tips.list.title": "تلميحات الصورة",
"id.verification.camera.access.failure.unsupported": "يبدو أن متصفحك لا يدعم الوصول إلى الكاميرا.",
"id.verification.camera.access.failure.unsupported.chrome.explanation": "لا يدعم متصفح Chrome حاليًا الوصول إلى الكاميرا على أجهزة iOS مثل أجهزة iPhone و iPad.",
"id.verification.camera.access.failure.unsupported.instructions": "رجاءً استخدم متصفحًا آخر لإكمال التحقق من الهوية.",
"id.verification.photo.tips.title": "نصائح مفيدة بخصوص الصورة",
"id.verification.photo.tips.description": "بعد ذلك، سنحتاج منك التقاط صورة لوجهك. يرجى مراجعة النصائح المفيدة أدناه.",
"id.verification.photo.tips.list.title": "نصائح بخصوص الصورة",
"id.verification.photo.tips.list.description": "لالتقاط صورة ناجحة، يُرجى التأكّد ممّا يلي:",
"id.verification.photo.tips.list.well.lit": "أنّ الإضاءة جيّدة على وجهك.",
"id.verification.photo.tips.list.inside.frame": "أنّ وجهك داخل إطار الصورة بالكامل.",
"id.verification.photo.tips.list.well.lit": "أنّ وجهك مُضاء جيدًا.",
"id.verification.photo.tips.list.inside.frame": "أنّ وجهك كلَّه داخل إطار الصورة.",
"id.verification.portrait.photo.title.camera": "التقط صورة لنفسك",
"id.verification.portrait.photo.title.upload": "ارفع صورتك",
"id.verification.portrait.photo.preview.alt": "معاينة صورة وجه المستخدم.",
"id.verification.portrait.photo.instructions.camera": "عندما يكون وجهك في موضعه، استخدم زر التقاط صورة أدناه لالتقاط الصورة.",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ",
"id.verification.camera.help.sight.question": "ماذا لو لم أتمكن من رؤية صورة الكاميرا ؟ أو إذا لم أتمكن من رؤية صورتي لتحديد أي جانب مرئي؟",
"id.verification.camera.help.sight.answer.portrait": "قد تتمكن من إكمال إجراء التقاط الصور من دون مساعدة، ولكن قد يتطلب الأمر بضع محاولات ضبط وضع الكاميرا بشكل صحيح. يختلف وضع الكاميرا المثالي باختلاف الكمبيوتر، ولكن بشكل عام، يكون أفضل موضع للتصوير في الرأس هو 12 إلى 18 بوصة (30-45 سم) تقريبًا من الكاميرا، مع وضع الرأس في المنتصف بالنسبة إلى شاشة الكمبيوتر. إذا تم رفض الصور التي ترسلها، فحاول تحريك اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة.",
"id.verification.camera.help.sight.answer.id": "قد تتمكن من إكمال إجراء التقاط الصور من دون مساعدة، ولكن قد يتطلب الأمر بضع محاولات لضبط وضع الكاميرا بشكل صحيح. يختلف الوضع الأمثل للكاميرا باختلاف جهاز الكمبيوتر، ولكن بشكل عام، يكون أفضل وضع لصورة بطاقة تعريف من 8 إلى 12 بوصة (من 20 إلى 30 سم) عن الكاميرا، مع وضع بطاقة الهوية في الوسط بالنسبة للكاميرا. إذا تم رفض الصور التي ترسلها، فحاول تحريك اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة. إن السبب الأكثر شيوعاً للرفض هو عدم القدرة على قراءة النص الموجود على بطاقة الهوية.",
"id.verification.portrait.photo.instructions.camera": "عندما يكون وجهك في موضعه، استخدم زر 'التقاط صورة' أدناه لالتقاط صورتك.",
"id.verification.camera.help.sight.question": "ماذا إن لم أتمكن من رؤية صورة الكاميرا أو إن لم أتمكن من رؤية صورتي لتحديد أي جانب مرئي؟",
"id.verification.camera.help.sight.answer.portrait": "قد تتمكن من إكمال إجراء التقاط الصور دون مساعدة، لكن قد يتطلب الأمر بضع محاولات لضبط موضع الكاميرا بشكل صحيح.يختلف موضع الكاميرا المثالي من حاسوب ﻵخر، لكن عمومًا يكون أفضل موضع لتصوير الرأس تقريبًا على بعد 12-18 بوصة (30-45 سنتمترًا) من الكاميرا، مع وضع رأسك في المنتصف بالنسبة إلى شاشة الحاسوب. إن تم رفض الصور التي ترسلها، فحاول تغيير اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة.",
"id.verification.camera.help.sight.answer.id": "قد تتمكن من إكمال إجراء التقاط الصور دون مساعدة، لكن قد يتطلب الأمر بضع محاولات لضبط موضع الكاميرا بشكل صحيح.يختلف موضع الكاميرا المثالي من حاسوب ﻵخر، لكن عمومًا يكون أفضل موضع لتصوير بطاقة تعريف تقريبًا على بعد 8-12 بوصة (20-30 سنتمترًا) من الكاميرا، مع وضع البطاقة في المنتصف بالنسبة للكاميرا. إن تم رفض الصور التي ترسلها، فحاول تغيير اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة. إن أكثر سبب للرفض هو عدم القدرة على قراءة نص بطاقة التعريف.",
"id.verification.camera.help.difficulty.question.portrait": "ماذا لو واجهت صعوبة في تثبيت رأسي في الموضع المناسب للكاميرا؟",
"id.verification.camera.help.difficulty.question.id": "ماذا لو واجهت صعوبة في تثبيت بطاقة هويتي في الموضع المناسب للكاميرا؟",
"id.verification.camera.help.difficulty.answer": ذا كنت بحاجة إلى المساعدة في التقاط صورة للتقديم، فاتصل بدعم edX للحصول على اقتراحات إضافية.",
"id.verification.camera.help.upload.question": "What if I want to upload a photo instead?",
"id.verification.camera.help.upload.answer": "On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.",
"id.verification.id.photo.unclear.question": "هل صورة بطاقة الهوية غير واضحة أو ضبابية؟",
"id.verification.id.tips.title": "تلميحات مفيدة للهوية ",
"id.verification.id.tips.description": "بعد ذلك، سنكون بحاجة إلى التقاط صورة لبطاقة هوية صالحة تتضمن اسمك الكامل وصورتك. يرجى تجهيز بطاقة هويتك.",
"id.verification.id.tips.list.well.lit": "إضاءة بطاقة هويتك جيدة.",
"id.verification.camera.help.difficulty.answer": ن احتجت لمساعدة في التقاط صورة لإرسالها، فاتصل بدعم {siteName} للحصول على اقتراحات إضافية.",
"id.verification.id.photo.unclear.question": "هل صورة بطاقة تعريفك غير واضحة أو ضبابية جدًا؟",
"id.verification.id.tips.title": "نصائح مفيدة بخصوص بطاقة التعريف",
"id.verification.id.tips.description": "بعد ذلك، عليك التقاط صورة لبطاقة تعريف صالحة تتضمن اسمك الكامل مع صورة، مثل رخصة القيادة أو جواز السفر.يرجى منك تجهيز بطاقة تعريفك.",
"id.verification.id.tips.list.well.lit": "أن تكون بطاقة تعريفك مضاءة جيدًا.",
"id.verification.id.tips.list.clear": "تأكد من قدرتك على رؤية صورتك وقراءة اسمك بوضوح.",
"id.verification.id.photo.title.camera": "التقط صورة لبطاقتك الشخصية",
"id.verification.id.photo.title.upload": "حمّل صورة لهويتك",
"id.verification.id.photo.title.camera": "التقط صورة لبطاقة تعريفك",
"id.verification.id.photo.title.upload": "ارفع صورة لبطاقة تعريفك",
"id.verification.id.photo.preview.alt": "معاينة صورة الهوية.",
"id.verification.id.photo.instructions.camera": "عندما تكون بطاقة هويتك في موضعها، استخدم زر التقاط صورة أدناه لالتقاط الصورة.",
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "التحقق من اسم الحساب",
"id.verification.account.name.instructions": "يجب أن يكون الاسم الموجود على حسابك والاسم الموجود على المعرّف الخاص بك متطابقًا تمامًا. إذا لم يكن الأمر كذلك، فيرجى النقر فوق \"لا\" لتحديث اسم حسابك.",
"id.verification.account.name.radio.label": "هل يتطابق الاسم الموجود على هويتك مع اسم الحساب أدناه؟",
"id.verification.account.name.radio.yes": "نعم",
"id.verification.account.name.radio.no": "لا",
"id.verification.account.name.error": "يرجى تحديث اسم الحساب لمطابقة الاسم على بطاقة الهوية.",
"id.verification.account.name.warning.prefix": "يُرجى الملاحظة:",
"id.verification.id.photo.instructions.camera": "عندما تكون بطاقتك في موضعها، استخدم زر 'التقاط صورة' أدناه لالتقاط صورتك. يرجى استخدام جواز سفر أو رخصة قيادة أو بطاقة تعريف أخرى تتضمن اسمك الكامل وصورة لوجهك.",
"id.verification.id.photo.instructions.upload": "يرجى رفع صورة لبطاقة تعريفك. تأكد من أن البطاقة كاملة داخل الإطار و أنها مضاءة جيدًا. يجب أن يكون حجم الملف أقل من 10 ميجابايت. الأنساق المدعومة:",
"id.verification.id.photo.instructions.upload.error.invalidFileType": "الملف الذي حددته ليس ضمن أنواع الصورة المدعومة. اختر رجاءً من بين الأنساق التالية:",
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "الملف الذي حددته كبير جداً. رجاءً أعد المحاولة باستخدام ملف ذي حجم أقل من 10 ميجابايت.",
"id.verification.name.check.title": "تحقق مرة أخرى من اسمك",
"id.verification.name.check.instructions": "هل الاسم أدناه يطابق الاسم الموجود في بطاقة تعريفك ذات الصورة. إن لم يكن كذلك، فقم بتحديث الاسم أدناه ليطابق بطاقة تعريفك.",
"id.verification.name.check.mismatch.information": "إن كان الاسم أدناه لا يطابق بطاقة تعريفك، فسيتم رفض تأكيد هويتك.",
"id.verification.name.error": "رجاءً أدخل اسمك كما يظهر في بطاقة تعريفك ذات الصورة.",
"id.verification.account.name.warning.prefix": "تُرجى الملاحظة:",
"id.verification.account.name.settings": "إعدادات الحساب",
"id.verification.account.name.label": "اسم الحساب",
"id.verification.account.name.photo.alt": "صورة من هويتك للتقديم.",
"id.verification.account.name.save": "حفظ ثم التالي",
"id.verification.name.label": "الاسم",
"id.verification.account.name.photo.alt": "صورة بطاقة هويتك التي ستُسلَّم.",
"id.verification.review.title": "مراجعة صورك",
"id.verification.review.description": "يُرجى التأكّد من أنّ الصور والمعلومات التي قدّمتها تمكّننا من التحقّق من هويّتك. ",
"id.verification.review.portrait.label": "صورتك الشخصية",
"id.verification.review.portrait.alt": "صورة شخصية للتقديم.",
"id.verification.review.portrait.retake": "إعادة التقاط الصورة شخصية",
"id.verification.review.id.label": "معرّف صورتك",
"id.verification.review.id.alt": "صورة من هويتك للتقديم.",
"id.verification.review.id.retake": "إعادة التقاط صورة الهوية",
"id.verification.review.confirm": "إرسال",
"id.verification.submission.alert.error.face": "مطلوب صورة لوجهك. يرجى إعادة التقاط الصورة الشخصية.",
"id.verification.submission.alert.error.id": "مطلوب صورة لبطاقة هويتك. يرجى إعادة التقاط صورة بطاقة الهوية.",
"id.verification.submission.alert.error.name": "مطلوب اسم حساب صالح. يرجى تحديث اسم حسابك لمطابقة الاسم على هويتك.",
"id.verification.submission.alert.error.unsupported": "One or more of the files you have uploaded is in an unsupported format. Please choose from the following: ",
"id.verification.review.error": "صفحة فريق دعم edX ",
"id.verification.submitted.title": "جارِ التحقق من الهوية",
"id.verification.submitted.text": "لقد تلقينا معلوماتك وجاري الآن العمل على التحقق من هويتك. ستصلك رسالة على لوحة المعلومات عند اكتمال عملية التحقق (عادةً خلال 5 أيام). في غضون ذلك، لا يزال بإمكانك الوصول إلى كل محتوى المساق المتوفر.",
"id.verification.return.dashboard": "العودة إلى لوحة المعلومات",
"id.verification.review.portrait.alt": "صورة وجهك التي ستُسلَّم.",
"id.verification.review.portrait.retake": "إعادة التقاط الصورة الشخصية",
"id.verification.review.id.label": "بطاقة تعريفك",
"id.verification.review.id.alt": "صورة بطاقة تعريفك التي ستُسلَّم.",
"id.verification.review.id.retake": "إعادة التقاط صورة بطاقة الهوية",
"id.verification.review.confirm": "تسليم",
"id.verification.submission.alert.error.face": "مطلوبة صورة لوجهك. رجاءً أعد التقاط صورتك الشخصية.",
"id.verification.submission.alert.error.id": "مطلوبة صورة لبطاقة تعريفك. رجاءً أعد التقاط صورة لبطاقة تعريفك.",
"id.verification.submission.alert.error.name": "مطلوب اسم حساب صحيح. يرجى تحديث اسم حسابك لمطابقة الاسم على بطاقة تعريفك.",
"id.verification.submission.alert.error.unsupported": "واحد أو أكثر من الملفات التي قمت برفعها في نَسقٍ غير مدعوم. رجاءً اختر مما يلي:",
"id.verification.review.error": "{siteName} صفحة دعم",
"id.verification.submitted.title": "التحقق من الهوية جارٍ",
"id.verification.submitted.text": "لقد تلقينا معلوماتك و نحن الآن نتحقق من هويتك. سيتم إخطارك عند اكتمال عملية التحقق (عادةً في ظرف 5 أيام). إلى ذلكم الحين، لا يزال يمكنك الوصول لجميع محتويات المساق المتاحة.",
"id.verification.return.dashboard": "العودة للوحة المعلومات",
"id.verification.return.course": "العودة للمساق",
"id.verification.photo.upload.help.title": "Upload a Photo Instead",
"id.verification.photo.camera.help.title": "Use Your Camera Instead",
"id.verification.photo.upload.help.text": "If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.",
"id.verification.photo.camera.help.text": "If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.",
"id.verification.upload.help.button": "Switch to Upload Mode",
"id.verification.camera.help.button": "Switch to Camera Mode",
"id.verification.choose.mode.title": "Photo Requirements Options",
"id.verification.choose.mode.hep.text": "To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.",
"id.verification.choose.mode.radio.upload": "Upload photos from my device",
"id.verification.choose.mode.radio.camera": "Take pictures using my camera",
"id.verification.account.name.managed.alert": "Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help.",
"id.verification.request.camera.access.instructions": "لالتقاط صورة باستخدام كاميرا الويب، قد تتلقى طلب المتصفح للوصول إلى الكاميرا. {clickAllow}",
"id.verification.requirements.account.managed.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help before completing the Photo Verification process.",
"id.verification.requirements.card.device.text": "أنت بحاجة إلى جهاز مزود بكاميرا. إذا تلقيت طلب المتصفح للوصول إلى الكاميرا، فيرجى التأكد من النقر فوق {السماح}.",
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
"idv.submission.alert.error": "\nواجهنا خطأ فني أثناء محاولة رفع طلب التحقق من الهوية\nقد تكون هذه مشكلة مؤقتة، لذا يرجى المحاولة مرة أخرى بعد بضع دقائق\nعند استمرار المشكلة يرجى الانتقال إلى {support_link} للحصول على المساعدة",
"id.verification.account.name.edit": "Edit {sr}"
"id.verification.return.generic": "العودة",
"id.verification.photo.upload.help.title": "قم يدلا من هذا برفع صورة",
"id.verification.photo.camera.help.title": "استخدم الكاميرا بدلاً من هذا",
"id.verification.photo.upload.help.text": "إن واجهتك مشكلة في استخدام مُلتقط الصور أعلاه، فقد ترغب بدﻷ من ذلك في رفع صورة. لرفع صورة، انقر على الزر أدناه.",
"id.verification.photo.camera.help.text": "إن واجهتك مشكلة في رفع صورة أعلاه، فقد ترغب بدلا من ذلك في استخدام كاميرا جهازك. لاستخدام الكاميرا، انقر على الزر أدناه.",
"id.verification.upload.help.button": "انتقل إلى وضع الرفع",
"id.verification.camera.help.button": "انتقل إلى وضع الكاميرا",
"id.verification.request.camera.access.instructions": "حتى تلتقط صورة باستخدام الكاميرا، قد تتلقى طلبًا من المتصفح للوصول إلى الكاميرا. {clickAllow}",
"id.verification.requirements.account.managed.alert": "إعدادات حسابك يديرها {managerTitle}. إن لم يكن الاسم في بطاقة هويتك ذات الصورة. مطابقًا للاسم الذي في حسابك، فيرجى الاتصال بالمسؤول {profileDataManager} أو ب{support} للحصول على المساعدة قبل إتمام عملية التحقق من الصورة..",
"id.verification.requirements.card.device.text": "أنت بحاجة إلى جهاز مزود بكاميرا. إذا تلقيت طلبًا من المتصفح للوصول إلى كاميرا جهازك، فتأكد رجاءً من النقر على {السماح}.",
"id.verification.account.name.summary.alert": "إعدادات حسابك يديرها {managerTitle}. إن لم يكن الاسم في بطاقة هويتك ذات الصورة. مطابقًا للاسم الذي في حسابك، فيرجى الاتصال بالمسؤول {profileDataManager} أو ب{support} للحصول على المساعدة قبل إتمام عملية التحقق من الصورة..",
"idv.submission.alert.error": "\nواجهنا خطأ فني أثناء محاولة رفع طلب التحقق من الهوية\nقد تكون هذه مشكلة مؤقتة، لذا يرجى المحاولة مجددًا بعد بضع دقائق\nإن استمرت المشكلة، فيرجى الذهاب إلى {support_link} للحصول على المساعدة.",
"id.verification.account.name.edit": "تعديل {sr}",
"notification.preference.heading": "Notifications",
"notification.preference.app.title": "{\n key, select,\n discussion {Discussions}\n coursework {Course Work}\n other {{key}}\n }",
"notification.preference.title": "{\n text, select,\n core {Core notifications}\n newDiscussionPost {New discussion posts}\n newQuestionPost {New question posts}\n other {{text}}\n }",
"notification.preference.type.label": "Type",
"notification.preference.web,label": "Web",
"notification.preference.help.email": "Email",
"notification.preference.help.push": "Push",
"notification.preference.load.more.courses": "Load more courses",
"notification.preference.guide.link": "as detailed here",
"notification.preference.guide.body": "Notifications for certain activities are enabled by default, "
}

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