Compare commits

..

372 Commits

Author SHA1 Message Date
Bilal Qamar
ff5281408b fix: updated package-lock to resolve dependency tree issue 2022-10-26 16:13:58 +05:00
renovate[bot]
e880c3b59c fix(deps): update react monorepo to v18 2022-10-24 14:31:23 +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
Bianca Severino
d1548b7287 Merge pull request #426 from edx/bseverino/fix-reroute
[MST-717] Reroute user to summary panel in A/B test
2021-04-07 11:12:03 -04:00
Bianca Severino
d1de13469f fix: reroute user to summary panel in a/b test
The new reroutes introduced in the current A/B test override the
summary panel reroute, resulting in incorrect behavior. This fix
ensures that, if a user has reached the summary panel once,
they are properly rerouted from the camera/upload panels when
going back to retake or reupload their photos.
2021-04-06 16:03:50 -04:00
alangsto
62418db3ba Merge pull request #425 from edx/alangsto/enable_camera_experiment
feat: Allow users in experiment to continue with upload if no camera access
2021-04-05 14:51:41 -04:00
Alie Langston
24f70972d8 feat: Allow users in experiment to continue with upload if no camera access
MST-716. If users are part of the A/B experiment for photo upload on the IDV flow, they should be allowed to continue with photo upload if they deny camera access or camera access is unsupported. If they have already denied camera access, they will not be allowed to switch between upload and photo capture mode throughout the flow. Instead the user will only be allowed to complete the flow by uploading photos from their device.
2021-04-05 13:42:38 -04:00
edX Transifex Bot
a7c783888f fix(i18n): update translations 2021-04-04 17:11:06 -04:00
Bianca Severino
00b1bf58a5 Merge pull request #422 from edx/bseverino/validate-file-type
[MST-718] Add extra validation to IDV image upload
2021-04-02 11:01:28 -04:00
Bianca Severino
b2c08193f7 fix: add extra validation to IDV image upload 2021-04-01 15:21:53 -04:00
alangsto
2bbe7dc3f1 Merge pull request #423 from edx/alangsto/update_context_pages_for_experiment
Added upload help text to photo context panels
2021-03-31 15:02:34 -04:00
Alie Langston
4d93fd6c0f Added upload help text to photo context panels
updated messages
2021-03-31 14:22:29 -04:00
edX Transifex Bot
613229bbd1 fix(i18n): update translations 2021-03-28 17:05:42 -04:00
alangsto
39db13435f Merge pull request #419 from edx/alangsto/remove_summary_upload_for_experiment
Remove option to upload if user is in experiment
2021-03-26 11:41:38 -04:00
Alie Langston
994f22ab41 MST-654. Remove option to upload on summary page if user is in experiment
remove import
2021-03-26 11:22:18 -04:00
alangsto
579654c092 Merge pull request #416 from edx/alangsto/add_upload_option
MST-652: Add option to upload photos if user is in experiment
2021-03-23 08:15:03 -04:00
Alie Langston
6094509679 allow upload options if user is in experiment
allow upload options if user is in experiment

testing

removed unused import

fixed unit test and corrected button text

combined assignment line for values coming from context

fixed error in message

added testing for photo mode panel

restructured

added checks for portrait and ID panels

removed unused import, added checks for reroutes

added tests and corrected messages

fix
2021-03-22 16:36:04 -04:00
Bianca Severino
82170f383f Merge pull request #418 from edx/bseverino/idv-profile-manager
[MST-535] Prevent IDV name change if name is managed by a third party
2021-03-22 11:21:42 -04:00
Bianca Severino
addc3640cc Prevent user from attempting name change during IDV if their account is managed by a third party 2021-03-22 11:11:45 -04:00
edX Transifex Bot
b9c0069117 fix(i18n): update translations 2021-03-21 17:15:38 -04:00
Bianca Severino
11c31ba216 Merge pull request #415 from edx/revert-414-bseverino/idv-enterprise-alert
Revert "[MST-535] Prevent IDV name change if name is managed by a third party"
2021-03-18 15:56:14 -04:00
Bianca Severino
ccd2a5c074 Revert "[MST-535] Prevent IDV name change if name is managed by a third party" 2021-03-18 15:51:22 -04:00
Bianca Severino
ff2e86fa13 Merge pull request #414 from edx/bseverino/idv-enterprise-alert
[MST-535] Prevent IDV name change if name is managed by a third party
2021-03-18 10:45:53 -04:00
Bianca Severino
f71033232e Prevent user from attempting name change during IDV if their account is managed by a third party 2021-03-18 10:14:10 -04:00
Bianca Severino
1ff20786ff Merge pull request #413 from edx/bseverino/optional-name
[MST-704] Make name parameter optional in IDV submission
2021-03-12 13:18:15 -05:00
Bianca Severino
232bead0c9 Only submit full_name with IDV photos if the account name should be changed 2021-03-12 13:05:55 -05:00
Bianca Severino
bd0c879a51 Merge pull request #411 from edx/bseverino/optimizely
[MST-655] Add Optimizely to be used with IDV experiments
2021-03-11 11:56:20 -05:00
Bianca Severino
a52e1ed2a5 Add optimizely to be used with IDV 2021-03-10 09:58:00 -05:00
Awais Jibran
b8356bc962 Revert "Workaround an issue between React and Google Translate (#397)" (#412)
This reverts commit fa4c5ef872.
2021-03-10 13:14:10 +05:00
Adam Butterworth
240e589310 Update renovate.json (#410) 2021-03-01 10:49:50 -05:00
Renovate Bot
152d3230d1 fix(deps): update dependency @tensorflow-models/blazeface to v0.0.7 2021-02-18 03:27:15 +00:00
Renovate Bot
95bd1cc66f chore(deps): update dependency @edx/frontend-build to v5.6.9 2021-02-17 23:45:23 +00:00
Renovate Bot
5db6d2b319 fix(deps): update dependency @edx/frontend-component-header to v2.2.4 2021-02-08 21:06:42 +00:00
edX Transifex Bot
368bc00321 fix(i18n): update translations 2021-02-07 16:04:18 -05:00
Awais Jibran
fa4c5ef872 Workaround an issue between React and Google Translate (#397)
* Workaround an issue between React and Google Translate

* Adding comments for disabling chrome translate

* linting changes

* linting changes

* linting changes
2021-02-05 22:19:58 +05:00
David Joy
01f7180bb9 Bumping footer version to 10.1.4 (#400) 2021-02-04 15:16:32 -05:00
David Joy
ce79213b21 Bumping frontend-platform to 1.8.4 (#399)
Trying to resolve peer dependency issues with paragon 13.
2021-02-04 13:25:27 -05:00
Renovate Bot
3870732da8 fix(deps): update dependency jslib-html5-camera-photo to v3.1.6 2021-02-04 13:14:51 +00:00
Renovate Bot
60d7a469a7 fix(deps): update dependency @edx/frontend-platform to v1.8.2 2021-02-04 12:37:42 +00:00
Renovate Bot
921107354c fix(deps): update dependency @edx/frontend-component-header to v2.2.3 2021-02-04 11:00:35 +00:00
Renovate Bot
9ea6120ded fix(deps): update dependency @edx/frontend-component-footer to v10.1.3 2021-02-04 10:08:30 +00:00
Renovate Bot
31493af69a chore(deps): update dependency @testing-library/react to v10.4.9 2021-02-04 09:45:00 +00:00
Renovate Bot
732cf11d96 chore(deps): update dependency @testing-library/jest-dom to v5.11.9 2021-02-04 08:24:01 +00:00
David Joy
42c1c49a85 Rebrand: Update to Paragon 13, use brand package, and LINT. (#386)
* Mild style whitespace linting

* Updating to latest paragon and brand.

Needed to update Button/StatefulButton props to use variant, primarily.

* Adding new environment variables.

* Fixing prop-types warning.

* Updating snapshots.  Modal and Button changed primarily.

I’ve reviewed the various snapshots and determined they’re all correct.  The button prop changes are in line with what I’ve seen elsewhere with the new react-bootstrap-based Button component replacing our own button.  The modal changes make sense, as I think we added some focus lock handling.

* Locking dependency versions in package.json

* Removing dataUtils functions, extraneous deps, and updating frontend-build

Committing all these at the same time since they affect package-lock.json together and splitting them out is nearly impossible now.

* Turning the linter on.

Hold on to your butts!

After this commit, there will be ~1600 linting errors to fix in subsequent commits.

* Main app auto-linting.

Not including coaching, demographics, or ID verification.

This is all the whitespace/syntax linting that my auto-formatter fixed.  I did a few small whitespace cleanups after it, but this commit is 95% automatic.

* Removing HeaderFooterLayout

The HeaderFooterLayout was only used in one place.  Collapsing it down again; also means we don’t have to have prop types for it or split it out into a separate file or anything.

* Main app propTypes cleanup

We were missing some propTypes in AccountSettingsPage and EditableField.

JumpNav had a default that was unused, since displayDemographicsLink is Required.

* Main app manual linting

AccountSettingsPage had some function-ordering issues, and some weak equality checking.

EditableField had an if without curly braces, followed by a variable named “value” which obscured a variable with the same name in the parent scope.  I renamed it to “finalValue” to avoid the name reuse.

* Coaching auto-fixed lint errors

These are general whitespace and syntax linting errors that my IDE automatically fixed.  I went in after and tweaked the whitespace a bit cause it didn’t finish the job, but this is 95% automatic.

* Coaching unused prop in CoachingConsent.

* Demographics auto-fixed lint errors.

Again, 95% auto-fixed linting errors, done by my IDE.  I tweaked a few here and there for spacing and such where it didn’t do a perfecto job.

* Demographics Checkboxes manual linting fixes.

A few things here:

- We were double-exporting Checkboxes, once as the default, once as a named export.  I removed the named export.
- Now uses === when checking string equality.
- id prop was always set, so I made it required
- PropTypes.array is not specific enough for the linter, so I found out what “values” and “options” were being set to and made some arrayOf PropTypes declarations that were more specific.
- onChange is also always set, so now it’s required.

* Demographics manual linting fixes for DemographicsSection

Bunch of things here:

- Reordered hasRetrievedDemographicsOptions and addDefaultOption to later in the file where the linter told me to put them.
- ethnicityFieldDisplay did not consistently return - only in the conditional did it return.  Now it uses the conditional to set a value for enthicities and then consistently returns at the end, processing the ethnicities array.
- handleSubmit didn’t use its “values” prop
- handleSubmit was iterating over an object using a for…in loop, which iterates over ALL properties of an object, not just the keys you might expect.  Probably not a problem, but not a good practice either.  It now uses Object.entries to get the iterable properties of the object.
- renderDemographicsServiceIssueWarning should use a || instead of a | for an OR.  It can also use ! instead of == false
- Using === for string equality in showSelfDescribe and showWorkStatusDescribe
- Using the Paragon Hyperlink component instead of an a tag.  It decorates links with additional metadata noting that the link will be external.
- Adding rel=“noopener noreferrer”, which is a security fix.
- Adding missing propTypes for formValues, drafts, forwardRef, and saveMultipleSettings.

* ID Verification auto-fixed linting errors

This commit includes whitespace and syntax linting errors that were auto-fixed by my IDE, with a little manual whitespace fixing by me where it didn’t get it quite right.  95% automated.

* ID Verification circular dependency between IdVerificationContext and AccessBlocked

This commit resolves a circular dependencybetween IdVerificationContext and the AccessBlocked component.

AccessBlocked imported ERROR_REASONS from IdVerificationContext and IdVerificationContext imported AccessBlocked itself.

We resolve this by moving IdVerificationContextProvider out into its own file.  Then it can safely import from AccessBlocked, and both can safely import the context and constants from IdVerificationContext.

This also sets IdVerificationContext as the default export from its file, which is responsible for the majority of the file changes in this commit, where the import shape changed.

* ID verification removing an unused import in SubmittedPanel

* Ignoring @tensorflow-models/blazeface as an unresolved import

We’re depending on an alias for @tensorflow-models/blazeface which confuses eslint.  I’ve just told it to ignore the problem, since the code is valid.

* ID Verification misc manual linting fixes

There’s a number of things in this commit:

- In Camera, we’re using the function version of setState so we can ensure we’re getting the right version of shouldDetect - you’re not supposed to set state from state directly, as there’s no guarantee that it’s still correct, and you might be setting it from stale data.
- In Camera, the takePhoto function inconsistently returned a value.  Returning after calling this.reset() makes it consistently return undefined.
- In Camera, the <button> was missing a type.
- In Camera, it’s also violating two accessibility rules - the video media has no caption, and there’s apparently not supposed to be an accessKey on buttons.  I don’t know how to fix either of those for this code so I’m punting - I’m leaving it to the owning teams.
- IdVerificationPage should be calling Object.prototype.hasOwnProperty instead of assuming a variable has Object prototype functions.
- ImagePreview didn’t set a default prop value for the id prop - I’ve set it to undefined.
- RequestCameraAccessPanel needed a button type on the “Enable” button.
- RequestCameraAccessPanel should use === for string equality.

* ID Verification manual linting fix to rearrange methods in Camera

The linter complained about the order of methods in the Camera component.  This commit rearranges them to suit it.

* Ignoring module.config.js file.

* Fixing package-lock.json after rebase.
2021-02-03 12:37:48 -05:00
alangsto
976652539c Merge pull request #387 from edx/alangsto/update_blazeface
Updated blazeface to es5 compatible version
2021-02-03 09:50:01 -05:00
Alie Langston
1e0b273579 updated blazeface to es5 compatible version 2021-02-03 09:29:14 -05:00
Bianca Severino
5995434b97 Merge pull request #385 from edx/bseverino/retake-photo
[MST-604] Change 'Retake Photo' to 'Retake Photo?'
2021-02-01 09:35:21 -05:00
edX Transifex Bot
2a854d53b6 fix(i18n): update translations 2021-01-31 16:04:06 -05:00
Bianca Severino
695a9e4297 Change 'Retake Photo' to 'Retake Photo?' to lessen learner confusion 2021-01-29 15:42:13 -05:00
Jawayria
8a79046f66 Updated the build status badge to point to travis-ci.com (#352) 2021-01-28 17:22:52 +05:00
Michael Roytman
27b2d3f853 Merge pull request #379 from edx/mroytman/MST-519-update-camera-directions
add a new component to handle camera unsupported errors in the IDV pr…
2021-01-26 16:06:50 -05:00
Michael Roytman
11a6d6b0ee add a new component to handle camera unsupported errors in the IDV process 2021-01-26 11:30:07 -05:00
Renovate Bot
0fd470ad5e fix(deps): update font awesome 2021-01-26 00:39:18 +00:00
Renovate Bot
251c799e18 fix(deps): update dependency qs to v6.9.6 2021-01-25 23:02:18 +00:00
Renovate Bot
69b902081e fix(deps): update dependency @edx/frontend-component-header to v2.2.2 2021-01-25 22:35:13 +00:00
Renovate Bot
b30012fd7b fix(deps): update dependency @edx/frontend-component-footer to v10.1.2 2021-01-25 21:52:13 +00:00
Renovate Bot
8dcf228faa chore(deps): update dependency enzyme-adapter-react-16 to v1.15.6 2021-01-25 20:51:35 +00:00
edX Transifex Bot
3db3f87375 fix(i18n): update translations 2021-01-17 16:13:41 -05:00
David Joy
6b51999890 Bumping frontend-platform to latest. (#372) 2021-01-05 16:47:48 -05:00
edX Transifex Bot
65b6b30f01 fix(i18n): update translations 2021-01-03 16:13:58 -05:00
edX Transifex Bot
00be4845b8 fix(i18n): update translations 2020-12-27 16:13:45 -05:00
visortelle
4a3270d0a8 Edit translation message on account page. (#361)
* Edit translation message on account page. 

Change the translation message from "Spoken languages" to "Spoken language". 
The plural form can confuse a user because select input allows selecting only a single value.

* Edit translation message on account page.

* Force travis build

* Revert src/i18n/messages content

according to https://github.com/edx/frontend-app-account/pull/361#issuecomment-749222209

* Revert "Revert src/i18n/messages content"

This reverts commit eff1daac03.

* Revert src/i18n/messages content

according to
https://github.com/edx/frontend-app-account/pull/361#issuecomment-749222209
2020-12-22 13:36:09 -05:00
edX Transifex Bot
fa91f25ad3 fix(i18n): update translations 2020-12-20 16:13:32 -05:00
Bianca Severino
f7bb06e109 Merge pull request #368 from edx/bseverino/idv-example-card
[MST-494] Clarify photo ID instructions with example image
2020-12-16 15:25:52 -05:00
Bianca Severino
340ec87522 Add example img for ID card and update copy 2020-12-16 13:37:31 -05:00
Renovate Bot
9e6a74c633 fix(deps): update dependency @fortawesome/react-fontawesome to v0.1.13 2020-12-16 01:10:38 +00:00
Adam Stankiewicz
7c05188e5f fix: bump header and footer versions to use logo from config (#365) 2020-12-15 18:42:53 -05:00
Bianca Severino
a19c37fa85 Merge pull request #364 from edx/bseverino/idv-errors
[MST-502] Add more descriptive errors for IDV submission
2020-12-15 13:14:16 -05:00
Bianca Severino
600a2b8fe2 Add more descriptive errors for IDV submission 2020-12-14 14:29:21 -05:00
Bianca Severino
402dbae44f Merge pull request #360 from edx/bseverino/parse-query-string
[MST-567] Allow decoding of query string in IDV flow
2020-12-14 11:40:46 -05:00
edX Transifex Bot
75fddb9b69 fix(i18n): update translations 2020-12-13 16:03:56 -05:00
Bianca Severino
6fff8630d4 Allow parsing of special characters in querystring 2020-12-11 13:13:51 -05:00
Bianca Severino
06b4fc641e Merge pull request #359 from edx/bseverino/remove-course-key
Remove use of course key temporarily
2020-12-09 16:49:22 -05:00
Bianca Severino
19d5411402 Remove course key 2020-12-09 16:43:08 -05:00
Bianca Severino
14160eaeb3 Merge pull request #358 from edx/bseverino/parse-course-key
Parse course_id query string correctly
2020-12-07 16:56:41 -05:00
Bianca Severino
b06138f96d Parse course_id query string correctly 2020-12-07 16:20:44 -05:00
Bianca Severino
01d0b3645b Merge pull request #357 from edx/revert-355-revert-351-bseverino/idv-block-audit
Revert "Revert "Prevent IDV access from audit courses""
2020-12-07 10:30:22 -05:00
Bianca Severino
0a4dcb4eaa Revert "Revert "Prevent IDV access from audit courses"" 2020-12-07 10:18:40 -05:00
edX Transifex Bot
37e338df0f fix(i18n): update translations 2020-12-06 16:13:59 -05:00
Bianca Severino
6a0e27c816 Merge pull request #355 from edx/revert-351-bseverino/idv-block-audit
Revert "Prevent IDV access from audit courses"
2020-12-04 13:05:15 -05:00
Bianca Severino
289341dd91 Revert "Prevent IDV access from audit courses" 2020-12-04 11:59:52 -05:00
edX Transifex Bot
45f9a15885 fix(i18n): update translations 2020-11-22 16:03:56 -05:00
Bianca Severino
f982187be6 Merge pull request #351 from edx/bseverino/idv-block-audit
Prevent IDV access from audit courses
2020-11-16 15:13:04 -05:00
Bianca Severino
9d0d03c402 Prevent IDV access from audit learners 2020-11-16 14:20:39 -05:00
152 changed files with 40011 additions and 25990 deletions

51
.env
View File

@@ -1,19 +1,32 @@
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
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=null
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=''
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_DOB_UPDATE=''
MARKETING_EMAILS_OPT_IN=''
APP_ID=
MFE_CONFIG_API_URL=

View File

@@ -1,23 +1,34 @@
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
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=null='http://localhost:8080'
ENABLE_COPPA_COMPLIANCE=''
ENABLE_DOB_UPDATE=''
MARKETING_EMAILS_OPT_IN=''
APP_ID=
MFE_CONFIG_API_URL=

View File

@@ -1,20 +1,32 @@
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=''
ENABLE_COPPA_COMPLIANCE=''
ENABLE_DOB_UPDATE=''
MARKETING_EMAILS_OPT_IN=''
APP_ID=
MFE_CONFIG_API_URL=

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 }}

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

@@ -0,0 +1,30 @@
name: ci
on:
push:
branches:
- master
pull_request:
jobs:
tests:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16]
npm: [8.5.0]
npm-test:
- i18n_extract
- lint
- test
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- run: npm install -g npm@${{ matrix.npm }}
- 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/lockfileversion-check.yml@master

View File

@@ -0,0 +1,41 @@
name: Update Browserlist DB
on:
schedule:
- cron: '0 0 * * 1'
workflow_dispatch:
jobs:
update-dep:
runs-on: ubuntu-latest
strategy:
matrix:
node: [16]
npm: [8.5.x]
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node }}
- run: npm install -g npm@${{ matrix.npm }}
- run: make requirements
- name: Update dependencies
run: npx browserslist@latest --update-db
- name: setup testeng-ci
run: |
git clone https://github.com/edx/testeng-ci.git
cd $GITHUB_WORKSPACE/testeng-ci
pip install -r requirements/base.txt
- name: create pull request
id: createpullrequest
env:
GITHUB_TOKEN: ${{ secrets.requirements_bot_github_token }}
GITHUB_USER_EMAIL: ${{ secrets.requirements_bot_github_email }}
run: |
cd $GITHUB_WORKSPACE/testeng-ci
python -m jenkins.pull_request_creator --repo-root=$GITHUB_WORKSPACE \
--target-branch="master" --base-branch-name="update-browserslist" \
--commit-message="chore: update browserslist" --pr-title="Update browserslist DB" \
--pr-body="Updated browserlist DB" --delete-old-pull-requests --output-pr-url-for-github-action

1
.gitignore vendored
View File

@@ -16,3 +16,4 @@ temp/babel-plugin-react-intl
*~
/temp
/.vscode
/module.config.js

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
v16

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

View File

@@ -1,3 +1,4 @@
export TRANSIFEX_RESOURCE = frontend-app-account
transifex_resource = frontend-app-account
transifex_langs = "ar,fr,es_419,zh_CN"
@@ -10,8 +11,19 @@ tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transi
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
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 +46,15 @@ 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
# Pulls translations from Transifex.
pull_translations:
tx pull -f --mode reviewed --language=$(transifex_langs)
tx pull -f --mode reviewed --languages=$(transifex_langs)
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:

View File

@@ -1,48 +1,110 @@
|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.
Please tag **@edx/community-engineering** on any PRs or issues. Thanks!
Development
-----------
Introduction
------------
Start Devstack
^^^^^^^^^^^^^^
This is a micro-frontend application responsible for the display and updating of a user's account information.
To use this application `devstack <https://github.com/edx/devstack>`__ must be running and you must be logged into it.
What is the domain of this MFE?
- Start devstack
- Log in (http://localhost:18000/login)
In this MFE: Private user settings UIs. Public facing profile is in a `separate MFE (Profile) <https://github.com/openedx/frontend-app-profile>`_
Start the development server
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- Account settings page
In this project, install requirements and start the development server by running:
- Demographics collection
.. code:: bash
- IDV (Identity Verification)
npm install
npm start # The server will run on port 1997
Installation
------------
Once the dev server is up visit http://localhost: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.
Configuration and Deployment
----------------------------
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
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:
2. Start up Devstack, if it's not already started.
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.
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.org/edx/frontend-app-account.svg?branch=master
:target: https://travis-ci.org/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}

53868
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,49 +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",
"lint": "fedx-scripts eslint",
"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/frontend-component-footer": "10.0.11",
"@edx/frontend-component-header": "2.0.5",
"@edx/frontend-platform": "1.6.1",
"@edx/paragon": "9.1.1",
"@fortawesome/fontawesome-svg-core": "1.2.32",
"@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.12",
"@tensorflow-models/blazeface": "git+https://github.com/alangsto/blazeface.git",
"@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",
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-footer": "11.2.1",
"@edx/frontend-component-header": "3.2.1",
"@edx/frontend-platform": "2.6.2",
"@edx/paragon": "19.20.0",
"@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.1.16",
"@tensorflow-models/blazeface": "0.0.7",
"@tensorflow/tfjs-converter": "3.18.0",
"@tensorflow/tfjs-core": "3.18.0",
"bowser": "2.11.0",
"classnames": "2.3.2",
"core-js": "3.19.3",
"font-awesome": "4.7.0",
"form-urlencoded": "4.0.1",
"formdata-polyfill": "3.0.20",
"formdata-polyfill": "4.0.10",
"history": "4.10.1",
"jslib-html5-camera-photo": "^3.1.3",
"jslib-html5-camera-photo": "3.1.8",
"lodash.camelcase": "4.3.0",
"lodash.debounce": "4.0.8",
"lodash.findindex": "4.6.0",
@@ -57,40 +56,39 @@
"lodash.merge": "4.6.2",
"lodash.omit": "4.5.0",
"lodash.pick": "4.4.0",
"lodash.pickby": "^4.6.0",
"lodash.snakecase": "4.1.1",
"memoize-one": "5.1.1",
"newrelic": "5.13.1",
"lodash.pickby": "4.6.0",
"long": "5.2.0",
"memoize-one": "5.2.1",
"prop-types": "15.7.2",
"qs": "6.9.4",
"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",
"qs": "6.10.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "5.2.1",
"react-router-dom": "5.3.0",
"react-router-hash-link": "1.2.2",
"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.1.2",
"redux-devtools-extension": "2.13.9",
"redux-logger": "3.0.6",
"redux-saga": "1.1.3",
"redux-thunk": "2.3.0",
"regenerator-runtime": "0.13.10",
"reselect": "4.0.0",
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@edx/frontend-build": "5.3.2",
"@testing-library/jest-dom": "^5.11.2",
"@testing-library/react": "^10.4.7",
"codecov": "3.7.2",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.5",
"es-check": "5.0.0",
"husky": "3.0.9",
"jest": "^26.1.0",
"react-test-renderer": "16.8.6",
"@edx/browserslist-config": "1.1.1",
"@edx/frontend-build": "12.3.0",
"@edx/reactifex": "1.1.0",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5",
"enzyme": "3.11.0",
"enzyme-adapter-react-16": "1.15.6",
"react-test-renderer": "18.2.0",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.4"
}
}
}

View File

@@ -1,10 +1,15 @@
<!doctype html>
<html lang="en-us">
<head>
<title>Account | edX</title>
<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="<%=webpackConfig.output.publicPath%>favicon.ico" type="image/x-icon" />
<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>
<div id="root"></div>

View File

@@ -1,9 +1,28 @@
{
"extends": [
"config:base"
"config:base",
"schedule:weekly",
":automergeLinters",
":automergeMinor",
":automergeTesters",
":enableVulnerabilityAlerts",
":rebaseStalePrs",
":semanticCommits",
":updateNotScheduled"
],
"patch": {
"automerge": true
},
"rebaseStalePrs": true
"packageRules": [
{
"matchDepTypes": [
"devDependencies"
],
"matchUpdateTypes": [
"lockFileMaintenance",
"minor",
"patch",
"pin"
],
"automerge": true
}
],
"timezone": "America/New_York"
}

File diff suppressed because it is too large Load Diff

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,46 @@ 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.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: 'Confirm your date of birth',
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 date 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 birth date 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',
@@ -279,18 +439,18 @@ const messages = defineMessages({
},
'account.settings.field.language.proficiencies': {
id: 'account.settings.field.language.proficiencies',
defaultMessage: 'Spoken languages',
description: 'Label for account settings spoken languages field.',
defaultMessage: 'Spoken language',
description: 'Label for account settings spoken language field.',
},
'account.settings.field.language.proficiencies.empty': {
id: 'account.settings.field.language.proficiencies.empty',
defaultMessage: 'Add a spoken language',
description: 'Placeholder for empty account settings spoken languages field.',
description: 'Placeholder for empty account settings spoken language field.',
},
'account.settings.field.language_proficiencies.options.empty': {
id: 'account.settings.field.language_proficiencies.options.empty',
defaultMessage: 'Select a Language',
description: 'Option for an empty value on account settings spoken languages field.',
description: 'Option for an empty value on account settings spoken language field.',
},
'account.settings.field.time.zone': {
@@ -331,7 +491,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': {

View File

@@ -2,20 +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,
@@ -29,5 +25,4 @@ Alert.defaultProps = {
children: undefined,
};
export default Alert;

View File

@@ -65,7 +65,7 @@ class BetaLanguageBanner extends React.Component {
})}
</p>
<div>
<Button onClick={this.handleRevertLanguage} className="btn btn-primary mr-2">
<Button onClick={this.handleRevertLanguage} className="mr-2">
{this.props.intl.formatMessage(
messages['account.settings.banner.beta.language.action.switch.back'],
{ previous_language: previousLanguage.name },

View File

@@ -0,0 +1,148 @@
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 } 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 handleSubmit = (e) => {
e.preventDefault();
const month = e.target.month.value;
const year = e.target.year.value;
const data = month !== '' && year !== '' ? [{ field_name: 'DOB', field_value: `${year}-${month}` }] : [];
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]);
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"
>
{[...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"
>
{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={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
}}
disabledStates={[]}
/>
</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

@@ -1,8 +1,12 @@
/* eslint-disable react/jsx-no-useless-fragment */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Input, StatefulButton, ValidationFormGroup } from '@edx/paragon';
import {
Button, Input, StatefulButton, ValidationFormGroup,
} from '@edx/paragon';
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -14,9 +18,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,
@@ -36,10 +40,15 @@ function EditableField(props) {
onChange,
isEditing,
isEditable,
isGrayedOut,
intl,
...others
} = props;
const id = `field-${name}`;
let inputOptions = options;
if (getConfig().ENABLE_COPPA_COMPLIANCE && name === 'level_of_education' && options) {
inputOptions = options.filter(option => option.value !== 'el');
}
const handleSubmit = (e) => {
e.preventDefault();
@@ -60,33 +69,37 @@ function EditableField(props) {
const renderEmptyLabel = () => {
if (isEditable) {
return <Button onClick={handleEdit} className="btn-link p-0">{emptyLabel}</Button>;
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 value = 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) {
value = selectedOption.label;
};
finalValue = selectedOption.label;
}
}
if (userSuppliedValue) {
value += `: ${userSuppliedValue}`;
finalValue += `: ${userSuppliedValue}`;
}
return value;
return finalValue;
};
const renderConfirmationMessage = () => {
if (!confirmationMessageDefinition || !confirmationValue) return null;
if (!confirmationMessageDefinition || !confirmationValue) {
return null;
}
return intl.formatMessage(confirmationMessageDefinition, {
value: confirmationValue,
});
@@ -97,78 +110,80 @@ 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="btn-primary 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
onClick={handleCancel}
className="btn-outline-primary"
<>
<form onSubmit={handleSubmit}>
<ValidationFormGroup
for={id}
invalid={error != null}
invalidMessage={error}
helpText={helpText}
>
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
</Button>
</p>
</form>
<label className="h6 d-block" htmlFor={id}>{label}</label>
<Input
data-hj-suppress
name={name}
id={id}
type={type}
value={value}
onChange={handleChange}
options={inputOptions}
{...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}
>
{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 onClick={handleEdit} className="ml-3 btn-link">
<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>{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]),
@@ -192,6 +207,7 @@ EditableField.propTypes = {
onChange: PropTypes.func.isRequired,
isEditing: PropTypes.bool,
isEditable: PropTypes.bool,
isGrayedOut: PropTypes.bool,
intl: intlShape.isRequired,
};
@@ -207,9 +223,10 @@ EditableField.defaultProps = {
helpText: undefined,
isEditing: false,
isEditable: true,
isGrayedOut: false,
userSuppliedValue: undefined,
};
export default connect(editableFieldSelector, {
onEdit: openForm,
onCancel: closeForm,

View File

@@ -2,7 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button, StatefulButton, Input, ValidationFormGroup } from '@edx/paragon';
import {
Button, StatefulButton, Input, ValidationFormGroup,
} from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
@@ -16,8 +18,7 @@ import {
} from './data/actions';
import { editableFieldSelector } from './data/selectors';
function EmailField(props) {
const EmailField = (props) => {
const {
name,
label,
@@ -56,7 +57,9 @@ function EmailField(props) {
};
const renderConfirmationMessage = () => {
if (!confirmationMessageDefinition || !confirmationValue) return null;
if (!confirmationMessageDefinition || !confirmationValue) {
return null;
}
return (
<Alert
className="alert-warning mt-n2"
@@ -85,13 +88,15 @@ function EmailField(props) {
const renderEmptyLabel = () => {
if (isEditable) {
return <Button onClick={handleEdit} className="btn-link p-0">{emptyLabel}</Button>;
return <Button variant="link" onClick={handleEdit} className="p-0">{emptyLabel}</Button>;
}
return <span className="text-muted">{emptyLabel}</span>;
};
const renderValue = () => {
if (confirmationValue) return renderConfirmationValue();
if (confirmationValue) {
return renderConfirmationValue();
}
return value || renderEmptyLabel();
};
@@ -120,7 +125,7 @@ function EmailField(props) {
<p>
<StatefulButton
type="submit"
className="btn-primary mr-2"
className="mr-2"
state={saveState}
labels={{
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
@@ -133,13 +138,13 @@ function EmailField(props) {
// 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();
if (saveState === 'pending') { e.preventDefault(); }
}}
disabledStates={[]}
/>
<Button
variant="outline-primary"
onClick={handleCancel}
className="btn-outline-primary"
>
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
</Button>
@@ -151,7 +156,7 @@ function EmailField(props) {
<div className="d-flex align-items-start">
<h6 aria-level="3">{label}</h6>
{isEditable ? (
<Button onClick={handleEdit} className="ml-3 btn-link">
<Button variant="link" onClick={handleEdit} className="ml-3">
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />
{intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
</Button>
@@ -164,8 +169,7 @@ function EmailField(props) {
}}
/>
);
}
};
EmailField.propTypes = {
name: PropTypes.string.isRequired,
@@ -203,7 +207,6 @@ EmailField.defaultProps = {
isEditable: true,
};
export default connect(editableFieldSelector, {
onEdit: openForm,
onCancel: closeForm,

View File

@@ -1,16 +1,20 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { breakpoints, useWindowSize } from '@edx/paragon';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import React from 'react';
import { NavHashLink } from 'react-router-hash-link';
import Scrollspy from 'react-scrollspy';
import messages from './AccountSettingsPage.messages';
import { getConfig } from '@edx/frontend-platform';
import PropTypes from 'prop-types';
function JumpNav({ intl, displayDemographicsLink }) {
const JumpNav = ({
intl,
displayDemographicsLink,
}) => {
const stickToTop = useWindowSize().width > breakpoints.small.minWidth;
return (
<div className="jump-nav">
<div className={classNames('jump-nav', { 'jump-nav-sm position-sticky pt-3': stickToTop })}>
<Scrollspy
items={[
'basic-information',
@@ -34,13 +38,14 @@ function JumpNav({ intl, displayDemographicsLink }) {
{intl.formatMessage(messages['account.settings.section.profile.information'])}
</NavHashLink>
</li>
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && displayDemographicsLink &&
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && displayDemographicsLink
&& (
<li>
<NavHashLink to="#demographics-information">
{intl.formatMessage(messages['account.settings.section.demographics.information'])}
</NavHashLink>
</li>
}
)}
<li>
<NavHashLink to="#social-media">
{intl.formatMessage(messages['account.settings.section.social.media'])}
@@ -64,16 +69,11 @@ function JumpNav({ intl, displayDemographicsLink }) {
</Scrollspy>
</div>
);
}
};
JumpNav.propTypes = {
intl: intlShape.isRequired,
displayDemographicsLink: PropTypes.bool.isRequired,
};
JumpNav.defaultProps = {
displayDemographicsLink: false,
}
export default injectIntl(JumpNav);

View File

@@ -1,16 +1,16 @@
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">
<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

@@ -2,7 +2,6 @@ import React from 'react';
import PropTypes from 'prop-types';
import { TransitionReplace } from '@edx/paragon';
const onChildExit = (htmlNode) => {
// If the leaving child has focus, take control and redirect it
if (htmlNode.contains(document.activeElement)) {
@@ -11,7 +10,9 @@ const onChildExit = (htmlNode) => {
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
// There's no replacement, do nothing.
if (!enteringChild) return;
if (!enteringChild) {
return;
}
// Get all the focusable elements in the entering child and focus the first one
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
@@ -21,15 +22,15 @@ const onChildExit = (htmlNode) => {
}
};
function SwitchContent({ expression, cases, className }) {
const SwitchContent = ({ expression, cases, className }) => {
const getContent = (caseKey) => {
if (cases[caseKey]) {
if (typeof cases[caseKey] === 'string') {
return getContent(cases[caseKey]);
}
return React.cloneElement(cases[caseKey], { key: caseKey });
} else if (cases.default) {
}
if (cases.default) {
if (typeof cases.default === 'string') {
return getContent(cases.default);
}
@@ -47,8 +48,7 @@ function SwitchContent({ expression, cases, className }) {
{getContent(expression)}
</TransitionReplace>
);
}
};
SwitchContent.propTypes = {
expression: PropTypes.string,
@@ -61,5 +61,4 @@ SwitchContent.defaultProps = {
className: null,
};
export default SwitchContent;

View File

@@ -2,9 +2,11 @@
.form-group {
margin-bottom: 1.5rem;
}
h6, .h6 {
margin-bottom: .25rem;
}
.btn-link {
line-height: 1.2;
border: none;
@@ -12,36 +14,33 @@
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;
a {
text-decoration: underline;
}
}
}
.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;
max-width: 500px;
.custom-control-label {
left: 2.25rem;
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,174 @@
/* eslint-disable no-import-assign */
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
fireEvent,
render,
screen,
} from '@testing-library/react';
import { createMemoryHistory } from 'history';
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 history = createMemoryHistory();
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 history={history}>
<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/`;
@@ -97,24 +111,6 @@ class CoachingConsent extends React.Component {
if (get(data, 'status') === 200) {
this.setState({ submissionSuccess: true });
}
}
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) {
@@ -135,28 +131,33 @@ class CoachingConsent extends React.Component {
case VIEWS.NOT_LOADED:
return <PageLoading srMessage="" />;
case VIEWS.LOADED:
return (<CoachingConsentForm
onSubmit={this.handleSubmit}
declineCoaching={this.declineCoaching}
formErrors={this.state.formErrors}
formValues={this.props.formValues}
redirectUrl={this.state.redirectUrl}
profileDataManager={this.props.profileDataManager}
/>);
return (
<CoachingConsentForm
onSubmit={this.handleSubmit}
declineCoaching={this.declineCoaching}
formErrors={this.state.formErrors}
formValues={this.props.formValues}
redirectUrl={this.state.redirectUrl}
profileDataManager={this.props.profileDataManager}
/>
);
case VIEWS.SUCCESS_PENDING:
return <PageLoading srMessage="Submitting..." />;
case VIEWS.SUCCESS:
return (<SuccessMessage
continueUrl={this.state.redirectUrl}
header={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.header'])}
message={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.message'])}
continue={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.continue'])}
/>);
return (
<SuccessMessage
continueUrl={this.state.redirectUrl}
header={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.header'])}
message={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.message'])}
continue={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.continue'])}
/>
);
case VIEWS.DECLINE_PENDING:
return <PageLoading srMessage="Redirecting..." />;
case VIEWS.DECLINED:
return <AutoRedirect redirectUrl={this.state.redirectUrl} />;
default:
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}
}
@@ -233,7 +234,6 @@ AutoRedirect.propTypes = {
CoachingConsent.defaultProps = {
loaded: false,
saveState: undefined,
profileDataManager: null,
};
@@ -251,15 +251,14 @@ 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,
saveState: PropTypes.string,
profileDataManager: PropTypes.string,
};

View File

@@ -8,23 +8,21 @@ 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'])}
@@ -33,12 +31,13 @@ const CoachingForm = props => (
<div>
<form onSubmit={props.onSubmit}>
<div className="py-3">
{
!!props.profileDataManager &&
<ManagedProfileAlert profileDataManager={props.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>
<label className="h6" htmlFor="fullName">
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}
</label>
<Input
type="text"
name="full-name"
@@ -49,7 +48,9 @@ const CoachingForm = props => (
</div>
<div className="py-3">
<ErrorMessage message={props.formErrors.phone_number} />
<label className="h6" htmlFor="phoneNumber">{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}</label>
<label className="h6" htmlFor="phoneNumber">
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}
</label>
<Input
type="text"
name="phone_number"
@@ -64,7 +65,7 @@ const CoachingForm = props => (
</div>
<ErrorMessage message={props.formErrors.coaching} />
<div className="d-flex flex-column align-items-center">
<Button className="w-100 btn-outline-primary" type="submit">
<Button variant="outline-primary" className="w-100" type="submit">
{props.intl.formatMessage(messages['account.settings.coaching.consent.accept-coaching'])}
</Button>
</div>
@@ -123,7 +124,6 @@ ErrorMessage.propTypes = {
ManagedProfileAlert.propTypes = {
profileDataManager: PropTypes.string.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(CoachingForm);

View File

@@ -8,8 +8,7 @@ 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"
@@ -33,10 +32,10 @@ const CoachingToggle = props => (
formId: 'phone_number',
commitValues: props.phone_number,
},
], 'phone_number');
}
], 'phone_number');
}
return props.saveSettings('phone_number', props.phone_number);
}}
}}
/>
<ValidationFormGroup
for="coachingConsent"
@@ -59,6 +58,7 @@ 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,
};

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';
@@ -13,9 +14,7 @@ jest.mock('@edx/frontend-platform/auth');
const IntlCoachingConsent = injectIntl(CoachingConsent);
jest.mock('../../data/selectors', () => {
return jest.fn().mockImplementation(() => ({ coachingConsentPageSelector: () => ({}) }));
});
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ coachingConsentPageSelector: () => ({}) })));
const mockStore = configureStore();

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
@@ -103,10 +101,8 @@ exports[`CoachingConsent disables name field on enterprise user 1`] = `
className="d-flex flex-column align-items-center"
>
<button
className="btn w-100 btn-outline-primary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
className="w-100 btn btn-outline-primary"
disabled={false}
type="submit"
>
Sign up for coaching
@@ -116,7 +112,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"
@@ -254,10 +250,8 @@ exports[`CoachingConsent should render 1`] = `
className="d-flex flex-column align-items-center"
>
<button
className="btn w-100 btn-outline-primary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
className="w-100 btn btn-outline-primary"
disabled={false}
type="submit"
>
Sign up for coaching
@@ -267,7 +261,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,
},
});
@@ -44,7 +47,6 @@ export const fetchSettingsReset = () => ({
type: FETCH_SETTINGS.RESET,
});
// FORM STATE ACTIONS
export const openForm = formId => ({
@@ -69,7 +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

@@ -1,4 +1,3 @@
export const YEAR_OF_BIRTH_OPTIONS = (() => {
const currentYear = new Date().getFullYear();
const years = [];
@@ -11,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) {
@@ -48,16 +55,15 @@ const reducer = (state = defaultState, action) => {
case FETCH_SETTINGS.SUCCESS:
return {
...state,
values: Object.assign({}, state.values, action.payload.values),
values: { ...state.values, ...action.payload.values },
// Dump the providers into thirdPartyAuth.
thirdPartyAuth: Object.assign({}, state.thirdPartyAuth, {
providers: action.payload.thirdPartyAuthProviders,
}),
thirdPartyAuth: { ...state.thirdPartyAuth, providers: action.payload.thirdPartyAuthProviders },
profileDataManager: action.payload.profileDataManager,
timeZones: action.payload.timeZones,
loading: false,
loaded: true,
loadingError: null,
verifiedNameHistory: action.payload.verifiedNameHistory,
};
case FETCH_SETTINGS.FAILURE:
return {
@@ -91,15 +97,14 @@ const reducer = (state = defaultState, action) => {
saveState: null,
errors: {},
drafts: {},
nameChangeModal: false,
};
}
return state;
case UPDATE_DRAFT:
return {
...state,
drafts: Object.assign({}, state.drafts, {
[action.payload.name]: action.payload.value,
}),
drafts: { ...state.drafts, [action.payload.name]: action.payload.value },
saveState: null,
errors: {},
};
@@ -110,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,
@@ -120,19 +134,18 @@ const reducer = (state = defaultState, action) => {
return {
...state,
saveState: 'complete',
values: Object.assign({}, state.values, action.payload.values),
values: { ...state.values, ...action.payload.values },
errors: {},
confirmationValues: Object.assign(
{},
state.confirmationValues,
action.payload.confirmationValues,
),
confirmationValues: {
...state.confirmationValues,
...action.payload.confirmationValues,
},
};
case SAVE_SETTINGS.FAILURE:
return {
...state,
saveState: 'error',
errors: Object.assign({}, state.errors, action.payload.errors),
errors: { ...state.errors, ...action.payload.errors },
};
case SAVE_SETTINGS.RESET:
return {
@@ -161,7 +174,7 @@ const reducer = (state = defaultState, action) => {
return {
...state,
saveState: 'error',
errors: Object.assign({}, state.errors, action.payload.errors),
errors: { ...state.errors, ...action.payload.errors },
};
case FETCH_TIME_ZONES.SUCCESS:
@@ -202,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

@@ -1,4 +1,6 @@
import { call, put, delay, takeEvery, all } from 'redux-saga/effects';
import {
call, put, delay, takeEvery, all,
} from 'redux-saga/effects';
import { publish } from '@edx/frontend-platform';
import { getLocale, handleRtl, LOCALE_CHANGED } from '@edx/frontend-platform/i18n';
@@ -23,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,
@@ -36,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 {
@@ -52,13 +61,16 @@ export function* handleFetchSettings() {
userId,
);
if (values.country) yield put(fetchTimeZones(values.country));
const verifiedNameHistory = yield call(getVerifiedNameHistory);
if (values.country) { yield put(fetchTimeZones(values.country)); }
yield put(fetchSettingsSuccess({
values,
thirdPartyAuthProviders,
profileDataManager,
timeZones,
verifiedNameHistory,
}));
} catch (e) {
yield put(fetchSettingsFailure(e.message));
@@ -91,11 +103,14 @@ export function* handleSaveSettings(action) {
savedValues = yield call(patchSettings, username, commitData, userId);
}
yield put(saveSettingsSuccess(savedValues, commitData));
if (savedValues.country) yield put(fetchTimeZones(savedValues.country));
if (savedValues.country) { yield put(fetchTimeZones(savedValues.country)); }
yield delay(1000);
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));
@@ -104,7 +119,6 @@ export function* handleSaveSettings(action) {
}
}
// handles mutiple settings saved at once, in order, and stops executing on first failure.
export function* handleSaveMultipleSettings(action) {
try {
@@ -125,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));
@@ -138,7 +155,6 @@ export function* handleFetchTimeZones(action) {
yield put(fetchTimeZonesSuccess(response, action.payload.country));
}
export default function* saga() {
yield takeEvery(FETCH_SETTINGS.BASE, handleFetchSettings);
yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings);
@@ -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,9 +140,19 @@ 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;
},
);
/**
* If there's no draft present at all (undefined), use the original committed value.
@@ -86,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;
},
@@ -94,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(
@@ -131,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,
@@ -156,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,
}),
);
@@ -205,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';
@@ -44,7 +45,9 @@ function packAccountCommitData(commitData) {
SOCIAL_PLATFORMS.forEach(({ id, key }) => {
// Skip missing values. Empty strings are valid values and should be preserved.
if (commitData[key] === undefined) return;
if (commitData[key] === undefined) {
return;
}
packedData.social_links = [{ platform: id, social_link: commitData[key] }];
delete packedData[key];
@@ -174,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(),
@@ -192,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,
};
}
@@ -215,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)) {
@@ -234,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,38 +0,0 @@
import camelCase from 'lodash.camelcase';
import snakeCase from 'lodash.snakecase';
export function modifyObjectKeys(object, modify) {
// If the passed in object is not an object, return it.
if (
object === undefined ||
object === null ||
(typeof object !== 'object' && !Array.isArray(object))
) {
return object;
}
if (Array.isArray(object)) {
return object.map(value => modifyObjectKeys(value, modify));
}
// Otherwise, process all its keys.
const result = {};
Object.entries(object).forEach(([key, value]) => {
result[modify(key)] = modifyObjectKeys(value, modify);
});
return result;
}
export function camelCaseObject(object) {
return modifyObjectKeys(object, camelCase);
}
export function snakeCaseObject(object) {
return modifyObjectKeys(object, snakeCase);
}
export function convertKeyNames(object, nameMap) {
const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]);
return modifyObjectKeys(object, transformer);
}

View File

@@ -1,90 +0,0 @@
import {
modifyObjectKeys,
camelCaseObject,
snakeCaseObject,
convertKeyNames,
} from './dataUtils';
describe('modifyObjectKeys', () => {
it('should use the provided modify function to change all keys in and object and its children', () => {
function meowKeys(key) {
return `${key}Meow`;
}
const result = modifyObjectKeys(
{
one: undefined,
two: null,
three: '',
four: 0,
five: NaN,
six: [1, 2, { seven: 'woof' }],
eight: { nine: { ten: 'bark' }, eleven: true },
},
meowKeys,
);
expect(result).toEqual({
oneMeow: undefined,
twoMeow: null,
threeMeow: '',
fourMeow: 0,
fiveMeow: NaN,
sixMeow: [1, 2, { sevenMeow: 'woof' }],
eightMeow: { nineMeow: { tenMeow: 'bark' }, elevenMeow: true },
});
});
});
describe('camelCaseObject', () => {
it('should make everything camelCase', () => {
const result = camelCaseObject({
what_now: 'brown cow',
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
'dot.dot.dot': 123,
});
expect(result).toEqual({
whatNow: 'brown cow',
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
dotDotDot: 123,
});
});
});
describe('snakeCaseObject', () => {
it('should make everything snake_case', () => {
const result = snakeCaseObject({
whatNow: 'brown cow',
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
'dot.dot.dot': 123,
});
expect(result).toEqual({
what_now: 'brown cow',
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
dot_dot_dot: 123,
});
});
});
describe('convertKeyNames', () => {
it('should replace the specified keynames', () => {
const result = convertKeyNames(
{
one: { two: { three: 'four' } },
five: 'six',
},
{
two: 'blue',
five: 'alive',
seven: 'heaven',
},
);
expect(result).toEqual({
one: { blue: { three: 'four' } },
alive: 'six',
});
});
});

View File

@@ -1,9 +1,3 @@
export {
camelCaseObject,
convertKeyNames,
modifyObjectKeys,
snakeCaseObject,
} from './dataUtils';
export {
AsyncActionType,
getModuleState,

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,14 @@ 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: (
<Hyperlink destination={supportArticleUrl}>
{intl.formatMessage(messages[instructionMessageId])}
</Hyperlink>
),
siteName: getConfig().SITE_NAME,
}}
/>
</Alert>

View File

@@ -1,11 +1,14 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Button, Input, Modal, ValidationFormGroup } from '@edx/paragon';
import {
Button, Input, Modal, ValidationFormGroup,
} 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';
@@ -33,10 +36,9 @@ export class ConfirmationModal extends Component {
return null;
}
const headerMessageId = this.getShortErrorMessageId(errorType);
const detailsMessageId =
reason === 'empty-password'
? null
: 'account.settings.delete.account.error.unable.to.delete.details';
const detailsMessageId = reason === 'empty-password'
? null
: 'account.settings.delete.account.error.unable.to.delete.details';
return (
<Alert
@@ -64,11 +66,18 @@ 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}
title={intl.formatMessage(messages['account.settings.delete.account.modal.header'])}
body={
body={(
<div>
{this.renderError()}
<Alert
@@ -76,9 +85,17 @@ export class ConfirmationModal extends Component {
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
>
<h6>
{intl.formatMessage(messages['account.settings.delete.account.modal.text.1'])}
{intl.formatMessage(
messages['account.settings.delete.account.modal.text.1'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>{intl.formatMessage(messages['account.settings.delete.account.modal.text.2'])}</p>
<p>
{intl.formatMessage(
messages[deleteAccountModalText2MessageKey],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<p>
<PrintingInstructions />
</p>
@@ -100,9 +117,9 @@ export class ConfirmationModal extends Component {
/>
</ValidationFormGroup>
</div>
}
)}
buttons={[
<Button className="btn-danger" onClick={onSubmit}>
<Button variant="danger" onClick={onSubmit}>
{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])}
</Button>,
]}

View File

@@ -24,9 +24,13 @@ import ConnectedSuccessModal from './SuccessModal';
import BeforeProceedingBanner from './BeforeProceedingBanner';
export class DeleteAccount extends React.Component {
state = {
password: '',
};
constructor(props) {
super(props);
this.state = {
password: '',
};
}
handleSubmit = () => {
if (this.state.password === '') {
@@ -56,19 +60,42 @@ export class DeleteAccount extends React.Component {
} = this.props;
const canDelete = isVerifiedAccount && !hasLinkedTPA;
// 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">
@@ -77,7 +104,7 @@ export class DeleteAccount extends React.Component {
</p>
<p>
<Button
className="btn-outline-danger"
variant="outline-danger"
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
disabled={!canDelete}
>
@@ -87,8 +114,8 @@ 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-"
/>
)}

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 () {
return <></>;
});
jest.mock('./SuccessModal', () => function () {
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

@@ -11,13 +11,13 @@ export const SuccessModal = (props) => {
<Modal
open={status === 'deleted'}
title={intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
body={
body={(
<div>
<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}

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"
@@ -47,9 +38,7 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
<button
className="btn btn-outline-danger"
disabled={false}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onClick={[MockFunction]}
type="button"
>
Delete My Account
@@ -61,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>
@@ -69,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"
@@ -105,9 +85,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
<button
className="btn btn-outline-danger"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onClick={null}
type="button"
>
Delete My Account
@@ -136,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>
@@ -155,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>
@@ -163,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"
@@ -199,9 +167,7 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
<button
className="btn btn-outline-danger"
disabled={true}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onClick={null}
type="button"
>
Delete My Account
@@ -230,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,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

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { CheckBox } from '@edx/paragon';
import { DECLINED } from '../data/constants';
export const Checkboxes = (props) => {
const Checkboxes = (props) => {
const {
id,
options,
@@ -13,12 +13,12 @@ export const Checkboxes = (props) => {
const [selected, setSelected] = useState(values);
useEffect(() => {
onChange(id, selected)
}, [selected])
onChange(id, selected);
}, [id, onChange, selected]);
const handleToggle = (value, option) => {
// If the user checked 'declined', uncheck all other options
if (value && option == DECLINED) {
if (value && option === DECLINED) {
setSelected([DECLINED]);
return;
}
@@ -33,46 +33,47 @@ export const Checkboxes = (props) => {
if (!value) {
setSelected(selected.filter(i => i !== option));
}
}
};
const renderCheckboxes = () => {
return options.map((option, index) => {
const isFirst = index == 0;
const isChecked = selected.includes(option.value);
return (
<div key={index} className="checkboxOption">
<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)}
/>
</div>
)
})
}
const renderCheckboxes = () => options.map((option, index) => {
const isFirst = index === 0;
const isChecked = selected.includes(option.value);
return (
<div key={option.value} className="checkboxOption">
<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)}
/>
</div>
);
});
return (
<div role="group">
{renderCheckboxes()}
</div>
)
}
);
};
Checkboxes.propTypes = {
id: PropTypes.string,
options: PropTypes.array,
values: PropTypes.array,
onChange: PropTypes.func,
id: PropTypes.string.isRequired,
options: PropTypes.arrayOf(PropTypes.shape({
value: PropTypes.string,
label: PropTypes.string,
})),
values: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func.isRequired,
};
Checkboxes.defaultProps = {
options: [],
values: [],
}
};
export default Checkboxes;

View File

@@ -1,31 +1,109 @@
import {
OTHER,
SELF_DESCRIBE,
} from '../data/constants';
import { getConfig } from '@edx/frontend-platform';
import {
FormattedMessage,
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { saveMultipleSettings, updateDraft } from '../data/actions';
import Alert from '../Alert';
import Checkboxes from './Checkboxes';
import EditableField from '../EditableField';
import { Input } from '@edx/paragon';
import { Hyperlink, Input } from '@edx/paragon';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { demographicsSectionSelector } from '../data/selectors';
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 Checkboxes from './Checkboxes';
import Alert from '../Alert';
import { saveMultipleSettings, updateDraft } from '../data/actions';
import {
OTHER,
SELF_DESCRIBE,
} from '../data/constants';
import messages from './DemographicsSection.messages';
class DemographicsSection extends React.Component {
constructor(props, context) {
super(props, context);
// We check the `demographicsOptions` prop to see if it is empty before we attempt to extract and
// format the available options for each question from the API response.
getApiOptions = memoize((demographicsOptions) => (this.hasRetrievedDemographicsOptions() && {
demographicsGenderOptions: this.addDefaultOption('account.settings.field.demographics.gender.options.empty')
.concat(demographicsOptions.actions.POST.gender.choices.map(key => ({
value: key.value,
label: key.display_name,
}))),
/* Ethnicity options don't need the blank/default option */
demographicsEthnicityOptions: demographicsOptions.actions.POST.user_ethnicity.child.children.ethnicity.choices.map(
key => ({
value: key.value,
label: key.display_name,
}),
),
demographicsIncomeOptions: this.addDefaultOption('account.settings.field.demographics.income.options.empty')
.concat(demographicsOptions.actions.POST.income.choices.map(key => ({
value: key.value,
label: key.display_name,
}))),
demographicsMilitaryHistoryOptions: this.addDefaultOption('account.settings.field.demographics.military_history.options.empty')
.concat(demographicsOptions.actions.POST.military_history.choices.map(key => ({
value: key.value,
label: key.display_name,
}))),
demographicsEducationLevelOptions: this.addDefaultOption('account.settings.field.demographics.education_level.options.empty')
.concat(demographicsOptions.actions.POST.learner_education_level.choices.map(key => ({
value: key.value,
label: key.display_name,
}))),
demographicsWorkStatusOptions: this.addDefaultOption('account.settings.field.demographics.work_status.options.empty')
.concat(demographicsOptions.actions.POST.work_status.choices.map(key => ({
value: key.value,
label: key.display_name,
}))),
demographicsWorkSectorOptions: this.addDefaultOption('account.settings.field.demographics.work_sector.options.empty')
.concat(demographicsOptions.actions.POST.current_work_sector.choices.map(key => ({
value: key.value,
label: key.display_name,
}))),
}));
ethnicityFieldDisplay = (demographicsEthnicityOptions) => {
let ethnicities = [];
if (get(this, 'props.formValues.demographics_user_ethnicity')) {
ethnicities = this.props.formValues.demographics_user_ethnicity;
}
return ethnicities.map((e) => {
const matchingOption = demographicsEthnicityOptions.filter(option => option.value === e)[0];
return matchingOption && matchingOption.label;
}).join(', ');
};
handleEditableFieldChange = (name, value) => {
this.props.updateDraft(name, value);
};
handleSubmit = (formId) => {
// We have some custom fields in this section. Instead of relying on the
// submitted values, submit the values stored in 'drafts'.
const { drafts } = this.props;
const settingsArray = Object.entries(drafts).map(([field, value]) => ({
formId: field,
commitValues: value,
}));
this.props.saveMultipleSettings(settingsArray, formId);
};
/**
* Utility method that adds the specified message as a default option to the list of available
* choices.
*
* @param {*} messageId id of message matching desired default label text
*/
addDefaultOption(messageId) {
return [{
value: '',
label: this.props.intl.formatMessage(messages[messageId]),
}];
}
/**
@@ -38,100 +116,19 @@ class DemographicsSection extends React.Component {
return !isEmpty(this.props.formValues.demographicsOptions);
}
/**
* Utility method that adds the specified message as a default option to the list of available
* choices.
*
* @param {*} messageId id of message matching desired default label text
*/
addDefaultOption(messageId) {
return [{
value: '',
label: this.props.intl.formatMessage(messages[messageId]),
}];
}
// We check the `demographicsOptions` prop to see if it is empty before we attempt to extract and
// format the available options for each question from the API response.
getApiOptions = memoize((demographicsOptions) => ( this.hasRetrievedDemographicsOptions() && {
demographicsGenderOptions: this.addDefaultOption('account.settings.field.demographics.gender.options.empty')
.concat(demographicsOptions.actions.POST.gender.choices.map(key => ({
value: key.value,
label: key.display_name
}))),
/* Ethnicity options don't need the blank/default option */
demographicsEthnicityOptions: demographicsOptions.actions.POST.user_ethnicity.child.children.ethnicity.choices.map(key => ({
value: key.value,
label: key.display_name
})),
demographicsIncomeOptions: this.addDefaultOption('account.settings.field.demographics.income.options.empty')
.concat(demographicsOptions.actions.POST.income.choices.map(key => ({
value: key.value,
label: key.display_name
}))),
demographicsMilitaryHistoryOptions: this.addDefaultOption('account.settings.field.demographics.military_history.options.empty')
.concat(demographicsOptions.actions.POST.military_history.choices.map(key => ({
value: key.value,
label: key.display_name
}))),
demographicsEducationLevelOptions: this.addDefaultOption('account.settings.field.demographics.education_level.options.empty')
.concat(demographicsOptions.actions.POST.learner_education_level.choices.map(key => ({
value: key.value,
label: key.display_name
}))),
demographicsWorkStatusOptions: this.addDefaultOption('account.settings.field.demographics.work_status.options.empty')
.concat(demographicsOptions.actions.POST.work_status.choices.map(key => ({
value: key.value,
label: key.display_name
}))),
demographicsWorkSectorOptions: this.addDefaultOption('account.settings.field.demographics.work_sector.options.empty')
.concat(demographicsOptions.actions.POST.current_work_sector.choices.map(key => ({
value: key.value,
label: key.display_name
}))),
}));
ethnicityFieldDisplay = (demographicsEthnicityOptions) => {
if (get(this, 'props.formValues.demographics_user_ethnicity')) {
const ethnicities = this.props.formValues.demographics_user_ethnicity;
return ethnicities.map((e) => {
var matchingOption = demographicsEthnicityOptions.filter(option => option.value === e)[0];
return matchingOption && matchingOption.label;
}).join(", ")
}
}
handleEditableFieldChange = (name, value) => {
this.props.updateDraft(name, value);
};
handleSubmit = (formId, values) => {
// We have some custom fields in this section. Instead of relying on the
// submitted values, submit the values stored in 'drafts'.
const drafts = this.props.drafts;
const settingsArray = []
for (let field in drafts) {
settingsArray.push({
formId: field,
commitValues: drafts[field]
})
}
this.props.saveMultipleSettings(settingsArray, formId);
};
/**
* If an error is encountered when trying to communicate with the Demographics IDA then we will
* display an Alert letting the user know that their info will not be displayed and temporarily
* cannot be updated.
*/
renderDemographicsServiceIssueWarning() {
if (!isEmpty(this.props.formErrors.demographicsError) |
this.hasRetrievedDemographicsOptions() == false) {
if (!isEmpty(this.props.formErrors.demographicsError)
|| !this.hasRetrievedDemographicsOptions()) {
return (
<div
tabIndex="-1"
ref={this.alertRef}>
ref={this.alertRef}
>
<Alert className="alert alert-danger" role="alert">
<FormattedMessage
id="account.settings.message.demographics.service.issue"
@@ -141,9 +138,8 @@ class DemographicsSection extends React.Component {
</Alert>
</div>
);
} else {
return null;
}
return null;
}
render() {
@@ -162,26 +158,35 @@ class DemographicsSection extends React.Component {
demographicsWorkSectorOptions,
} = this.getApiOptions(this.props.formValues.demographicsOptions);
const showSelfDescribe = this.props.formValues.demographics_gender == SELF_DESCRIBE;
const showWorkStatusDescribe = this.props.formValues.demographics_work_status == OTHER;
const showSelfDescribe = this.props.formValues.demographics_gender === SELF_DESCRIBE;
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>
<a href={getConfig().MARKETING_SITE_BASE_URL + '/demographics'} target="_blank">
{this.props.intl.formatMessage(messages['account.settings.section.demographics.why'])}
</a>
<Hyperlink
destination={`${getConfig().MARKETING_SITE_BASE_URL}/demographics`}
target="_blank"
rel="noopener noreferrer"
>
{this.props.intl.formatMessage(
messages['account.settings.section.demographics.why'],
{
siteName: getConfig().SITE_NAME,
},
)}
</Hyperlink>
</p>
{this.renderDemographicsServiceIssueWarning()}
{/*
{/*
If the demographicsOptions props are empty then there is no need to display the fields as
the user will not have any choices available to select, nor will they be able to update
their answers.
*/}
{ this.hasRetrievedDemographicsOptions() &&
{this.hasRetrievedDemographicsOptions() && (
<div id="demographics-fields">
<EditableField
name="demographics_gender"
@@ -193,18 +198,18 @@ class DemographicsSection extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender.empty'])}
{...editableFieldProps}
>
{showSelfDescribe &&
{showSelfDescribe && (
<Input
name='demographics_gender_description'
id='field-demographics_gender_description'
type='text'
name="demographics_gender_description"
id="field-demographics_gender_description"
type="text"
placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description.empty'])}
value={this.props.formValues.demographics_gender_description}
onChange={(e) => this.handleEditableFieldChange(`demographics_gender_description`, e.target.value)}
onChange={(e) => this.handleEditableFieldChange('demographics_gender_description', e.target.value)}
aria-label={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description'])}
className="mt-1"
/>
}
)}
</EditableField>
<EditableField
name="demographics_user_ethnicity"
@@ -262,24 +267,26 @@ class DemographicsSection extends React.Component {
name="demographics_work_status"
type="select"
value={this.props.formValues.demographics_work_status}
userSuppliedValue={showWorkStatusDescribe ? this.props.formValues.demographics_work_status_description : null}
userSuppliedValue={showWorkStatusDescribe
? this.props.formValues.demographics_work_status_description
: null}
options={demographicsWorkStatusOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status'])}
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status.empty'])}
{...editableFieldProps}
>
{showWorkStatusDescribe &&
{showWorkStatusDescribe && (
<Input
name='demographics_work_status_description'
id='field-demographics_work_status_description'
type='text'
name="demographics_work_status_description"
id="field-demographics_work_status_description"
type="text"
placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status_description.empty'])}
value={this.props.formValues.demographics_work_status_description}
onChange={(e) => this.handleEditableFieldChange(`demographics_work_status_description`, e.target.value)}
onChange={(e) => this.handleEditableFieldChange('demographics_work_status_description', e.target.value)}
aria-label={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status_description'])}
className="mt-1"
/>
}
)}
</EditableField>
<EditableField
name="demographics_current_work_sector"
@@ -300,17 +307,17 @@ class DemographicsSection extends React.Component {
{...editableFieldProps}
/>
</div>
}
)}
</div>
)
);
}
};
}
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,
@@ -318,11 +325,30 @@ DemographicsSection.propTypes = {
demographics_work_status: PropTypes.string,
demographics_current_work_sector: PropTypes.string,
demographics_future_work_sector: PropTypes.string,
demographics_work_status_description: PropTypes.string,
demographics_gender_description: PropTypes.string,
demographicsOptions: PropTypes.shape({}),
}).isRequired,
drafts: PropTypes.shape({
demographics_gender: PropTypes.string,
demographics_user_ethnicity: PropTypes.shape([]),
demographics_income: PropTypes.string,
demographics_military_history: PropTypes.string,
demographics_learner_education_level: PropTypes.string,
demographics_parent_education_level: PropTypes.string,
demographics_work_status: PropTypes.string,
demographics_current_work_sector: PropTypes.string,
demographics_future_work_sector: PropTypes.string,
demographics_work_status_description: PropTypes.string,
demographics_gender_description: PropTypes.string,
demographicsOptions: PropTypes.shape({}),
}).isRequired,
formErrors: PropTypes.shape({
demographicsError: PropTypes.string,
}).isRequired,
updateDraft: PropTypes.func.isRequired
forwardRef: PropTypes.func.isRequired,
updateDraft: PropTypes.func.isRequired,
saveMultipleSettings: PropTypes.func.isRequired,
};
export default connect(demographicsSectionSelector, {

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

@@ -22,8 +22,7 @@ export function createDemographicsError(error) {
delete apiError.fieldErrors.gender_description;
} else if (get(apiError, 'fieldErrors.work_status_description')) {
// eslint-disable-next-line prefer-destructuring
apiError.fieldErrors.demographics_work_status =
apiError.fieldErrors.work_status_description[0];
apiError.fieldErrors.demographics_work_status = apiError.fieldErrors.work_status_description[0];
delete apiError.fieldErrors.work_status_description;
}
// Otherwise, when the service is down, the error response will not contain a

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 { useHistory } 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 { push } = useHistory();
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();
push(`/id-verification?next=${encodeURIComponent('account/settings')}`);
}
}, [handleClose, push, 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,173 @@
/* eslint-disable no-import-assign */
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
fireEvent,
render,
screen,
} from '@testing-library/react';
import { createMemoryHistory } from 'history';
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 history = createMemoryHistory();
const IntlNameChange = injectIntl(NameChange);
const mockStore = configureStore();
describe('NameChange', () => {
let props = {};
let store = {};
const reduxWrapper = children => (
<Router history={history}>
<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(history.location.pathname).toEqual('/id-verification');
});
});

View File

@@ -5,20 +5,17 @@ import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import Alert from '../Alert';
const RequestInProgressAlert = (props) => {
return (
<Alert
className="alert-warning mt-n2"
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
>
<FormattedMessage
id="account.settings.editable.field.password.reset.button.forbidden"
defaultMessage="Your previous request is in progress, please try again in few moments."
description="A message displayed when a previous password reset request is still in progress."
/>
</Alert>
);
};
const RequestInProgressAlert = () => (
<Alert
className="alert-warning mt-n2"
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
>
<FormattedMessage
id="account.settings.editable.field.password.reset.button.forbidden"
defaultMessage="Your previous request is in progress, please try again in few moments."
description="A message displayed when a previous password reset request is still in progress."
/>
</Alert>
);
export default RequestInProgressAlert;

View File

@@ -22,7 +22,7 @@ const ResetPassword = (props) => {
</h6>
<p>
<StatefulButton
className="btn-link"
variant="link"
state={status}
onClick={(e) => {
// Swallow clicks if the state is pending.

View File

@@ -1,6 +1,8 @@
import { put, call, takeEvery } from 'redux-saga/effects';
import { resetPasswordBegin, resetPasswordForbidden, resetPasswordSuccess, RESET_PASSWORD } from './actions';
import {
resetPasswordBegin, resetPasswordForbidden, resetPasswordSuccess, RESET_PASSWORD,
} from './actions';
import { postResetPassword } from './service';
function* handleResetPassword(action) {

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

@@ -12,9 +12,8 @@ export const siteLanguageListSelector = createSelector(
export const siteLanguageOptionsSelector = createSelector(
siteLanguageSelector,
siteLanguage =>
siteLanguage.siteLanguageList.map(({ code, name }) => ({
value: code,
label: name,
})),
siteLanguage => siteLanguage.siteLanguageList.map(({ code, name }) => ({
value: code,
label: name,
})),
);

View File

@@ -1,7 +1,7 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { convertKeyNames, snakeCaseObject } from '@edx/frontend-platform/utils';
import siteLanguageList from './constants';
import { snakeCaseObject, convertKeyNames } from '../data/utils';
export async function getSiteLanguageList() {
return siteLanguageList;

View File

@@ -2,61 +2,61 @@ import React from 'react';
import renderer from 'react-test-renderer';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import JumpNav from "../JumpNav";
import { BrowserRouter as Router } from 'react-router-dom';
import { mergeConfig, setConfig } from '@edx/frontend-platform';
import JumpNav from '../JumpNav';
const IntlJumpNav = injectIntl(JumpNav);
const IntlJumpNav = injectIntl(JumpNav);
describe('JumpNav', () => {
mergeConfig({
ENABLE_DEMOGRAPHICS_COLLECTION: false,
});
let props = {};
beforeEach(() => {
props = {
intl: {},
displayDemographicsLink: false,
};
});
it('should not render Optional Information link', () => {
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">
<IntlJumpNav {...props} />
</IntlProvider>
</Router>
))
.toJSON();
mergeConfig({
ENABLE_DEMOGRAPHICS_COLLECTION: false,
});
expect(tree).toMatchSnapshot();
let props = {};
beforeEach(() => {
props = {
intl: {},
displayDemographicsLink: false,
};
});
it('should not render Optional Information link', () => {
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">
<IntlJumpNav {...props} />
</IntlProvider>
</Router>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
it('should render Optional Information link', () => {
setConfig({
ENABLE_DEMOGRAPHICS_COLLECTION: true,
});
it('should render Optional Information link', () => {
setConfig({
ENABLE_DEMOGRAPHICS_COLLECTION: true,
});
props = {
...props,
displayDemographicsLink: true,
};
props = {
...props,
displayDemographicsLink: true,
}
const tree = renderer.create((
// Same as previous test
<Router>
<IntlProvider locale="en">
<IntlJumpNav {...props} />
</IntlProvider>
</Router>
))
.toJSON();
const tree = renderer.create((
// Same as previous test
<Router>
<IntlProvider locale="en">
<IntlJumpNav {...props} />
</IntlProvider>
</Router>
))
.toJSON();
expect(tree).toMatchSnapshot();
});
expect(tree).toMatchSnapshot();
});
});

View File

@@ -2,7 +2,7 @@
exports[`JumpNav should not render Optional Information link 1`] = `
<div
className="jump-nav"
className="jump-nav jump-nav-sm position-sticky pt-3"
>
<ul
className="list-unstyled"
@@ -92,7 +92,7 @@ exports[`JumpNav should not render Optional Information link 1`] = `
exports[`JumpNav should render Optional Information link 1`] = `
<div
className="jump-nav"
className="jump-nav jump-nav-sm position-sticky pt-3"
>
<ul
className="list-unstyled"

View File

@@ -11,14 +11,16 @@ class ThirdPartyAuth extends Component {
onClickDisconnect = (e) => {
e.preventDefault();
const providerId = e.currentTarget.getAttribute('data-provider-id');
if (this.props.disconnectionStatuses[providerId] === 'pending') return;
if (this.props.disconnectionStatuses[providerId] === 'pending') {
return;
}
const disconnectUrl = e.currentTarget.getAttribute('data-disconnect-url');
this.props.disconnectAuth(disconnectUrl, providerId);
}
};
renderUnconnectedProvider(url, name) {
return (
<React.Fragment>
<>
<h6 aria-level="3">{name}</h6>
<Hyperlink destination={url} className="btn btn-outline-primary">
<FormattedMessage
@@ -28,7 +30,7 @@ class ThirdPartyAuth extends Component {
values={{ name }}
/>
</Hyperlink>
</React.Fragment>
</>
);
}
@@ -36,7 +38,7 @@ class ThirdPartyAuth extends Component {
const hasError = this.props.errors[id];
return (
<React.Fragment>
<>
<h6 aria-level="3">
{name}
<span className="small font-weight-normal text-muted ml-2">
@@ -58,7 +60,7 @@ class ThirdPartyAuth extends Component {
) : null}
<StatefulButton
className="btn-link"
variant="link"
state={this.props.disconnectionStatuses[id]}
labels={{
default: (
@@ -75,7 +77,7 @@ class ThirdPartyAuth extends Component {
data-disconnect-url={url}
data-provider-id={id}
/>
</React.Fragment>
</>
);
}
@@ -85,9 +87,9 @@ class ThirdPartyAuth extends Component {
return (
<div className="form-group" key={id}>
{
connected ?
this.renderConnectedProvider(disconnectUrl, name, id) :
this.renderUnconnectedProvider(connectUrl, name)
connected
? this.renderConnectedProvider(disconnectUrl, name, id)
: this.renderUnconnectedProvider(connectUrl, name)
}
</div>
);
@@ -98,13 +100,15 @@ 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."
/>
);
}
render() {
if (this.props.providers === undefined) return null;
if (this.props.providers === undefined) {
return null;
}
if (this.props.providers.length === 0) {
return this.renderNoProviders();
@@ -114,7 +118,6 @@ class ThirdPartyAuth extends Component {
}
}
ThirdPartyAuth.propTypes = {
providers: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string,

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';

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;

55
src/hooks.js Normal file
View File

@@ -0,0 +1,55 @@
import { useEffect, useState } from 'react';
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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"account.settings.message.duplicate.tpa.provider": "La cuenta de {provider} seleccionada ya está vinculada con otra cuenta de edX. ",
"account.settings.message.duplicate.tpa.provider": "La cuenta de {provider} seleccionada ya está vinculada con otra cuenta de {siteName}. ",
"account.settings.message.managed.settings": "Los ajustes en el perfil son administrados por {managerTitle}. Contacte su administrador o {support} para obtener ayuda.",
"account.settings.message.managed.settings.support": "soporte",
"account.settings.page.heading": "Configuración de cuenta",
@@ -14,16 +14,40 @@
"account.settings.section.demographics.information": "Información opcional",
"account.settings.section.site.preferences": "Preferencias del sitio",
"account.settings.section.linked.accounts": "Cuentas vinculadas",
"account.settings.section.linked.accounts.description": "Puedes vincular tus cuentas de redes sociales para simplificar el proceso de iniciar sesión en edX.",
"account.settings.section.linked.accounts.description": "Puedes vincular tus cuentas de redes sociales para simplificar el proceso de iniciar sesión en {siteName}.",
"account.settings.field.username": "Nombre de usuario",
"account.settings.field.username.help.text": "El nombre que lo identifica en edX. No podrá cambiar el nombre de usuario.",
"account.settings.field.username.help.text": "El nombre que lo identifica en {siteName}. No podrá cambiar el nombre de usuario.",
"account.settings.field.full.name": "Nombre completo",
"account.settings.field.full.name.empty": "Añade nombre",
"account.settings.field.full.name.help.text": "El nombre que es usado para la verificación de identidad y aparece en sus certificados.",
"account.settings.field.full.name.help.text.default": "El nombre que aparece en tu perfil público.",
"account.settings.field.full.name.help.text.default.certificate": "Este nombre está seleccionado para aparecer en tus certificados y registros públicos.",
"account.settings.field.name.verified": "Nombre verificado",
"account.settings.field.name.verified.help.text.verified": "Este nombre ha sido verificado por una identificación con foto.",
"account.settings.field.name.verified.help.text.verified.proctored": "Este nombre ha sido verificado por supervisión.",
"account.settings.field.name.verified.help.text.verified.certificate": "Este nombre ha sido verificado por una identificación con foto y está seleccionado para aparecer en sus certificados y registros públicos.",
"account.settings.field.name.verified.help.text.verified.proctored.certificate": "Este nombre se ha verificado mediante supervisión y se ha seleccionado para que aparezca en sus certificados y registros públicos.",
"account.settings.field.name.verified.help.text.submitted": "La verificación ha sido enviada. Este proceso usualmente toma 48 horas o menos. El nombre verificado no puede ser modificado en este momento. ",
"account.settings.field.name.verified.help.text.submitted.proctored": "Su examen supervisado ha sido enviado. El nombre verificado no se puede cambiar en este momento. Vuelva a consultar en 2-5 días.",
"account.settings.field.name.verified.help.text.submitted.certificate": "Cuando el proceso de verificación de identidad sea exitoso, este nombre aparecerá en tus certificados y registros públicos. El nombre verificado no puede ser cambiado en este momento. ",
"account.settings.field.name.verified.help.text.submitted.proctored.certificate": "Una vez que su examen supervisado pase la revisión, este nombre aparecerá en su certificado y en los registros públicos. El nombre verificado no se puede cambiar en este momento.",
"account.settings.field.name.verified.verification.help": "Ingrese su nombre tal como aparece en su tarjeta de identificación vigente de estudiante, trabajo o emitida por el gobierno.",
"account.settings.field.full.name.help.text.submitted": "La verificación ha sido enviada. Este proceso usualmente toma 48 horas o menos. El nombre verificado no puede ser modificado en este momento. ",
"account.settings.field.full.name.help.text.submitted.proctored": "Su examen supervisado ha sido enviado. El nombre completo no se puede cambiar en este momento. Vuelva a consultar en 2-5 días.",
"account.settings.field.full.name.help.text.submitted.certificate": "En cuanto el proceso de verificación de identidad sea exitoso, este nombre aparecerá en tus certificados y registros públicos. El nombre completo no puede ser cambiado en este momento. ",
"account.settings.field.full.name.help.text.submitted.proctored.certificate": "Una vez que su examen supervisado pase la revisión, este nombre aparecerá en sus certificados y registros públicos. El nombre completo no se puede cambiar en este momento.",
"account.settings.field.name.verified.success.message": "Tu solicitud de verificación de identidad se ha completado exitosamente. Ahora puedes seleccionar el nombre prefieres que aparezca en tus certificados y registros públicos.",
"account.settings.field.name.verified.success.message.header": "¡Tu solicitud de cambio de nombre está completada!",
"account.settings.field.name.verified.failure.message": "Tu más reciente intento de verificación de ID no fue aprobado. Las configuraciones relacionadas con el proceso se han restablecido.",
"account.settings.field.name.verified.failure.message.header": "No pudimos verificar tu identidad.",
"account.settings.field.name.verified.failure.message.help.link": "Más información sobre la verificación de ID",
"account.settings.field.name.verified.submitted.message": "Tu solicitud de verificación de identidad ha sido enviada y usualmente se tarda entre 24 a 48 horas para completarse. ",
"account.settings.field.name.verified.submitted.message.certificate": "Cuando la solicitud esté aprobada, la actualización de tu nombre aparecerá en todos los certificados asociados y registros públicos.",
"account.settings.field.name.verified.submitted.message.header": "¡Tu solicitud de cambio de nombre esta por completarse!",
"account.settings.field.email": "Correo electrónico (Ingresar)",
"account.settings.field.email.empty": "Agregar correo electrónico",
"account.settings.field.email.confirmation": "Le enviamos un mensaje de confirmación a {value}. Hacer click en la liga del mensaje para actualizar su correo electrónico.",
"account.settings.field.email.help.text": "Recibes mensajes de edX y equipos del curso en esta dirección.",
"account.settings.field.email.help.text": "Tienes un mensaje de {siteName} y el equipo de curso en esta dirección. ",
"account.settings.field.secondary.email": "Correo electrónico de recuperación",
"account.settings.field.secondary.email.empty": "Agregar un correo electrónico de recuperación",
"account.settings.field.secondary.email.confirmation": "Le enviamos un mensaje de confirmación a {value}. Hacer click en la liga del mensaje para actualizar su correo electrónico.",
@@ -31,6 +55,14 @@
"account.settings.field.dob": "Año de nacimiento",
"account.settings.field.dob.empty": "Agregar año de nacimiento",
"account.settings.field.year_of_birth.options.empty": "Selecciona año de nacimiento",
"account.settings.field.dob.month": "Mes",
"account.settings.field.dob.year": "Año",
"account.settings.field.dob.form.button": "Por favor confirme su fecha de nacimiento",
"account.settings.field.dob.form.title": "Confirma tu fecha de nacimiento",
"account.settings.field.dob.form.help.text": "Solicitamos información sobre la fecha de nacimiento para ayudarnos a cumplir con nuestras obligaciones legales.",
"account.settings.field.dob.form.success": "Gracias por ingresar la información de su fecha de nacimiento.",
"account.settings.field.month_of_birth.options.empty": "Seleccione un mes de nacimiento",
"account.settingsfield.dob.error.general": "Ha ocurrido un error técnico. Por favor intentalo de nuevo.",
"account.settings.field.country": "País",
"account.settings.field.country.empty": "Agregar país",
"account.settings.field.country.options.empty": "Seleccionar un país",
@@ -57,7 +89,7 @@
"account.settings.field.gender.options.f": "Femenino",
"account.settings.field.gender.options.m": "Masculino",
"account.settings.field.gender.options.o": "Otro",
"account.settings.field.language.proficiencies": "Idiomas hablados",
"account.settings.field.language.proficiencies": "Idioma hablado",
"account.settings.field.language.proficiencies.empty": "Agregar un idioma hablado",
"account.settings.field.language_proficiencies.options.empty": "Selecciona lenguaje",
"account.settings.field.time.zone": "Zona horaria",
@@ -67,7 +99,7 @@
"account.settings.field.time.zone.all": "Todas las zonas horarias",
"account.settings.field.time.zone.country": "Zonas horarias",
"account.settings.section.social.media": "Enlaces de redes sociales",
"account.settings.section.social.media.description": "Opcionalmente, conecte sus cuentas personales a los iconos de redes sociales en su perfil de edX.",
"account.settings.section.social.media.description": "Opcionalmente, conecte sus cuentas personales a los iconos de redes sociales en su perfil de {siteName}.",
"account.settings.field.social.platform.name.linkedin": "LinkedIn",
"account.settings.field.social.platform.name.linkedin.empty": "Agregar perfil de LinkedIn",
"account.settings.jump.nav.delete.account": "Eliminar mi cuenta",
@@ -80,6 +112,12 @@
"account.settings.editable.field.action.edit": "Editar",
"account.settings.static.field.empty": "No hay valor establecido. Contacte su administrador {enterprise} para hacer cambios.",
"account.settings.static.field.empty.no.admin": "No hay valor establecido.",
"account.settings.field.name.certificate.select": "En caso de ser seleccionado, este nombre aparecerá en tus certificados y registros públicos. ",
"account.settings.field.name.modal.certificate.title": "Escoge un nombre de preferencia para tus certificados y registros públicos.",
"account.settings.field.name.modal.certificate.select": "Selecciona un nombre",
"account.settings.field.name.modal.certificate.option.full": "Nombre completo",
"account.settings.field.name.modal.certificate.option.verified": "Nombre verificado",
"account.settings.field.name.modal.certificate.button.choose": "Escoge un nombre",
"account.settings.coaching.consent.welcome.header": "Empecemos",
"account.settings.coaching.consent.welcome.subheader": "Estamos aquí para ustede desde el inicio hasta el final",
"account.settings.coaching.consent.description": "Los programas de MicroBachelors incluyen entrenamiento que se enfoca en su carrera, educación y cómo logrará resultados a través de la comunicación individual con un profesional experimentado. Si está interesado, proporcione la información a continuación y haga clic en \"Enviar\", y nuestro socio asesor se comunicará con usted por correo electrónico y / o mensaje de texto para ayudarlo a avanzar. Los términos y Condiciones aplican.*",
@@ -89,7 +127,7 @@
"account.settings.coaching.consent.label.name": "Por favor confirme su nombre",
"account.settings.coaching.consent.label.phone-number": "Ingrese su número de teléfono móvil",
"account.settings.coaching.consent.success.header": "¡Éxito!",
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
"account.settings.coaching.consent.success.message": "Estás inscrito para coaching. Puedes esperar un mensaje por correo electrónico o SMS en los próximos días.",
"account.settings.coaching.consent.success.continue": "Iniciar mi curso",
"account.settings.coaching.managed.support": "soporte",
"account.settings.coaching.managed.alert": "{ManagerTitle} administra su Nombre. Póngase en contacto con su administrador para obtener ayuda.",
@@ -101,17 +139,20 @@
"account.settings.delete.account.before.proceeding": "Antes de continuar, por favor {actionLink}.",
"account.settings.delete.account.header": "Eliminar mi cuenta",
"account.settings.delete.account.subheader": "¡Sentimos que te vayas!",
"account.settings.delete.account.text.1": "Cuidado: la eliminación de tu cuenta y datos personales es permanente e irreversible. edX no podrá recuperar ni tu cuenta ni los datos eliminados.",
"account.settings.delete.account.text.2": "Una vez su cuenta haya sido eliminada, no la podrá usar para tomar cursos en la app de edX, edx.org o en cualquier otro sitio administrado por edX. Esto incluye el acceso a edx.org desde el sistema de su empleador o universidad y el acceso a páginas privadas ofrecidas por MIT Open Learning, Wharton Executive Education y Harvard Medical School.",
"account.settings.delete.account.text.3.link": "siga las instrucciones para imprimir o descargar el certificado",
"account.settings.delete.account.text.warning": "Warning: La eliminación de la cuenta es permanente. Por favor lee la información de más arriba con atención antes de proceder. Esta es una acción irreversible, y no podrás volver a usar el mismo correo electrónico en edX.",
"account.settings.delete.account.text.1": "Para tener en cuenta: La eliminación de su cuenta y sus datos personales es permanente y no se puede deshacer. {siteName} no podrá recuperar tu cuenta o la información que sea eliminada.",
"account.settings.delete.account.text.2": "Una véz tu cuenta haya sido eliminada, no podrás usarla para tomar cursos en {siteName}. ",
"account.settings.delete.account.text.2.edX": "Una vez su cuenta haya sido eliminada, no la podrá usar para tomar cursos en la app de edX, edx.org o en cualquier otro sitio administrado por edX. Esto incluye el acceso a edx.org desde el sistema de su empleador o universidad y el acceso a páginas privadas ofrecidas por MIT Open Learning, Wharton Executive Education y Harvard Medical School.",
"account.settings.delete.account.text.3.link": "Sigue estas instrucciones para imprimir o descargar un certificado. ",
"account.settings.delete.account.text.warning": "Atención: la eliminación de su cuenta es permanente. Por favor lea el previo aviso cautelosamente antes de proceder, ya que esto es una acción irreversible, y no podrá usar el mismo correo electrónico en {siteName}.",
"account.settings.delete.account.text.change.instead": "En lugar de eso, ¿quieres cambiar tu correo electrónico, nombre o contraseña?",
"account.settings.delete.account.button": "Eliminar mi cuenta",
"account.settings.delete.account.please.activate": "activar su cuenta",
"account.settings.delete.account.please.confirm": "Confirma tu cuenta",
"account.settings.delete.account.please.unlink": "Desvincular todas las cuentas de redes sociales.",
"account.settings.delete.account.modal.header": "¿Está seguro?",
"account.settings.delete.account.modal.text.1": "Has seleccionado Eliminar mi cuenta”. La eliminación de tu cuenta y datos personales es permanente e irreversible. edX no será capaz de recuperar tu cuenta o los datos que se hayan borrado.",
"account.settings.delete.account.modal.text.2": "Si procedes, no será posible usar esta cuenta para tomar cursos ni en la aplicación móvil de edX, ni en edx.org, ni en cualquier otro sitio hospedado por edX. Esto incluye el acceso a edx.org desde el sistema de tu empleador o universidad, y el acceso a sitios privados ofrecidos por MIT Open Learning, Wharton Executive Education, y Harvard Medical School.",
"account.settings.delete.account.modal.text.1": "Has seleccionado la opción ''Eliminar mi cuenta''. Ten en cuenta que la eliminación de tu cuenta e información personal es permanente y no puede revertirse. {siteName} no será capaz de recuperar tu cuenta o información una vez esta sea eliminada. ",
"account.settings.delete.account.modal.text.2": "Si aceptas proceder, ya no podrás usar esta cuenta para tomar cursos en {siteName}.",
"account.settings.delete.account.modal.text.2.edX": "Si procedes, no será posible usar esta cuenta para tomar cursos ni en la aplicación móvil de edX, ni en edx.org, ni en cualquier otro sitio hospedado por edX. Esto incluye el acceso a edx.org desde el sistema de tu empleador o universidad, y el acceso a sitios privados ofrecidos por MIT Open Learning, Wharton Executive Education, y Harvard Medical School.",
"account.settings.delete.account.modal.enter.password": "Si deseas continuar y eliminar tu cuenta, por favor introduce la contraseña de tu cuenta:",
"account.settings.delete.account.modal.confirm.delete": "Si, Eliminar",
"account.settings.delete.account.modal.confirm.cancel": "Cancelar",
@@ -122,7 +163,8 @@
"account.settings.delete.account.modal.after.header": "¡Sentimos que te vayas! Tu cuenta será eliminada en breve.",
"account.settings.delete.account.modal.after.text": "La eliminación de cuenta, incluyendo la eliminación de las listas de correo electrónico, puede tardar unas semanas en procesarse totalmente en nuestro sistema. Si quieres renunciar a recibir correos antes de que la eliminación se haya completado, por favor date de baja mediante el enlace que aparece al final de los correos.",
"account.settings.delete.account.modal.after.button": "Cerrar",
"account.settings.delete.account.text.3": "Puede que también pierdas el acceso a los certificados verificados y otros certificados de programas como los de los MicroMasters. Si quieres hacer una copia de dichos certificados para tus archivos antes de proceder a la eliminación, {actionLink}.",
"account.settings.delete.account.text.3.edX": "También podrás perder el acceso a certificados verificados y a otras credenciales del programa como los certificados MicroMasters. Puedes hacer una copia de estos registros antes de proceder con la eliminación. {actionLink}.",
"account.settings.delete.account.text.3": "También podrás perder el acceso a certificados verificados y a otras credenciales del programa. Puedes hacer una copia de estos registros antes de proceder con la eliminación. ",
"account.settings.message.demographics.service.issue": "Ocurrió un error al intentar recuperar o guardar la información de tu cuenta. Por favor inténtalo más tarde.",
"account.settings.field.demographics.gender": "Identidad de género",
"account.settings.field.demographics.gender.empty": "Añade identidad de género",
@@ -136,7 +178,7 @@
"account.settings.field.demographics.income.empty": "Añade ingreso familiar",
"account.settings.field.demographics.income.options.empty": "Selecciona un rango de ingreso familiar",
"account.settings.field.demographics.military_history": "Estatus militar en EE.UU.",
"account.settings.field.demographics.military_history.empty": "Add military status",
"account.settings.field.demographics.military_history.empty": "Añade estatus militar",
"account.settings.field.demographics.military_history.options.empty": "Selecciona estatus militar",
"account.settings.field.demographics.learner_education_level": "Tu nivel educacional",
"account.settings.field.demographics.learner_education_level.empty": "Añade nivel educacional",
@@ -153,7 +195,17 @@
"account.settings.field.demographics.future_work_sector": "Área profesional futura",
"account.settings.field.demographics.future_work_sector.empty": "Añade área profesional",
"account.settings.field.demographics.work_sector.options.empty": "Selecciona área profesional",
"account.settings.section.demographics.why": "¿Por qué edX obtiene esta información?",
"account.settings.section.demographics.why": "¿Por qué {siteName} recauda esta información?",
"account.settings.name.change.title.id": "Este cambio de nombre requiere de verificación de identidad.",
"account.settings.name.change.title.begin": "Antes de empezar",
"account.settings.name.change.warning.one": "Atención: esta acción actualizará el nombre que aparece en todos los certificados obtenidos a través de esta cuenta en el pasado y en aquellos en que actualmente estás obteniendo o que adquirirás en el futuro.",
"account.settings.name.change.warning.two": "Esta acción no podrá revertirse sin antes verificar tu identidad.",
"account.settings.name.change.id.name.label": "Ingrese su nombre tal como aparece en su tarjeta de identificación vigente de estudiante, trabajo o emitida por el gobierno.",
"account.settings.name.change.id.name.placeholder": "Ingrese el nombre en su identificación con foto",
"account.settings.name.change.error.valid.name": "Por favor ingresa un nombre valido.",
"account.settings.name.change.error.general": "Ha ocurrido un error técnico. Por favor intentalo de nuevo.",
"account.settings.name.change.continue": "Continuar",
"account.settings.name.change.cancel": "Cancelar",
"error.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, comprueba la URL y vuelve a intentarlo.",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "soporte técnico",
"account.settings.editable.field.password.reset.button.confirmation": "Hemos mandado un mensaje a {email}. Haz clic en el enlace en el mensaje para restablecer tu contraseña. ¿No recibiste el mensaje? Contáctate con {technicalSupportLink}.",
@@ -165,128 +217,143 @@
"account.settings.sso.account.disconnect.error": "Hubo un problema al desconectar esta Cuenta. Si el problema persiste, contacte soporte.",
"account.settings.sso.unlink.account": "Desvincular la cuenta de {accountName} ",
"account.settings.sso.no.providers": "No se pueden vincular cuentas en este momento.",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}.",
"account.page.title": "Cuenta | {siteName}",
"id.verification.access.blocked.denied": "No podemos verificar tu identidad en este momento. Si aún no has activado tu cuenta, comprueba en tu carpeta de correo no deseado el correo de activación de {email}.",
"id.verification.next": "Siguiente",
"id.verification.support": "soporte",
"id.verification.example.card.alt": "Ejemplo de un documento de identidad válido con foto y nombre completo.",
"id.verification.requirements.title": "Requerimientos de verificación por foto",
"id.verification.requirements.description": "Para completar la verificación por foto en línea, necesitarás lo siguiente:",
"id.verification.requirements.description": "Para completa la verificación de foto, necesitarás lo siguiente:",
"id.verification.requirements.card.device.title": "Dispositivo con cámara",
"id.verification.requirements.card.device.allow": "Permitir",
"id.verification.requirements.card.id.title": "Identificación por foto",
"id.verification.requirements.card.id.text": "Necesitas un ID válido que contenga tu nombre completo y tu foto.",
"id.verification.privacy.title": "Privacy Information",
"id.verification.privacy.need.photo.question": "¿Por qué edX necesita mi foto?",
"id.verification.requirements.card.id.title": "Tarjeta de identificación con foto",
"id.verification.requirements.card.id.text": "Necesitas una tarjeta de identificación válida que contenga tu nombre completo y foto, similar a una licencia de conducir o pasaporte.",
"id.verification.privacy.title": "Información de privacidad",
"id.verification.privacy.need.photo.question": "¿Por qué {siteName} necesita mi foto?",
"id.verification.privacy.need.photo.answer": "Utilizamos tus fotos de verificación para confirmar tu identidad y garantizar la validez de tu certificado.",
"id.verification.privacy.do.with.photo.question": "¿Qué hace edX con esta foto?",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.existing.request.title": "Verificación de identidad",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).",
"id.verification.privacy.do.with.photo.question": "¿Qué hace {siteName} con esta foto?",
"id.verification.privacy.do.with.photo.answer": "Encriptamos de forma segura tu foto y la enviamos a nuestro servicio de autorización para su revisión. Tu foto e información no se guardan ni se ven en ninguna parte de {siteName} después de que se completa el proceso de verificación.",
"id.verification.access.blocked.title": "Verificación de identidad",
"id.verification.access.blocked.enrollment": "Actualmente, no estás inscrito en un curso que requiera verificación de identidad.",
"id.verification.access.blocked.pending": "Ya has enviado tu información de verificación de identidad. Recibirás un mensaje en tu panel principal cuando el proceso de verificación esté completado (usualmente dentro de los 5 días).",
"id.verification.photo.take": "Tomar la foto",
"id.verification.photo.retake": "Tomar nuevamente la foto",
"id.verification.photo.enable.detection": "Enable Face Detection",
"id.verification.photo.enable.detection.portrait.help.text": "If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.enable.detection.id.help.text": "If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.feedback.correct": "Face is in a good position.",
"id.verification.photo.feedback.two.faces": "More than one face detected.",
"id.verification.photo.feedback.no.faces": "No face detected.",
"id.verification.photo.feedback.top.left": "Incorrect position. Top left.",
"id.verification.photo.feedback.top.center": "Incorrect position. Top center.",
"id.verification.photo.feedback.top.right": "Incorrect position. Top right.",
"id.verification.photo.feedback.center.left": "Incorrect position. Center left.",
"id.verification.photo.feedback.center.center": "Incorrect position. Too close to camera.",
"id.verification.photo.feedback.center.right": "Incorrect position. Center right.",
"id.verification.photo.feedback.bottom.left": "Incorrect position. Bottom left.",
"id.verification.photo.feedback.bottom.center": "Incorrect position. Bottom center.",
"id.verification.photo.feedback.bottom.right": "Incorrect position. Bottom right.",
"id.verification.photo.retake": "¿Tomar nuevamente la foto?",
"id.verification.photo.enable.detection": "Habilitar la detección de rostro",
"id.verification.photo.enable.detection.portrait.help.text": "Si está marcada, aparecerá un cuadro alrededor de tu cara. Tu rostro se puede ver claramente si el cuadro que lo rodea es azul. Si Tu cara no está en una buena posición o es indetectable, el cuadro será rojo.",
"id.verification.photo.enable.detection.id.help.text": "Si está marcada, aparecerá una casilla alrededor de la cara de tu documento de identificación. La cara se puede ver claramente si la caja que la rodea es azul. Si la cara no está en una buena posición o es indetectable, el cuadro será rojo.",
"id.verification.photo.feedback.correct": "La cara está en una buena posición.",
"id.verification.photo.feedback.two.faces": "Más de un rostro detectado.",
"id.verification.photo.feedback.no.faces": "No se detectó el rostro.",
"id.verification.photo.feedback.top.left": "Posición incorrecta. Arriba a la izquierda.",
"id.verification.photo.feedback.top.center": "Posición incorrecta. Centro Superior.",
"id.verification.photo.feedback.top.right": "Posición incorrecta. Parte superior derecha.",
"id.verification.photo.feedback.center.left": "Posición incorrecta. Centro izquierda.",
"id.verification.photo.feedback.center.center": "Posición incorrecta. Demasiado cerca de la cámara.",
"id.verification.photo.feedback.center.right": "Posición incorrecta. Centro derecha.",
"id.verification.photo.feedback.bottom.left": "Posición incorrecta. Abajo a la izquierda.",
"id.verification.photo.feedback.bottom.center": "Posición incorrecta. Parte inferior central.",
"id.verification.photo.feedback.bottom.right": "Posición incorrecta. Abajo a la derecha.",
"id.verification.camera.access.title": "Permisos de la cámara",
"id.verification.camera.access.title.success": "Camera Access Enabled",
"id.verification.camera.access.title.failed": "Camera Access Failed",
"id.verification.camera.access.title.success": "Acceso a la cámara habilitado",
"id.verification.camera.access.title.failed": "Acceso a la cámara no habilitado",
"id.verification.camera.access.click.allow": "Por favor asegúrate de hacer clic en \"Permitir\"",
"id.verification.camera.access.enable": "Habilitar cámara",
"id.verification.camera.access.problems": "¿Tienes problemas?",
"id.verification.camera.access.skip": "Omitir y cargar un archivo de imagen",
"id.verification.camera.access.success": "Parece que tu cámara está funcionando y está lista.",
"id.verification.camera.access.failure": "It looks like we're unable to access your camera. You will need to upload image files of you and your photo id.",
"id.verification.camera.access.failure.temporary": "It looks like we're unable to access your camera. Please verify that your webcam is connected and that you have allowed your browser to access it.",
"id.verification.camera.access.failure.temporary.chrome": "To enable camera access in Chrome:",
"id.verification.camera.access.failure.temporary.chrome.step1": "Open Chrome.",
"id.verification.camera.access.failure.temporary.chrome.step2": "Navigate to More > Settings.",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "For Windows: Alt+F, Alt+E, or F10 followed by the spacebar",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "For Mac: Command+,",
"id.verification.camera.access.failure.temporary.chrome.step3": "Under the \"Privacy and security\" tab, select \"Site Settings\" and then \"Camera.\"",
"id.verification.camera.access.failure.temporary.chrome.step4": "Under \"Blocked,\" find \"edx.org\" and select it.",
"id.verification.camera.access.failure.temporary.chrome.step5": "In the \"Permissions\" section, update the camera permissions to \"Allow.\"",
"id.verification.camera.access.failure.temporary.ie11": "To enable camera access in Internet Explorer:",
"id.verification.camera.access.failure.temporary.ie11.step1": "Open the Flash Player Settings Manager by navigating to Windows Settings > Control Panel > Flash Player.",
"id.verification.camera.access.failure.temporary.ie11.step2": "Select the \"Camera and Mic\" tab, and then select the \"Camera and Microphone Settings by Site\" button.",
"id.verification.camera.access.failure.temporary.ie11.step3": "Choose \"edx.org\" from the list of websites and change the permissions by selecting \"Allow\" in the dropdown menu.",
"id.verification.camera.access.failure.temporary.firefox": "To enable camera access in Firefox:",
"id.verification.camera.access.failure.temporary.firefox.step1": "Open Firefox.",
"id.verification.camera.access.failure.temporary.firefox.step2": "Enter \"about:preferences\" in the URL bar.",
"id.verification.camera.access.failure.temporary.firefox.step3": "Select the \"Privacy & Security\" tab, and navigate to the \"Permissions\" section.",
"id.verification.camera.access.failure.temporary.firefox.step4": "Next to \"Camera,\" select the \"Settings…\" button.",
"id.verification.camera.access.failure.temporary.firefox.step5": "In the search bar, enter \"edx.org.\"",
"id.verification.camera.access.failure.temporary.firefox.step6": "In the status column for \"edx.org,\" select \"Allow\" from the drop down.",
"id.verification.camera.access.failure.temporary.firefox.step7": "Select \"Save Changes.\"",
"id.verification.camera.access.failure.temporary.safari": "To enable camera access in Safari:",
"id.verification.camera.access.failure.temporary.safari.step1": "Open Safari.",
"id.verification.camera.access.failure.temporary.safari.step2": "Click on the Safari app menu, then select \"Preferences.\" You can also use Command+, as a keyboard shortcut.",
"id.verification.camera.access.failure.temporary.safari.step3": "Select the \"Websites\" tab and then select \"Camera.\"",
"id.verification.camera.access.failure.temporary.safari.step4": "Select \"edx.org\" and change the camera permissions to \"Allow.\"",
"id.verification.camera.access.failure": "Parece que no podemos acceder a tu cámara. Deberás cargar archivos de imagen de tu rostro y tu identificación con foto.",
"id.verification.camera.access.failure.temporary": "Parece que no podemos acceder a tu cámara. Verifica que tu cámara web esté conectada y que permitiste que tu navegador acceda a ella.",
"id.verification.camera.access.failure.temporary.chrome": "Para habilitar el acceso a la cámara en Chrome:",
"id.verification.camera.access.failure.temporary.chrome.step1": "Abrir Chrome.",
"id.verification.camera.access.failure.temporary.chrome.step2": "Navega a Más > Configuración.",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "Para Windows: Alt + F, Alt + E o F10 seguido de la barra espaciadora",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "Para Mac: Comando +,",
"id.verification.camera.access.failure.temporary.chrome.step3": "En la pestaña \"Privacidad y seguridad\", selecciona \"Configuración del sitio\" y luego \"Cámara.\"",
"id.verification.camera.access.failure.temporary.chrome.step4": "En \"Bloqueado\", busca \"edx.org\" y selecciónalo.",
"id.verification.camera.access.failure.temporary.chrome.step5": "En la sección \"Permisos\", actualiza los permisos de la cámara a \"Permitir\".",
"id.verification.camera.access.failure.temporary.ie11": "Para habilitar el acceso a la cámara en Internet Explorer:",
"id.verification.camera.access.failure.temporary.ie11.step1": "Abre el Administrador de configuración de Flash Player navegando a Configuración de Windows > Panel de control > Flash Player.",
"id.verification.camera.access.failure.temporary.ie11.step2": "Selecciona la pestaña \"Cámara y micrófono,\" y luego selecciona el botón \"Configuración de cámara y micrófono por sitio.\"",
"id.verification.camera.access.failure.temporary.ie11.step3": "Elige \"edx.org\" de la lista de sitios web y cambia los permisos seleccionando \"Permitir\" en el menú desplegable.",
"id.verification.camera.access.failure.temporary.firefox": "Para habilitar el acceso a la cámara en Firefox:",
"id.verification.camera.access.failure.temporary.firefox.step1": "Abrir Firefox.",
"id.verification.camera.access.failure.temporary.firefox.step2": "Coloca \"about:preferences\" en la barra del URL.",
"id.verification.camera.access.failure.temporary.firefox.step3": "Selecciona la pestaña \"Privacidad y seguridad\" y navega hasta la sección \"Permisos.\"",
"id.verification.camera.access.failure.temporary.firefox.step4": "Junto a \"Cámara,\" selecciona el botón \"Configuración...\"",
"id.verification.camera.access.failure.temporary.firefox.step5": "En la barra de búsqueda, ingresa \"edx.org\".",
"id.verification.camera.access.failure.temporary.firefox.step6": "En la columna de estado de \"edx.org\", selecciona \"Permitir\" en el menú desplegable.",
"id.verification.camera.access.failure.temporary.firefox.step7": "Selecciona \"Guardar cambios.\"",
"id.verification.camera.access.failure.temporary.safari": "Para habilitar el acceso a la cámara en Safari:",
"id.verification.camera.access.failure.temporary.safari.step1": "Abrir Safari.",
"id.verification.camera.access.failure.temporary.safari.step2": "Haz clic en el menú de la aplicación Safari y luego selecciona \"Preferencias\". También puedes utilizar Command +, como método abreviado de teclado.",
"id.verification.camera.access.failure.temporary.safari.step3": "Selecciona la pestaña \"Sitios web\" y luego selecciona \"Cámara\".",
"id.verification.camera.access.failure.temporary.safari.step4": "Selecciona \"edx.org\" y cambia los permisos de la cámara a \"Permitir.\"",
"id.verification.camera.access.failure.unsupported": "Parece que tu navegar no soporta el acceso a la cámara.",
"id.verification.camera.access.failure.unsupported.chrome.explanation": "El navegador de Chrome actualmente no soporta el acceso a la cámara en dispositivos iOS, como iPhones y iPads.",
"id.verification.camera.access.failure.unsupported.instructions": "Por favor utilice otro navegador para completar la verificación de identidad.",
"id.verification.photo.tips.title": "Consejos útiles de fotos",
"id.verification.photo.tips.description": "Next, we'll need you to take a photo of your face. Please review the helpful tips below.",
"id.verification.photo.tips.list.title": "Photo Tips",
"id.verification.photo.tips.list.description": "To take a successful photo, make sure that:",
"id.verification.photo.tips.list.well.lit": "Your face is well-lit.",
"id.verification.photo.tips.list.inside.frame": "Your entire face fits inside the frame.",
"id.verification.portrait.photo.title.camera": "Take a Photo of Yourself",
"id.verification.portrait.photo.title.upload": "Upload a Photo of Yourself",
"id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"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: .jpg, .jpeg, .png)",
"id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.help.sight.answer.portrait": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle.",
"id.verification.camera.help.sight.answer.id": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally, the best position for a photo of an ID card is 8-12 inches (20-30 centimeters) from the camera, with the ID card centered relative to the camera. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.difficulty.question.portrait": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.difficulty.question.id": "What if I have difficulty holding my ID in position relative to the camera?",
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.id.photo.unclear.question": "Is your ID image not clear or too blurry?",
"id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid ID that includes your name. Please have your ID ready.",
"id.verification.id.tips.list.well.lit": "Your ID is well-lit.",
"id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.",
"id.verification.id.photo.title.camera": "Take a Photo of Your ID",
"id.verification.id.photo.title.upload": "Upload a Photo of Your ID",
"id.verification.id.photo.preview.alt": "Preview of photo ID.",
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
"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: .jpg, .jpeg, .png)",
"id.verification.id.photo.instructions.upload.error": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "Account Name Check",
"id.verification.account.name.instructions": "The name on your account and the name on your ID must be an exact match. If not, please click \"No\" to update your account name.",
"id.verification.account.name.radio.label": "Does the name on your ID match the Account Name below?",
"id.verification.account.name.radio.yes": "Yes",
"id.verification.account.name.radio.no": "No",
"id.verification.account.name.error": "Please update account name to match the name on your ID.",
"id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.account.name.settings": "Account Settings",
"id.verification.account.name.label": "Account Name",
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.account.name.save": "Save and Next",
"id.verification.review.title": "Review Your Photos",
"id.verification.review.description": "Make sure we can verify your identity with the photos and information you have provided.",
"id.verification.review.portrait.label": "Your Portrait",
"id.verification.review.portrait.alt": "Photo of your face to be submitted.",
"id.verification.review.portrait.retake": "Retake Portrait Photo",
"id.verification.review.id.label": "Your Photo ID",
"id.verification.review.id.alt": "Photo of your ID to be submitted.",
"id.verification.review.id.retake": "Retake ID Photo",
"id.verification.review.confirm": "Submit",
"id.verification.review.error": "edX Support Page",
"id.verification.submitted.title": "Identity Verification in Progress",
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.",
"id.verification.return.dashboard": "Return to Your Dashboard",
"id.verification.return.course": "Return to Course",
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}.",
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists,\n please go to {support_link} for help.\n ",
"id.verification.account.name.edit": "Edit{sr}"
"id.verification.photo.tips.description": "A continuación, necesitaremos que tomes una foto de tu rostro. Por favor, revisa los siguientes consejos útiles.",
"id.verification.photo.tips.list.title": "Consejos para fotos",
"id.verification.photo.tips.list.description": "Para tomar la foto correctamente, asegúrate de: ",
"id.verification.photo.tips.list.well.lit": "El rostro esté bien iluminado",
"id.verification.photo.tips.list.inside.frame": "Tu cara está completamente dentro del marco de la foto.",
"id.verification.portrait.photo.title.camera": "Toma una foto de ti mismo",
"id.verification.portrait.photo.instructions.camera": "Cuando tu rostro esté en posición, usa el botón Tomar foto a continuación para tomar tu foto.",
"id.verification.camera.help.sight.question": "¿Qué pasa si no puedo ver la imagen de la cámara o si no puedo ver mi foto para determinar qué lado es visible?",
"id.verification.camera.help.sight.answer.portrait": "Es posible que puedas completar el procedimiento de captura de imágenes sin ayuda, pero es posible que necesites un par de intentos de envío para que la cámara se coloque correctamente. La posición óptima de la cámara varía con cada computadora, pero generalmente la mejor distancia para una foto de rostro es aproximadamente a 12 a 18 pulgadas (30 a 45 centímetros) de la cámara, con la cabeza centrada en relación con la pantalla de la computadora. Si las fotos que envías son rechazadas, intenta mover la computadora o la orientación de la cámara para cambiar el ángulo de iluminación.",
"id.verification.camera.help.sight.answer.id": "Es posible que puedas completar el procedimiento de captura de imágenes sin ayuda, pero es posible que necesites un par de intentos de envío para que la cámara se coloque correctamente. El posicionamiento óptimo de la cámara varía con cada computadora pero, generalmente, la mejor distancia para una foto de un documento de identificación es a 8 a 12 pulgadas (20 a 30 centímetros) de la cámara, con el documento de identificación centrado en relación con la cámara. Si las fotos que envías son rechazadas, intenta mover la computadora o la orientación de la cámara para cambiar el ángulo de iluminación. La razón más común de rechazo es la imposibilidad de leer el texto del documento de identidad.",
"id.verification.camera.help.difficulty.question.portrait": "¿Qué sucede si tengo dificultades para mantener la cabeza en posición con respecto a la cámara?",
"id.verification.camera.help.difficulty.question.id": "¿Qué sucede si tengo dificultades para mantener mi identificación en posición con respecto a la cámara?",
"id.verification.camera.help.difficulty.answer": "Si requieres de asistencia para cargar una foto, contacta el equipo de soporte de {siteName} para sugerencias adicionales.",
"id.verification.id.photo.unclear.question": "¿No es clara la foto de tu tarjeta de ID ó es muy borrosa?",
"id.verification.id.tips.title": "Tips de ayuda para tarjetas de identificación",
"id.verification.id.tips.description": "Después, necesitaremos que tomes una foto de una tarjeta de ID válida que tengas en la que incluya tu nombre completo y foto, como una licencia de conducción o un pasaporte. Por favor ten tú ID lista.",
"id.verification.id.tips.list.well.lit": "Tu tarjeta de identificación está bien iluminada.",
"id.verification.id.tips.list.clear": "Asegúrate de que puedes ver tu foto y leer claramente tu nombre.",
"id.verification.id.photo.title.camera": "Toma una foto de tu tarjeta de identificación",
"id.verification.id.photo.title.upload": "Sube una foto de tu tarjeta de identificación",
"id.verification.id.photo.preview.alt": "Previsualización de Foto ID",
"id.verification.id.photo.instructions.camera": "Cuando tú ID este en posición, utiliza el botón de tomar foto que se encuentra debajo para proceder a tomar tu foto. Por favor usa un pasaporte, licencia de conducción, u otra tarjeta de identificación que incluya tu nombre completo y una foto de tu rostro.",
"id.verification.id.photo.instructions.upload": "Por favor sube una foto de tu identificación. Asegurate de que todo el ID encaje dentro de el marco y que este bien iluminado. El tamaño del archivo debe ser por debajo de 10 MB. Los formatos soportados son: ",
"id.verification.id.photo.instructions.upload.error.invalidFileType": "El archivo que has seleccionado no cumple con el tipo de imágenes soportadas. Por favor escoge entre los siguientes formatos:",
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "El archivo que has seleccionado es demasiado grande. Vuelve a intentarlo con un archivo de menos de 10 MB.",
"id.verification.name.check.title": "Revisa nuevamente tu nombre",
"id.verification.name.check.instructions": "¿El nombre a continuación coincide con el nombre en su identificación con foto? De lo contrario, actualice el nombre a continuación para que coincida con su identificación con foto.",
"id.verification.name.check.mismatch.information": "Si el nombre a continuación no coincide con su identificación con foto, se denegará su verificación de identidad.",
"id.verification.name.error": "Ingrese su nombre tal como aparece en su identificación con foto.",
"id.verification.account.name.warning.prefix": "Ten en cuenta:",
"id.verification.account.name.settings": "Configuración de cuenta",
"id.verification.name.label": "Nombre",
"id.verification.account.name.photo.alt": "Foto de tu identificación a enviar.",
"id.verification.review.title": "Revisar tus fotos",
"id.verification.review.description": "Asegúrate de que podamos verificar tu identidad con las imágenes y la información suministradas.",
"id.verification.review.portrait.label": "Tu Retrato",
"id.verification.review.portrait.alt": "Foto de tu rostro a enviar.",
"id.verification.review.portrait.retake": "Retomar Foto de Retrato",
"id.verification.review.id.label": "Tú tarjeta de identificación",
"id.verification.review.id.alt": "Foto de tu tarjeta de identificación para ser enviada.",
"id.verification.review.id.retake": "Retomar Foto de identificación",
"id.verification.review.confirm": "Enviar",
"id.verification.submission.alert.error.face": "Se requiere una foto de tu rostro. Vuelve a tomar tu foto de retrato.",
"id.verification.submission.alert.error.id": "Se requiere una foto de tu documento de ID. Vuelve a tomar tu foto de ID.",
"id.verification.submission.alert.error.name": "Se requiere un nombre de cuenta válido. Actualiza el nombre de tu cuenta para que coincida con el nombre que figura en tu ID.",
"id.verification.submission.alert.error.unsupported": "Uno o más de los siguientes archivos que has subido se encuentran en un formato no soportado. Por favor escoge de los siguientes formatos:",
"id.verification.review.error": "Página de soporte de {siteName}",
"id.verification.submitted.title": "Verificación de identidad en progreso.",
"id.verification.submitted.text": "Hemos recibido su información y estamos verificando su identidad. Se le notificará cuando se complete el proceso de verificación (generalmente dentro de los 5 días). Mientras tanto, aún puede acceder a todo el contenido del curso disponible.",
"id.verification.return.dashboard": "Volver al panel principal",
"id.verification.return.course": "Regresar al curso",
"id.verification.return.generic": "Regresar",
"id.verification.photo.upload.help.title": "Cargar una imágen en vez",
"id.verification.photo.camera.help.title": "Usar la cámara en vez",
"id.verification.photo.upload.help.text": "Si estás presentando problemas usando el capturador de fotos, es posible que prefieras subir una foto en vez. Para subir una foto, has clic en el botón a continuación ",
"id.verification.photo.camera.help.text": "Si estás presentando problemas subiendo una foto, es posible que prefieras usar la cámara en vez. Para usar una cámara, has clic en el botón a continuación.",
"id.verification.upload.help.button": "Cambia a modo de carga",
"id.verification.camera.help.button": "Cambia a modo de cámara",
"id.verification.request.camera.access.instructions": "Para tomar una foto con tu cámara web, es posible que recibas un aviso del navegador para acceder a tu cámara. {clickAllow}",
"id.verification.requirements.account.managed.alert": "La configuración de tu perfil es administrada por {managerTitle}. Por lo tanto si el nombre en tu ID de foto coincide con tu nombre de cuenta por favor ponte en contacto con tu administrador de {profileDataManager} o con {soporte} para solicitar ayuda antes de completar el proceso de verificación de foto.",
"id.verification.requirements.card.device.text": "Necesitas un dispositivo que tenga una cámara. Si has recibido un aviso del navegador para habilitar acceso a tu cámara, por favor asegúrate de seleccionar [allow].",
"id.verification.account.name.summary.alert": "La configuración de tu perfil es administrada por {managerTitle}. Por lo tanto si el nombre en tu ID de foto coincide con tu nombre de cuenta por favor ponte en contacto con tu administrador de {profileDataManager} o con {soporte} para solicitar ayuda.",
"idv.submission.alert.error": "\n Se produjo un error técnico al intentar enviar la verificación de ID.\n Es posible que sea una cuestión temporal, así que inténtalo de nuevo en unos minutos.\n Si el problema continúa, dirígete a {support_link} para obtener ayuda.\n ",
"id.verification.account.name.edit": "Editar {sr}"
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another edX account.",
"account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another {siteName} account.",
"account.settings.message.managed.settings": "Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help.",
"account.settings.message.managed.settings.support": "support",
"account.settings.page.heading": "Account Settings",
@@ -14,16 +14,40 @@
"account.settings.section.demographics.information": "Optional Information",
"account.settings.section.site.preferences": "Site Preferences",
"account.settings.section.linked.accounts": "Linked Accounts",
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to edX.",
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to {siteName}.",
"account.settings.field.username": "Username",
"account.settings.field.username.help.text": "The name that identifies you on edX. You cannot change your username.",
"account.settings.field.username.help.text": "The name that identifies you on {siteName}. You cannot change your username.",
"account.settings.field.full.name": "Full name",
"account.settings.field.full.name.empty": "Add name",
"account.settings.field.full.name.help.text": "The name that is used for ID verification and that appears on your certificates.",
"account.settings.field.full.name.help.text.default": "The name that appears on your public profile.",
"account.settings.field.full.name.help.text.default.certificate": "This name is selected to appear on your certificates and public-facing records.",
"account.settings.field.name.verified": "Verified name",
"account.settings.field.name.verified.help.text.verified": "This name has been verified by photo ID.",
"account.settings.field.name.verified.help.text.verified.proctored": "This name has been verified by proctoring.",
"account.settings.field.name.verified.help.text.verified.certificate": "This name has been verified by photo ID, and is selected to appear on your certificates and public-facing records.",
"account.settings.field.name.verified.help.text.verified.proctored.certificate": "This name has been verified by proctoring, and is selected to appear on your certificates and public-facing records.",
"account.settings.field.name.verified.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.",
"account.settings.field.name.verified.help.text.submitted.proctored": "Your proctored exam has been submitted. Verified name cannot be changed at this time. Please check back in 2-5 days.",
"account.settings.field.name.verified.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Verified name cannot be changed at this time.",
"account.settings.field.name.verified.help.text.submitted.proctored.certificate": "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.",
"account.settings.field.name.verified.verification.help": "Enter your name as it appears on your unexpired student, work, or government-issued identification card.",
"account.settings.field.full.name.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.",
"account.settings.field.full.name.help.text.submitted.proctored": "Your proctored exam has been submitted. Full name cannot be changed at this time. Please check back in 2-5 days.",
"account.settings.field.full.name.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.",
"account.settings.field.full.name.help.text.submitted.proctored.certificate": "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.",
"account.settings.field.name.verified.success.message": "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.",
"account.settings.field.name.verified.success.message.header": "Your name change request is complete!",
"account.settings.field.name.verified.failure.message": "Your most recent identity verification attempt did not pass. Related account settings have been restored.",
"account.settings.field.name.verified.failure.message.header": "We were not able to verify your identity.",
"account.settings.field.name.verified.failure.message.help.link": "Learn more about ID verification",
"account.settings.field.name.verified.submitted.message": "Your identity verification request has been submitted and usually takes between 24 and 48 hours to complete.",
"account.settings.field.name.verified.submitted.message.certificate": "When your request is approved, your updated name will appear on all associated certificates and public-facing records.",
"account.settings.field.name.verified.submitted.message.header": "Your name change request is almost complete!",
"account.settings.field.email": "Email address (Sign in)",
"account.settings.field.email.empty": "Add email address",
"account.settings.field.email.confirmation": "Weve sent a confirmation message to {value}. Click the link in the message to update your email address.",
"account.settings.field.email.help.text": "You receive messages from edX and course teams at this address.",
"account.settings.field.email.help.text": "You receive messages from {siteName} and course teams at this address.",
"account.settings.field.secondary.email": "Recovery email address",
"account.settings.field.secondary.email.empty": "Add a recovery email address",
"account.settings.field.secondary.email.confirmation": "Weve sent a confirmation message to {value}. Click the link in the message to update your recovery email address.",
@@ -31,6 +55,14 @@
"account.settings.field.dob": "Year of birth",
"account.settings.field.dob.empty": "Add year of birth",
"account.settings.field.year_of_birth.options.empty": "Select a year of birth",
"account.settings.field.dob.month": "Month",
"account.settings.field.dob.year": "Year",
"account.settings.field.dob.form.button": "Please confirm your date of birth",
"account.settings.field.dob.form.title": "Confirm your date of birth",
"account.settings.field.dob.form.help.text": "We ask for birth date information to help us comply with our legal obligations.",
"account.settings.field.dob.form.success": "Thank you for entering your birth date information.",
"account.settings.field.month_of_birth.options.empty": "Select a month of birth",
"account.settingsfield.dob.error.general": "A technical error occurred. Please try again.",
"account.settings.field.country": "Country",
"account.settings.field.country.empty": "Add country",
"account.settings.field.country.options.empty": "Select a Country",
@@ -57,7 +89,7 @@
"account.settings.field.gender.options.f": "Female",
"account.settings.field.gender.options.m": "Male",
"account.settings.field.gender.options.o": "Other",
"account.settings.field.language.proficiencies": "Spoken languages",
"account.settings.field.language.proficiencies": "Spoken language",
"account.settings.field.language.proficiencies.empty": "Add a spoken language",
"account.settings.field.language_proficiencies.options.empty": "Select a Language",
"account.settings.field.time.zone": "Time zone",
@@ -67,7 +99,7 @@
"account.settings.field.time.zone.all": "All time zones",
"account.settings.field.time.zone.country": "Country time zones",
"account.settings.section.social.media": "Social Media Links",
"account.settings.section.social.media.description": "Optionally, link your personal accounts to the social media icons on your edX profile.",
"account.settings.section.social.media.description": "Optionally, link your personal accounts to the social media icons on your {siteName} profile.",
"account.settings.field.social.platform.name.linkedin": "LinkedIn",
"account.settings.field.social.platform.name.linkedin.empty": "Add LinkedIn profile",
"account.settings.jump.nav.delete.account": "Delete My Account",
@@ -80,6 +112,12 @@
"account.settings.editable.field.action.edit": "Edit",
"account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
"account.settings.static.field.empty.no.admin": "No value set.",
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
"account.settings.field.name.modal.certificate.select": "Select a name",
"account.settings.field.name.modal.certificate.option.full": "Full Name",
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
"account.settings.coaching.consent.welcome.header": "Lets get started.",
"account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish",
"account.settings.coaching.consent.description": "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If youre interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
@@ -101,17 +139,20 @@
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
"account.settings.delete.account.header": "Delete My Account",
"account.settings.delete.account.subheader": "We're sorry to see you go!",
"account.settings.delete.account.text.1": "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.",
"account.settings.delete.account.text.2": "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.",
"account.settings.delete.account.text.3.link": "follow the instructions for printing or downloading a certificate",
"account.settings.delete.account.text.warning": "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.",
"account.settings.delete.account.text.1": "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.",
"account.settings.delete.account.text.2": "Once your account is deleted, you cannot use it to take courses on {siteName}.",
"account.settings.delete.account.text.2.edX": "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.",
"account.settings.delete.account.text.3.link": "Follow these instructions for printing or downloading a certificate",
"account.settings.delete.account.text.warning": "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}.",
"account.settings.delete.account.text.change.instead": "Want to change your email, name, or password instead?",
"account.settings.delete.account.button": "Delete My Account",
"account.settings.delete.account.please.activate": "activate your account",
"account.settings.delete.account.please.confirm": "confirm your account",
"account.settings.delete.account.please.unlink": "unlink all social media accounts",
"account.settings.delete.account.modal.header": "Are you sure?",
"account.settings.delete.account.modal.text.1": "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.",
"account.settings.delete.account.modal.text.2": "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.",
"account.settings.delete.account.modal.text.1": "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.",
"account.settings.delete.account.modal.text.2": "If you proceed, you will be unable to use this account to take courses on {siteName}.",
"account.settings.delete.account.modal.text.2.edX": "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.",
"account.settings.delete.account.modal.enter.password": "If you still wish to continue and delete your account, please enter your account password:",
"account.settings.delete.account.modal.confirm.delete": "Yes, Delete",
"account.settings.delete.account.modal.confirm.cancel": "Cancel",
@@ -122,7 +163,8 @@
"account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.",
"account.settings.delete.account.modal.after.text": "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.",
"account.settings.delete.account.modal.after.button": "Close",
"account.settings.delete.account.text.3": "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}.",
"account.settings.delete.account.text.3.edX": "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}.",
"account.settings.delete.account.text.3": "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.",
"account.settings.message.demographics.service.issue": "An error occurred attempting to retrieve or save your account information. Please try again later.",
"account.settings.field.demographics.gender": "Gender identity",
"account.settings.field.demographics.gender.empty": "Add gender identity",
@@ -153,7 +195,17 @@
"account.settings.field.demographics.future_work_sector": "Future work industry",
"account.settings.field.demographics.future_work_sector.empty": "Add work industry",
"account.settings.field.demographics.work_sector.options.empty": "Select work industry",
"account.settings.section.demographics.why": "Why does edX collect this information?",
"account.settings.section.demographics.why": "Why does {siteName} collect this information?",
"account.settings.name.change.title.id": "This name change requires identity verification",
"account.settings.name.change.title.begin": "Before we begin",
"account.settings.name.change.warning.one": "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.",
"account.settings.name.change.warning.two": "This action cannot be undone without verifying your identity.",
"account.settings.name.change.id.name.label": "Enter your name as it appears on your unexpired student, work, or government-issued identification card.",
"account.settings.name.change.id.name.placeholder": "Enter the name on your photo ID",
"account.settings.name.change.error.valid.name": "Please enter a valid name.",
"account.settings.name.change.error.general": "A technical error occurred. Please try again.",
"account.settings.name.change.continue": "Continue",
"account.settings.name.change.cancel": "Cancel",
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
@@ -165,23 +217,27 @@
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
"account.settings.sso.unlink.account": "Unlink {name} account",
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}.",
"account.page.title": "Account | {siteName}",
"id.verification.access.blocked.denied": "We cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}.",
"id.verification.next": "Next",
"id.verification.support": "support",
"id.verification.example.card.alt": "Example of a valid identification card with a full name and photo.",
"id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
"id.verification.requirements.description": "In order to complete Photo Verification, you will need the following:",
"id.verification.requirements.card.device.title": "Device with Camera",
"id.verification.requirements.card.device.allow": "Allow",
"id.verification.requirements.card.id.title": "Photo Identification",
"id.verification.requirements.card.id.text": "You need a valid ID that contains your full name and photo.",
"id.verification.requirements.card.id.title": "Photo Identification Card",
"id.verification.requirements.card.id.text": "You need a valid identification card that contains your full name and photo, such as a drivers license or passport.",
"id.verification.privacy.title": "Privacy Information",
"id.verification.privacy.need.photo.question": "Why does edX need my photo?",
"id.verification.privacy.need.photo.question": "Why does {siteName} need my photo?",
"id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
"id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.existing.request.title": "Identity Verification",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).",
"id.verification.privacy.do.with.photo.question": "What does {siteName} do with this photo?",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on {siteName} after the verification process is complete.",
"id.verification.access.blocked.title": "Identity Verification",
"id.verification.access.blocked.enrollment": "You are not currently enrolled in a course that requires identity verification.",
"id.verification.access.blocked.pending": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).",
"id.verification.photo.take": "Take Photo",
"id.verification.photo.retake": "Retake Photo",
"id.verification.photo.retake": "Retake Photo?",
"id.verification.photo.enable.detection": "Enable Face Detection",
"id.verification.photo.enable.detection.portrait.help.text": "If checked, a box will appear around your face. Your face can be seen clearly if the box around it is blue. If your face is not in a good position or undetectable, the box will be red.",
"id.verification.photo.enable.detection.id.help.text": "If checked, a box will appear around the face on your ID card. The face can be seen clearly if the box around it is blue. If the face is not in a good position or undetectable, the box will be red.",
@@ -232,6 +288,9 @@
"id.verification.camera.access.failure.temporary.safari.step2": "Click on the Safari app menu, then select \"Preferences.\" You can also use Command+, as a keyboard shortcut.",
"id.verification.camera.access.failure.temporary.safari.step3": "Select the \"Websites\" tab and then select \"Camera.\"",
"id.verification.camera.access.failure.temporary.safari.step4": "Select \"edx.org\" and change the camera permissions to \"Allow.\"",
"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": "Helpful Photo Tips",
"id.verification.photo.tips.description": "Next, we'll need you to take a photo of your face. Please review the helpful tips below.",
"id.verification.photo.tips.list.title": "Photo Tips",
@@ -239,54 +298,62 @@
"id.verification.photo.tips.list.well.lit": "Your face is well-lit.",
"id.verification.photo.tips.list.inside.frame": "Your entire face fits inside the frame.",
"id.verification.portrait.photo.title.camera": "Take a Photo of Yourself",
"id.verification.portrait.photo.title.upload": "Upload a Photo of Yourself",
"id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"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: .jpg, .jpeg, .png)",
"id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.help.sight.answer.portrait": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally the best position for a headshot is approximately 12-18 inches (30-45 centimeters) from the camera, with your head centered relative to the computer screen. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle.",
"id.verification.camera.help.sight.answer.id": "You may be able to complete the image capture procedure without assistance, but it may take a couple of submission attempts to get the camera positioning right. Optimal camera positioning varies with each computer, but generally, the best position for a photo of an ID card is 8-12 inches (20-30 centimeters) from the camera, with the ID card centered relative to the camera. If the photos you submit are rejected, try moving the computer or camera orientation to change the lighting angle. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.difficulty.question.portrait": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.difficulty.question.id": "What if I have difficulty holding my ID in position relative to the camera?",
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.id.photo.unclear.question": "Is your ID image not clear or too blurry?",
"id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid ID that includes your name. Please have your ID ready.",
"id.verification.id.tips.list.well.lit": "Your ID is well-lit.",
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact {siteName} support for additional suggestions.",
"id.verification.id.photo.unclear.question": "Is your ID card image not clear or too blurry?",
"id.verification.id.tips.title": "Helpful Identification Card Tips",
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid identification card that includes your full name and photo, such as a drivers license or passport. Please have your ID ready.",
"id.verification.id.tips.list.well.lit": "Your identification card is well-lit.",
"id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.",
"id.verification.id.photo.title.camera": "Take a Photo of Your ID",
"id.verification.id.photo.title.upload": "Upload a Photo of Your ID",
"id.verification.id.photo.title.camera": "Take a Photo of Your Identification Card",
"id.verification.id.photo.title.upload": "Upload a Photo of Your Identification Card",
"id.verification.id.photo.preview.alt": "Preview of photo ID.",
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
"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: .jpg, .jpeg, .png)",
"id.verification.id.photo.instructions.upload.error": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "Account Name Check",
"id.verification.account.name.instructions": "The name on your account and the name on your ID must be an exact match. If not, please click \"No\" to update your account name.",
"id.verification.account.name.radio.label": "Does the name on your ID match the Account Name below?",
"id.verification.account.name.radio.yes": "Yes",
"id.verification.account.name.radio.no": "No",
"id.verification.account.name.error": "Please update account name to match the name on your ID.",
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo. Please use a passport, drivers license, or another identification card that includes your full name and a picture of your face.",
"id.verification.id.photo.instructions.upload": "Please upload a photo of your identification card. 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.name.check.title": "Double-Check Your Name",
"id.verification.name.check.instructions": "Does the name below match the name on your photo ID? If not, update the name below to match your photo ID.",
"id.verification.name.check.mismatch.information": "If the name below does not match your photo ID, your identity verification will be denied.",
"id.verification.name.error": "Please enter your name as it appears on your photo ID.",
"id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.account.name.settings": "Account Settings",
"id.verification.account.name.label": "Account Name",
"id.verification.name.label": "Name",
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.account.name.save": "Save and Next",
"id.verification.review.title": "Review Your Photos",
"id.verification.review.description": "Make sure we can verify your identity with the photos and information you have provided.",
"id.verification.review.portrait.label": "Your Portrait",
"id.verification.review.portrait.alt": "Photo of your face to be submitted.",
"id.verification.review.portrait.retake": "Retake Portrait Photo",
"id.verification.review.id.label": "Your Photo ID",
"id.verification.review.id.alt": "Photo of your ID to be submitted.",
"id.verification.review.id.label": "Your Identification Card",
"id.verification.review.id.alt": "Photo of your identification card to be submitted.",
"id.verification.review.id.retake": "Retake ID Photo",
"id.verification.review.confirm": "Submit",
"id.verification.review.error": "edX Support Page",
"id.verification.submission.alert.error.face": "A photo of your face is required. Please retake your portrait photo.",
"id.verification.submission.alert.error.id": "A photo of your ID card is required. Please retake your ID photo.",
"id.verification.submission.alert.error.name": "A valid account name is required. Please update your account name to match the name on your ID.",
"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": "{siteName} Support Page",
"id.verification.submitted.title": "Identity Verification in Progress",
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.",
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will be notified when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.",
"id.verification.return.dashboard": "Return to Your Dashboard",
"id.verification.return.course": "Return to Course",
"id.verification.return.generic": "Return",
"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.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {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": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}.",
"idv.submission.alert.error": "\n We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists,\n please go to {support_link} for help.\n ",
"id.verification.account.name.edit": "Edit{sr}"
"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 We encountered a technical error while trying to submit ID verification.\n This might be a temporary issue, so please try again in a few minutes.\n If the problem persists, please go to {support_link} for help.\n ",
"id.verification.account.name.edit": "Edit {sr}"
}

View File

@@ -0,0 +1,49 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './IdVerification.messages';
import { ERROR_REASONS } from './IdVerificationContext';
const AccessBlocked = ({ error, intl }) => {
const handleMessage = () => {
if (error === ERROR_REASONS.COURSE_ENROLLMENT) {
return <p>{intl.formatMessage(messages['id.verification.access.blocked.enrollment'])}</p>;
}
if (error === ERROR_REASONS.EXISTING_REQUEST) {
return <p>{intl.formatMessage(messages['id.verification.access.blocked.pending'])}</p>;
}
return (
<FormattedMessage
id="id.verification.access.blocked.denied"
defaultMessage="We cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}."
description="Text that displays when user is denied from making a request, and to check their email for an activation email."
values={{
email: <strong>no-reply@registration.edx.org</strong>,
}}
/>
);
};
return (
<div>
<h3 aria-level="1" tabIndex="-1">
{intl.formatMessage(messages['id.verification.access.blocked.title'])}
</h3>
{handleMessage()}
<div className="action-row">
<a className="btn btn-primary mt-3" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{intl.formatMessage(messages['id.verification.return.dashboard'])}
</a>
</div>
</div>
);
};
AccessBlocked.propTypes = {
intl: intlShape.isRequired,
error: PropTypes.string.isRequired,
};
export default injectIntl(AccessBlocked);

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