Compare commits

...

98 Commits

Author SHA1 Message Date
stvn
71553c98a7 build(ci): always run npm ci via Makefile 2021-06-02 01:31:41 -07:00
stvn
29c21c1b1d build(ci): convert travis-ci to github actions 2021-06-02 01:31:03 -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
109 changed files with 9114 additions and 14522 deletions

10
.env
View File

@@ -13,7 +13,13 @@ NODE_ENV=null
ORDER_HISTORY_URL=null
REFRESH_ACCESS_TOKEN_ENDPOINT=null
SEGMENT_KEY=null
SITE_NAME=null
SITE_NAME=''
SUPPORT_URL=null
USER_INFO_COOKIE_NAME=null
ENTERPRISE_LEARNER_PORTAL_HOSTNAME=null
LOGO_URL=''
LOGO_TRADEMARK_URL=''
LOGO_WHITE_URL=''
FAVICON_URL=''
PUBLISHER_BASE_URL=
STUDIO_BASE_URL=
DISCOVERY_API_BASE_URL=

View File

@@ -14,10 +14,15 @@ ORDER_HISTORY_URL='localhost:1996/orders'
PORT=1997
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
SITE_NAME=localhost
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'
COACHING_ENABLED=false
ENABLE_DEMOGRAPHICS_COLLECTION=false
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
PUBLISHER_BASE_URL=
STUDIO_BASE_URL=
DISCOVERY_API_BASE_URL=

View File

@@ -13,8 +13,15 @@ NODE_ENV=null
ORDER_HISTORY_URL='localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
SITE_NAME=localhost
SUPPORT_URL='http://localhost:18000/support'
USER_INFO_COOKIE_NAME='edx-user-info'
COACHING_ENABLED=''
ENABLE_DEMOGRAPHICS_COLLECTION=''
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
PUBLISHER_BASE_URL=
STUDIO_BASE_URL=
DISCOVERY_API_BASE_URL=

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @edx/community-engineering

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

@@ -0,0 +1,26 @@
---
name: validate
on:
push:
branches:
- master
pull_request:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version:
- 12
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
- run: npm install -g npm@6
- run: make requirements
- run: make test
- name: upload coverage
uses: codecov/codecov-action@v1
with:
fail_ci_if_error: true

1
.gitignore vendored
View File

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

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

@@ -10,8 +10,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=i18n_extract lint test build is-es5
.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...

View File

@@ -3,46 +3,107 @@
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/edx/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/edx/devstack>`_, see the `Getting Started <https://github.com/edx/devstack#getting-started>`_ section for setup instructions.
Configuration and Deployment
----------------------------
1. Install Devstack using the `Getting Started <https://github.com/edx/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.
==============================
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-account.svg?branch=master
:target: https://travis-ci.com/edx/frontend-app-account
.. |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

15289
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@
"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"
@@ -29,27 +29,27 @@
"ie 11"
],
"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",
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
"@edx/frontend-component-footer": "10.1.4",
"@edx/frontend-component-header": "2.2.4",
"@edx/frontend-platform": "1.9.5",
"@edx/paragon": "13.1.2",
"@fortawesome/fontawesome-svg-core": "1.2.34",
"@fortawesome/free-brands-svg-icons": "5.8.2",
"@fortawesome/free-regular-svg-icons": "5.7.2",
"@fortawesome/free-solid-svg-icons": "5.8.2",
"@fortawesome/react-fontawesome": "0.1.12",
"@tensorflow-models/blazeface": "git+https://github.com/alangsto/blazeface.git",
"@fortawesome/react-fontawesome": "0.1.14",
"@tensorflow-models/blazeface": "0.0.7",
"@tensorflow/tfjs-converter": "1.6.1",
"@tensorflow/tfjs-core": "1.6.1",
"babel-polyfill": "6.26.0",
"bowser": "^2.10.0",
"bowser": "2.10.0",
"classnames": "2.2.6",
"font-awesome": "4.7.0",
"form-urlencoded": "4.0.1",
"formdata-polyfill": "3.0.20",
"history": "4.10.1",
"jslib-html5-camera-photo": "^3.1.3",
"lodash.camelcase": "4.3.0",
"jslib-html5-camera-photo": "3.1.6",
"lodash.debounce": "4.0.8",
"lodash.findindex": "4.6.0",
"lodash.get": "4.4.2",
@@ -57,12 +57,11 @@
"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",
"lodash.pickby": "4.6.0",
"memoize-one": "5.1.1",
"newrelic": "5.13.1",
"prop-types": "15.7.2",
"qs": "6.9.4",
"qs": "6.9.6",
"react": "16.10.2",
"react-dom": "16.10.2",
"react-redux": "7.1.3",
@@ -80,15 +79,14 @@
"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",
"@edx/frontend-build": "5.6.9",
"@testing-library/jest-dom": "5.11.9",
"@testing-library/react": "10.4.9",
"codecov": "3.7.2",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.5",
"enzyme-adapter-react-16": "1.15.6",
"es-check": "5.0.0",
"husky": "3.0.9",
"jest": "^26.1.0",
"react-test-renderer": "16.8.6",
"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" />
<% 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,15 @@
{
"extends": [
"config:base"
"config:base",
":automergeLinters",
":automergeTesters",
":automergeMinor",
":noUnscheduledUpdates",
":semanticCommits"
],
"patch": {
"automerge": true
},
"rebaseStalePrs": true
"rebaseStalePrs": true,
"schedule": [
"every weekend"
],
"timezone": "America/New_York"
}

View File

@@ -38,15 +38,14 @@ import { fetchSiteLanguages } from './site-language';
import CoachingToggle from './coaching/CoachingToggle';
import DemographicsSection from './demographics/DemographicsSection';
class AccountSettingsPage extends React.Component {
constructor(props, context) {
super(props, context);
// If there is a "duplicate_provider" query parameter, that's the backend's
// way of telling us that the provider account the user tried to link is already linked
// to another Open edX account. We use this to display a message to that effect, and remove the
// parameter from the URL.
// to another user account on the platform. We use this to display a message to that effect,
// and remove the parameter from the URL.
const duplicateTpaProvider = getQueryParameters().duplicate_provider;
if (duplicateTpaProvider !== undefined) {
history.replace(history.location.pathname);
@@ -81,10 +80,11 @@ class AccountSettingsPage extends React.Component {
const locationHash = global.location.hash;
// Check for the locationHash in the URL and then scroll to it if it is in the
// NavLinks list
if (typeof locationHash !== 'string')
if (typeof locationHash !== 'string') {
return;
}
if (Object.keys(this.navLinkRefs).includes(locationHash) && this.navLinkRefs[locationHash].current) {
window.scrollTo(0, this.navLinkRefs[locationHash].current.offsetTop)
window.scrollTo(0, this.navLinkRefs[locationHash].current.offsetTop);
}
}
}
@@ -136,6 +136,14 @@ class AccountSettingsPage extends React.Component {
})),
}));
handleEditableFieldChange = (name, value) => {
this.props.updateDraft(name, value);
};
handleSubmit = (formId, values) => {
this.props.saveSettings(formId, values);
};
isEditable(fieldName) {
return !this.props.staticFields.includes(fieldName);
}
@@ -146,14 +154,6 @@ class AccountSettingsPage extends React.Component {
return Boolean(this.props.profileDataManager);
}
handleEditableFieldChange = (name, value) => {
this.props.updateDraft(name, value);
};
handleSubmit = (formId, values) => {
this.props.saveSettings(formId, values);
};
renderDuplicateTpaProviderMessage() {
if (!this.state.duplicateTpaProvider) {
return null;
@@ -164,10 +164,11 @@ class AccountSettingsPage extends React.Component {
<Alert className="alert alert-danger" role="alert">
<FormattedMessage
id="account.settings.message.duplicate.tpa.provider"
defaultMessage="The {provider} account you selected is already linked to another edX account."
description="alert message informing the user that the third-party account they attempted to link is already linked to another edX account"
defaultMessage="The {provider} account you selected is already linked to another {siteName} account."
description="alert message informing the user that the third-party account they attempted to link is already linked to another account"
values={{
provider: <b>{this.state.duplicateTpaProvider}</b>,
siteName: getConfig().SITE_NAME,
}}
/>
</Alert>
@@ -215,7 +216,7 @@ class AccountSettingsPage extends React.Component {
}
renderSecondaryEmailField(editableFieldProps) {
if (!Boolean(this.props.formValues.secondary_email_enabled)) {
if (!this.props.formValues.secondary_email_enabled) {
return null;
}
@@ -235,11 +236,10 @@ class AccountSettingsPage extends React.Component {
// check the result of an LMS API call to determine if we should render the DemographicsSection component
if (this.props.formValues.shouldDisplayDemographicsSection) {
return (
<DemographicsSection forwardRef={this.navLinkRefs['#demographics-information']}/>
<DemographicsSection forwardRef={this.navLinkRefs['#demographics-information']} />
);
} else {
return null;
}
return null;
}
renderContent() {
@@ -259,7 +259,7 @@ class AccountSettingsPage extends React.Component {
} = this.getLocalizedOptions(this.context.locale, this.props.formValues.country);
// Show State field only if the country is US (could include Canada later)
const showState = this.props.formValues.country == COUNTRY_WITH_STATES;
const showState = this.props.formValues.country === COUNTRY_WITH_STATES;
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
this.props.timeZoneOptions,
@@ -270,7 +270,7 @@ class AccountSettingsPage extends React.Component {
const hasLinkedTPA = findIndex(this.props.tpaProviders, provider => provider.connected) >= 0;
return (
<React.Fragment>
<>
<div className="account-section" id="basic-information" ref={this.navLinkRefs['#basic-information']}>
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.account.information'])}
@@ -283,7 +283,10 @@ class AccountSettingsPage extends React.Component {
type="text"
value={this.props.formValues.username}
label={this.props.intl.formatMessage(messages['account.settings.field.username'])}
helpText={this.props.intl.formatMessage(messages['account.settings.field.username.help.text'])}
helpText={this.props.intl.formatMessage(
messages['account.settings.field.username.help.text'],
{ siteName: getConfig().SITE_NAME },
)}
isEditable={false}
{...editableFieldProps}
/>
@@ -293,9 +296,9 @@ class AccountSettingsPage extends React.Component {
value={this.props.formValues.name}
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
emptyLabel={
this.isEditable('name') ?
this.props.intl.formatMessage(messages['account.settings.field.full.name.empty']) :
this.renderEmptyStaticFieldMessage()
this.isEditable('name')
? this.props.intl.formatMessage(messages['account.settings.field.full.name.empty'])
: this.renderEmptyStaticFieldMessage()
}
helpText={this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])}
isEditable={this.isEditable('name')}
@@ -305,13 +308,16 @@ class AccountSettingsPage extends React.Component {
name="email"
label={this.props.intl.formatMessage(messages['account.settings.field.email'])}
emptyLabel={
this.isEditable('email') ?
this.props.intl.formatMessage(messages['account.settings.field.email.empty']) :
this.renderEmptyStaticFieldMessage()
this.isEditable('email')
? this.props.intl.formatMessage(messages['account.settings.field.email.empty'])
: this.renderEmptyStaticFieldMessage()
}
value={this.props.formValues.email}
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
helpText={this.props.intl.formatMessage(messages['account.settings.field.email.help.text'])}
helpText={this.props.intl.formatMessage(
messages['account.settings.field.email.help.text'],
{ siteName: getConfig().SITE_NAME },
)}
isEditable={this.isEditable('email')}
{...editableFieldProps}
/>
@@ -333,14 +339,15 @@ class AccountSettingsPage extends React.Component {
options={countryOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.country'])}
emptyLabel={
this.isEditable('country') ?
this.props.intl.formatMessage(messages['account.settings.field.country.empty']) :
this.renderEmptyStaticFieldMessage()
this.isEditable('country')
? this.props.intl.formatMessage(messages['account.settings.field.country.empty'])
: this.renderEmptyStaticFieldMessage()
}
isEditable={this.isEditable('country')}
{...editableFieldProps}
/>
{showState &&
{showState
&& (
<EditableField
name="state"
type="select"
@@ -348,14 +355,14 @@ class AccountSettingsPage extends React.Component {
options={stateOptions}
label={this.props.intl.formatMessage(messages['account.settings.field.state'])}
emptyLabel={
this.isEditable('state') ?
this.props.intl.formatMessage(messages['account.settings.field.state.empty']) :
this.renderEmptyStaticFieldMessage()
this.isEditable('state')
? this.props.intl.formatMessage(messages['account.settings.field.state.empty'])
: this.renderEmptyStaticFieldMessage()
}
isEditable={this.isEditable('state')}
{...editableFieldProps}
/>
}
)}
</div>
<div className="account-section" id="profile-information" ref={this.navLinkRefs['#profile-information']}>
@@ -390,21 +397,27 @@ class AccountSettingsPage extends React.Component {
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
{...editableFieldProps}
/>
{getConfig().COACHING_ENABLED &&
this.props.formValues.coaching.eligible_for_coaching &&
{getConfig().COACHING_ENABLED
&& this.props.formValues.coaching.eligible_for_coaching
&& (
<CoachingToggle
name="coaching"
phone_number={this.props.formValues.phone_number}
coaching={this.props.formValues.coaching}
/>
}
)}
</div>
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && this.renderDemographicsSection()}
<div className="account-section" id="social-media">
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.social.media.description'])}</p>
<p>
{this.props.intl.formatMessage(
messages['account.settings.section.social.media.description'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<EditableField
name="social_link_linkedin"
@@ -465,7 +478,12 @@ class AccountSettingsPage extends React.Component {
<div className="account-section" id="linked-accounts" ref={this.navLinkRefs['#linked-accounts']}>
<h2 className="section-heading">{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts'])}</h2>
<p>{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts.description'])}</p>
<p>
{this.props.intl.formatMessage(
messages['account.settings.section.linked.accounts.description'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<ThirdPartyAuth />
</div>
@@ -476,7 +494,7 @@ class AccountSettingsPage extends React.Component {
/>
</div>
</React.Fragment>
</>
);
}
@@ -542,6 +560,7 @@ AccountSettingsPage.propTypes = {
name: PropTypes.string,
email: PropTypes.string,
secondary_email: PropTypes.string,
secondary_email_enabled: PropTypes.bool,
year_of_birth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
country: PropTypes.string,
level_of_education: PropTypes.string,
@@ -557,6 +576,8 @@ AccountSettingsPage.propTypes = {
user: PropTypes.number.isRequired,
eligible_for_coaching: PropTypes.bool.isRequired,
}),
state: PropTypes.string,
shouldDisplayDemographicsSection: PropTypes.bool,
}).isRequired,
siteLanguage: PropTypes.shape({
previousValue: PropTypes.string,

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': {
@@ -108,7 +108,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': {
@@ -279,18 +279,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 +331,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,7 +2,6 @@ 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)}>
@@ -16,7 +15,6 @@ function Alert(props) {
);
}
Alert.propTypes = {
className: PropTypes.string,
icon: PropTypes.node,
@@ -29,5 +27,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

@@ -2,7 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
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';
@@ -15,7 +17,6 @@ import {
} from './data/actions';
import { editableFieldSelector } from './data/selectors';
function EditableField(props) {
const {
name,
@@ -60,33 +61,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,
});
@@ -120,7 +125,7 @@ function EditableField(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 EditableField(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 EditableField(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>
) : null}
@@ -165,7 +170,6 @@ function EditableField(props) {
);
}
EditableField.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@@ -207,9 +211,9 @@ EditableField.defaultProps = {
helpText: undefined,
isEditing: false,
isEditable: true,
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,7 +18,6 @@ import {
} from './data/actions';
import { editableFieldSelector } from './data/selectors';
function EmailField(props) {
const {
name,
@@ -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>
@@ -166,7 +171,6 @@ function EmailField(props) {
);
}
EmailField.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
@@ -203,7 +207,6 @@ EmailField.defaultProps = {
isEditable: true,
};
export default connect(editableFieldSelector, {
onEdit: openForm,
onCancel: closeForm,

View File

@@ -3,10 +3,9 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
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';
import messages from './AccountSettingsPage.messages';
function JumpNav({ intl, displayDemographicsLink }) {
return (
@@ -34,13 +33,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'])}
@@ -66,14 +66,9 @@ function JumpNav({ intl, displayDemographicsLink }) {
);
}
JumpNav.propTypes = {
intl: intlShape.isRequired,
displayDemographicsLink: PropTypes.bool.isRequired,
};
JumpNav.defaultProps = {
displayDemographicsLink: false,
}
export default injectIntl(JumpNav);

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,7 +22,6 @@ const onChildExit = (htmlNode) => {
}
};
function SwitchContent({ expression, cases, className }) {
const getContent = (caseKey) => {
if (cases[caseKey]) {
@@ -29,7 +29,8 @@ function SwitchContent({ expression, cases, className }) {
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);
}
@@ -49,7 +50,6 @@ function SwitchContent({ expression, cases, className }) {
);
}
SwitchContent.propTypes = {
expression: PropTypes.string,
cases: PropTypes.objectOf(PropTypes.node).isRequired,
@@ -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;
@@ -18,8 +20,10 @@
position: sticky;
top: 1rem;
}
li {
margin-bottom: .5rem;
a {
text-decoration: underline;
}
@@ -30,15 +34,17 @@
@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;

View File

@@ -97,7 +97,6 @@ class CoachingConsent extends React.Component {
if (get(data, 'status') === 200) {
this.setState({ submissionSuccess: true });
}
}
handleSubmit(e) {
@@ -135,23 +134,27 @@ 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:
@@ -233,7 +236,6 @@ AutoRedirect.propTypes = {
CoachingConsent.defaultProps = {
loaded: false,
saveState: undefined,
profileDataManager: null,
};
@@ -259,7 +261,6 @@ CoachingConsent.propTypes = {
phone_number: PropTypes.object,
}).isRequired,
fetchSettings: PropTypes.func.isRequired,
saveState: PropTypes.string,
profileDataManager: PropTypes.string,
};

View File

@@ -33,12 +33,13 @@ const CoachingForm = props => (
<div>
<form onSubmit={props.onSubmit}>
<div className="py-3">
{
!!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 +50,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 +67,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 +126,6 @@ ErrorMessage.propTypes = {
ManagedProfileAlert.propTypes = {
profileDataManager: PropTypes.string.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(CoachingForm);

View File

@@ -8,7 +8,6 @@ import { editableFieldSelector } from '../data/selectors';
import { saveSettings, updateDraft, saveMultipleSettings } from '../data/actions';
import EditableField from '../EditableField';
const CoachingToggle = props => (
<>
<EditableField
@@ -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"

View File

@@ -13,9 +13,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

@@ -103,10 +103,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
@@ -254,10 +252,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

View File

@@ -44,7 +44,6 @@ export const fetchSettingsReset = () => ({
type: FETCH_SETTINGS.RESET,
});
// FORM STATE ACTIONS
export const openForm = formId => ({
@@ -69,7 +68,6 @@ export const resetDrafts = () => ({
type: RESET_DRAFTS,
});
// 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 = [];

View File

@@ -48,11 +48,9 @@ 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,
@@ -97,9 +95,7 @@ const reducer = (state = defaultState, action) => {
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: {},
};
@@ -120,19 +116,19 @@ 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 +157,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:

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';
@@ -52,7 +54,7 @@ export function* handleFetchSettings() {
userId,
);
if (values.country) yield put(fetchTimeZones(values.country));
if (values.country) { yield put(fetchTimeZones(values.country)); }
yield put(fetchSettingsSuccess({
values,
@@ -91,7 +93,7 @@ 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) {
@@ -104,7 +106,6 @@ export function* handleSaveSettings(action) {
}
}
// handles mutiple settings saved at once, in order, and stops executing on first failure.
export function* handleSaveMultipleSettings(action) {
try {
@@ -138,7 +139,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);

View File

@@ -72,7 +72,6 @@ export const staticFieldsSelector = createSelector(
accountSettings => (accountSettings.profileDataManager ? ['name', 'email', 'country'] : []),
);
/**
* If there's no draft present at all (undefined), use the original committed value.
*/

View File

@@ -44,7 +44,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];

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,36 @@ 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';
return (
<div>
<h2 className="section-heading">
{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 +98,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}
>

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

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

View File

@@ -11,28 +11,20 @@ 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>
.
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.
</span>
</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
@@ -47,9 +39,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
@@ -69,28 +59,20 @@ 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>
.
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.
</span>
</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
@@ -105,9 +87,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
@@ -163,28 +143,20 @@ 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>
.
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.
</span>
</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
@@ -199,9 +171,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

View File

@@ -1,14 +1,125 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SuccessModal should match default closed success modal snapshot 1`] = `
<div>
Array [
<div
className="fade"
role="presentation"
/>
/>,
<div
className="modal js-close-modal-on-click fade"
onMouseDown={[Function]}
className="modal fade"
role="presentation"
>
<div
aria-labelledby="id1"
aria-modal={true}
className=""
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id1"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
]
`;
exports[`SuccessModal should match default closed success modal snapshot 2`] = `
Array [
<div
className="fade"
role="presentation"
/>,
<div
className="modal fade"
role="presentation"
>
<div
@@ -16,61 +127,223 @@ exports[`SuccessModal should match default closed success modal snapshot 1`] = `
aria-modal={true}
className=""
role="dialog"
tabIndex="-1"
>
<div
className="modal-content"
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-header"
className="modal-content"
>
<h2
className="modal-title"
id="id2"
<div
className="modal-header"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
<h2
className="modal-title"
id="id2"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton1"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Close
</button>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>
</div>
</div>,
]
`;
exports[`SuccessModal should match default closed success modal snapshot 2`] = `
<div>
exports[`SuccessModal should match default closed success modal snapshot 3`] = `
Array [
<div
className="fade"
role="presentation"
/>
/>,
<div
className="modal js-close-modal-on-click fade"
onMouseDown={[Function]}
className="modal fade"
role="presentation"
>
<div
aria-labelledby="id3"
aria-modal={true}
className=""
role="dialog"
>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id3"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>,
]
`;
exports[`SuccessModal should match default closed success modal snapshot 4`] = `
Array [
<div
className="fade"
role="presentation"
/>,
<div
className="modal fade"
role="presentation"
>
<div
@@ -78,234 +351,213 @@ exports[`SuccessModal should match default closed success modal snapshot 2`] = `
aria-modal={true}
className=""
role="dialog"
tabIndex="-1"
>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id4"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton3"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`SuccessModal should match default closed success modal snapshot 3`] = `
<div>
<div
className="fade"
role="presentation"
/>
<div
className="modal js-close-modal-on-click fade"
onMouseDown={[Function]}
role="presentation"
>
<div
aria-labelledby="id6"
aria-modal={true}
className=""
role="dialog"
tabIndex="-1"
>
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
className="modal-content"
>
<div
className="modal-header"
>
<h2
className="modal-title"
id="id6"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton5"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Close
</button>
</div>
</div>
</div>
</div>
</div>
`;
exports[`SuccessModal should match default closed success modal snapshot 4`] = `
<div>
<div
className="fade"
role="presentation"
/>
<div
className="modal js-close-modal-on-click fade"
onMouseDown={[Function]}
role="presentation"
>
<div
aria-labelledby="id8"
aria-modal={true}
className=""
role="dialog"
tabIndex="-1"
>
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
<div
className="modal-content"
data-focus-lock-disabled="disabled"
onBlur={[Function]}
onFocus={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-header"
className="modal-content"
>
<h2
className="modal-title"
id="id8"
<div
className="modal-header"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
<h2
className="modal-title"
id="id4"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton7"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Close
</button>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={-1}
/>
</div>
</div>
</div>
</div>,
]
`;
exports[`SuccessModal should match open success modal snapshot 1`] = `
<div>
Array [
<div
className="modal-backdrop show"
role="presentation"
/>
/>,
<div
className="modal js-close-modal-on-click show d-block"
onMouseDown={[Function]}
className="modal show d-block"
role="presentation"
>
<div
aria-labelledby="id10"
aria-labelledby="id5"
aria-modal={true}
className="modal-dialog"
role="dialog"
tabIndex="-1"
>
<div
className="modal-content"
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={1}
/>
<div
data-focus-lock-disabled={false}
onBlur={[Function]}
onFocus={[Function]}
onMouseDown={[Function]}
onScrollCapture={[Function]}
onTouchMoveCapture={[Function]}
onTouchStart={[Function]}
onWheelCapture={[Function]}
>
<div
className="modal-header"
className="modal-content"
>
<h2
className="modal-title"
id="id10"
<div
className="modal-header"
>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
<h2
className="modal-title"
id="id5"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
We're sorry to see you go! Your account will be deleted shortly.
</h2>
</div>
<div
className="modal-body"
>
<div>
<p
className="h6"
>
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
</p>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn btn-link"
disabled={false}
onClick={[Function]}
type="button"
>
Close
</button>
</div>
</div>
<div
className="modal-footer"
>
<button
className="btn js-close-modal-on-click btn-secondary"
id="paragonCloseModalButton9"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Close
</button>
</div>
</div>
<div
data-focus-guard={true}
style={
Object {
"height": "0px",
"left": "1px",
"overflow": "hidden",
"padding": 0,
"position": "fixed",
"top": "1px",
"width": "1px",
}
}
tabIndex={0}
/>
</div>
</div>
</div>
</div>,
]
`;

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,7 +44,7 @@ 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',
@@ -58,11 +63,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);
}, [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,8 +158,8 @@ 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}>
@@ -171,17 +167,26 @@ class DemographicsSection extends React.Component {
{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,11 +307,11 @@ class DemographicsSection extends React.Component {
{...editableFieldProps}
/>
</div>
}
)}
</div>
)
);
}
};
}
DemographicsSection.propTypes = {
intl: intlShape.isRequired,
@@ -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.object,
}).isRequired,
drafts: PropTypes.shape({
demographics_gender: PropTypes.string,
demographics_user_ethnicity: PropTypes.array,
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.object,
}).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

@@ -2,582 +2,582 @@ import * as auth from '@edx/frontend-platform/auth';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import DemographicsSection from '../DemographicsSection';
import { Provider } from 'react-redux';
import React from 'react';
import configureStore from 'redux-mock-store';
import renderer from 'react-test-renderer';
import DemographicsSection from '../DemographicsSection';
jest.mock('@edx/frontend-platform/auth');
const IntlDemographicsSection = injectIntl(DemographicsSection);
jest.mock('../../data/selectors', () => {
return jest.fn().mockImplementation(() => ({ demographicsSectionSelector: () => ({}) }));
});
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ demographicsSectionSelector: () => ({}) })));
const mockStore = configureStore();
describe('DemographicsSection', () => {
let props = {};
let store = {};
const reduxWrapper = children => (
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
beforeEach(() => {
store = mockStore();
props = {
updateDraft: jest.fn(),
formValues: {
demographics_gender: 'declined',
demographics_gender_description: '',
demographics_user_ethnicity: [],
demographics_income: 'declined',
demographics_military_history: 'declined',
demographics_learner_education_level: 'declined',
demographics_parent_education_level: 'declined',
demographics_work_status: 'declined',
demographics_work_status_description: '',
demographics_current_work_sector: 'declined',
demographics_future_work_sector: 'declined',
demographics_user: 1,
demographicsOptions: {
actions: {
POST: {
gender: {
choices: [
{
"value": "woman",
"display_name": "Woman"
},
{
"value": "man",
"display_name": "Man"
},
{
"value": "non-binary",
"display_name": "Non-binary"
},
{
"value": "self-describe",
"display_name": "Prefer to self describe"
},
{
"value": "declined",
"display_name": "Prefer not to respond"
}
]
},
income: {
choices: [
{
"value": "less-than-10k",
"display_name": "Less than US $10,000"
},
{
"value": "10k-25k",
"display_name": "US $10,000 - $25,000"
},
{
"value": "25k-50k",
"display_name": "US $25,000 - $50,000"
},
{
"value": "50k-75k",
"display_name": "US $50,000 - $75,000"
},
{
"value": "75k-100k",
"display_name": "US $75,000 - $100,000"
},
{
"value": "over-100k",
"display_name": "Over US $100,000"
},
{
"value": "unsure",
"display_name": "I don't know"
},
{
"value": "declined",
"display_name": "Prefer not to respond"
}
]
},
learner_education_level: {
choices: [
{
"value": "no-high-school",
"display_name": "No High School"
},
{
"value": "some-high-school",
"display_name": "Some High School"
},
{
"value": "high-school-ged-equivalent",
"display_name": "High School diploma, GED, or equivalent"
},
{
"value": "some-college",
"display_name": "Some college, but no degree"
},
{
"value": "associates",
"display_name": "Associates degree"
},
{
"value": "bachelors",
"display_name": "Bachelors degree"
},
{
"value": "masters",
"display_name": "Masters degree"
},
{
"value": "professional",
"display_name": "Professional degree"
},
{
"value": "doctorate",
"display_name": "Doctorate degree"
},
{
"value": "declined",
"display_name": "Prefer not to respond"
}
]
},
parent_education_level: {
choices: [
{
"value": "no-high-school",
"display_name": "No High School"
},
{
"value": "some-high-school",
"display_name": "Some High School"
},
{
"value": "high-school-ged-equivalent",
"display_name": "High School diploma, GED, or equivalent"
},
{
"value": "some-college",
"display_name": "Some college, but no degree"
},
{
"value": "associates",
"display_name": "Associates degree"
},
{
"value": "bachelors",
"display_name": "Bachelors degree"
},
{
"value": "masters",
"display_name": "Masters degree"
},
{
"value": "professional",
"display_name": "Professional degree"
},
{
"value": "doctorate",
"display_name": "Doctorate degree"
},
{
"value": "declined",
"display_name": "Prefer not to respond"
}
]
},
military_history: {
choices: [
{
"value": "never-served",
"display_name": "Never served in the military"
},
{
"value": "training",
"display_name": "Only on active duty for training"
},
{
"value": "active",
"display_name": "Now on active duty"
},
{
"value": "previously-active",
"display_name": "On active duty in the past, but not now"
},
{
"value": "declined",
"display_name": "Prefer not to respond"
}
]
},
work_status: {
choices: [
{
"value": "full-time",
"display_name": "Employed, working full-time"
},
{
"value": "part-time",
"display_name": "Employed, working part-time"
},
{
"value": "self-employed",
"display_name": "Self-Employed"
},
{
"value": "not-employed-looking",
"display_name": "Not employed, looking for work"
},
{
"value": "not-employed-not-looking",
"display_name": "Not employed, not looking for work"
},
{
"value": "unable",
"display_name": "Unable to work"
},
{
"value": "retired",
"display_name": "Retired"
},
{
"value": "other",
"display_name": "Other"
},
{
"value": "declined",
"display_name": "Prefer not to respond"
}
]
},
current_work_sector: {
choices: [
{
"value": "accommodation-food",
"display_name": "Accommodation and Food Services"
},
{
"value": "administrative-support-waste-remediation",
"display_name": "Administrative and Support and Waste Management and Remediation Services"
},
{
"value": "agriculture-forestry-fishing-hunting",
"display_name": "Agriculture, Forestry, Fishing and Hunting"
},
{
"value": "arts-entertainment-recreation",
"display_name": "Arts, Entertainment, and Recreation"
},
{
"value": "construction",
"display_name": "Construction"
},
{
"value": "educational",
"display_name": "Education Services"
},
{
"value": "finance-insurance",
"display_name": "Finance and Insurance"
},
{
"value": "healthcare-social",
"display_name": "Health Care and Social Assistance"
},
{
"value": "information",
"display_name": "Information"
},
{
"value": "management",
"display_name": "Management of Companies and Enterprises"
},
{
"value": "manufacturing",
"display_name": "Manufacturing"
},
{
"value": "mining-quarry-oil-gas",
"display_name": "Mining, Quarrying, and Oil and Gas Extraction"
},
{
"value": "professional-scientific-technical",
"display_name": "Professional, Scientific, and Technical Services"
},
{
"value": "public-admin",
"display_name": "Public Administration"
},
{
"value": "real-estate",
"display_name": "Real Estate and Rental and Leasing"
},
{
"value": "retail",
"display_name": "Retail Trade"
},
{
"value": "transport-warehousing",
"display_name": "Transportation and Warehousing"
},
{
"value": "utilities",
"display_name": "Utilities"
},
{
"value": "trade",
"display_name": "Wholesale Trade"
},
{
"value": "other",
"display_name": "Other"
},
{
"value": "declined",
"display_name": "Prefer not to respond"
}
]
},
future_work_sector: {
choices: [
{
"value": "accommodation-food",
"display_name": "Accommodation and Food Services"
},
{
"value": "administrative-support-waste-remediation",
"display_name": "Administrative and Support and Waste Management and Remediation Services"
},
{
"value": "agriculture-forestry-fishing-hunting",
"display_name": "Agriculture, Forestry, Fishing and Hunting"
},
{
"value": "arts-entertainment-recreation",
"display_name": "Arts, Entertainment, and Recreation"
},
{
"value": "construction",
"display_name": "Construction"
},
{
"value": "educational",
"display_name": "Education Services"
},
{
"value": "finance-insurance",
"display_name": "Finance and Insurance"
},
{
"value": "healthcare-social",
"display_name": "Health Care and Social Assistance"
},
{
"value": "information",
"display_name": "Information"
},
{
"value": "management",
"display_name": "Management of Companies and Enterprises"
},
{
"value": "manufacturing",
"display_name": "Manufacturing"
},
{
"value": "mining-quarry-oil-gas",
"display_name": "Mining, Quarrying, and Oil and Gas Extraction"
},
{
"value": "professional-scientific-technical",
"display_name": "Professional, Scientific, and Technical Services"
},
{
"value": "public-admin",
"display_name": "Public Administration"
},
{
"value": "real-estate",
"display_name": "Real Estate and Rental and Leasing"
},
{
"value": "retail",
"display_name": "Retail Trade"
},
{
"value": "transport-warehousing",
"display_name": "Transportation and Warehousing"
},
{
"value": "utilities",
"display_name": "Utilities"
},
{
"value": "trade",
"display_name": "Wholesale Trade"
},
{
"value": "other",
"display_name": "Other"
},
{
"value": "declined",
"display_name": "Prefer not to respond"
}
]
},
user_ethnicity: {
child: {
children: {
ethnicity: {
choices: [
{
"value": "american-indian-or-alaska-native",
"display_name": "American Indian or Alaska Native"
},
{
"value": "asian",
"display_name": "Asian"
},
{
"value": "black-or-african-american",
"display_name": "Black or African American"
},
{
"value": "hispanic-latin-spanish",
"display_name": "Hispanic, Latin, or Spanish origin"
},
{
"value": "middle-eastern-or-north-african",
"display_name": "Middle Eastern or North African"
},
{
"value": "native-hawaiian-or-pacific-islander",
"display_name": "Native Hawaiian or Other Pacific Islander"
},
{
"value": "white",
"display_name": "White"
},
{
"value": "other",
"display_name": "Some other race, ethnicity, or origin"
},
{
"value": "declined",
"display_name": "Prefer not to respond"
}
]
}
}
}
}
}
}
}
let props = {};
let store = {};
const reduxWrapper = children => (
<IntlProvider locale="en">
<Provider store={store}>{children}</Provider>
</IntlProvider>
);
beforeEach(() => {
store = mockStore();
props = {
updateDraft: jest.fn(),
formValues: {
demographics_gender: 'declined',
demographics_gender_description: '',
demographics_user_ethnicity: [],
demographics_income: 'declined',
demographics_military_history: 'declined',
demographics_learner_education_level: 'declined',
demographics_parent_education_level: 'declined',
demographics_work_status: 'declined',
demographics_work_status_description: '',
demographics_current_work_sector: 'declined',
demographics_future_work_sector: 'declined',
demographics_user: 1,
demographicsOptions: {
actions: {
POST: {
gender: {
choices: [
{
value: 'woman',
display_name: 'Woman',
},
{
value: 'man',
display_name: 'Man',
},
{
value: 'non-binary',
display_name: 'Non-binary',
},
{
value: 'self-describe',
display_name: 'Prefer to self describe',
},
{
value: 'declined',
display_name: 'Prefer not to respond',
},
],
},
income: {
choices: [
{
value: 'less-than-10k',
display_name: 'Less than US $10,000',
},
{
value: '10k-25k',
display_name: 'US $10,000 - $25,000',
},
{
value: '25k-50k',
display_name: 'US $25,000 - $50,000',
},
{
value: '50k-75k',
display_name: 'US $50,000 - $75,000',
},
{
value: '75k-100k',
display_name: 'US $75,000 - $100,000',
},
{
value: 'over-100k',
display_name: 'Over US $100,000',
},
{
value: 'unsure',
display_name: "I don't know",
},
{
value: 'declined',
display_name: 'Prefer not to respond',
},
],
},
learner_education_level: {
choices: [
{
value: 'no-high-school',
display_name: 'No High School',
},
{
value: 'some-high-school',
display_name: 'Some High School',
},
{
value: 'high-school-ged-equivalent',
display_name: 'High School diploma, GED, or equivalent',
},
{
value: 'some-college',
display_name: 'Some college, but no degree',
},
{
value: 'associates',
display_name: 'Associates degree',
},
{
value: 'bachelors',
display_name: 'Bachelors degree',
},
{
value: 'masters',
display_name: 'Masters degree',
},
{
value: 'professional',
display_name: 'Professional degree',
},
{
value: 'doctorate',
display_name: 'Doctorate degree',
},
{
value: 'declined',
display_name: 'Prefer not to respond',
},
],
},
parent_education_level: {
choices: [
{
value: 'no-high-school',
display_name: 'No High School',
},
{
value: 'some-high-school',
display_name: 'Some High School',
},
{
value: 'high-school-ged-equivalent',
display_name: 'High School diploma, GED, or equivalent',
},
{
value: 'some-college',
display_name: 'Some college, but no degree',
},
{
value: 'associates',
display_name: 'Associates degree',
},
{
value: 'bachelors',
display_name: 'Bachelors degree',
},
{
value: 'masters',
display_name: 'Masters degree',
},
{
value: 'professional',
display_name: 'Professional degree',
},
{
value: 'doctorate',
display_name: 'Doctorate degree',
},
{
value: 'declined',
display_name: 'Prefer not to respond',
},
],
},
military_history: {
choices: [
{
value: 'never-served',
display_name: 'Never served in the military',
},
{
value: 'training',
display_name: 'Only on active duty for training',
},
{
value: 'active',
display_name: 'Now on active duty',
},
{
value: 'previously-active',
display_name: 'On active duty in the past, but not now',
},
{
value: 'declined',
display_name: 'Prefer not to respond',
},
],
},
work_status: {
choices: [
{
value: 'full-time',
display_name: 'Employed, working full-time',
},
{
value: 'part-time',
display_name: 'Employed, working part-time',
},
{
value: 'self-employed',
display_name: 'Self-Employed',
},
{
value: 'not-employed-looking',
display_name: 'Not employed, looking for work',
},
{
value: 'not-employed-not-looking',
display_name: 'Not employed, not looking for work',
},
{
value: 'unable',
display_name: 'Unable to work',
},
{
value: 'retired',
display_name: 'Retired',
},
{
value: 'other',
display_name: 'Other',
},
{
value: 'declined',
display_name: 'Prefer not to respond',
},
],
},
current_work_sector: {
choices: [
{
value: 'accommodation-food',
display_name: 'Accommodation and Food Services',
},
{
value: 'administrative-support-waste-remediation',
display_name: 'Administrative and Support and Waste Management and Remediation Services',
},
{
value: 'agriculture-forestry-fishing-hunting',
display_name: 'Agriculture, Forestry, Fishing and Hunting',
},
{
value: 'arts-entertainment-recreation',
display_name: 'Arts, Entertainment, and Recreation',
},
{
value: 'construction',
display_name: 'Construction',
},
{
value: 'educational',
display_name: 'Education Services',
},
{
value: 'finance-insurance',
display_name: 'Finance and Insurance',
},
{
value: 'healthcare-social',
display_name: 'Health Care and Social Assistance',
},
{
value: 'information',
display_name: 'Information',
},
{
value: 'management',
display_name: 'Management of Companies and Enterprises',
},
{
value: 'manufacturing',
display_name: 'Manufacturing',
},
{
value: 'mining-quarry-oil-gas',
display_name: 'Mining, Quarrying, and Oil and Gas Extraction',
},
{
value: 'professional-scientific-technical',
display_name: 'Professional, Scientific, and Technical Services',
},
{
value: 'public-admin',
display_name: 'Public Administration',
},
{
value: 'real-estate',
display_name: 'Real Estate and Rental and Leasing',
},
{
value: 'retail',
display_name: 'Retail Trade',
},
{
value: 'transport-warehousing',
display_name: 'Transportation and Warehousing',
},
{
value: 'utilities',
display_name: 'Utilities',
},
{
value: 'trade',
display_name: 'Wholesale Trade',
},
{
value: 'other',
display_name: 'Other',
},
{
value: 'declined',
display_name: 'Prefer not to respond',
},
],
},
future_work_sector: {
choices: [
{
value: 'accommodation-food',
display_name: 'Accommodation and Food Services',
},
{
value: 'administrative-support-waste-remediation',
display_name: 'Administrative and Support and Waste Management and Remediation Services',
},
{
value: 'agriculture-forestry-fishing-hunting',
display_name: 'Agriculture, Forestry, Fishing and Hunting',
},
{
value: 'arts-entertainment-recreation',
display_name: 'Arts, Entertainment, and Recreation',
},
{
value: 'construction',
display_name: 'Construction',
},
{
value: 'educational',
display_name: 'Education Services',
},
{
value: 'finance-insurance',
display_name: 'Finance and Insurance',
},
{
value: 'healthcare-social',
display_name: 'Health Care and Social Assistance',
},
{
value: 'information',
display_name: 'Information',
},
{
value: 'management',
display_name: 'Management of Companies and Enterprises',
},
{
value: 'manufacturing',
display_name: 'Manufacturing',
},
{
value: 'mining-quarry-oil-gas',
display_name: 'Mining, Quarrying, and Oil and Gas Extraction',
},
{
value: 'professional-scientific-technical',
display_name: 'Professional, Scientific, and Technical Services',
},
{
value: 'public-admin',
display_name: 'Public Administration',
},
{
value: 'real-estate',
display_name: 'Real Estate and Rental and Leasing',
},
{
value: 'retail',
display_name: 'Retail Trade',
},
{
value: 'transport-warehousing',
display_name: 'Transportation and Warehousing',
},
{
value: 'utilities',
display_name: 'Utilities',
},
{
value: 'trade',
display_name: 'Wholesale Trade',
},
{
value: 'other',
display_name: 'Other',
},
{
value: 'declined',
display_name: 'Prefer not to respond',
},
],
},
user_ethnicity: {
child: {
children: {
ethnicity: {
choices: [
{
value: 'american-indian-or-alaska-native',
display_name: 'American Indian or Alaska Native',
},
{
value: 'asian',
display_name: 'Asian',
},
{
value: 'black-or-african-american',
display_name: 'Black or African American',
},
{
value: 'hispanic-latin-spanish',
display_name: 'Hispanic, Latin, or Spanish origin',
},
{
value: 'middle-eastern-or-north-african',
display_name: 'Middle Eastern or North African',
},
{
value: 'native-hawaiian-or-pacific-islander',
display_name: 'Native Hawaiian or Other Pacific Islander',
},
{
value: 'white',
display_name: 'White',
},
{
value: 'other',
display_name: 'Some other race, ethnicity, or origin',
},
{
value: 'declined',
display_name: 'Prefer not to respond',
},
],
},
},
},
},
},
formErrors: {},
intl: {},
};
auth.getAuthenticatedHttpClient = jest.fn(() => ({
patch: async () => ({
data: { status: 200 },
catch: () => {},
}),
}));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 1 }));
});
},
},
},
formErrors: {},
intl: {},
forwardRef: () => {},
drafts: {},
};
auth.getAuthenticatedHttpClient = jest.fn(() => ({
patch: async () => ({
data: { status: 200 },
catch: () => {},
}),
}));
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 1 }));
});
it('should render', () => {
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
it('should render', () => {
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
it('should render an Alert if an error occurs', () => {
props = {
...props,
formErrors: {
demographicsError: "api-error"
}
};
it('should render an Alert if an error occurs', () => {
props = {
...props,
formErrors: {
demographicsError: 'api-error',
},
};
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
it('should set user input correctly when user provides gender self-description', () => {
props = {
...props,
formValues: {
...props.formValues,
demographics_gender: 'self-describe',
demographics_gender_description: 'test',
},
};
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
it('should set user input correctly when user provides answers to work_status question', () => {
props = {
...props,
formValues: {
...props.formValues,
demographics_work_status: 'other',
demographics_work_status_description: 'test',
}
}
it('should set user input correctly when user provides gender self-description', () => {
props = {
...props,
formValues: {
...props.formValues,
demographics_gender: 'self-describe',
demographics_gender_description: 'test',
},
};
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
it('should render ethnicity text correctly', () => {
props = {
...props,
formValues: {
...props.formValues,
demographics_user_ethnicity: ['asian']
}
}
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
it('should set user input correctly when user provides answers to work_status question', () => {
props = {
...props,
formValues: {
...props.formValues,
demographics_work_status: 'other',
demographics_work_status_description: 'test',
},
};
it('should render ethnicity correctly when multiple options are selected', () => {
props = {
...props,
formValues: {
...props.formValues,
demographics_user_ethnicity: ['hispanic-latin-spanish', 'white']
}
}
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
it('should render ethnicity text correctly', () => {
props = {
...props,
formValues: {
...props.formValues,
demographics_user_ethnicity: ['asian'],
},
};
it('should render an Alert when demographicsOptions props are empty', () => {
props = {
...props,
formValues: {
demographicsOptions: ""
}
}
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
it('should render ethnicity correctly when multiple options are selected', () => {
props = {
...props,
formValues: {
...props.formValues,
demographics_user_ethnicity: ['hispanic-latin-spanish', 'white'],
},
};
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
it('should render an Alert when demographicsOptions props are empty', () => {
props = {
...props,
formValues: {
demographicsOptions: null,
},
};
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -13,9 +13,20 @@ exports[`DemographicsSection should render 1`] = `
<p>
<a
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
Why does localhost collect this information?
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</p>
<div
@@ -48,10 +59,9 @@ exports[`DemographicsSection should render 1`] = `
Gender identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -112,10 +122,9 @@ exports[`DemographicsSection should render 1`] = `
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -142,10 +151,9 @@ exports[`DemographicsSection should render 1`] = `
data-hj-suppress={true}
>
<button
className="btn btn-link p-0"
onBlur={[Function]}
className="p-0 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add race/ethnicity identity
@@ -184,10 +192,9 @@ exports[`DemographicsSection should render 1`] = `
Family income
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -248,10 +255,9 @@ exports[`DemographicsSection should render 1`] = `
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -312,10 +318,9 @@ exports[`DemographicsSection should render 1`] = `
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -376,10 +381,9 @@ exports[`DemographicsSection should render 1`] = `
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -440,10 +444,9 @@ exports[`DemographicsSection should render 1`] = `
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -504,10 +507,9 @@ exports[`DemographicsSection should render 1`] = `
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -568,10 +570,9 @@ exports[`DemographicsSection should render 1`] = `
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -622,9 +623,20 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
<p>
<a
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
Why does localhost collect this information?
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</p>
<div
@@ -671,10 +683,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
Gender identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -735,10 +746,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -765,10 +775,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
data-hj-suppress={true}
>
<button
className="btn btn-link p-0"
onBlur={[Function]}
className="p-0 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add race/ethnicity identity
@@ -807,10 +816,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
Family income
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -871,10 +879,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -935,10 +942,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -999,10 +1005,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1063,10 +1068,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1127,10 +1131,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1191,10 +1194,9 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1245,9 +1247,20 @@ exports[`DemographicsSection should render an Alert when demographicsOptions pro
<p>
<a
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
Why does localhost collect this information?
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</p>
<div
@@ -1280,9 +1293,20 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
<p>
<a
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
Why does localhost collect this information?
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</p>
<div
@@ -1315,10 +1339,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
Gender identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1379,10 +1402,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1443,10 +1465,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
Family income
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1507,10 +1528,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1571,10 +1591,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1635,10 +1654,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1699,10 +1717,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1763,10 +1780,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1827,10 +1843,9 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1881,9 +1896,20 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
<p>
<a
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
Why does localhost collect this information?
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</p>
<div
@@ -1916,10 +1942,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
Gender identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -1980,10 +2005,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2044,10 +2068,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
Family income
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2108,10 +2131,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2172,10 +2194,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2236,10 +2257,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2300,10 +2320,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2364,10 +2383,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2428,10 +2446,9 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2482,9 +2499,20 @@ exports[`DemographicsSection should set user input correctly when user provides
<p>
<a
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
Why does localhost collect this information?
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</p>
<div
@@ -2517,10 +2545,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Gender identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2581,10 +2608,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2611,10 +2637,9 @@ exports[`DemographicsSection should set user input correctly when user provides
data-hj-suppress={true}
>
<button
className="btn btn-link p-0"
onBlur={[Function]}
className="p-0 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add race/ethnicity identity
@@ -2653,10 +2678,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Family income
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2717,10 +2741,9 @@ exports[`DemographicsSection should set user input correctly when user provides
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2781,10 +2804,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2845,10 +2867,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2909,10 +2930,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -2973,10 +2993,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -3037,10 +3056,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -3091,9 +3109,20 @@ exports[`DemographicsSection should set user input correctly when user provides
<p>
<a
href="http://localhost:5335/demographics"
onClick={[Function]}
rel="noopener noopener noreferrer"
target="_blank"
>
Why does edX collect this information?
Why does localhost collect this information?
<span>
<span
aria-hidden={false}
aria-label="Opens in a new window"
className="fa fa-external-link"
title="Opens in a new window"
/>
</span>
</a>
</p>
<div
@@ -3126,10 +3155,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Gender identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -3190,10 +3218,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -3220,10 +3247,9 @@ exports[`DemographicsSection should set user input correctly when user provides
data-hj-suppress={true}
>
<button
className="btn btn-link p-0"
onBlur={[Function]}
className="p-0 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add race/ethnicity identity
@@ -3262,10 +3288,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Family income
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -3326,10 +3351,9 @@ exports[`DemographicsSection should set user input correctly when user provides
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -3390,10 +3414,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -3454,10 +3477,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -3518,10 +3540,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -3582,10 +3603,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg
@@ -3646,10 +3666,9 @@ exports[`DemographicsSection should set user input correctly when user provides
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
className="ml-3 btn btn-link"
disabled={false}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
<svg

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

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

@@ -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,292 +1,322 @@
{
"account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another edX 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",
"account.settings.loading.message": "Loading...",
"account.settings.loading.error": "Error: {error}",
"account.settings.banner.beta.language": "You have set your language to {beta_language}, which is currently not fully translated. You can help us translate this language fully by joining the Transifex community and adding translations from English for learners that speak {beta_language}.",
"account.settings.banner.beta.language.action.switch.back": "Switch Back to {previous_language}",
"account.settings.banner.beta.language.action.help.translate": "Help Translate into {beta_language}",
"account.settings.section.account.information": "Account Information",
"account.settings.section.account.information.description": "These settings include basic information about your account.",
"account.settings.section.profile.information": "Profile Information",
"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.field.username": "Username",
"account.settings.field.username.help.text": "The name that identifies you on edX. 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.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.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.",
"account.settings.email.field.confirmation.header": "Pending confirmation",
"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.country": "Country",
"account.settings.field.country.empty": "Add country",
"account.settings.field.country.options.empty": "Select a Country",
"account.settings.field.state": "State",
"account.settings.field.state.empty": "Add state",
"account.settings.field.state.options.empty": "Select a State",
"account.settings.field.site.language": "Site language",
"account.settings.field.site.language.help.text": "The language used throughout this site. This site is currently available in a limited number of languages.",
"account.settings.field.education": "Education",
"account.settings.field.education.empty": "Add level of education",
"account.settings.field.education.levels.empty": "Select a level of education",
"account.settings.field.education.levels.p": "Doctorate",
"account.settings.field.education.levels.m": "Master's or professional degree",
"account.settings.field.education.levels.b": "Bachelor's Degree",
"account.settings.field.education.levels.a": "Associate's degree",
"account.settings.field.education.levels.hs": "Secondary/high school",
"account.settings.field.education.levels.jhs": "Junior secondary/junior high/middle school",
"account.settings.field.education.levels.el": "Elementary/primary school",
"account.settings.field.education.levels.none": "No formal education",
"account.settings.field.education.levels.o": "Other education",
"account.settings.field.gender": "Gender",
"account.settings.field.gender.empty": "Add gender",
"account.settings.field.gender.options.empty": "Select a gender",
"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.empty": "Add a spoken language",
"account.settings.field.language_proficiencies.options.empty": "Select a Language",
"account.settings.field.time.zone": "Time zone",
"account.settings.field.time.zone.empty": "Set time zone",
"account.settings.field.time.zone.description": "Select the time zone for displaying course dates. If you do not specify a time zone, course dates, including assignment deadlines, will be displayed in your browsers local time zone.",
"account.settings.field.time.zone.default": "Default (Local Time Zone)",
"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.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",
"account.settings.field.social.platform.name.twitter": "Twitter",
"account.settings.field.social.platform.name.twitter.empty": "Add Twitter profile",
"account.settings.field.social.platform.name.facebook": "Facebook",
"account.settings.field.social.platform.name.facebook.empty": "Add Facebook profile",
"account.settings.editable.field.action.save": "Save",
"account.settings.editable.field.action.cancel": "Cancel",
"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.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.*",
"account.settings.coaching.consent.text-messaging.disclaimer": "* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.",
"account.settings.coaching.consent.accept-coaching": "Sign up for coaching",
"account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
"account.settings.coaching.consent.label.name": "Please confirm your name",
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
"account.settings.coaching.consent.success.header": "Success!",
"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.continue": "Start my course",
"account.settings.coaching.managed.support": "support",
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
"account.settings.field.phone_number": "Phone Number",
"account.settings.field.phone_number.empty": "Add a phone number",
"account.settings.field.coaching_consent": "Coaching consent",
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.",
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
"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.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.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.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",
"account.settings.delete.account.error.unable.to.delete": "Unable to delete account",
"account.settings.delete.account.error.no.password": "A password is required",
"account.settings.delete.account.error.invalid.password": "Password is incorrect",
"account.settings.delete.account.error.unable.to.delete.details": "Sorry, there was an error trying to process your request. Please try again later.",
"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.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",
"account.settings.field.demographics.gender.options.empty": "Select a gender identity",
"account.settings.field.demographics.gender_description": "Gender identity description",
"account.settings.field.demographics.gender_description.empty": "Enter description",
"account.settings.field.demographics.ethnicity": "Race/Ethnicity identity",
"account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity",
"account.settings.field.demographics.ethnicity.options.empty": "Select all that apply",
"account.settings.field.demographics.income": "Family income",
"account.settings.field.demographics.income.empty": "Add family income",
"account.settings.field.demographics.income.options.empty": "Select a family income range",
"account.settings.field.demographics.military_history": "U.S. Military status",
"account.settings.field.demographics.military_history.empty": "Add military status",
"account.settings.field.demographics.military_history.options.empty": "Select military status",
"account.settings.field.demographics.learner_education_level": "Your education level",
"account.settings.field.demographics.learner_education_level.empty": "Add education level",
"account.settings.field.demographics.parent_education_level": "Parents/Guardians education level",
"account.settings.field.demographics.parent_education_level.empty": "Add education level",
"account.settings.field.demographics.education_level.options.empty": "Select education level",
"account.settings.field.demographics.work_status": "Employment status",
"account.settings.field.demographics.work_status.empty": "Add employment status",
"account.settings.field.demographics.work_status.options.empty": "Select employment status",
"account.settings.field.demographics.work_status_description": "Employment status description",
"account.settings.field.demographics.work_status_description.empty": "Enter description",
"account.settings.field.demographics.current_work_sector": "Current work industry",
"account.settings.field.demographics.current_work_sector.empty": "Add work industry",
"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?",
"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}.",
"account.settings.editable.field.password.reset.button": "Reset Password",
"account.settings.editable.field.password.reset.button.forbidden": "Your previous request is in progress, please try again in few moments.",
"account.settings.editable.field.password.reset.label": "Password",
"account.settings.sso.link.account": "Sign in with {name}",
"account.settings.sso.account.connected": "Linked",
"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}.",
"id.verification.next": "Next",
"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.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.privacy.title": "Privacy Information",
"id.verification.privacy.need.photo.question": "Why does edX 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.photo.take": "Take 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.",
"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.camera.access.title": "Camera Permissions",
"id.verification.camera.access.title.success": "Camera Access Enabled",
"id.verification.camera.access.title.failed": "Camera Access Failed",
"id.verification.camera.access.click.allow": "Please make sure to click \"Allow\"",
"id.verification.camera.access.enable": "Enable Camera",
"id.verification.camera.access.problems": "Having problems?",
"id.verification.camera.access.skip": "Skip and upload image files instead",
"id.verification.camera.access.success": "Looks like your camera is working and ready.",
"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.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",
"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}"
"account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another {siteName} account.",
"account.settings.message.managed.settings": "تتم إدارة إعدادات الملف الشخصي بواسطة {ManagerTitle}. اتصل بالمسؤول أو {support} للحصول على المساعدة.",
"account.settings.message.managed.settings.support": "الدعم",
"account.settings.page.heading": "إعدادات الحساب",
"account.settings.loading.message": "جاري التحميل...",
"account.settings.loading.error": "خطأ: {error}",
"account.settings.banner.beta.language": "لقد قمت بتعيين لغتك الى {beta_language}، والتي لم تتم ترجمتها بالكامل حاليًا. يمكنك مساعدتنا في ترجمة هذه اللغة بالكامل من خلال الانضمام إلى مجتمع Transifex وإضافة ترجمات من الإنجليزية للمتعلمين الذين يتحدثون {beta_language}.",
"account.settings.banner.beta.language.action.switch.back": "العودة لـِ {previous_language}",
"account.settings.banner.beta.language.action.help.translate": "المساعدة في الترجمة إلى {beta_language}",
"account.settings.section.account.information": "معلومات الحساب",
"account.settings.section.account.information.description": "تتضمن هذه الإعدادات معلومات أساسية عن حسابك.",
"account.settings.section.profile.information": "معلومات الملف الشخصي",
"account.settings.section.demographics.information": "معلومات اختيارية",
"account.settings.section.site.preferences": "تفضيلات الموقع",
"account.settings.section.linked.accounts": "الحسابات المرتبطة",
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to {siteName}.",
"account.settings.field.username": "اسم المستخدم",
"account.settings.field.username.help.text": "The name that identifies you on {siteName}. You cannot change your username.",
"account.settings.field.full.name": "الاسم الكامل",
"account.settings.field.full.name.empty": "إضافة اسم",
"account.settings.field.full.name.help.text": "الاسم المستخدم للتحقق من هويتك والذي سوف يظهر على الشهادات الخاصة بك.",
"account.settings.field.email": "البريد الالكتروني (الدخول)",
"account.settings.field.email.empty": "إضافة عنوان البريد الإلكتروني",
"account.settings.field.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر فوق الرابط في الرسالة لتحديث عنوان بريدك الإلكتروني.",
"account.settings.field.email.help.text": "You receive messages from {siteName} and course teams at this address.",
"account.settings.field.secondary.email": "عنوان البريد الإلكتروني للاسترداد",
"account.settings.field.secondary.email.empty": "إضافة عنوان البريد الإلكتروني للاسترداد",
"account.settings.field.secondary.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر فوق الرابط في الرسالة لتحديث عنوان بريد الاسترداد الإلكتروني.",
"account.settings.email.field.confirmation.header": "تعليق عملية التأكيد",
"account.settings.field.dob": "سنة الميلاد",
"account.settings.field.dob.empty": "إضافة سنة الميلاد",
"account.settings.field.year_of_birth.options.empty": "اختر سنة ميلاد",
"account.settings.field.country": "الدولة",
"account.settings.field.country.empty": "إضافة البلد ",
"account.settings.field.country.options.empty": "اختر دولة",
"account.settings.field.state": "الحالة",
"account.settings.field.state.empty": "إضافة منطقة",
"account.settings.field.state.options.empty": "اختر منطقة",
"account.settings.field.site.language": "لغة الموقع",
"account.settings.field.site.language.help.text": "اللغة المستخدمة في كافة أقسام هذا الموقع. يتوفّر هذا الموقع حاليًا بعدد محدود من اللغات.",
"account.settings.field.education": "المستوى التعليمي",
"account.settings.field.education.empty": "إضافة المستوى التعليمي",
"account.settings.field.education.levels.empty": "اختر المستوى التعليمي",
"account.settings.field.education.levels.p": "دكتوراه",
"account.settings.field.education.levels.m": "ماجستير أو شهادة مهنيّة",
"account.settings.field.education.levels.b": "بكالوريوس",
"account.settings.field.education.levels.a": "زمالة",
"account.settings.field.education.levels.hs": "شهادة الثانوية العامة",
"account.settings.field.education.levels.jhs": "شهادة الثانوية الصغرى/الإعدادية/المرحلة المتوسّطة",
"account.settings.field.education.levels.el": "شهادة المدرسة الابتدائية",
"account.settings.field.education.levels.none": "لا يوجد تعليم رسمي",
"account.settings.field.education.levels.o": "نوع آخر من التعليم",
"account.settings.field.gender": "الجنس",
"account.settings.field.gender.empty": "إضافة الجنس",
"account.settings.field.gender.options.empty": "اختر جنسًا",
"account.settings.field.gender.options.f": "أنثى",
"account.settings.field.gender.options.m": "ذكر",
"account.settings.field.gender.options.o": "آخر",
"account.settings.field.language.proficiencies": "لغة التحدّث",
"account.settings.field.language.proficiencies.empty": "إضافة لغة التحدث",
"account.settings.field.language_proficiencies.options.empty": "اختر لغة",
"account.settings.field.time.zone": "المنطقة الزمنية",
"account.settings.field.time.zone.empty": "ضبط المنطقة الزمنية",
"account.settings.field.time.zone.description": "حدد المنطقة الزمنية لعرض تواريخ المساق. إذا لم تحدد منطقة زمنية، فسيتم عرض تواريخ المساق، بما في ذلك المواعيد النهائية للواجب في المنطقة الزمنية المحلية في المتصفح.",
"account.settings.field.time.zone.default": "الافتراضي (منطقة التوقيت المحلي)",
"account.settings.field.time.zone.all": "جميع المناطق الزمنية",
"account.settings.field.time.zone.country": "المنطقة الزمنية للدولة",
"account.settings.section.social.media": "روابط منصات التواصل الإجتماعي",
"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": "لينكد إن",
"account.settings.field.social.platform.name.linkedin.empty": "إضافة عنوان ملف لينكد إن ",
"account.settings.jump.nav.delete.account": "احذف حسابي",
"account.settings.field.social.platform.name.twitter": "تويتر",
"account.settings.field.social.platform.name.twitter.empty": "إضافة عنوان صفحة تويتر",
"account.settings.field.social.platform.name.facebook": "فيسبوك",
"account.settings.field.social.platform.name.facebook.empty": "إضافة عنوان صفحة فيسبوك",
"account.settings.editable.field.action.save": "حفظ",
"account.settings.editable.field.action.cancel": "إلغاء",
"account.settings.editable.field.action.edit": "تحرير",
"account.settings.static.field.empty": "لم يتم تحديد قيمة، فضلًا اتصل بمدير {enterprise} لتعيين بعض التغييرات.",
"account.settings.static.field.empty.no.admin": "لم يتم تحديد قيمة",
"account.settings.coaching.consent.welcome.header": "لنبدأ",
"account.settings.coaching.consent.welcome.subheader": "نحن هنا لأجلك من البداية حتى النهاية",
"account.settings.coaching.consent.description": "تتضمن برامج البكالوريوس التدريب الذي يركز على مهنتك وتعليمك وكيفية تحقيق نتائج مبهرة من خلال التواصل الشخصي مع خبراء متمرسين. إذا كنت مهتمًا، فقدّم المعلومات أدناه وانقر فوق \"إرسال\"، وسيتصل بك شريكنا في التدريب عبر البريد الإلكتروني و/أو الرسائل النصية لمساعدتك على المضي قدمًا. تنطبق الشروط والأحكام.*",
"account.settings.coaching.consent.text-messaging.disclaimer": "* يتم تضمين خدمات التدريب بدون أي تكلفة إضافية للمتعلمين الذين لديهم أرقام هواتف أمريكية. يتضمن التدريب رسائل نصية متكررة. قد تنطبق أسعار الرسائل والبيانات. إيقاف النص لإلغاء الاشتراك.",
"account.settings.coaching.consent.accept-coaching": "سجّل للاستفادة من خدمات التدريب",
"account.settings.coaching.consent.decline-coaching": "أفضّل عدم الاتصال بخدمات التدريب المجانية",
"account.settings.coaching.consent.label.name": "يرجى تأكيد الاسم",
"account.settings.coaching.consent.label.phone-number": "فضلًا أدخل رقم الهاتف ",
"account.settings.coaching.consent.success.header": "تمت العملية بنجاح",
"account.settings.coaching.consent.success.message": "لقد اشتركت في التدريب. يمكنك توقع استلام رسالة عبر البريد الإلكتروني أو الرسائل القصيرة في الأيام المقبلة.",
"account.settings.coaching.consent.success.continue": "البدء في مساقي",
"account.settings.coaching.managed.support": "الدعم",
"account.settings.coaching.managed.alert": "تتم إدارة اسمك بواسطة {ManagerTitle}. اتصل بالمسؤول للحصول على المساعدة.",
"account.settings.field.phone_number": "رقم الهاتف",
"account.settings.field.phone_number.empty": "إضافة رقم الهاتف",
"account.settings.field.coaching_consent": "اتفاقية التدريب",
"account.settings.field.coaching_consent.tooltip": "تتضمن برامج البكالوريوس التدريب القائم على الرسائل النصية الذي يساعدك على إقران التجارب التعليمية مع أهدافك المهنية من خلال النصائح الشخصية. يتم تضمين خدمات التدريب بدون أي تكلفة إضافية، وهي متوفرة للمتعلمين الذين لديهم أرقام هواتف نقالة أمريكية. تنطبق أسعار المراسلة القياسية. أرسل \"قف\" في أي وقت لإلغاء الرسائل.",
"account.settings.field.coaching_consent.error": "مطلوب رقم هاتف أمريكي صالح للدخول في التدريب",
"account.settings.delete.account.before.proceeding": "قبل المتابعة، يرجى {actionLink}.",
"account.settings.delete.account.header": "احذف حسابي",
"account.settings.delete.account.subheader": "نأسف لذهابك!",
"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": "هل تريد تغيير البريد الإلكتروني أو الاسم أو كلمة المرور بدلاً من ذلك؟",
"account.settings.delete.account.button": "احذف حسابي",
"account.settings.delete.account.please.activate": "تنشيط حسابك",
"account.settings.delete.account.please.unlink": "إلغاء ربط جميع حسابات التواصل الاجتماعي",
"account.settings.delete.account.modal.header": "هل أنت متأكد؟",
"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": "إذا كنت لا تزال ترغب في المتابعة وحذف حسابك ، فيرجى إدخال كلمة مرور حسابك:",
"account.settings.delete.account.modal.confirm.delete": "تعم، أحذف",
"account.settings.delete.account.modal.confirm.cancel": "إلغاء",
"account.settings.delete.account.error.unable.to.delete": "تعذر حذف الحساب",
"account.settings.delete.account.error.no.password": "كلمة المرور مطلوبة",
"account.settings.delete.account.error.invalid.password": "كلمة المرور المدخلة غير صحيحة",
"account.settings.delete.account.error.unable.to.delete.details": "عذراً ، حدث خطأ أثناء محاولة معالجة طلبك. الرجاء معاودة المحاولة في وقت لاحق.",
"account.settings.delete.account.modal.after.header": "نأسف لذهابك! سيتم حذف حسابك قريبا.",
"account.settings.delete.account.modal.after.text": "قد يستغرق حذف الحساب ، بما في ذلك الإزالة من قوائم البريد الإلكتروني ، بضعة أسابيع حتى تتم معالجته بالكامل من خلال نظامنا. إذا كنت ترغب في إلغاء الاشتراك في رسائل البريد الإلكتروني قبل ذلك الحين ، يرجى إلغاء الاشتراك من تذييل أي بريد إلكتروني.",
"account.settings.delete.account.modal.after.button": "إغلاق ",
"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": "حدث خطأ أثناء محاولة استرداد معلومات حسابك أو حفظها. يرجى المحاولة مرة أخرى لاحقًا.",
"account.settings.field.demographics.gender": "هوية الجنس",
"account.settings.field.demographics.gender.empty": "إضافة هوية الجنس",
"account.settings.field.demographics.gender.options.empty": "حدد هوية الجنس",
"account.settings.field.demographics.gender_description": "وصف هوية الجنس",
"account.settings.field.demographics.gender_description.empty": "أدخل وصفًا",
"account.settings.field.demographics.ethnicity": "هوية العرق/ الأصل",
"account.settings.field.demographics.ethnicity.empty": "إضافة هوية العرق/الأصل",
"account.settings.field.demographics.ethnicity.options.empty": "فضلًا اختر جميع ما ينطبق",
"account.settings.field.demographics.income": "الدخل المادي الأسري",
"account.settings.field.demographics.income.empty": "إضافة الدخل المادي الأسري",
"account.settings.field.demographics.income.options.empty": "حدد نطاق الدخل المادي للأسرة",
"account.settings.field.demographics.military_history": "حالة الخدمة العسكرية في الولايات المتحدة الأمريكية",
"account.settings.field.demographics.military_history.empty": "إضافة حالة الخدمة العسكرية",
"account.settings.field.demographics.military_history.options.empty": "اختر حالة الخدمة العسكرية",
"account.settings.field.demographics.learner_education_level": "مؤهلك التعليمي",
"account.settings.field.demographics.learner_education_level.empty": "إضافة مؤهلك التعليمي",
"account.settings.field.demographics.parent_education_level": "مؤهل الوالدين/الآوصياء التعليمي",
"account.settings.field.demographics.parent_education_level.empty": "إضافة المؤهل التعليمي",
"account.settings.field.demographics.education_level.options.empty": "حدد موهلاً تعليميًا",
"account.settings.field.demographics.work_status": "الحالة الوظيفية",
"account.settings.field.demographics.work_status.empty": "إضافة الحالة الوظيفية",
"account.settings.field.demographics.work_status.options.empty": "فضلًا حدد حالتك الوظيفية",
"account.settings.field.demographics.work_status_description": "وصف الحالة الوظيفية",
"account.settings.field.demographics.work_status_description.empty": "أدخل وصفًا",
"account.settings.field.demographics.current_work_sector": "مجال العمل الحالي",
"account.settings.field.demographics.current_work_sector.empty": "إضافة مجال العمل",
"account.settings.field.demographics.future_work_sector": "مجال العمل المستقبلي",
"account.settings.field.demographics.future_work_sector.empty": "إضافة مجال العمل",
"account.settings.field.demographics.work_sector.options.empty": "حدد مجال العمل",
"account.settings.section.demographics.why": "Why does {siteName} collect this information?",
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في نص الرابط. الرجاء التحقق من الرابط والمحاولة مجددا.",
"account.settings.editable.field.password.reset.button.confirmation.support.link": "الدعم الفني",
"account.settings.editable.field.password.reset.button.confirmation": "لقد أرسلنا رسالة إلى {email}. انقر فوق الرابط في الرسالة لإعادة تعيين كلمة المرور. إذا لم يتم استلام الرسالة؟ اتصل بـ {technicalSupportLink}.",
"account.settings.editable.field.password.reset.button": "تغيير كلمة المرور",
"account.settings.editable.field.password.reset.button.forbidden": "طلبك السابق قيد التقدم، يرجى إعادة المحاولة بعد لحظات قليلة.",
"account.settings.editable.field.password.reset.label": "كلمة المرور",
"account.settings.sso.link.account": "تسجيل الدخول كـ {name}",
"account.settings.sso.account.connected": "مربوط",
"account.settings.sso.account.disconnect.error": "حدثت مشكلة أثناء قطع اتصال هذا الحساب، اتصل بالدعم عند استمرار المشكلة.",
"account.settings.sso.unlink.account": "إلغاء ربط حساب {name} ",
"account.settings.sso.no.providers": "لا يمكن ربط أية حسابات حاليًا",
"id.verification.access.blocked.denied": "لا يمكنك التحقق من هويتك في الوقت الحالي. إذا لم تقم بعد بتنشيط حسابك، فيرجى التحقق من مجلد البريد المهمل للحصول على رسالة التفعيل من {email}.",
"id.verification.next": "التالي",
"id.verification.support": "support",
"id.verification.continue.upload": "Continue with Upload",
"id.verification.example.card.alt": "مثال على بطاقة هوية صحيحة بالاسم الكامل وصورة.",
"id.verification.requirements.title": "متطلبات التحقق من الصورة",
"id.verification.requirements.description": "يجب عليك اتباع الآتي لإكمال عملية التحقق الإلكتروني من هويتك:",
"id.verification.requirements.card.device.title": "جهاز مزود بكاميرا",
"id.verification.requirements.card.device.allow": "موافق",
"id.verification.requirements.card.id.title": "صورة التحقق من الشخصية.",
"id.verification.requirements.card.id.text": "تحتاج إلى بطاقة هوية صحيحة للتحقق تحوي اسمك الكامل وصورتك.",
"id.verification.privacy.title": "بيانات الخصوصية.",
"id.verification.privacy.need.photo.question": "Why does {siteName} need my photo?",
"id.verification.privacy.need.photo.answer": "نستخدم صور التحقق الخاصة بك لتأكيد هويتك والتأكد من صحة شهادتك.",
"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": "التحقق من الهوية",
"id.verification.access.blocked.enrollment": "أنت الآن ملتحق بمساق يتطلب التحقق من الهوية.",
"id.verification.access.blocked.pending": "لقد قمت بالفعل بإرسال معلومات التحقق الخاصة بك. ستصلك رسالة على لوحة المعلومات عند اكتمال عملية التحقق (عادةً خلال 5 أيام).",
"id.verification.photo.take": "التقاط صورة ",
"id.verification.photo.retake": "Retake Photo?",
"id.verification.photo.enable.detection": "تمكين خاصية التعرف على الوجه",
"id.verification.photo.enable.detection.portrait.help.text": "عند تحديد هذا الخيار، فسيظهر مربع حول وجهك. يمكن رؤية وجهك بوضوح إذا كان المربع المحيط به أزرق. إذا لم يكن وجهك في وضع جيد أو إذا لم يكن قابلاً للاكتشاف، فسيكون المربع باللون الأحمر.",
"id.verification.photo.enable.detection.id.help.text": "عند تحديد هذا الخيار، فسيظهر مربع حول صورة وجهك في بطاقة الهوية. يمكن رؤية وجهك بوضوح إذا كان المربع المحيط به أزرق. إذا لم يكن وجهك في وضع جيد أو إذا لم يكن قابلاً للاكتشاف، فسيكون المربع باللون الأحمر.",
"id.verification.photo.feedback.correct": "وضع الوجه جيد.",
"id.verification.photo.feedback.two.faces": "تم تحديد أكثر من وجه.",
"id.verification.photo.feedback.no.faces": "لم يتم تحديد أي وجه.",
"id.verification.photo.feedback.top.left": "وضع خاطئ. أعلى اليسار.",
"id.verification.photo.feedback.top.center": "وضع خاطئ. أعلى الوسط.",
"id.verification.photo.feedback.top.right": "وضع خاطئ. أعلى اليمين.",
"id.verification.photo.feedback.center.left": "وضع خاطئ. وسط اليسار.",
"id.verification.photo.feedback.center.center": "وضع خاطئ. قريب جدًا من الكاميرا.",
"id.verification.photo.feedback.center.right": "وضع خاطئ. وسط اليمين.",
"id.verification.photo.feedback.bottom.left": "وضع خاطئ. أسفل اليسار.",
"id.verification.photo.feedback.bottom.center": "وضع خاطئ. أسفل الوسط.",
"id.verification.photo.feedback.bottom.right": "وضع خاطئ. أسفل اليمين.",
"id.verification.camera.access.title": "صلاحيات الكاميرا",
"id.verification.camera.access.title.success": "تمكين الوصول للكاميرا",
"id.verification.camera.access.title.failed": "تعذّر الوصول للكاميرا.",
"id.verification.camera.access.click.allow": "فضلًا تأكد من اختيار الأمر \"السماح\"",
"id.verification.camera.access.enable": "تمكين الكاميرا",
"id.verification.camera.access.problems": "هل تواجه أية مشكلة؟",
"id.verification.camera.access.skip": "قم بتخطي ملفات الصور وتحميلها بدلاً من ذلك",
"id.verification.camera.access.success": "يبدو أن الكاميرا تعمل وجاهزة.",
"id.verification.camera.access.failure": "يبدو أننا غير قادرين على الوصول إلى الكاميرا. ستحتاج إلى تحميل ملفات الصور الخاصة بك و معرّف الصور الخاص بك.",
"id.verification.camera.access.failure.temporary": "يبدو أننا غير قادرين على الوصول إلى الكاميرا. يرجى التحقق من أن كاميرا الويب متصلة ومن أنك سمحت للمتصفح بالوصول إليها.",
"id.verification.camera.access.failure.temporary.chrome": "تمكين الوصول للكاميرا في متصفّح كروم: ",
"id.verification.camera.access.failure.temporary.chrome.step1": "فتح متصفّح كروم.",
"id.verification.camera.access.failure.temporary.chrome.step2": "توجه إلى المزيد > الإعدادات.",
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "لنظام ويندوز: Alt+F أو Alt+E أو F10 متبوعاً بـ SPACEBAR",
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "لنظام ماك: Command+,",
"id.verification.camera.access.failure.temporary.chrome.step3": "ضمن علامة التبويب \"الخصوصية والأمان\"، حدد \"إعدادات الموقع\" ثم \"الكاميرا\".",
"id.verification.camera.access.failure.temporary.chrome.step4": "ضمن \"قائمة الحظر\"، ابحث عن \"edx.org\" وحدده.",
"id.verification.camera.access.failure.temporary.chrome.step5": "في القسم \"الصلاحيات\"، قم بتحديث صلاحيات الكاميرا إلى \"السماح\".",
"id.verification.camera.access.failure.temporary.ie11": "تمكين الوصول للكاميرا في متصفح انترنت إكسبلورر:",
"id.verification.camera.access.failure.temporary.ie11.step1": "افتح إدارة إعدادات Flash Player عن طريق الانتقال إلى إعدادات Windows > لوحة التحكم > Flash Player.",
"id.verification.camera.access.failure.temporary.ie11.step2": "حدد علامة التبويب \"Camera and Mic\" (الكاميرا والميكروفون)، ثم حدد الزر \"Camera and Microphone Settings by Site\" (إعدادات الكاميرا والميكروفون حسب الموقع).",
"id.verification.camera.access.failure.temporary.ie11.step3": "اختر \"edx.org\" من قائمة مواقع ويب وقم بتغيير الصلاحيات من خلال تحديد \"السماح\" في القائمة المنسدلة.",
"id.verification.camera.access.failure.temporary.firefox": "لتمكين الوصول للكاميرا في متصفّح فايرفوكس: ",
"id.verification.camera.access.failure.temporary.firefox.step1": "فتح متصفّح فايرفوكس.",
"id.verification.camera.access.failure.temporary.firefox.step2": "أدخل \"about:preferences\" في شريط عنوان الرابط.",
"id.verification.camera.access.failure.temporary.firefox.step3": "حدد علامة التبويب \"الخصوصية والأمان\"، وانتقل إلى القسم \"الصلاحيات\".",
"id.verification.camera.access.failure.temporary.firefox.step4": "بجوار \"Camera\" (الكاميرا)، حدد \"Settings…\" (الإعدادات…) الزر.",
"id.verification.camera.access.failure.temporary.firefox.step5": "في شريط البحث أدخل \"edx.org.\"",
"id.verification.camera.access.failure.temporary.firefox.step6": "في عمود الحالة لـ \"edx.org,\" حدد \"السماح\" من القائمة المنسدلة.",
"id.verification.camera.access.failure.temporary.firefox.step7": "اختر \"حفظ التغييرات.\"",
"id.verification.camera.access.failure.temporary.safari": "تمكين الوصول للكاميرا في متصفّح سفاري: ",
"id.verification.camera.access.failure.temporary.safari.step1": "فتح متصفّح سفاري.",
"id.verification.camera.access.failure.temporary.safari.step2": "انقر فوق قائمة تطبيق سفاري، ثم حدد \"Preferences\" (التفضيلات). يمكنك أيضاً استخدام الأمرCommand+ كاختصار للوحة المفاتيح",
"id.verification.camera.access.failure.temporary.safari.step3": "حدد علامة التبويب \"مواقع ويب\" ثم حدد \"كاميرا\".",
"id.verification.camera.access.failure.temporary.safari.step4": "حدد \"edx.org\" وقم بتغيير صلاحيات الكاميرا إلى \"السماح\".",
"id.verification.camera.access.failure.unsupported": "It looks like your browser does not support camera access.",
"id.verification.camera.access.failure.unsupported.chrome.explanation": "The Chrome browser currently does not support camera access on iOS devices, such as iPhones and iPads.",
"id.verification.camera.access.failure.unsupported.instructions": "Please use another browser to complete Identity Verification.",
"id.verification.photo.tips.title": "تلميحات مفيدة للصورة",
"id.verification.photo.tips.description": "بعد ذلك، سنحتاج منك التقاط صورة لوجهك. يرجى مراجعة التلميحات المفيدة أدناه.",
"id.verification.photo.tips.list.title": "تلميحات الصورة",
"id.verification.photo.tips.list.description": "لالتقاط صورة ناجحة، يُرجى التأكّد ممّا يلي:",
"id.verification.photo.tips.list.well.lit": "أنّ الإضاءة جيّدة على وجهك.",
"id.verification.photo.tips.list.inside.frame": "أنّ وجهك داخل إطار الصورة بالكامل.",
"id.verification.portrait.photo.title.camera": "التقط صورة لنفسك",
"id.verification.portrait.photo.title.upload": "ارفع صورتك",
"id.verification.portrait.photo.preview.alt": "معاينة صورة وجه المستخدم.",
"id.verification.portrait.photo.instructions.camera": "عندما يكون وجهك في موضعه، استخدم زر التقاط صورة أدناه لالتقاط الصورة.",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ",
"id.verification.camera.help.sight.question": "ماذا لو لم أتمكن من رؤية صورة الكاميرا ؟ أو إذا لم أتمكن من رؤية صورتي لتحديد أي جانب مرئي؟",
"id.verification.camera.help.sight.answer.portrait": "قد تتمكن من إكمال إجراء التقاط الصور من دون مساعدة، ولكن قد يتطلب الأمر بضع محاولات ضبط وضع الكاميرا بشكل صحيح. يختلف وضع الكاميرا المثالي باختلاف الكمبيوتر، ولكن بشكل عام، يكون أفضل موضع للتصوير في الرأس هو 12 إلى 18 بوصة (30-45 سم) تقريبًا من الكاميرا، مع وضع الرأس في المنتصف بالنسبة إلى شاشة الكمبيوتر. إذا تم رفض الصور التي ترسلها، فحاول تحريك اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة.",
"id.verification.camera.help.sight.answer.id": "قد تتمكن من إكمال إجراء التقاط الصور من دون مساعدة، ولكن قد يتطلب الأمر بضع محاولات لضبط وضع الكاميرا بشكل صحيح. يختلف الوضع الأمثل للكاميرا باختلاف جهاز الكمبيوتر، ولكن بشكل عام، يكون أفضل وضع لصورة بطاقة تعريف من 8 إلى 12 بوصة (من 20 إلى 30 سم) عن الكاميرا، مع وضع بطاقة الهوية في الوسط بالنسبة للكاميرا. إذا تم رفض الصور التي ترسلها، فحاول تحريك اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة. إن السبب الأكثر شيوعاً للرفض هو عدم القدرة على قراءة النص الموجود على بطاقة الهوية.",
"id.verification.camera.help.difficulty.question.portrait": "ماذا لو واجهت صعوبة في تثبيت رأسي في الموضع المناسب للكاميرا؟",
"id.verification.camera.help.difficulty.question.id": "ماذا لو واجهت صعوبة في تثبيت بطاقة هويتي في الموضع المناسب للكاميرا؟",
"id.verification.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact {siteName} support for additional suggestions.",
"id.verification.camera.help.upload.question": "What if I want to upload a photo instead?",
"id.verification.camera.help.upload.answer": "On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.",
"id.verification.id.photo.unclear.question": "هل صورة بطاقة الهوية غير واضحة أو ضبابية؟",
"id.verification.id.tips.title": "تلميحات مفيدة للهوية ",
"id.verification.id.tips.description": "بعد ذلك، سنكون بحاجة إلى التقاط صورة لبطاقة هوية صالحة تتضمن اسمك الكامل وصورتك. يرجى تجهيز بطاقة هويتك.",
"id.verification.id.tips.list.well.lit": "إضاءة بطاقة هويتك جيدة.",
"id.verification.id.tips.list.clear": "تأكد من قدرتك على رؤية صورتك وقراءة اسمك بوضوح.",
"id.verification.id.photo.title.camera": "التقط صورة لبطاقتك الشخصية",
"id.verification.id.photo.title.upload": "حمّل صورة لهويتك",
"id.verification.id.photo.preview.alt": "معاينة صورة الهوية.",
"id.verification.id.photo.instructions.camera": "عندما تكون بطاقة هويتك في موضعها، استخدم زر التقاط صورة أدناه لالتقاط الصورة.",
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "التحقق من اسم الحساب",
"id.verification.account.name.instructions": "يجب أن يكون الاسم الموجود على حسابك والاسم الموجود على المعرّف الخاص بك متطابقًا تمامًا. إذا لم يكن الأمر كذلك، فيرجى النقر فوق \"لا\" لتحديث اسم حسابك.",
"id.verification.account.name.radio.label": "هل يتطابق الاسم الموجود على هويتك مع اسم الحساب أدناه؟",
"id.verification.account.name.radio.yes": "نعم",
"id.verification.account.name.radio.no": "لا",
"id.verification.account.name.error": "يرجى تحديث اسم الحساب لمطابقة الاسم على بطاقة الهوية.",
"id.verification.account.name.warning.prefix": "يُرجى الملاحظة:",
"id.verification.account.name.settings": "إعدادات الحساب",
"id.verification.account.name.label": "اسم الحساب",
"id.verification.account.name.photo.alt": "صورة من هويتك للتقديم.",
"id.verification.account.name.save": "حفظ ثم التالي",
"id.verification.review.title": "مراجعة صورك",
"id.verification.review.description": "يُرجى التأكّد من أنّ الصور والمعلومات التي قدّمتها تمكّننا من التحقّق من هويّتك. ",
"id.verification.review.portrait.label": "صورتك الشخصية",
"id.verification.review.portrait.alt": "صورة شخصية للتقديم.",
"id.verification.review.portrait.retake": "إعادة التقاط الصورة شخصية",
"id.verification.review.id.label": "معرّف صورتك",
"id.verification.review.id.alt": "صورة من هويتك للتقديم.",
"id.verification.review.id.retake": "إعادة التقاط صورة الهوية",
"id.verification.review.confirm": "إرسال",
"id.verification.submission.alert.error.face": "مطلوب صورة لوجهك. يرجى إعادة التقاط الصورة الشخصية.",
"id.verification.submission.alert.error.id": "مطلوب صورة لبطاقة هويتك. يرجى إعادة التقاط صورة بطاقة الهوية.",
"id.verification.submission.alert.error.name": "مطلوب اسم حساب صالح. يرجى تحديث اسم حسابك لمطابقة الاسم على هويتك.",
"id.verification.submission.alert.error.unsupported": "One or more of the files you have uploaded is in an unsupported format. Please choose from the following: ",
"id.verification.review.error": "{siteName} Support Page",
"id.verification.submitted.title": "جارِ التحقق من الهوية",
"id.verification.submitted.text": "لقد تلقينا معلوماتك وجاري الآن العمل على التحقق من هويتك. ستصلك رسالة على لوحة المعلومات عند اكتمال عملية التحقق (عادةً خلال 5 أيام). في غضون ذلك، لا يزال بإمكانك الوصول إلى كل محتوى المساق المتوفر.",
"id.verification.return.dashboard": "العودة إلى لوحة المعلومات",
"id.verification.return.course": "العودة للمساق",
"id.verification.photo.upload.help.title": "Upload a Photo Instead",
"id.verification.photo.camera.help.title": "Use Your Camera Instead",
"id.verification.photo.upload.help.text": "If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.",
"id.verification.photo.camera.help.text": "If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.",
"id.verification.upload.help.button": "Switch to Upload Mode",
"id.verification.camera.help.button": "Switch to Camera Mode",
"id.verification.choose.mode.title": "Photo Requirements Options",
"id.verification.choose.mode.hep.text": "To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.",
"id.verification.choose.mode.radio.upload": "Upload photos from my device",
"id.verification.choose.mode.radio.camera": "Take pictures using my camera",
"id.verification.account.name.managed.alert": "Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help.",
"id.verification.request.camera.access.instructions": "لالتقاط صورة باستخدام كاميرا الويب، قد تتلقى طلب المتصفح للوصول إلى الكاميرا. {clickAllow}",
"id.verification.requirements.account.managed.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help before completing the Photo Verification process.",
"id.verification.requirements.card.device.text": "أنت بحاجة إلى جهاز مزود بكاميرا. إذا تلقيت طلب المتصفح للوصول إلى الكاميرا، فيرجى التأكد من النقر فوق {السماح}.",
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
"idv.submission.alert.error": "\nواجهنا خطأ فني أثناء محاولة رفع طلب التحقق من الهوية\nقد تكون هذه مشكلة مؤقتة، لذا يرجى المحاولة مرة أخرى بعد بضع دقائق\nعند استمرار المشكلة يرجى الانتقال إلى {support_link} للحصول على المساعدة",
"id.verification.account.name.edit": "Edit {sr}"
}

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": "The {provider} account you selected is already linked to another {siteName} account.",
"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,16 @@
"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": "You can link your identity accounts to simplify signing in to {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": "The name that identifies you on {siteName}. You cannot change your username.",
"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.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": "You receive messages from {siteName} and course teams at this address.",
"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.",
@@ -57,7 +57,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 +67,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": "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": "Agregar perfil de LinkedIn",
"account.settings.jump.nav.delete.account": "Eliminar mi cuenta",
@@ -89,7 +89,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 +101,19 @@
"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": "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": "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.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": "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": "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 +124,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": "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": "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 +139,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 +156,7 @@
"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": "Why does {siteName} collect this information?",
"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 +168,155 @@
"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}.",
"id.verification.access.blocked.denied": "No puedes verificar tu identidad en este momento. Si aún tienes que activar tu cuenta, revisa tu carpeta de correo no deseado y busca el correo electrónico de activación de {email}.",
"id.verification.next": "Siguiente",
"id.verification.support": "support",
"id.verification.continue.upload": "Continue with Upload",
"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.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.text": "Necesitas un documento de identidad válido que contenga tu foto y nombre completo.",
"id.verification.privacy.title": "Información de privacidad",
"id.verification.privacy.need.photo.question": "Why does {siteName} need my photo?",
"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": "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": "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": "Retake Photo?",
"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": "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": "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.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.title.upload": "Sube una foto tuya",
"id.verification.portrait.photo.preview.alt": "Previsualización de la foto con el rostro del usuario",
"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.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ",
"id.verification.camera.help.sight.question": "¿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": "If you require assistance with taking a photo for submission, contact {siteName} support for additional suggestions.",
"id.verification.camera.help.upload.question": "What if I want to upload a photo instead?",
"id.verification.camera.help.upload.answer": "On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.",
"id.verification.id.photo.unclear.question": "¿La imagen de tu identificación no es clara o está demasiado borrosa?",
"id.verification.id.tips.title": "Consejos útiles para la verificación",
"id.verification.id.tips.description": "A continuación, necesitaremos que tomes una foto de un documento de identidad válido que incluya tu foto y nombre completo. Ten tu ID a mano.",
"id.verification.id.tips.list.well.lit": "Tu 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 Identificación",
"id.verification.id.photo.title.upload": "Carga una foto de tu identificación",
"id.verification.id.photo.preview.alt": "Previsualización de Foto ID",
"id.verification.id.photo.instructions.camera": "Cuando tu identificación esté en su lugar, usa el botón Tomar foto a continuación para tomar tu foto.",
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "Verificación de nombre de cuenta",
"id.verification.account.name.instructions": "El nombre de tu cuenta y el nombre de tu identificación deben coincidir exactamente. De lo contrario, haz clic en \"No\" para actualizar el nombre de tu cuenta.",
"id.verification.account.name.radio.label": "¿El nombre de tu identificación coincide con el nombre de la cuenta a continuación?",
"id.verification.account.name.radio.yes": "Si",
"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.account.name.error": "Actualiza el nombre de la cuenta para que coincida con el nombre de tu identificación.",
"id.verification.account.name.warning.prefix": "Ten en cuenta:",
"id.verification.account.name.settings": "Configuración de cuenta",
"id.verification.account.name.label": "Nombre de la cuenta",
"id.verification.account.name.photo.alt": "Foto de tu identificación a enviar.",
"id.verification.account.name.save": "Guardar y siguiente",
"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": "Tu Foto de identificación",
"id.verification.review.id.alt": "Foto de tu identificación a enviar.",
"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": "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": "Verificación de identidad en progreso.",
"id.verification.submitted.text": "Hemos recibido tu información y estamos verificando tu identidad. Verás un mensaje en tu tablero cuando se complete el proceso de verificación (generalmente en un periodo de 5 días). Mientras tanto, aún puedes 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.photo.upload.help.title": "Upload a Photo Instead",
"id.verification.photo.camera.help.title": "Use Your Camera Instead",
"id.verification.photo.upload.help.text": "If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.",
"id.verification.photo.camera.help.text": "If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.",
"id.verification.upload.help.button": "Switch to Upload Mode",
"id.verification.camera.help.button": "Switch to Camera Mode",
"id.verification.choose.mode.title": "Photo Requirements Options",
"id.verification.choose.mode.hep.text": "To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.",
"id.verification.choose.mode.radio.upload": "Upload photos from my device",
"id.verification.choose.mode.radio.camera": "Take pictures using my camera",
"id.verification.account.name.managed.alert": "Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help.",
"id.verification.request.camera.access.instructions": "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": "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": "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": "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 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": "Edit {sr}"
}

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,16 @@
"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.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.",
@@ -57,7 +57,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 +67,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",
@@ -101,17 +101,19 @@
"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.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 +124,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 +156,7 @@
"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?",
"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 +168,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}.",
"id.verification.access.blocked.denied": "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}.",
"id.verification.next": "Next",
"id.verification.support": "support",
"id.verification.continue.upload": "Continue with Upload",
"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.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.text": "You need a valid identification card that contains your full name and photo.",
"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 +239,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",
@@ -242,24 +252,27 @@
"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.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ",
"id.verification.camera.help.sight.question": "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.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact {siteName} support for additional suggestions.",
"id.verification.camera.help.upload.question": "What if I want to upload a photo instead?",
"id.verification.camera.help.upload.answer": "On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.",
"id.verification.id.photo.unclear.question": "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.description": "Next, we'll need you to take a photo of a valid identification card that includes your full name and photo. 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.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "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?",
@@ -280,13 +293,30 @@
"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.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.return.dashboard": "Return to Your Dashboard",
"id.verification.return.course": "Return to Course",
"id.verification.photo.upload.help.title": "Upload a Photo Instead",
"id.verification.photo.camera.help.title": "Use Your Camera Instead",
"id.verification.photo.upload.help.text": "If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.",
"id.verification.photo.camera.help.text": "If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.",
"id.verification.upload.help.button": "Switch to Upload Mode",
"id.verification.camera.help.button": "Switch to Camera Mode",
"id.verification.choose.mode.title": "Photo Requirements Options",
"id.verification.choose.mode.hep.text": "To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.",
"id.verification.choose.mode.radio.upload": "Upload photos from my device",
"id.verification.choose.mode.radio.camera": "Take pictures using my camera",
"id.verification.account.name.managed.alert": "Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help.",
"id.verification.request.camera.access.instructions": "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

@@ -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,16 @@
"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.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.",
@@ -57,7 +57,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 +67,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",
@@ -101,17 +101,19 @@
"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.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 +124,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 +156,7 @@
"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?",
"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 +168,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}.",
"id.verification.access.blocked.denied": "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}.",
"id.verification.next": "Next",
"id.verification.support": "support",
"id.verification.continue.upload": "Continue with Upload",
"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.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.text": "You need a valid identification card that contains your full name and photo.",
"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 +239,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",
@@ -242,24 +252,27 @@
"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.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ",
"id.verification.camera.help.sight.question": "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.camera.help.difficulty.answer": "If you require assistance with taking a photo for submission, contact {siteName} support for additional suggestions.",
"id.verification.camera.help.upload.question": "What if I want to upload a photo instead?",
"id.verification.camera.help.upload.answer": "On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.",
"id.verification.id.photo.unclear.question": "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.description": "Next, we'll need you to take a photo of a valid identification card that includes your full name and photo. 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.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
"id.verification.account.name.title": "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?",
@@ -280,13 +293,30 @@
"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.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.return.dashboard": "Return to Your Dashboard",
"id.verification.return.course": "Return to Course",
"id.verification.photo.upload.help.title": "Upload a Photo Instead",
"id.verification.photo.camera.help.title": "Use Your Camera Instead",
"id.verification.photo.upload.help.text": "If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.",
"id.verification.photo.camera.help.text": "If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.",
"id.verification.upload.help.button": "Switch to Upload Mode",
"id.verification.camera.help.button": "Switch to Camera Mode",
"id.verification.choose.mode.title": "Photo Requirements Options",
"id.verification.choose.mode.hep.text": "To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.",
"id.verification.choose.mode.radio.upload": "Upload photos from my device",
"id.verification.choose.mode.radio.camera": "Take pictures using my camera",
"id.verification.account.name.managed.alert": "Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help.",
"id.verification.request.camera.access.instructions": "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';
function 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="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}."
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);

View File

@@ -1,6 +1,9 @@
/* eslint-disable jsx-a11y/media-has-caption */
/* eslint-disable jsx-a11y/no-access-key */
import React from 'react';
import PropTypes from 'prop-types';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
// eslint-disable-next-line import/no-unresolved
import * as blazeface from '@tensorflow-models/blazeface';
import CameraPhoto, { FACING_MODES } from 'jslib-html5-camera-photo';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
@@ -38,25 +41,9 @@ class Camera extends React.Component {
this.cameraPhoto.stopCamera();
}
sendEvent() {
let eventName = 'edx.id_verification';
if (this.props.isPortrait) {
eventName += '.user_photo';
} else {
eventName += '.id_photo';
}
if (this.state.shouldDetect) {
eventName += '.face_detection_enabled';
} else {
eventName += '.face_detection_disabled';
}
sendTrackEvent(eventName);
}
setDetection() {
this.setState(
{ shouldDetect: !this.state.shouldDetect },
(state) => ({ shouldDetect: !state.shouldDetect }),
() => {
if (this.state.shouldDetect) {
this.setState({ isFinishedLoadingDetection: false });
@@ -67,21 +54,65 @@ class Camera extends React.Component {
);
}
startDetection() {
setTimeout(() => {
if (this.state.videoHasLoaded) {
const loadModelPromise = blazeface.load();
Promise.all([loadModelPromise])
.then((values) => {
this.setState({ isFinishedLoadingDetection: true });
this.detectFromVideoFrame(values[0], this.videoRef.current);
});
} else {
this.setState({ isFinishedLoadingDetection: true });
this.setState({ shouldDetect: false });
// TODO: add error message
setVideoHasLoaded() {
this.setState({ videoHasLoaded: 'true' });
}
getGridPosition(coordinates) {
// Used to determine where a face is (i.e. top-left, center-right, bottom-center, etc.)
const x = coordinates[0];
const y = coordinates[1];
let messageBase = 'id.verification.photo.feedback';
const heightUpperLimit = 320;
const heightMiddleLimit = 160;
if (y < heightMiddleLimit) {
messageBase += '.top';
} else if (y < heightUpperLimit && y >= heightMiddleLimit) {
messageBase += '.center';
} else {
messageBase += '.bottom';
}
const widthRightLimit = 213;
const widthMiddleLimit = 427;
if (x < widthRightLimit) {
messageBase += '.right';
} else if (x >= widthRightLimit && x < widthMiddleLimit) {
messageBase += '.center';
} else {
messageBase += '.left';
}
return messageBase;
}
getSizeFactor() {
let sizeFactor = 1;
const settings = this.cameraPhoto.getCameraSettings();
if (settings) {
const videoWidth = settings.width;
const videoHeight = settings.height;
// need to multiply by 3 because each pixel contains 3 bytes
const currentSize = videoWidth * videoHeight * 3;
// chose a limit of 9,999,999 (bytes) so that result will
// always be less than 10MB
const ratio = 9999999 / currentSize;
if (ratio < 1) {
// if the current resolution creates an image larger than 10 MB, adjust sizeFactor (resolution)
// to ensure that image will have a file size of less than 10 MB.
sizeFactor = ratio;
} else if (videoWidth === 640 && videoHeight === 480) {
// otherwise increase the resolution to try and prevent blurry images.
sizeFactor = 2;
}
}, 1000);
}
return sizeFactor;
}
detectFromVideoFrame = (model, video) => {
@@ -146,6 +177,39 @@ class Camera extends React.Component {
}
}
startDetection() {
setTimeout(() => {
if (this.state.videoHasLoaded) {
const loadModelPromise = blazeface.load();
Promise.all([loadModelPromise])
.then((values) => {
this.setState({ isFinishedLoadingDetection: true });
this.detectFromVideoFrame(values[0], this.videoRef.current);
});
} else {
this.setState({ isFinishedLoadingDetection: true });
this.setState({ shouldDetect: false });
// TODO: add error message
}
}, 1000);
}
sendEvent() {
let eventName = 'edx.id_verification';
if (this.props.isPortrait) {
eventName += '.user_photo';
} else {
eventName += '.id_photo';
}
if (this.state.shouldDetect) {
eventName += '.face_detection_enabled';
} else {
eventName += '.face_detection_disabled';
}
sendTrackEvent(eventName);
}
giveFeedback(numFaces, rightEye, isCorrect) {
if (this.state.shouldGiveFeedback) {
const currentFeedback = this.state.feedback;
@@ -176,39 +240,6 @@ class Camera extends React.Component {
}
}
getGridPosition(coordinates) {
// Used to determine where a face is (i.e. top-left, center-right, bottom-center, etc.)
const x = coordinates[0];
const y = coordinates[1];
let messageBase = 'id.verification.photo.feedback';
const heightUpperLimit = 320;
const heightMiddleLimit = 160;
if (y < heightMiddleLimit) {
messageBase += '.top';
} else if (y < heightUpperLimit && y >= heightMiddleLimit) {
messageBase += '.center';
} else {
messageBase += '.bottom';
}
const widthRightLimit = 213;
const widthMiddleLimit = 427;
if (x < widthRightLimit) {
messageBase += '.right';
} else if (x >= widthRightLimit && x < widthMiddleLimit) {
messageBase += '.center';
} else {
messageBase += '.left';
}
return messageBase;
}
isInRangeForPortrait(x, y) {
return x > 47 && x < 570 && y > 100 && y < 410;
}
@@ -217,13 +248,10 @@ class Camera extends React.Component {
return x > 120 && x < 470 && y > 120 && y < 350;
}
setVideoHasLoaded() {
this.setState({ videoHasLoaded: 'true' });
}
takePhoto() {
if (this.state.dataUri) {
return this.reset();
this.reset();
return;
}
const config = {
@@ -234,30 +262,7 @@ class Camera extends React.Component {
const dataUri = this.cameraPhoto.getDataUri(config);
this.setState({ dataUri });
this.props.onImageCapture(dataUri);
}
getSizeFactor() {
let sizeFactor = 1;
const settings = this.cameraPhoto.getCameraSettings();
if (settings) {
const videoWidth = settings.width;
const videoHeight = settings.height;
// need to multiply by 3 because each pixel contains 3 bytes
const currentSize = videoWidth * videoHeight * 3;
// chose a limit of 9,999,999 (bytes) so that result will
// always be less than 10MB
const ratio = 9999999 / currentSize;
if (ratio < 1) {
// if the current resolution creates an image larger than 10 MB, adjust sizeFactor (resolution)
// to ensure that image will have a file size of less than 10 MB.
sizeFactor = ratio;
} else if (videoWidth === 640 && videoHeight === 480) {
// otherwise increase the resolution to try and prevent blurry images.
sizeFactor = 2;
}
}
return sizeFactor;
this.props.setPhotoMode('camera');
}
playShutterClick() {
@@ -278,7 +283,7 @@ class Camera extends React.Component {
: 'camera-flash';
return (
<div className="camera-outer-wrapper shadow">
<Form.Group style={{ textAlign: 'left', padding: '0.5rem', marginBottom: '0.5rem' }} >
<Form.Group style={{ textAlign: 'left', padding: '0.5rem', marginBottom: '0.5rem' }}>
<Form.Check
id="videoDetection"
name="videoDetection"
@@ -332,9 +337,10 @@ class Camera extends React.Component {
<div role="status" className="sr-only">{this.state.feedback}</div>
</div>
<button
type="button"
className={`btn camera-btn ${
this.state.dataUri ?
'btn-outline-primary'
this.state.dataUri
? 'btn-outline-primary'
: 'btn-primary'
}`}
accessKey="c"
@@ -354,6 +360,7 @@ class Camera extends React.Component {
Camera.propTypes = {
intl: intlShape.isRequired,
onImageCapture: PropTypes.func.isRequired,
setPhotoMode: PropTypes.func.isRequired,
isPortrait: PropTypes.bool.isRequired,
};

View File

@@ -1,14 +1,30 @@
import React from 'react';
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { Collapsible } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import messages from './IdVerification.messages';
import IdVerificationContext from './IdVerificationContext';
function CameraHelp(props) {
const { optimizelyExperimentName } = useContext(IdVerificationContext);
return (
<div>
{ optimizelyExperimentName
&& (
<Collapsible
styling="card"
title={props.intl.formatMessage(messages['id.verification.camera.help.upload.question'])}
className="mb-4 shadow"
defaultOpen={props.isOpen}
>
<p>
{props.intl.formatMessage(messages['id.verification.camera.help.upload.answer'])}
</p>
</Collapsible>
)}
<Collapsible
styling="card"
title={props.intl.formatMessage(messages['id.verification.camera.help.sight.question'])}
@@ -26,7 +42,10 @@ function CameraHelp(props) {
defaultOpen={props.isOpen}
>
<p>
{props.intl.formatMessage(messages['id.verification.camera.help.difficulty.answer'])}
{props.intl.formatMessage(
messages['id.verification.camera.help.difficulty.answer'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
</Collapsible>
</div>

View File

@@ -6,8 +6,9 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import messages from './IdVerification.messages';
import ImageFileUpload from './ImageFileUpload';
import { IdVerificationContext } from './IdVerificationContext';
import IdVerificationContext from './IdVerificationContext';
import ImagePreview from './ImagePreview';
import SupportedMediaTypes from './SupportedMediaTypes';
function CameraHelpWithUpload(props) {
const { setIdPhotoFile, idPhotoFile, userId } = useContext(IdVerificationContext);
@@ -34,6 +35,7 @@ function CameraHelpWithUpload(props) {
{idPhotoFile && hasUploadedImage && <ImagePreview src={idPhotoFile} alt={props.intl.formatMessage(messages['id.verification.id.photo.preview.alt'])} />}
<p>
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
<SupportedMediaTypes />
</p>
<ImageFileUpload onFileChange={setAndTrackIdPhotoFile} intl={props.intl} />
</Collapsible>

View File

@@ -0,0 +1,73 @@
import React, { useContext } from 'react';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Collapsible } from '@edx/paragon';
import { Link } from 'react-router-dom';
import PropTypes from 'prop-types';
import IdVerificationContext, { MEDIA_ACCESS } from './IdVerificationContext';
import messages from './IdVerification.messages';
function CollapsibleImageHelp(props) {
const {
userId, shouldUseCamera, setShouldUseCamera, optimizelyExperimentName, mediaAccess,
} = useContext(IdVerificationContext);
function handleClick() {
const toggleTo = shouldUseCamera ? 'upload' : 'camera';
const eventName = `edx.id_verification.toggle_to.${toggleTo}`;
sendTrackEvent(eventName, {
category: 'id_verification',
user_id: userId,
});
setShouldUseCamera(!shouldUseCamera);
}
if (optimizelyExperimentName && mediaAccess !== MEDIA_ACCESS.DENIED && mediaAccess !== MEDIA_ACCESS.UNSUPPORTED) {
return (
<Collapsible
styling="card"
title={shouldUseCamera ? props.intl.formatMessage(messages['id.verification.photo.upload.help.title']) : props.intl.formatMessage(messages['id.verification.photo.camera.help.title'])}
className="mb-4 shadow"
defaultOpen
>
<p data-testid="help-text">
{shouldUseCamera
? props.intl.formatMessage(messages['id.verification.photo.upload.help.text'])
: props.intl.formatMessage(messages['id.verification.photo.camera.help.text'])}
</p>
{ (mediaAccess === MEDIA_ACCESS.PENDING && !shouldUseCamera)
? (
// if a user has not enabled camera access yet, and they are trying to switch
// to camera mode, direct them to panel that requests camera access
<Link
to={{ pathname: 'request-camera-access', state: { fromPortraitCapture: props.isPortrait, fromIdCapture: !props.isPortrait } }}
className="btn btn-primary"
data-testid="access-link"
>
{props.intl.formatMessage(messages['id.verification.photo.camera.help.button'])}
</Link>
)
: (
<Button
title={shouldUseCamera ? 'Upload Portrait Photo' : 'Take Portrait Photo'}
data-testid="toggle-button"
onClick={handleClick}
style={{ marginTop: '0.5rem' }}
>
{shouldUseCamera ? props.intl.formatMessage(messages['id.verification.photo.upload.help.button']) : props.intl.formatMessage(messages['id.verification.photo.camera.help.button'])}
</Button>
)}
</Collapsible>
);
}
return null;
}
CollapsibleImageHelp.propTypes = {
intl: intlShape.isRequired,
isPortrait: PropTypes.bool.isRequired,
};
export default injectIntl(CollapsibleImageHelp);

View File

@@ -1,37 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import messages from './IdVerification.messages';
function ExistingRequest(props) {
return (
<div>
<h3 aria-level="1" tabIndex="-1">
{props.intl.formatMessage(messages['id.verification.existing.request.title'])}
</h3>
{props.status === 'pending' || props.status === 'approved'
? <p>{props.intl.formatMessage(messages['id.verification.existing.request.pending.text'])}</p>
: <FormattedMessage
id="id.verification.existing.request.denied.text"
defaultMessage="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}."
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>,
}}
/>
}
<div className="action-row">
<a className="btn btn-primary mt-3" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
{props.intl.formatMessage(messages['id.verification.return.dashboard'])}
</a>
</div>
</div>
);
}
ExistingRequest.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(ExistingRequest);

View File

@@ -6,6 +6,21 @@ const messages = defineMessages({
defaultMessage: 'Next',
description: 'Next button.',
},
'id.verification.support': {
id: 'id.verification.support',
defaultMessage: 'support',
description: 'Website support.',
},
'id.verification.continue.upload': {
id: 'id.verification.continue.upload',
defaultMessage: 'Continue with Upload',
description: 'Button to continue with upload.',
},
'id.verification.example.card.alt': {
id: 'id.verification.example.card.alt',
defaultMessage: 'Example of a valid identification card with a full name and photo.',
description: 'Alt text for an example identification card.',
},
'id.verification.requirements.title': {
id: 'id.verification.requirements.title',
defaultMessage: 'Photo Verification Requirements',
@@ -33,7 +48,7 @@ const messages = defineMessages({
},
'id.verification.requirements.card.id.text': {
id: 'id.verification.requirements.card.id.text',
defaultMessage: 'You need a valid ID that contains your full name and photo.',
defaultMessage: 'You need a valid identification card that contains your full name and photo.',
description: 'Text that explains that the user needs a photo ID.',
},
'id.verification.privacy.title': {
@@ -43,31 +58,36 @@ const messages = defineMessages({
},
'id.verification.privacy.need.photo.question': {
id: 'id.verification.privacy.need.photo.question',
defaultMessage: 'Why does edX need my photo?',
description: 'Question about why edX needs a verification photo.',
defaultMessage: 'Why does {siteName} need my photo?',
description: 'Question about why the platform needs a verification photo.',
},
'id.verification.privacy.need.photo.answer': {
id: 'id.verification.privacy.need.photo.answer',
defaultMessage: 'We use your verification photos to confirm your identity and ensure the validity of your certificate.',
description: 'Answering why edX needs a verification photo.',
description: 'Answering why the platform needs a verification photo.',
},
'id.verification.privacy.do.with.photo.question': {
id: 'id.verification.privacy.do.with.photo.question',
defaultMessage: 'What does edX do with this photo?',
description: 'Question about what edX does with the verification photo.',
defaultMessage: 'What does {siteName} do with this photo?',
description: 'Question about what the platform does with the verification photo.',
},
'id.verification.privacy.do.with.photo.answer': {
id: 'id.verification.privacy.do.with.photo.answer',
defaultMessage: '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.',
description: 'Answering what edX does with the verification photo.',
defaultMessage: '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.',
description: 'Answering what the platform does with the verification photo.',
},
'id.verification.existing.request.title': {
id: 'id.verification.existing.request.title',
'id.verification.access.blocked.title': {
id: 'id.verification.access.blocked.title',
defaultMessage: 'Identity Verification',
description: 'Title for text that displays when user has already made a request.',
description: 'Title for text that displays when a user is blocked from ID verification.',
},
'id.verification.existing.request.pending.text': {
id: 'id.verification.existing.request.pending.text',
'id.verification.access.blocked.enrollment': {
id: 'id.verification.access.blocked.enrollment',
defaultMessage: 'You are not currently enrolled in a course that requires identity verification.',
description: 'Text that displays when user is trying to verify while not enrolled in a course that requires ID verification.',
},
'id.verification.access.blocked.pending': {
id: 'id.verification.access.blocked.pending',
defaultMessage: '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).',
description: 'Text that displays when user has a pending or approved request.',
},
@@ -78,7 +98,7 @@ const messages = defineMessages({
},
'id.verification.photo.retake': {
id: 'id.verification.photo.retake',
defaultMessage: 'Retake Photo',
defaultMessage: 'Retake Photo?',
description: 'Button to retake photo.',
},
'id.verification.photo.enable.detection': {
@@ -331,6 +351,21 @@ const messages = defineMessages({
defaultMessage: 'Select "edx.org" and change the camera permissions to "Allow."',
description: 'Text for step four of enabling camera access.',
},
'id.verification.camera.access.failure.unsupported': {
id: 'id.verification.camera.access.failure.unsupported',
defaultMessage: 'It looks like your browser does not support camera access.',
description: 'Text indicating that the user\'s browser does not support camera access.',
},
'id.verification.camera.access.failure.unsupported.chrome.explanation': {
id: 'id.verification.camera.access.failure.unsupported.chrome.explanation',
defaultMessage: 'The Chrome browser currently does not support camera access on iOS devices, such as iPhones and iPads.',
description: 'Explanation for why certain web browsers, like Chrome, do not support accessing the user\'s camera.',
},
'id.verification.camera.access.failure.unsupported.instructions': {
id: 'id.verification.camera.access.failure.unsupported.instructions',
defaultMessage: 'Please use another browser to complete Identity Verification.',
description: 'Instructions for the user to user another web browser to complete the process.',
},
'id.verification.photo.tips.title': {
id: 'id.verification.photo.tips.title',
defaultMessage: 'Helpful Photo Tips',
@@ -383,7 +418,7 @@ const messages = defineMessages({
},
'id.verification.portrait.photo.instructions.upload': {
id: 'id.verification.portrait.photo.instructions.upload',
defaultMessage: 'Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)',
defaultMessage: 'Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ',
description: 'Instructions for portrait photo upload.',
},
'id.verification.camera.help.sight.question': {
@@ -413,9 +448,19 @@ const messages = defineMessages({
},
'id.verification.camera.help.difficulty.answer': {
id: 'id.verification.camera.help.difficulty.answer',
defaultMessage: 'If you require assistance with taking a photo for submission, contact edX support for additional suggestions.',
defaultMessage: 'If you require assistance with taking a photo for submission, contact {siteName} support for additional suggestions.',
description: 'Confirming what to do if the user has difficult holding their head relative to the camera.',
},
'id.verification.camera.help.upload.question': {
id: 'id.verification.camera.help.upload.question',
defaultMessage: 'What if I want to upload a photo instead?',
description: 'Question on what to do if the user would like to upload a photo instead.',
},
'id.verification.camera.help.upload.answer': {
id: 'id.verification.camera.help.upload.answer',
defaultMessage: 'On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.',
description: 'Confirming what to do if the user would like to upload a photo.',
},
'id.verification.id.photo.unclear.question': {
id: 'id.verification.id.photo.unclear.question',
defaultMessage: 'Is your ID image not clear or too blurry?',
@@ -428,7 +473,7 @@ const messages = defineMessages({
},
'id.verification.id.tips.description': {
id: 'id.verification.id.tips.description',
defaultMessage: 'Next, we\'ll need you to take a photo of a valid ID that includes your name. Please have your ID ready.',
defaultMessage: 'Next, we\'ll need you to take a photo of a valid identification card that includes your full name and photo. Please have your ID ready.',
description: 'Description for the ID Tips page.',
},
'id.verification.id.tips.list.well.lit': {
@@ -463,11 +508,16 @@ const messages = defineMessages({
},
'id.verification.id.photo.instructions.upload': {
id: 'id.verification.id.photo.instructions.upload',
defaultMessage: '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)',
defaultMessage: '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: ',
description: 'Instructions for ID photo upload.',
},
'id.verification.id.photo.instructions.upload.error': {
id: 'id.verification.id.photo.instructions.upload.error',
'id.verification.id.photo.instructions.upload.error.invalidFileType': {
id: 'id.verification.id.photo.instructions.upload.error.invalidFileType',
defaultMessage: 'The file you have selected is not a supported image type. Please choose from the following formats: ',
description: 'Error message for file upload that is not a supported image type.',
},
'id.verification.id.photo.instructions.upload.error.fileTooLarge': {
id: 'id.verification.id.photo.instructions.upload.error.fileTooLarge',
defaultMessage: 'The file you have selected is too large. Please try again with a file less than 10MB.',
description: 'Error message for file upload that is larger than 10MB.',
},
@@ -571,10 +621,30 @@ const messages = defineMessages({
defaultMessage: 'Submit',
description: 'Button to confirm all information is correct and submit.',
},
'id.verification.submission.alert.error.face': {
id: 'id.verification.submission.alert.error.face',
defaultMessage: 'A photo of your face is required. Please retake your portrait photo.',
description: 'Error message displayed when the user\'s portrait photo is missing.',
},
'id.verification.submission.alert.error.id': {
id: 'id.verification.submission.alert.error.id',
defaultMessage: 'A photo of your ID card is required. Please retake your ID photo.',
description: 'Error message displayed when the user\'s ID photo is missing.',
},
'id.verification.submission.alert.error.name': {
id: 'id.verification.submission.alert.error.name',
defaultMessage: 'A valid account name is required. Please update your account name to match the name on your ID.',
description: 'Error message displayed when the user\'s account name is missing.',
},
'id.verification.submission.alert.error.unsupported': {
id: 'id.verification.submission.alert.error.unsupported',
defaultMessage: 'One or more of the files you have uploaded is in an unsupported format. Please choose from the following: ',
description: 'Error message displayed when the user uploads an unsupported file type.',
},
'id.verification.review.error': {
id: 'id.verification.review.error',
defaultMessage: 'edX Support Page',
description: 'Text linking to the support page.',
defaultMessage: '{siteName} Support Page',
description: 'Text linking to the platform support page.',
},
'id.verification.submitted.title': {
id: 'id.verification.submitted.title',
@@ -596,6 +666,56 @@ const messages = defineMessages({
defaultMessage: 'Return to Course',
description: 'Return to the course which ID verification was accessed from.',
},
'id.verification.photo.upload.help.title': {
id: 'id.verification.photo.upload.help.title',
defaultMessage: 'Upload a Photo Instead',
description: 'Title for section that allows switching to photo upload mode.',
},
'id.verification.photo.camera.help.title': {
id: 'id.verification.photo.camera.help.title',
defaultMessage: 'Use Your Camera Instead',
description: 'Title for section that allows switching to camera mode.',
},
'id.verification.photo.upload.help.text': {
id: 'id.verification.photo.upload.help.text',
defaultMessage: '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.',
description: 'Help text for switching to upload mode.',
},
'id.verification.photo.camera.help.text': {
id: 'id.verification.photo.camera.help.text',
defaultMessage: '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.',
description: 'Help text for switching to camera mode.',
},
'id.verification.photo.upload.help.button': {
id: 'id.verification.upload.help.button',
defaultMessage: 'Switch to Upload Mode',
description: 'Button used to switch to upload mode.',
},
'id.verification.photo.camera.help.button': {
id: 'id.verification.camera.help.button',
defaultMessage: 'Switch to Camera Mode',
description: 'Button used to switch to camera mode.',
},
'id.verification.choose.mode.title': {
id: 'id.verification.choose.mode.title',
defaultMessage: 'Photo Requirements Options',
description: 'Title for section that allows user to choose photo mode.',
},
'id.verification.choose.mode.help.text': {
id: 'id.verification.choose.mode.hep.text',
defaultMessage: 'To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.',
description: 'Help text for section that allows user to choose photo mode.',
},
'id.verification.choose.mode.radio.upload': {
id: 'id.verification.choose.mode.radio.upload',
defaultMessage: 'Upload photos from my device',
description: 'Radio button to choose to upload photos.',
},
'id.verification.choose.mode.radio.camera': {
id: 'id.verification.choose.mode.radio.camera',
defaultMessage: 'Take pictures using my camera',
description: 'Radio button to choose to use camera for photos.',
},
});
export default messages;

View File

@@ -1,11 +1,4 @@
import React, { useState, useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import { AppContext } from '@edx/frontend-platform/react';
import { hasGetUserMediaSupport } from './getUserMediaShim';
import { getExistingIdVerification } from './data/service';
import PageLoading from '../account-settings/PageLoading';
import ExistingRequest from './ExistingRequest';
import React from 'react';
const IdVerificationContext = React.createContext({});
@@ -16,83 +9,17 @@ const MEDIA_ACCESS = {
GRANTED: 'granted',
};
function IdVerificationContextProvider({ children }) {
const [existingIdVerification, setExistingIdVerification] = useState(null);
const [facePhotoFile, setFacePhotoFile] = useState(null);
const [idPhotoFile, setIdPhotoFile] = useState(null);
const [idPhotoName, setIdPhotoName] = useState(null);
const [mediaStream, setMediaStream] = useState(null);
const [mediaAccess, setMediaAccess] = useState(hasGetUserMediaSupport ?
MEDIA_ACCESS.PENDING :
MEDIA_ACCESS.UNSUPPORTED);
const { authenticatedUser } = useContext(AppContext);
const contextValue = {
existingIdVerification,
facePhotoFile,
idPhotoFile,
idPhotoName,
mediaStream,
mediaAccess,
userId: authenticatedUser.userId,
nameOnAccount: authenticatedUser.name,
setExistingIdVerification,
setFacePhotoFile,
setIdPhotoFile,
setIdPhotoName,
tryGetUserMedia: async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
setMediaAccess(MEDIA_ACCESS.GRANTED);
setMediaStream(stream);
// stop the stream, as we are not using it yet
const tracks = stream.getTracks();
tracks.forEach(track => track.stop());
} catch (err) {
setMediaAccess(MEDIA_ACCESS.DENIED);
}
},
stopUserMedia: () => {
if (mediaStream) {
const tracks = mediaStream.getTracks();
tracks.forEach(track => track.stop());
setMediaStream(null);
}
},
};
// Call verification status endpoint to check whether we can verify.
useEffect(() => {
(async () => {
const existingIdV = await getExistingIdVerification();
setExistingIdVerification(existingIdV);
})();
}, []);
// If we are waiting for verification status endpoint, show spinner.
if (!existingIdVerification) {
return <PageLoading srMessage="Loading verification status" />;
}
if (!existingIdVerification.canVerify) {
const { status } = existingIdVerification;
return (
<ExistingRequest status={status} />
);
}
return (
<IdVerificationContext.Provider value={contextValue}>
{children}
</IdVerificationContext.Provider>
);
}
IdVerificationContextProvider.propTypes = {
children: PropTypes.node.isRequired,
const ERROR_REASONS = {
COURSE_ENROLLMENT: 'course_enrollment',
EXISTING_REQUEST: 'existing_request',
CANNOT_VERIFY: 'cannot_verify',
};
const VERIFIED_MODES = ['verified', 'professional', 'masters', 'executive_education'];
export default IdVerificationContext;
export {
IdVerificationContext,
IdVerificationContextProvider,
MEDIA_ACCESS,
ERROR_REASONS,
VERIFIED_MODES,
};

View File

@@ -0,0 +1,155 @@
import React, { useState, useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import { AppContext } from '@edx/frontend-platform/react';
import { getProfileDataManager } from '../account-settings/data/service';
import PageLoading from '../account-settings/PageLoading';
import { getExistingIdVerification, getEnrollments } from './data/service';
import AccessBlocked from './AccessBlocked';
import { hasGetUserMediaSupport } from './getUserMediaShim';
import IdVerificationContext, { MEDIA_ACCESS, ERROR_REASONS, VERIFIED_MODES } from './IdVerificationContext';
export default function IdVerificationContextProvider({ children }) {
const { authenticatedUser } = useContext(AppContext);
const [existingIdVerification, setExistingIdVerification] = useState(null);
useEffect(() => {
// Call verification status endpoint to check whether we can verify.
(async () => {
const existingIdV = await getExistingIdVerification();
setExistingIdVerification(existingIdV);
})();
}, []);
const [facePhotoFile, setFacePhotoFile] = useState(null);
const [idPhotoFile, setIdPhotoFile] = useState(null);
const [idPhotoName, setIdPhotoName] = useState(null);
const [mediaStream, setMediaStream] = useState(null);
const [mediaAccess, setMediaAccess] = useState(
hasGetUserMediaSupport ? MEDIA_ACCESS.PENDING : MEDIA_ACCESS.UNSUPPORTED,
);
const [canVerify, setCanVerify] = useState(true);
const [error, setError] = useState('');
useEffect(() => {
// Check for an existing verification attempt
if (existingIdVerification && !existingIdVerification.canVerify) {
const { status } = existingIdVerification;
setCanVerify(false);
if (status === 'pending' || status === 'approved') {
setError(ERROR_REASONS.EXISTING_REQUEST);
} else {
setError(ERROR_REASONS.CANNOT_VERIFY);
}
}
}, [existingIdVerification]);
useEffect(() => {
// Check whether the learner is enrolled in a verified course mode.
(async () => {
/* eslint-disable arrow-body-style */
const enrollments = await getEnrollments();
const verifiedEnrollments = enrollments.filter((enrollment) => {
return VERIFIED_MODES.includes(enrollment.mode);
});
if (verifiedEnrollments.length === 0) {
setCanVerify(false);
setError(ERROR_REASONS.COURSE_ENROLLMENT);
}
})();
}, []);
const [profileDataManager, setProfileDataManager] = useState(null);
useEffect(() => {
// Determine if the user's profile data is managed by a third-party identity provider.
// If so, they cannot update their account name manually.
if (authenticatedUser.roles.length > 0) {
(async () => {
const thirdPartyManager = await getProfileDataManager(
authenticatedUser.username,
authenticatedUser.roles,
);
if (thirdPartyManager) {
setProfileDataManager(thirdPartyManager);
}
})();
}
}, [authenticatedUser]);
const [optimizelyExperimentName, setOptimizelyExperimentName] = useState('');
const [shouldUseCamera, setShouldUseCamera] = useState(false);
// The following are used to keep track of how a user has submitted photos
const [portraitPhotoMode, setPortraitPhotoMode] = useState('');
const [idPhotoMode, setIdPhotoMode] = useState('');
// If the user reaches the end of the flow and goes back to retake their photos,
// this flag ensures that they are directed straight back to the summary panel
const [reachedSummary, setReachedSummary] = useState(false);
const contextValue = {
existingIdVerification,
facePhotoFile,
idPhotoFile,
idPhotoName,
mediaStream,
mediaAccess,
userId: authenticatedUser.userId,
nameOnAccount: authenticatedUser.name,
profileDataManager,
optimizelyExperimentName,
shouldUseCamera,
portraitPhotoMode,
idPhotoMode,
reachedSummary,
setExistingIdVerification,
setFacePhotoFile,
setIdPhotoFile,
setIdPhotoName,
setOptimizelyExperimentName,
setShouldUseCamera,
setPortraitPhotoMode,
setIdPhotoMode,
setReachedSummary,
tryGetUserMedia: async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
setMediaAccess(MEDIA_ACCESS.GRANTED);
setMediaStream(stream);
setShouldUseCamera(true);
// stop the stream, as we are not using it yet
const tracks = stream.getTracks();
tracks.forEach(track => track.stop());
} catch (err) {
setMediaAccess(MEDIA_ACCESS.DENIED);
setShouldUseCamera(false);
}
},
stopUserMedia: () => {
if (mediaStream) {
const tracks = mediaStream.getTracks();
tracks.forEach(track => track.stop());
setMediaStream(null);
}
},
};
// If we are waiting for verification status endpoint, show spinner.
if (!existingIdVerification) {
return <PageLoading srMessage="Loading verification status" />;
}
if (!canVerify) {
return <AccessBlocked error={error} />;
}
return (
<IdVerificationContext.Provider value={contextValue}>
{children}
</IdVerificationContext.Provider>
);
}
IdVerificationContextProvider.propTypes = {
children: PropTypes.node.isRequired,
};

View File

@@ -1,13 +1,18 @@
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { Route, Switch, Redirect, useRouteMatch, useLocation } from 'react-router-dom';
import {
Route, Switch, Redirect, useRouteMatch, useLocation,
} from 'react-router-dom';
import qs from 'qs';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Modal, Button } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { idVerificationSelector } from './data/selectors';
import './getUserMediaShim';
import { IdVerificationContextProvider } from './IdVerificationContext';
import IdVerificationContextProvider from './IdVerificationContextProvider';
import ReviewRequirementsPanel from './panels/ReviewRequirementsPanel';
import ChooseModePanel from './panels/ChooseModePanel';
import RequestCameraAccessPanel from './panels/RequestCameraAccessPanel';
import PortraitPhotoContextPanel from './panels/PortraitPhotoContextPanel';
import TakePortraitPhotoPanel from './panels/TakePortraitPhotoPanel';
@@ -29,7 +34,13 @@ function IdVerificationPage(props) {
// Course run key is passed as a query string
useEffect(() => {
if (search) {
sessionStorage.setItem('courseRunKey', search.substring(1));
const parsed = qs.parse(search, {
ignoreQueryPrefix: true,
interpretNumericEntities: true,
});
if (Object.prototype.hasOwnProperty.call(parsed, 'course_id') && parsed.course_id) {
sessionStorage.setItem('courseRunKey', parsed.course_id);
}
}
}, [search]);
@@ -43,6 +54,7 @@ function IdVerificationPage(props) {
<IdVerificationContextProvider>
<Switch>
<Route path={`${path}/review-requirements`} component={ReviewRequirementsPanel} />
<Route path={`${path}/choose-mode`} component={ChooseModePanel} />
<Route path={`${path}/request-camera-access`} component={RequestCameraAccessPanel} />
<Route path={`${path}/portrait-photo-context`} component={PortraitPhotoContextPanel} />
<Route path={`${path}/take-portrait-photo`} component={TakePortraitPhotoPanel} />
@@ -55,27 +67,40 @@ function IdVerificationPage(props) {
</IdVerificationContextProvider>
</div>
<div className="col-lg-6 col-md-4 pt-md-0 pt-4 text-right">
<Button className="btn-link px-0" onClick={() => setIsModalOpen(true)}>
Privacy Information
<Button variant="link" className="px-0" onClick={() => setIsModalOpen(true)}>
Privacy Information
</Button>
</div>
</div>
<Modal
open={isModalOpen}
title={props.intl.formatMessage(messages['id.verification.privacy.title'])}
body={(
<div>
<h6>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.question'])}</h6>
<h6>
{props.intl.formatMessage(
messages['id.verification.privacy.need.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}</p>
<h6>{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.question'])}</h6>
<p>{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.answer'])}</p>
<h6>
{props.intl.formatMessage(
messages['id.verification.privacy.do.with.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{props.intl.formatMessage(
messages['id.verification.privacy.do.with.photo.answer'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
</div>
)}
onClose={() => setIsModalOpen(false)}
/>
</div>
</>
);
@@ -85,6 +110,4 @@ IdVerificationPage.propTypes = {
intl: intlShape.isRequired,
};
export default connect(idVerificationSelector, {
})(injectIntl(IdVerificationPage));
export default connect(idVerificationSelector, {})(injectIntl(IdVerificationPage));

View File

@@ -3,10 +3,14 @@ import { intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import { Alert } from '@edx/paragon';
import messages from './IdVerification.messages';
import SupportedMediaTypes from './SupportedMediaTypes';
export default function ImageFileUpload({ onFileChange, intl }) {
const [fileTooLargeError, setFileTooLargeError] = useState(false);
export default function ImageFileUpload({ onFileChange, setPhotoMode, intl }) {
const [error, setError] = useState(null);
const errorTypes = {
invalidFileType: 'invalidFileType',
fileTooLarge: 'fileTooLarge',
};
const maxFileSize = 10000000;
const handleChange = useCallback((e) => {
@@ -15,12 +19,18 @@ export default function ImageFileUpload({ onFileChange, intl }) {
}
const fileObject = e.target.files[0];
if (fileObject.size < maxFileSize) {
const fileReader = new FileReader();
fileReader.addEventListener('load', () => onFileChange(fileReader.result));
fileReader.readAsDataURL(fileObject);
if (!fileObject.type.startsWith('image')) {
setError(errorTypes.invalidFileType);
} else if (fileObject.size >= maxFileSize) {
setError(errorTypes.fileTooLarge);
} else {
setFileTooLargeError(true);
setError(null);
const fileReader = new FileReader();
fileReader.addEventListener('load', () => {
onFileChange(fileReader.result);
setPhotoMode('upload');
});
fileReader.readAsDataURL(fileObject);
}
}, []);
@@ -32,14 +42,15 @@ export default function ImageFileUpload({ onFileChange, intl }) {
data-testid="fileUpload"
onChange={handleChange}
/>
{fileTooLargeError && (
{error && (
<Alert
id="fileTooLargeError"
id="fileError"
variant="danger"
tabIndex="-1"
style={{ marginTop: '1rem' }}
>
{intl.formatMessage(messages['id.verification.id.photo.instructions.upload.error'])}
{intl.formatMessage(messages[`id.verification.id.photo.instructions.upload.error.${error}`])}
<SupportedMediaTypes />
</Alert>
)}
</>
@@ -48,5 +59,6 @@ export default function ImageFileUpload({ onFileChange, intl }) {
ImageFileUpload.propTypes = {
onFileChange: PropTypes.func.isRequired,
setPhotoMode: PropTypes.func.isRequired,
intl: intlShape.isRequired,
};

View File

@@ -16,3 +16,7 @@ ImagePreview.propTypes = {
alt: PropTypes.string.isRequired,
id: PropTypes.string,
};
ImagePreview.defaultProps = {
id: undefined,
};

View File

@@ -0,0 +1,14 @@
import React from 'react';
export default function SupportedMediaTypes() {
const SUPPORTED_TYPES = ['.png', '.jpeg', '.jpg', '.bmp', '.webp', '.tiff'];
const getSupportedTypes = () => SUPPORTED_TYPES.map((type, index) => {
if (index === SUPPORTED_TYPES.length - 1) {
return type;
}
return `${type}, `;
});
return <span>{getSupportedTypes()}</span>;
}

View File

@@ -1,5 +1,8 @@
.page__id-verification {
.verification-panel {
img {
max-width: 100%;
}
.card.accent {
border-top: solid 4px theme-color('warning');
}
@@ -8,7 +11,6 @@
max-width: 20rem;
img {
display: block;
max-width: 100%;
max-height: 10rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

View File

@@ -29,6 +29,25 @@ export async function getExistingIdVerification() {
}
}
/**
* Get the learner's enrollments. Used to check whether the learner is enrolled
* in a verified course mode.
*
* Returns an array: [{...data, mode: String}]
*/
export async function getEnrollments() {
const url = `${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment`;
const requestConfig = {
headers: { Accept: 'application/json' },
};
try {
const { data } = await getAuthenticatedHttpClient().get(url, requestConfig);
return data;
} catch (e) {
return [];
}
}
/**
* Submit ID verifiction to LMS.
*
@@ -45,7 +64,9 @@ export async function submitIdVerification(verificationData) {
facePhotoFile: 'face_image',
idPhotoFile: 'photo_id_image',
idPhotoName: 'full_name',
courseRunKey: 'course_key',
optimizelyExperimentName: 'experiment_name',
portraitPhotoMode: 'portrait_photo_mode',
idPhotoMode: 'id_photo_mode',
};
const postData = {};
// Don't include blank/null/undefined values.
@@ -66,6 +87,10 @@ export async function submitIdVerification(verificationData) {
await getAuthenticatedHttpClient().post(url, urlEncodedPostData, requestConfig);
return { success: true, message: null };
} catch (e) {
return { success: false, message: String(e) }; // TODO: is String(e) right?
return {
success: false,
status: e.customAttributes.httpErrorStatus,
message: String(e),
};
}
}

View File

@@ -0,0 +1,67 @@
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Form } from '@edx/paragon';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import IdVerificationContext from '../IdVerificationContext';
import messages from '../IdVerification.messages';
function ChooseModePanel(props) {
const panelSlug = 'choose-mode';
const { userId, shouldUseCamera, setShouldUseCamera } = useContext(IdVerificationContext);
function onPhotoModeChange(value) {
setShouldUseCamera(value);
const mode = value ? 'camera' : 'upload';
const eventName = `edx.id_verification.choose.${mode}`;
sendTrackEvent(eventName, {
category: 'id_verification',
user_id: userId,
});
}
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.choose.mode.title'])}
>
<p>
{props.intl.formatMessage(messages['id.verification.choose.mode.help.text'])}
</p>
<fieldset>
<Form.Group controlId="formChoosePhotoOption" style={{ marginLeft: '1.25rem' }}>
<Form.Check
type="radio"
id="useUploadMode"
label={props.intl.formatMessage(messages['id.verification.choose.mode.radio.upload'])}
name="photoMode"
checked={!shouldUseCamera}
onChange={() => onPhotoModeChange(false)}
/>
<Form.Check
type="radio"
id="useCameraMode"
label={props.intl.formatMessage(messages['id.verification.choose.mode.radio.camera'])}
name="photoMode"
checked={shouldUseCamera}
onChange={() => onPhotoModeChange(true)}
/>
</Form.Group>
</fieldset>
<div className="action-row">
<Link to={useNextPanelSlug(panelSlug)} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</BasePanel>
);
}
ChooseModePanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(ChooseModePanel);

View File

@@ -15,7 +15,8 @@ export function EnableCameraDirectionsPanel(props) {
</ol>
</>
);
} else if (props.browserName === 'Chrome') {
}
if (props.browserName === 'Chrome') {
return (
<>
<h6>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome'])}</h6>
@@ -32,7 +33,8 @@ export function EnableCameraDirectionsPanel(props) {
</ol>
</>
);
} else if (props.browserName === 'Firefox') {
}
if (props.browserName === 'Firefox') {
return (
<>
<h6>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox'])}</h6>
@@ -47,7 +49,8 @@ export function EnableCameraDirectionsPanel(props) {
</ol>
</>
);
} else if (props.browserName === 'Safari') {
}
if (props.browserName === 'Safari') {
return (
<>
<h6>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari'])}</h6>

View File

@@ -1,12 +1,15 @@
import React, { useContext, useState, useEffect, useRef } from 'react';
import { Form } from '@edx/paragon';
import React, {
useContext, useState, useEffect, useRef,
} from 'react';
import { getConfig } from '@edx/frontend-platform';
import { Hyperlink, Form } from '@edx/paragon';
import { Link, useHistory } from 'react-router-dom';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import { IdVerificationContext } from '../IdVerificationContext';
import IdVerificationContext from '../IdVerificationContext';
import messages from '../IdVerification.messages';
@@ -18,14 +21,14 @@ function GetNameIdPanel(props) {
const nextPanelSlug = useNextPanelSlug(panelSlug);
const {
nameOnAccount, userId, idPhotoName, setIdPhotoName,
nameOnAccount, userId, profileDataManager, idPhotoName, setIdPhotoName,
} = useContext(IdVerificationContext);
const nameOnAccountValue = nameOnAccount || '';
const invalidName = !nameMatches && (!idPhotoName || idPhotoName === nameOnAccount);
const blankName = !nameOnAccount && !idPhotoName;
useEffect(() => {
setIdPhotoName('');
setIdPhotoName(null);
}, []);
useEffect(() => {
@@ -43,14 +46,47 @@ function GetNameIdPanel(props) {
}
}, [nameMatches, blankName]);
const handleSubmit = (e) => {
function getNameValue() {
if (!nameMatches) {
// Explicitly check for null, as an empty string should still be used here
if (idPhotoName === null) {
return nameOnAccountValue;
}
return idPhotoName;
}
return nameOnAccountValue;
}
function getErrorMessage() {
if (profileDataManager) {
return (
<FormattedMessage
id="id.verification.account.name.managed.alert"
defaultMessage="Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help."
description="Alert message informing the user their account name is managed by a third party."
values={{
managerTitle: <strong>{profileDataManager}</strong>,
profileDataManager,
support: (
<Hyperlink destination={getConfig().SUPPORT_URL} target="_blank">
{props.intl.formatMessage(messages['id.verification.support'])}
</Hyperlink>
),
}}
/>
);
}
return props.intl.formatMessage(messages['id.verification.account.name.error']);
}
function handleSubmit(e) {
e.preventDefault();
// If the input is empty, or if no changes have been made to the
// mismatching name, the user should not be able to proceed.
if (!invalidName && !blankName) {
push(nextPanelSlug);
}
};
}
return (
<BasePanel
@@ -78,7 +114,7 @@ function GetNameIdPanel(props) {
inline
onChange={() => {
setNameMatches(true);
setIdPhotoName('');
setIdPhotoName(null);
}}
/>
<Form.Check
@@ -103,19 +139,15 @@ function GetNameIdPanel(props) {
size="lg"
type="text"
ref={nameInputRef}
readOnly={nameMatches}
readOnly={nameMatches || profileDataManager}
isInvalid={invalidName || blankName}
aria-describedby="photo-id-name-feedback"
value={
!nameMatches ?
idPhotoName || nameOnAccountValue
: nameOnAccountValue
}
value={getNameValue()}
onChange={e => setIdPhotoName(e.target.value)}
data-testid="name-input"
/>
<Form.Control.Feedback id="photo-id-name-feedback" type="invalid">
{props.intl.formatMessage(messages['id.verification.account.name.error'])}
{getErrorMessage()}
</Form.Control.Feedback>
</Form.Group>
</Form>
@@ -128,8 +160,8 @@ function GetNameIdPanel(props) {
aria-disabled={invalidName || blankName}
>
{
!nameMatches ?
props.intl.formatMessage(messages['id.verification.account.name.save'])
!nameMatches
? props.intl.formatMessage(messages['id.verification.account.name.save'])
: props.intl.formatMessage(messages['id.verification.next'])
}
</Link>

View File

@@ -6,6 +6,7 @@ import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import CameraHelp from '../CameraHelp';
import messages from '../IdVerification.messages';
import exampleCard from '../assets/example-card.png';
function IdContextPanel(props) {
const panelSlug = 'id-context';
@@ -24,7 +25,7 @@ function IdContextPanel(props) {
<p>
{props.intl.formatMessage(messages['id.verification.photo.tips.list.description'])}
</p>
<ul className="mb-0">
<ul>
<li>
{props.intl.formatMessage(messages['id.verification.id.tips.list.well.lit'])}
</li>
@@ -32,6 +33,10 @@ function IdContextPanel(props) {
{props.intl.formatMessage(messages['id.verification.id.tips.list.clear'])}
</li>
</ul>
<img
src={exampleCard}
alt={props.intl.formatMessage(messages['id.verification.example.card.alt'])}
/>
</div>
</div>
<CameraHelp isOpen />

View File

@@ -7,8 +7,9 @@ import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import { IdVerificationContext, MEDIA_ACCESS } from '../IdVerificationContext';
import IdVerificationContext, { MEDIA_ACCESS } from '../IdVerificationContext';
import { EnableCameraDirectionsPanel } from './EnableCameraDirectionsPanel';
import { UnsupportedCameraDirectionsPanel } from './UnsupportedCameraDirectionsPanel';
import messages from '../IdVerification.messages';
@@ -17,7 +18,9 @@ function RequestCameraAccessPanel(props) {
const [returnText, setReturnText] = useState('id.verification.return.dashboard');
const panelSlug = 'request-camera-access';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const { tryGetUserMedia, mediaAccess, userId } = useContext(IdVerificationContext);
const {
tryGetUserMedia, mediaAccess, userId, optimizelyExperimentName,
} = useContext(IdVerificationContext);
const browserName = Bowser.parse(window.navigator.userAgent).browser.name;
useEffect(() => {
@@ -47,12 +50,25 @@ function RequestCameraAccessPanel(props) {
const getTitle = () => {
if (mediaAccess === MEDIA_ACCESS.GRANTED) {
return props.intl.formatMessage(messages['id.verification.camera.access.title.success']);
} else if ([MEDIA_ACCESS.UNSUPPORTED, MEDIA_ACCESS.DENIED].includes(mediaAccess)) {
}
if ([MEDIA_ACCESS.UNSUPPORTED, MEDIA_ACCESS.DENIED].includes(mediaAccess)) {
return props.intl.formatMessage(messages['id.verification.camera.access.title.failed']);
}
return props.intl.formatMessage(messages['id.verification.camera.access.title']);
};
const returnToDashboardLink = (
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/${returnUrl}`}>
{props.intl.formatMessage(messages[returnText])}
</a>
);
const nextButtonLink = (
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.continue.upload'])}
</Link>
);
return (
<BasePanel
name={panelSlug}
@@ -71,7 +87,7 @@ function RequestCameraAccessPanel(props) {
/>
</p>
<div className="action-row">
<button className="btn btn-primary" onClick={tryGetUserMedia}>
<button type="button" className="btn btn-primary" onClick={tryGetUserMedia}>
{props.intl.formatMessage(messages['id.verification.camera.access.enable'])}
</button>
</div>
@@ -91,16 +107,26 @@ function RequestCameraAccessPanel(props) {
</div>
)}
{[MEDIA_ACCESS.UNSUPPORTED, MEDIA_ACCESS.DENIED].includes(mediaAccess) && (
{mediaAccess === MEDIA_ACCESS.DENIED && (
<div data-testid="camera-failure-instructions">
<p data-testid="camera-access-failure">
{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary'])}
</p>
<EnableCameraDirectionsPanel browserName={browserName} intl={props.intl} />
<div className="action-row">
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/${returnUrl}`}>
{props.intl.formatMessage(messages[returnText])}
</a>
{optimizelyExperimentName ? nextButtonLink : returnToDashboardLink}
</div>
</div>
)}
{mediaAccess === MEDIA_ACCESS.UNSUPPORTED && (
<div data-testid="camera-unsupported-instructions">
<p data-testid="camera-unsupported-failure">
{props.intl.formatMessage(messages['id.verification.camera.access.failure.unsupported'])}
</p>
<UnsupportedCameraDirectionsPanel browserName={browserName} intl={props.intl} />
<div className="action-row">
{optimizelyExperimentName ? nextButtonLink : returnToDashboardLink}
</div>
</div>
)}

View File

@@ -1,32 +1,79 @@
import React, { useEffect, useContext } from 'react';
import { Link } from 'react-router-dom';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { Alert, Hyperlink } from '@edx/paragon';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import { IdVerificationContext } from '../IdVerificationContext';
import IdVerificationContext from '../IdVerificationContext';
import messages from '../IdVerification.messages';
import exampleCard from '../assets/example-card.png';
function ReviewRequirementsPanel(props) {
const { userId } = useContext(IdVerificationContext);
const {
userId, profileDataManager, setOptimizelyExperimentName,
} = useContext(IdVerificationContext);
const panelSlug = 'review-requirements';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const getExperiments = () => {
const {
experimentVariables: {
experimentName = '',
} = {},
} = window;
if (experimentName) {
setOptimizelyExperimentName(experimentName);
}
};
useEffect(() => {
sendTrackEvent('edx.id_verification.started', {
category: 'id_verification',
user_id: userId,
});
getExperiments();
}, [userId]);
function renderManagedProfileMessage() {
if (!profileDataManager) {
return null;
}
return (
<div>
<Alert className="alert alert-primary" role="alert">
<FormattedMessage
id="id.verification.requirements.account.managed.alert"
defaultMessage="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."
description="Alert message informing the user their account data is managed by a third party."
values={{
managerTitle: <strong>{profileDataManager}</strong>,
profileDataManager,
support: (
<Hyperlink destination={getConfig().SUPPORT_URL} target="_blank">
{props.intl.formatMessage(messages['id.verification.support'])}
</Hyperlink>
),
}}
/>
</Alert>
</div>
);
}
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.requirements.title'])}
focusOnMount={false}
>
{renderManagedProfileMessage()}
<p>
{props.intl.formatMessage(messages['id.verification.requirements.description'])}
</p>
@@ -54,6 +101,10 @@ function ReviewRequirementsPanel(props) {
</h6>
<p className="mb-0">
{props.intl.formatMessage(messages['id.verification.requirements.card.id.text'])}
<img
src={exampleCard}
alt={props.intl.formatMessage(messages['id.verification.example.card.alt'])}
/>
</p>
</div>
</div>
@@ -61,16 +112,25 @@ function ReviewRequirementsPanel(props) {
{props.intl.formatMessage(messages['id.verification.privacy.title'])}
</h4>
<h6 aria-level="3">
{props.intl.formatMessage(messages['id.verification.privacy.need.photo.question'])}
{props.intl.formatMessage(
messages['id.verification.privacy.need.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}
</p>
<h6 aria-level="3">
{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.question'])}
{props.intl.formatMessage(
messages['id.verification.privacy.do.with.photo.question'],
{ siteName: getConfig().SITE_NAME },
)}
</h6>
<p>
{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.answer'])}
{props.intl.formatMessage(
messages['id.verification.privacy.do.with.photo.answer'],
{ siteName: getConfig().SITE_NAME },
)}
</p>
<div className="action-row">

View File

@@ -1,26 +1,18 @@
import React, { useState, useEffect, useContext } from 'react';
import React, { useEffect, useContext } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import BasePanel from './BasePanel';
import { IdVerificationContext } from '../IdVerificationContext';
import IdVerificationContext from '../IdVerificationContext';
import messages from '../IdVerification.messages';
function SubmittedPanel(props) {
const { userId } = useContext(IdVerificationContext);
const [returnUrl, setReturnUrl] = useState('dashboard');
const [returnText, setReturnText] = useState('id.verification.return.dashboard');
const panelSlug = 'submitted';
// If the user accessed IDV through a course,
// link back to that course rather than the dashboard
useEffect(() => {
if (sessionStorage.getItem('courseRunKey')) {
setReturnUrl(`courses/${sessionStorage.getItem('courseRunKey')}`);
setReturnText('id.verification.return.course');
}
sendTrackEvent('edx.id_verification.submitted', {
category: 'id_verification',
user_id: userId,
@@ -37,10 +29,10 @@ function SubmittedPanel(props) {
</p>
<a
className="btn btn-primary"
href={`${getConfig().LMS_BASE_URL}/${returnUrl}`}
href={`${getConfig().LMS_BASE_URL}/dashboard`}
data-testid="return-button"
>
{props.intl.formatMessage(messages[returnText])}
{props.intl.formatMessage(messages['id.verification.return.dashboard'])}
</a>
</BasePanel>
);

View File

@@ -1,17 +1,20 @@
import React, { useState, useContext } from 'react';
import { history } from '@edx/frontend-platform';
import { Input, Button, Spinner, Alert } from '@edx/paragon';
import React, { useState, useContext, useEffect } from 'react';
import { getConfig, history } from '@edx/frontend-platform';
import {
Alert, Hyperlink, Input, Button, Spinner,
} from '@edx/paragon';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { submitIdVerification } from '../data/service';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import { IdVerificationContext } from '../IdVerificationContext';
import IdVerificationContext from '../IdVerificationContext';
import ImagePreview from '../ImagePreview';
import messages from '../IdVerification.messages';
import CameraHelpWithUpload from '../CameraHelpWithUpload';
import SupportedMediaTypes from '../SupportedMediaTypes';
function SummaryPanel(props) {
const panelSlug = 'summary';
@@ -19,13 +22,45 @@ function SummaryPanel(props) {
const {
facePhotoFile,
idPhotoFile,
profileDataManager,
nameOnAccount,
idPhotoName,
stopUserMedia,
optimizelyExperimentName,
setReachedSummary,
portraitPhotoMode,
idPhotoMode,
} = useContext(IdVerificationContext);
const nameToBeUsed = idPhotoName || nameOnAccount || '';
const [isSubmitting, setIsSubmitting] = useState(false);
const [submissionError, setSubmissionError] = useState(false);
const [submissionError, setSubmissionError] = useState(null);
useEffect(() => setReachedSummary(true), []);
function renderManagedProfileMessage() {
if (!profileDataManager) {
return null;
}
return (
<p id="profile-manager-warning">
<FormattedMessage
id="id.verification.account.name.summary.alert"
defaultMessage="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."
description="Alert message informing the user their account data is managed by a third party."
values={{
managerTitle: <strong>{profileDataManager}</strong>,
profileDataManager,
support: (
<Hyperlink destination={getConfig().SUPPORT_URL} target="_blank">
{props.intl.formatMessage(messages['id.verification.support'])}
</Hyperlink>
),
}}
/>
</p>
);
}
function SubmitButton() {
async function handleClick() {
@@ -33,9 +68,16 @@ function SummaryPanel(props) {
const verificationData = {
facePhotoFile,
idPhotoFile,
idPhotoName: nameToBeUsed,
courseRunKey: sessionStorage.getItem('courseRunKey'),
};
if (idPhotoName) {
verificationData.idPhotoName = idPhotoName;
}
if (optimizelyExperimentName) {
verificationData.optimizelyExperimentName = optimizelyExperimentName;
verificationData.portraitPhotoMode = portraitPhotoMode;
verificationData.idPhotoMode = idPhotoMode;
}
const result = await submitIdVerification(verificationData);
if (result.success) {
stopUserMedia();
@@ -43,12 +85,11 @@ function SummaryPanel(props) {
} else {
stopUserMedia();
setIsSubmitting(false);
setSubmissionError(true);
setSubmissionError(result);
}
}
return (
<Button
className="btn btn-primary"
title="Confirmation"
disabled={isSubmitting}
onClick={handleClick}
@@ -59,35 +100,69 @@ function SummaryPanel(props) {
);
}
function getError() {
if (submissionError.status === 400) {
if (submissionError.message.includes('face_image')) {
return props.intl.formatMessage(messages['id.verification.submission.alert.error.face']);
}
if (submissionError.message.includes('Photo ID image')) {
return props.intl.formatMessage(messages['id.verification.submission.alert.error.id']);
}
if (submissionError.message.includes('Name')) {
return props.intl.formatMessage(messages['id.verification.submission.alert.error.name']);
}
if (submissionError.message.includes('unsupported format')) {
return (
<>
{props.intl.formatMessage(messages['id.verification.submission.alert.error.unsupported'])}
<SupportedMediaTypes />
</>
);
}
}
return (
<FormattedMessage
id="idv.submission.alert.error"
defaultMessage={`
We encountered a technical error while trying to submit ID verification.
This might be a temporary issue, so please try again in a few minutes.
If the problem persists, please go to {support_link} for help.
`}
values={{
support_link: (
<Alert.Link href="https://support.edx.org/hc/en-us">
{props.intl.formatMessage(
messages['id.verification.review.error'],
{ siteName: getConfig().SITE_NAME },
)}
</Alert.Link>
),
}}
/>
);
}
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.review.title'])}
>
{submissionError &&
<Alert
variant="danger"
data-testid="submission-error"
dismissible
onClose={() => setSubmissionError(false)}
>
<FormattedMessage
id="idv.submission.alert.error"
defaultMessage={`
We encountered a technical error while trying to submit ID verification.
This might be a temporary issue, so please try again in a few minutes.
If the problem persists,
please go to {support_link} for help.
`}
values={{ support_link: <Alert.Link href="https://support.edx.org/hc/en-us">{props.intl.formatMessage(messages['id.verification.review.error'])}</Alert.Link> }}
/>
</Alert>}
{submissionError && (
<Alert
variant="danger"
data-testid="submission-error"
dismissible
onClose={() => setSubmissionError(null)}
>
{getError()}
</Alert>
)}
<p>
{props.intl.formatMessage(messages['id.verification.review.description'])}
</p>
<div className="row mb-4">
<div className="col-6">
<label htmlFor="photo-of-face">
<label htmlFor="photo-of-face" className="font-weight-bold">
{props.intl.formatMessage(messages['id.verification.review.portrait.label'])}
</label>
<ImagePreview
@@ -107,7 +182,7 @@ function SummaryPanel(props) {
</Link>
</div>
<div className="col-6">
<label htmlFor="photo-of-id/edit">
<label htmlFor="photo-of-id/edit" className="font-weight-bold">
{props.intl.formatMessage(messages['id.verification.review.id.label'])}
</label>
<ImagePreview
@@ -127,11 +202,12 @@ function SummaryPanel(props) {
</Link>
</div>
</div>
<CameraHelpWithUpload />
{!optimizelyExperimentName && <CameraHelpWithUpload />}
<div className="form-group">
<label htmlFor="name-to-be-used">
<label htmlFor="name-to-be-used" className="font-weight-bold">
{props.intl.formatMessage(messages['id.verification.account.name.label'])}
</label>
{renderManagedProfileMessage()}
<div className="d-flex">
<Input
id="name-to-be-used"
@@ -139,24 +215,26 @@ function SummaryPanel(props) {
readOnly
value={nameToBeUsed}
onChange={() => {}}
aria-describedby={profileDataManager ? 'profile-manager-warning' : null}
/>
<Link
className="btn btn-link ml-3 px-0"
to={{
{!profileDataManager && (
<Link
className="btn btn-link ml-3 px-0"
to={{
pathname: 'get-name-id',
state: { fromSummary: true },
}}
>
<FormattedMessage
id="id.verification.account.name.edit"
defaultMessage="Edit{sr}"
description="Button to edit account name, with clarifying information for screen readers."
values={{
sr: <span className="sr-only">Account Name</span>,
}}
/>
</Link>
>
<FormattedMessage
id="id.verification.account.name.edit"
defaultMessage="Edit {sr}"
description="Button to edit account name, with clarifying information for screen readers."
values={{
sr: <span className="sr-only">Account Name</span>,
}}
/>
</Link>
)}
</div>
</div>
<SubmitButton />{' '}

View File

@@ -5,28 +5,49 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import Camera from '../Camera';
import { IdVerificationContext } from '../IdVerificationContext';
import IdVerificationContext from '../IdVerificationContext';
import messages from '../IdVerification.messages';
import CameraHelp from '../CameraHelp';
import ImagePreview from '../ImagePreview';
import ImageFileUpload from '../ImageFileUpload';
import CollapsibleImageHelp from '../CollapsibleImageHelp';
import SupportedMediaTypes from '../SupportedMediaTypes';
function TakeIdPhotoPanel(props) {
const panelSlug = 'take-id-photo';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const { setIdPhotoFile, idPhotoFile } = useContext(IdVerificationContext);
const {
setIdPhotoFile, idPhotoFile, optimizelyExperimentName, shouldUseCamera, setIdPhotoMode,
} = useContext(IdVerificationContext);
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.id.photo.title.camera'])}
title={shouldUseCamera ? props.intl.formatMessage(messages['id.verification.id.photo.title.camera']) : props.intl.formatMessage(messages['id.verification.id.photo.title.upload'])}
>
<div>
<p>
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.camera'])}
</p>
<Camera onImageCapture={setIdPhotoFile} isPortrait={false} />
{idPhotoFile && !shouldUseCamera && <ImagePreview src={idPhotoFile} alt={props.intl.formatMessage(messages['id.verification.id.photo.preview.alt'])} />}
{shouldUseCamera ? (
<div>
<p>
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.camera'])}
</p>
<Camera onImageCapture={setIdPhotoFile} setPhotoMode={setIdPhotoMode} isPortrait={false} />
</div>
) : (
<div style={{ marginBottom: '1.25rem' }}>
<p data-testid="upload-text">
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
<SupportedMediaTypes />
</p>
<ImageFileUpload onFileChange={setIdPhotoFile} setPhotoMode={setIdPhotoMode} intl={props.intl} />
</div>
)}
</div>
<CameraHelp />
{shouldUseCamera && !optimizelyExperimentName && <CameraHelp />}
<CollapsibleImageHelp isPortrait={false} />
<div className="action-row" style={{ visibility: idPhotoFile ? 'unset' : 'hidden' }}>
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}

View File

@@ -8,17 +8,18 @@ import ImageFileUpload from '../ImageFileUpload';
import ImagePreview from '../ImagePreview';
import Camera from '../Camera';
import CameraHelp from '../CameraHelp';
import { IdVerificationContext } from '../IdVerificationContext';
import IdVerificationContext from '../IdVerificationContext';
import messages from '../IdVerification.messages';
import CollapsibleImageHelp from '../CollapsibleImageHelp';
import SupportedMediaTypes from '../SupportedMediaTypes';
function TakePortraitPhotoPanel(props) {
const panelSlug = 'take-portrait-photo';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const { setFacePhotoFile, facePhotoFile } = useContext(IdVerificationContext);
const shouldUseCamera = true;
// to reenable upload component:
// const shouldUseCamera = mediaAccess === MEDIA_ACCESS.GRANTED;
const {
setFacePhotoFile, facePhotoFile, shouldUseCamera, optimizelyExperimentName, setPortraitPhotoMode,
} = useContext(IdVerificationContext);
return (
<BasePanel
@@ -33,18 +34,20 @@ function TakePortraitPhotoPanel(props) {
<p>
{props.intl.formatMessage(messages['id.verification.portrait.photo.instructions.camera'])}
</p>
<Camera onImageCapture={setFacePhotoFile} isPortrait />
<Camera onImageCapture={setFacePhotoFile} setPhotoMode={setPortraitPhotoMode} isPortrait />
</div>
) : (
<div>
<p>
<div style={{ marginBottom: '1.25rem' }}>
<p data-testid="upload-text">
{props.intl.formatMessage(messages['id.verification.portrait.photo.instructions.upload'])}
<SupportedMediaTypes />
</p>
<ImageFileUpload onFileChange={setFacePhotoFile} />
<ImageFileUpload onFileChange={setFacePhotoFile} setPhotoMode={setPortraitPhotoMode} intl={props.intl} />
</div>
)}
</div>
{shouldUseCamera && <CameraHelp isPortrait />}
{shouldUseCamera && !optimizelyExperimentName && <CameraHelp isPortrait />}
<CollapsibleImageHelp isPortrait />
<div className="action-row" style={{ visibility: facePhotoFile ? 'unset' : 'hidden' }}>
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
{props.intl.formatMessage(messages['id.verification.next'])}

View File

@@ -0,0 +1,21 @@
import PropTypes from 'prop-types';
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from '../IdVerification.messages';
export function UnsupportedCameraDirectionsPanel(props) {
return (
<>
{props.browserName === 'Chrome' && <span>{props.intl.formatMessage(messages['id.verification.camera.access.failure.unsupported.chrome.explanation'])}</span>}
<span> </span>
<span>{props.intl.formatMessage(messages['id.verification.camera.access.failure.unsupported.instructions'])}</span>
</>
);
}
UnsupportedCameraDirectionsPanel.propTypes = {
intl: intlShape.isRequired,
browserName: PropTypes.string.isRequired,
};
export default injectIntl(UnsupportedCameraDirectionsPanel);

View File

@@ -1,26 +1,80 @@
import { useContext } from 'react';
import { useLocation } from 'react-router-dom';
import { IdVerificationContext } from './IdVerificationContext';
import IdVerificationContext, { MEDIA_ACCESS } from './IdVerificationContext';
const SLUGS = {
REVIEW_REQUIREMENTS: 'review-requirements',
CHOOSE_MODE: 'choose-mode',
REQUEST_CAMERA_ACCESS: 'request-camera-access',
PORTRAIT_PHOTO_CONTEXT: 'portrait-photo-context',
TAKE_PORTRAIT_PHOTO: 'take-portrait-photo',
ID_CONTEXT: 'id-context',
TAKE_ID_PHOTO: 'take-id-photo',
GET_NAME_ID: 'get-name-id',
SUMMARY: 'summary',
SUBMITTED: 'submitted',
};
const panelSteps = [
'review-requirements',
'request-camera-access',
'portrait-photo-context',
'take-portrait-photo',
'id-context',
'take-id-photo',
'get-name-id',
'summary',
'submitted',
SLUGS.REVIEW_REQUIREMENTS,
SLUGS.CHOOSE_MODE,
SLUGS.REQUEST_CAMERA_ACCESS,
SLUGS.PORTRAIT_PHOTO_CONTEXT,
SLUGS.TAKE_PORTRAIT_PHOTO,
SLUGS.ID_CONTEXT,
SLUGS.TAKE_ID_PHOTO,
SLUGS.GET_NAME_ID,
SLUGS.SUMMARY,
SLUGS.SUBMITTED,
];
// eslint-disable-next-line import/prefer-default-export
export const useNextPanelSlug = (originSlug) => {
// Go back to the summary view if that's where they came from
const location = useLocation();
const isFromSummary = location.state && location.state.fromSummary;
if (isFromSummary) {
return 'summary';
const isFromPortrait = location.state && location.state.fromPortraitCapture;
const isFromId = location.state && location.state.fromIdCapture;
const {
mediaAccess,
optimizelyExperimentName,
reachedSummary,
shouldUseCamera,
} = useContext(IdVerificationContext);
const canRerouteToSummary = [
SLUGS.TAKE_PORTRAIT_PHOTO,
SLUGS.TAKE_ID_PHOTO,
SLUGS.GET_NAME_ID,
];
if (reachedSummary && canRerouteToSummary.includes(originSlug)) {
return SLUGS.SUMMARY;
}
// the following are used as part of an A/B experiment
if (isFromPortrait) {
if (mediaAccess === MEDIA_ACCESS.GRANTED) {
return SLUGS.PORTRAIT_PHOTO_CONTEXT;
}
return SLUGS.TAKE_PORTRAIT_PHOTO;
}
if (isFromId) {
if (mediaAccess === MEDIA_ACCESS.GRANTED) {
return SLUGS.ID_CONTEXT;
}
return SLUGS.TAKE_ID_PHOTO;
}
if (originSlug === SLUGS.REVIEW_REQUIREMENTS && !optimizelyExperimentName) {
return SLUGS.REQUEST_CAMERA_ACCESS;
}
if (originSlug === SLUGS.CHOOSE_MODE && !shouldUseCamera) {
return SLUGS.TAKE_PORTRAIT_PHOTO;
}
if (originSlug === SLUGS.TAKE_PORTRAIT_PHOTO && !shouldUseCamera) {
return SLUGS.TAKE_ID_PHOTO;
}
if (originSlug === SLUGS.REQUEST_CAMERA_ACCESS && mediaAccess !== MEDIA_ACCESS.GRANTED) {
return SLUGS.TAKE_PORTRAIT_PHOTO;
}
const nextIndex = panelSteps.indexOf(originSlug) + 1;
@@ -30,16 +84,18 @@ export const useNextPanelSlug = (originSlug) => {
// check if the user is too far into the flow and if so, return the slug of the
// furthest panel they are allow to be.
export const useVerificationRedirectSlug = (slug) => {
const { facePhotoFile, idPhotoFile } = useContext(IdVerificationContext);
const { facePhotoFile, idPhotoFile, optimizelyExperimentName } = useContext(IdVerificationContext);
const indexOfCurrentPanel = panelSteps.indexOf(slug);
if (!optimizelyExperimentName && slug === SLUGS.CHOOSE_MODE) {
return SLUGS.REVIEW_REQUIREMENTS;
}
if (!facePhotoFile) {
if (indexOfCurrentPanel > panelSteps.indexOf('take-portrait-photo')) {
return 'portrait-photo-context';
if (indexOfCurrentPanel > panelSteps.indexOf(SLUGS.TAKE_PORTRAIT_PHOTO)) {
return SLUGS.PORTRAIT_PHOTO_CONTEXT;
}
} else if (!idPhotoFile) {
if (indexOfCurrentPanel > panelSteps.indexOf('take-id-photo')) {
return 'id-context';
if (indexOfCurrentPanel > panelSteps.indexOf(SLUGS.TAKE_ID_PHOTO)) {
return SLUGS.ID_CONTEXT;
}
}

View File

@@ -1,33 +1,37 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, cleanup, act, screen } from '@testing-library/react';
import {
render, cleanup, act, screen,
} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import ExistingRequest from '../ExistingRequest';
const IntlExistingRequest = injectIntl(ExistingRequest);
import { ERROR_REASONS } from '../IdVerificationContext';
import AccessBlocked from '../AccessBlocked';
const IntlAccessBlocked = injectIntl(AccessBlocked);
const history = createMemoryHistory();
describe('ExistingRequest', () => {
describe('AccessBlocked', () => {
const defaultProps = {
intl: {},
status: '',
error: '',
};
afterEach(() => {
cleanup();
});
it('renders correctly when status is pending', async () => {
defaultProps.status = 'pending';
it('renders correctly when there is an existing request', async () => {
defaultProps.error = ERROR_REASONS.EXISTING_REQUEST;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IntlExistingRequest {...defaultProps} />
<IntlAccessBlocked {...defaultProps} />
</IntlProvider>
</Router>
)));
@@ -37,29 +41,29 @@ describe('ExistingRequest', () => {
expect(text).toBeInTheDocument();
});
it('renders correctly when status is approved', async () => {
defaultProps.status = 'approved';
it('renders correctly when learner is not enrolled in a verified course mode', async () => {
defaultProps.error = ERROR_REASONS.COURSE_ENROLLMENT;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IntlExistingRequest {...defaultProps} />
<IntlAccessBlocked {...defaultProps} />
</IntlProvider>
</Router>
)));
const text = screen.getByText(/You have already submitted your verification information./);
const text = screen.getByText(/You are not currently enrolled in a course that requires identity verification./);
expect(text).toBeInTheDocument();
});
it('renders correctly when status is denied', async () => {
defaultProps.status = 'denied';
defaultProps.error = ERROR_REASONS.CANNOT_VERIFY;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IntlExistingRequest {...defaultProps} />
<IntlAccessBlocked {...defaultProps} />
</IntlProvider>
</Router>
)));

View File

@@ -2,11 +2,14 @@ import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import '@testing-library/jest-dom/extend-expect';
import { render, cleanup, screen, act, fireEvent } from '@testing-library/react';
import {
render, cleanup, screen, act, fireEvent,
} from '@testing-library/react';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
// eslint-disable-next-line import/no-unresolved
import * as blazeface from '@tensorflow-models/blazeface';
import * as analytics from '@edx/frontend-platform/analytics';
import { IdVerificationContext } from '../IdVerificationContext';
import IdVerificationContext from '../IdVerificationContext';
import Camera from '../Camera';
jest.mock('jslib-html5-camera-photo');
@@ -25,6 +28,7 @@ describe('SubmittedPanel', () => {
const defaultProps = {
intl: {},
onImageCapture: jest.fn(),
setPhotoMode: jest.fn(),
isPortrait: true,
};
@@ -54,6 +58,7 @@ describe('SubmittedPanel', () => {
expect(button).toHaveTextContent('Take Photo');
fireEvent.click(button);
expect(defaultProps.onImageCapture).toHaveBeenCalled();
expect(defaultProps.setPhotoMode).toHaveBeenCalledWith('camera');
});
it('shows correct help text for portrait photo capture', async () => {

View File

@@ -0,0 +1,153 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import '@testing-library/jest-dom/extend-expect';
import {
render, cleanup, screen, act, fireEvent,
} from '@testing-library/react';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import * as analytics from '@edx/frontend-platform/analytics';
import IdVerificationContext from '../IdVerificationContext';
import CollapsibleImageHelp from '../CollapsibleImageHelp';
jest.mock('jslib-html5-camera-photo');
jest.mock('@tensorflow-models/blazeface');
jest.mock('@edx/frontend-platform/analytics');
analytics.sendTrackEvent = jest.fn();
window.HTMLMediaElement.prototype.play = () => {};
const IntlCollapsible = injectIntl(CollapsibleImageHelp);
const history = createMemoryHistory();
describe('CollapsibleImageHelpPanel', () => {
const defaultProps = {
intl: {},
isPortrait: true,
};
const contextValue = {
shouldUseCamera: true,
setShouldUseCamera: jest.fn(),
optimizelyExperimentName: '',
mediaAccess: 'granted',
};
afterEach(() => {
cleanup();
});
it('does not return if not part of experiment', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCollapsible {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const titleText = screen.queryByText('Upload a Photo Instead');
expect(titleText).not.toBeInTheDocument();
});
it('does not return if media access denied or unsupported', async () => {
let titleText = '';
contextValue.mediaAccess = 'denied';
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCollapsible {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
titleText = screen.queryByText('Upload a Photo Instead');
expect(titleText).not.toBeInTheDocument();
contextValue.mediaAccess = 'unsupported';
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCollapsible {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
titleText = screen.queryByText('Upload a Photo Instead');
expect(titleText).not.toBeInTheDocument();
});
it('shows the correct text if user should switch to upload', async () => {
contextValue.optimizelyExperimentName = 'test';
contextValue.mediaAccess = 'granted';
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCollapsible {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const titleText = screen.getByText('Upload a Photo Instead');
expect(titleText).toBeInTheDocument();
const helpText = screen.getByTestId('help-text');
expect(helpText.textContent).toContain('If you are having trouble using the photo capture above');
const button = screen.getByTestId('toggle-button');
expect(button).toHaveTextContent('Switch to Upload Mode');
});
it('shows the correct text if user should switch to camera', async () => {
contextValue.optimizelyExperimentName = 'test';
contextValue.mediaAccess = 'granted';
contextValue.shouldUseCamera = false;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCollapsible {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const titleText = screen.getByText('Use Your Camera Instead');
expect(titleText).toBeInTheDocument();
const helpText = screen.getByTestId('help-text');
expect(helpText.textContent).toContain('If you are having trouble uploading a photo above');
const button = screen.getByTestId('toggle-button');
expect(button).toHaveTextContent('Switch to Camera Mode');
});
it('shows the correct text if user should switch to camera with pending media access', async () => {
contextValue.optimizelyExperimentName = 'test';
contextValue.mediaAccess = 'pending';
contextValue.shouldUseCamera = false;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlCollapsible {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const titleText = screen.getByText('Use Your Camera Instead');
expect(titleText).toBeInTheDocument();
const helpText = screen.getByTestId('help-text');
expect(helpText.textContent).toContain('If you are having trouble uploading a photo above');
const accessLink = screen.getByTestId('access-link');
fireEvent.click(accessLink);
expect(history.location.pathname).toEqual('/request-camera-access');
});
});

View File

@@ -1,33 +0,0 @@
import React from 'react';
import { render, cleanup, act } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { getExistingIdVerification } from '../data/service';
import { IdVerificationContextProvider } from '../IdVerificationContext';
jest.mock('../data/service', () => ({
getExistingIdVerification: jest.fn(),
}));
describe('IdVerificationContext', () => {
const defaultProps = {
children: <div />,
intl: {},
};
afterEach(() => {
cleanup();
});
it('renders correctly and calls getExistingIdVerification', async () => {
await act(async () => render((
<AppContext.Provider value={{ authenticatedUser: { userId: 3 } }}>
<IntlProvider locale="en">
<IdVerificationContextProvider {...defaultProps} />
</IntlProvider>
</AppContext.Provider>
)));
expect(getExistingIdVerification).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,65 @@
import React from 'react';
import { render, cleanup, act } from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import { getProfileDataManager } from '../../account-settings/data/service';
import { getExistingIdVerification, getEnrollments } from '../data/service';
import IdVerificationContextProvider from '../IdVerificationContextProvider';
jest.mock('../../account-settings/data/service', () => ({
getProfileDataManager: jest.fn(),
}));
jest.mock('../data/service', () => ({
getExistingIdVerification: jest.fn(),
getEnrollments: jest.fn(() => []),
}));
describe('IdVerificationContextProvider', () => {
const defaultProps = {
children: <div />,
intl: {},
};
afterEach(() => {
cleanup();
});
it('renders correctly and calls getExistingIdVerification + getEnrollments', async () => {
const context = { authenticatedUser: { userId: 3, roles: [] } };
await act(async () => render((
<AppContext.Provider value={context}>
<IntlProvider locale="en">
<IdVerificationContextProvider {...defaultProps} />
</IntlProvider>
</AppContext.Provider>
)));
expect(getExistingIdVerification).toHaveBeenCalled();
expect(getEnrollments).toHaveBeenCalled();
});
it('calls getProfileDataManager if the user has any roles', async () => {
const context = {
authenticatedUser: {
userId: 3,
username: 'testname',
roles: ['enterprise_learner'],
},
};
await act(async () => render((
<AppContext.Provider value={context}>
<IntlProvider locale="en">
<IdVerificationContextProvider {...defaultProps} />
</IntlProvider>
</AppContext.Provider>
)));
expect(getProfileDataManager).toHaveBeenCalledWith(
context.authenticatedUser.username,
context.authenticatedUser.roles,
);
});
});

View File

@@ -0,0 +1,81 @@
import React from 'react';
import { Provider } from 'react-redux';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import configureStore from 'redux-mock-store';
import { render, act } from '@testing-library/react';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import IdVerificationPage from '../IdVerificationPage';
import * as selectors from '../data/selectors';
jest.mock('../data/selectors', () => jest.fn().mockImplementation(() => ({ idVerificationSelector: () => ({}) })));
jest.mock('../IdVerificationContextProvider', () => jest.fn(({ children }) => children));
jest.mock('../panels/ReviewRequirementsPanel');
jest.mock('../panels/RequestCameraAccessPanel');
jest.mock('../panels/PortraitPhotoContextPanel');
jest.mock('../panels/TakePortraitPhotoPanel');
jest.mock('../panels/IdContextPanel');
jest.mock('../panels/GetNameIdPanel');
jest.mock('../panels/TakeIdPhotoPanel');
jest.mock('../panels/SummaryPanel');
jest.mock('../panels/SubmittedPanel');
const IntlIdVerificationPage = injectIntl(IdVerificationPage);
const mockStore = configureStore();
const history = createMemoryHistory();
describe('IdVerificationPage', () => {
selectors.mockClear();
jest.spyOn(Storage.prototype, 'setItem');
const store = mockStore();
const props = {
intl: {},
};
it('does not store irrelevant query params', async () => {
history.push('/?test=irrelevant');
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
</Provider>
</IntlProvider>
</Router>
)));
expect(sessionStorage.setItem).toHaveBeenCalledTimes(0);
});
it('does not store empty course_id', async () => {
history.push('/?course_id=');
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
</Provider>
</IntlProvider>
</Router>
)));
expect(sessionStorage.setItem).toHaveBeenCalledTimes(0);
});
it('decodes and stores course_id', async () => {
history.push('/?course_id=course-v1%3AedX%2BDemoX%2BDemo_Course');
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<Provider store={store}>
<IntlIdVerificationPage {...props} />
</Provider>
</IntlProvider>
</Router>
)));
expect(sessionStorage.setItem).toHaveBeenCalledWith(
'courseRunKey',
'course-v1:edX+DemoX+Demo_Course',
);
});
});

View File

@@ -0,0 +1,111 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import {
render, cleanup, act, screen, fireEvent,
} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import IdVerificationContext from '../../IdVerificationContext';
import ChooseModePanel from '../../panels/ChooseModePanel';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackEvent: jest.fn(),
}));
const IntlChooseModePanel = injectIntl(ChooseModePanel);
const history = createMemoryHistory();
describe('ChooseModePanel', () => {
const defaultProps = {
intl: {},
};
const contextValue = {
optimizelyExperimentName: 'test',
shouldUseCamera: false,
reachedSummary: false,
};
afterEach(() => {
cleanup();
});
it('renders correctly', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlChooseModePanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
// check that radio button for upload is selected
const uploadRadioButton = await screen.findByLabelText('Upload photos from my device');
expect(uploadRadioButton).toBeChecked();
// check that if upload is selected, next button goes to correct screen
const nextButton = await screen.findByTestId('next-button');
expect(nextButton.getAttribute('href')).toEqual('/take-portrait-photo');
});
it('renders correctly if user wants to use camera', async () => {
contextValue.shouldUseCamera = true;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlChooseModePanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
// check that radio button for camera is selected
const cameraRadioButton = await screen.findByLabelText('Take pictures using my camera');
expect(cameraRadioButton).toBeChecked();
// check that if upload is selected, next button goes to correct screen
const nextButton = await screen.findByTestId('next-button');
expect(nextButton.getAttribute('href')).toEqual('/request-camera-access');
});
it('reroutes correctly if reachedSummary is true', async () => {
contextValue.shouldUseCamera = true;
contextValue.reachedSummary = true;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlChooseModePanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const nextButton = await screen.findByTestId('next-button');
fireEvent.click(nextButton);
expect(history.location.pathname).toEqual('/request-camera-access');
});
it('redirects if user is not part of experiment', async () => {
contextValue.optimizelyExperimentName = '';
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlChooseModePanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
// check that radio button is not in document
const cameraRadioButton = await screen.queryByLabelText('Take pictures using my camera');
expect(cameraRadioButton).not.toBeInTheDocument();
});
});

View File

@@ -1,10 +1,12 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, cleanup, act, screen, fireEvent } from '@testing-library/react';
import {
render, cleanup, act, screen, fireEvent,
} from '@testing-library/react';
import '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IdVerificationContext } from '../../IdVerificationContext';
import IdVerificationContext from '../../IdVerificationContext';
import GetNameIdPanel from '../../panels/GetNameIdPanel';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -29,11 +31,7 @@ describe('GetNameIdPanel', () => {
idPhotoFile: 'test.jpg',
};
afterEach(() => {
cleanup();
});
it('edits', async () => {
const getPanel = async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
@@ -43,16 +41,29 @@ describe('GetNameIdPanel', () => {
</IntlProvider>
</Router>
)));
};
afterEach(() => {
cleanup();
});
it('edits', async () => {
await getPanel();
const yesButton = await screen.findByTestId('name-matches-yes');
const noButton = await screen.findByTestId('name-matches-no');
const input = await screen.findByTestId('name-input');
const nextButton = await screen.findByTestId('next-button');
expect(input).toHaveProperty('readOnly');
fireEvent.click(noButton);
expect(input).toHaveProperty('readOnly', false);
expect(nextButton.classList.contains('disabled')).toBe(true);
fireEvent.change(input, { target: { value: 'test change' } });
expect(contextValue.setIdPhotoName).toHaveBeenCalled();
fireEvent.click(yesButton);
expect(input).toHaveProperty('readOnly');
expect(contextValue.setIdPhotoName).toHaveBeenCalled();
@@ -60,36 +71,39 @@ describe('GetNameIdPanel', () => {
it('disables radio buttons + next button and enables input if account name is blank', async () => {
contextValue.nameOnAccount = '';
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlGetNameIdPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
await getPanel();
const yesButton = await screen.findByTestId('name-matches-yes');
const noButton = await screen.findByTestId('name-matches-no');
const input = await screen.findByTestId('name-input');
const nextButton = await screen.findByTestId('next-button');
expect(yesButton).toHaveProperty('disabled');
expect(noButton).toHaveProperty('disabled');
expect(input).toHaveProperty('readOnly', false);
expect(nextButton.classList.contains('disabled')).toBe(true);
});
it('blocks the user from changing account name if managed by a third party', async () => {
contextValue.profileDataManager = 'test-org';
await getPanel();
const noButton = await screen.findByTestId('name-matches-no');
const input = await screen.findByTestId('name-input');
const nextButton = await screen.findByTestId('next-button');
fireEvent.click(noButton);
expect(input).toHaveProperty('readOnly');
expect(nextButton.classList.contains('disabled')).toBe(true);
const warning = await screen.getAllByText('test-org');
expect(warning.length).toEqual(1);
});
it('routes to SummaryPanel', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlGetNameIdPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
await getPanel();
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/summary');
});

View File

@@ -1,10 +1,13 @@
import React from 'react';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { render, cleanup, act, screen, fireEvent } from '@testing-library/react';
import {
render, cleanup, act, screen, fireEvent,
} from '@testing-library/react';
import '@testing-library/jest-dom/extend-expect';
import '@edx/frontend-platform/analytics';
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
import { IdVerificationContext } from '../../IdVerificationContext';
import IdVerificationContext from '../../IdVerificationContext';
import IdContextPanel from '../../panels/IdContextPanel';
jest.mock('@edx/frontend-platform/analytics', () => ({
@@ -21,14 +24,16 @@ describe('IdContextPanel', () => {
};
const contextValue = {
optimizelyExperimentName: '',
facePhotoFile: 'test.jpg',
reachedSummary: false,
};
afterEach(() => {
cleanup();
});
it('routes to TakeIdPhotoPanel', async () => {
it('routes to TakeIdPhotoPanel normally', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
@@ -42,4 +47,49 @@ describe('IdContextPanel', () => {
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-id-photo');
});
it('routes to TakeIdPhotoPanel if reachedSummary is true', async () => {
contextValue.reachedSummary = true;
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlIdContextPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const button = await screen.findByTestId('next-button');
fireEvent.click(button);
expect(history.location.pathname).toEqual('/take-id-photo');
});
it('does not show help text for photo upload if not part of experiment', async () => {
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlIdContextPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const title = await screen.queryByText('What if I want to upload a photo instead?');
expect(title).not.toBeInTheDocument();
});
it('shows help text for photo upload if part of experiment', async () => {
contextValue.optimizelyExperimentName = 'test';
await act(async () => render((
<Router history={history}>
<IntlProvider locale="en">
<IdVerificationContext.Provider value={contextValue}>
<IntlIdContextPanel {...defaultProps} />
</IdVerificationContext.Provider>
</IntlProvider>
</Router>
)));
const title = await screen.queryByText('What if I want to upload a photo instead?');
expect(title).toBeInTheDocument();
});
});

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