Compare commits
157 Commits
build/remo
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f1b6cccea | ||
|
|
4046c263b5 | ||
|
|
51b758a18f | ||
|
|
30bd145bdd | ||
|
|
1f15802bc6 | ||
|
|
5ba476a570 | ||
|
|
8c57140640 | ||
|
|
8239209cd7 | ||
|
|
65bb042443 | ||
|
|
4dd7529799 | ||
|
|
e6684f5048 | ||
|
|
e276d6a5c4 | ||
|
|
36f1d1dbfb | ||
|
|
6c324e85fc | ||
|
|
16b5d066ff | ||
|
|
6e45abbe8b | ||
|
|
b3e6335396 | ||
|
|
08a2da5459 | ||
|
|
094361c689 | ||
|
|
b7b33ef597 | ||
|
|
afa808ff5d | ||
|
|
5279f2a9c9 | ||
|
|
4cc00fd7e3 | ||
|
|
8815626411 | ||
|
|
db319b6cdf | ||
|
|
50edcb1c50 | ||
|
|
d6519bc825 | ||
|
|
b54aeb9446 | ||
|
|
bb1bd6e648 | ||
|
|
7df9f92dd8 | ||
|
|
3627915985 | ||
|
|
9fe1a04a0a | ||
|
|
7455821500 | ||
|
|
817980be00 | ||
|
|
7d4e31f69d | ||
|
|
34dde09ccc | ||
|
|
aee4e44f8c | ||
|
|
7381cfd3b6 | ||
|
|
1d0bd3986c | ||
|
|
1c0dc36907 | ||
|
|
ac0ab9daea | ||
|
|
0d45d17cd3 | ||
|
|
a6d265b885 | ||
|
|
3508bc6c34 | ||
|
|
28de621fc7 | ||
|
|
e6df5e77ae | ||
|
|
8bc5c1fae8 | ||
|
|
6e48c9d2d1 | ||
|
|
d3469d648f | ||
|
|
cc65ffc96f | ||
|
|
5640fb95c2 | ||
|
|
7bbb889258 | ||
|
|
53b59231cb | ||
|
|
8fb25fd89b | ||
|
|
15d2bf60f9 | ||
|
|
135826bc52 | ||
|
|
d7251e6aec | ||
|
|
0b0846fb00 | ||
|
|
b1cd1b1995 | ||
|
|
50c468857a | ||
|
|
c940d3463c | ||
|
|
376deba866 | ||
|
|
37d0e6e0fb | ||
|
|
9261711d4a | ||
|
|
3e42d42ad7 | ||
|
|
7de4edc002 | ||
|
|
2936498b02 | ||
|
|
44d26c444b | ||
|
|
b1c1c6502d | ||
|
|
21dda3f25b | ||
|
|
f87b5040a3 | ||
|
|
0dc1df07d4 | ||
|
|
efa682092f | ||
|
|
0d9e6f8b87 | ||
|
|
26d2b50859 | ||
|
|
3eb63cd624 | ||
|
|
ba0774c5c4 | ||
|
|
ac47d0b180 | ||
|
|
02038b8ac9 | ||
|
|
458f9f7e3d | ||
|
|
c7d9c270f9 | ||
|
|
d0ecbbfb8a | ||
|
|
22db0d9202 | ||
|
|
34a142f55f | ||
|
|
6e00915f98 | ||
|
|
4cfa1707de | ||
|
|
a721887886 | ||
|
|
9cdbb93bf3 | ||
|
|
f9e7519e26 | ||
|
|
ff8d5a4d09 | ||
|
|
f14c71c4fb | ||
|
|
43caac8430 | ||
|
|
ee1ecb8ab9 | ||
|
|
020aa84986 | ||
|
|
24459daf6d | ||
|
|
587533703e | ||
|
|
866746d1c6 | ||
|
|
4c618a55c0 | ||
|
|
c9f6cf708e | ||
|
|
5f314ee65f | ||
|
|
7e35b23b36 | ||
|
|
842bd11d89 | ||
|
|
b1e11dfb36 | ||
|
|
aa57b69924 | ||
|
|
e7769b37e9 | ||
|
|
fcc7b26c28 | ||
|
|
16d844528d | ||
|
|
223234f623 | ||
|
|
d8a1c0ca8c | ||
|
|
1da8f630eb | ||
|
|
228eec0afa | ||
|
|
cf62b4b82c | ||
|
|
0184c1fa25 | ||
|
|
8ed103b2ad | ||
|
|
84a9de44a5 | ||
|
|
84df0a0b3e | ||
|
|
a3917ae550 | ||
|
|
bfd6a07a2c | ||
|
|
1df570989b | ||
|
|
b7e433876e | ||
|
|
1669d577f6 | ||
|
|
d1ca7decce | ||
|
|
79a43ae713 | ||
|
|
9d0b315714 | ||
|
|
4b2bc11378 | ||
|
|
fc7ce6b91e | ||
|
|
39a25fe5bc | ||
|
|
307cb1541b | ||
|
|
03e026ce4e | ||
|
|
a29876aff0 | ||
|
|
ab77246015 | ||
|
|
572b05e7f1 | ||
|
|
3e4de47ba6 | ||
|
|
6c6cedd422 | ||
|
|
43694921ca | ||
|
|
06ded1e66e | ||
|
|
aba1bb3382 | ||
|
|
d67b880028 | ||
|
|
29692add53 | ||
|
|
7f53bf32ca | ||
|
|
add22d9756 | ||
|
|
bef9bf76fd | ||
|
|
bf6b2fb8b8 | ||
|
|
a77cd6d91a | ||
|
|
9b3f222191 | ||
|
|
90f2ed8393 | ||
|
|
95c53ad380 | ||
|
|
3023cd3d55 | ||
|
|
50f85674b1 | ||
|
|
9437ee36f3 | ||
|
|
463944012c | ||
|
|
2d297aa7be | ||
|
|
1ab5901d24 | ||
|
|
ba678d92f7 | ||
|
|
884651a702 | ||
|
|
a152c631da | ||
|
|
fbe91ce7e4 |
50
.env
50
.env
@@ -1,25 +1,27 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME=null
|
||||
BASE_URL=null
|
||||
CREDENTIALS_BASE_URL=null
|
||||
CSRF_TOKEN_API_PATH=null
|
||||
ECOMMERCE_BASE_URL=null
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME=null
|
||||
LMS_BASE_URL=null
|
||||
DEMOGRAPHICS_BASE_URL=null
|
||||
LOGIN_URL=null
|
||||
LOGOUT_URL=null
|
||||
MARKETING_SITE_BASE_URL=null
|
||||
NODE_ENV=null
|
||||
ORDER_HISTORY_URL=null
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME=''
|
||||
SUPPORT_URL=null
|
||||
USER_INFO_COOKIE_NAME=null
|
||||
LOGO_URL=''
|
||||
LOGO_TRADEMARK_URL=''
|
||||
LOGO_WHITE_URL=''
|
||||
ACCESS_TOKEN_COOKIE_NAME=''
|
||||
BASE_URL=''
|
||||
COACHING_ENABLED=''
|
||||
CREDENTIALS_BASE_URL=''
|
||||
CSRF_TOKEN_API_PATH=''
|
||||
DEMOGRAPHICS_BASE_URL=''
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL=''
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
FAVICON_URL=''
|
||||
PUBLISHER_BASE_URL=
|
||||
STUDIO_BASE_URL=
|
||||
DISCOVERY_API_BASE_URL=
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME=''
|
||||
LMS_BASE_URL=''
|
||||
LOGIN_URL=''
|
||||
LOGO_TRADEMARK_URL=''
|
||||
LOGO_URL=''
|
||||
LOGO_WHITE_URL=''
|
||||
LOGOUT_URL=''
|
||||
MARKETING_SITE_BASE_URL=''
|
||||
NODE_ENV=''
|
||||
ORDER_HISTORY_URL=''
|
||||
PUBLISHER_BASE_URL=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=''
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=''
|
||||
STUDIO_BASE_URL=''
|
||||
SUPPORT_URL=''
|
||||
USER_INFO_COOKIE_NAME=''
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
COACHING_ENABLED=''
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:5335'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
NODE_ENV='development'
|
||||
ORDER_HISTORY_URL='localhost:1996/orders'
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
PORT=1997
|
||||
PUBLISHER_BASE_URL=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=localhost
|
||||
STUDIO_BASE_URL=''
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
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=
|
||||
|
||||
26
.env.test
26
.env.test
@@ -1,27 +1,27 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
COACHING_ENABLED=''
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
DISCOVERY_API_BASE_URL=''
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:5335'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
NODE_ENV=null
|
||||
ORDER_HISTORY_URL='localhost:1996/orders'
|
||||
NODE_ENV=''
|
||||
ORDER_HISTORY_URL='http://localhost:1996/orders'
|
||||
PUBLISHER_BASE_URL=''
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SEGMENT_KEY=''
|
||||
SITE_NAME=localhost
|
||||
STUDIO_BASE_URL=''
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
COACHING_ENABLED=''
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
PUBLISHER_BASE_URL=
|
||||
STUDIO_BASE_URL=
|
||||
DISCOVERY_API_BASE_URL=
|
||||
|
||||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: validate
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
@@ -12,15 +12,21 @@ jobs:
|
||||
matrix:
|
||||
node-version:
|
||||
- 12
|
||||
npm-test:
|
||||
- i18n_extract
|
||||
- is-es5
|
||||
- lint
|
||||
- test
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v1
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: npm install -g npm@6
|
||||
- run: make requirements
|
||||
- run: make test
|
||||
- run: make test NPM_TESTS=build
|
||||
- run: make test NPM_TESTS=${{ matrix.npm-test }}
|
||||
- name: upload coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
fail_ci_if_error: true
|
||||
fail_ci_if_error: false
|
||||
|
||||
10
.github/workflows/commitlint.yml
vendored
Normal file
10
.github/workflows/commitlint.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
# Run commitlint on the commit messages in a pull request.
|
||||
|
||||
name: Lint Commit Messages
|
||||
|
||||
on:
|
||||
- pull_request
|
||||
|
||||
jobs:
|
||||
commitlint:
|
||||
uses: edx/.github/.github/workflows/commitlint.yml@master
|
||||
2
Makefile
2
Makefile
@@ -10,7 +10,7 @@ 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
|
||||
|
||||
NPM_TESTS=i18n_extract lint test build is-es5
|
||||
NPM_TESTS=build i18n_extract lint test is-es5
|
||||
|
||||
.PHONY: test
|
||||
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|Build Status| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
|
||||
|ci-badge| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
|
||||
|
||||
frontend-app-account
|
||||
====================
|
||||
@@ -102,8 +102,9 @@ In the future, it's possible that demographics could be modeled as a plugin rath
|
||||
|
||||
==============================
|
||||
|
||||
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-account.svg?branch=master
|
||||
:target: https://travis-ci.com/edx/frontend-app-account
|
||||
.. |ci-badge| image:: https://github.com/edx/edx-developer-docs/actions/workflows/ci.yml/badge.svg
|
||||
:target: https://github.com/edx/edx-developer-docs/actions/workflows/ci.yml
|
||||
:alt: Continuous Integration
|
||||
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-account
|
||||
:target: https://codecov.io/gh/edx/frontend-app-account
|
||||
.. |npm_version| image:: https://img.shields.io/npm/v/@edx/frontend-app-account.svg
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFiles: [
|
||||
setupFilesAfterEnv: [
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -3,5 +3,4 @@
|
||||
|
||||
nick: acct
|
||||
oeps: {}
|
||||
owner: edx/arch-team
|
||||
openedx-release: {ref: master}
|
||||
|
||||
16694
package-lock.json
generated
16694
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
69
package.json
69
package.json
@@ -30,26 +30,26 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@1.1.0",
|
||||
"@edx/frontend-component-footer": "10.1.4",
|
||||
"@edx/frontend-component-header": "2.2.4",
|
||||
"@edx/frontend-platform": "1.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.14",
|
||||
"@edx/frontend-component-footer": "10.1.6",
|
||||
"@edx/frontend-component-header": "^2.4.3",
|
||||
"@edx/frontend-platform": "1.12.7",
|
||||
"@edx/paragon": "16.1.0",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "0.1.15",
|
||||
"@tensorflow-models/blazeface": "0.0.7",
|
||||
"@tensorflow/tfjs-converter": "1.6.1",
|
||||
"@tensorflow/tfjs-core": "1.6.1",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"bowser": "2.10.0",
|
||||
"classnames": "2.2.6",
|
||||
"@tensorflow/tfjs-converter": "1.7.4",
|
||||
"@tensorflow/tfjs-core": "1.7.4",
|
||||
"bowser": "2.11.0",
|
||||
"classnames": "2.3.1",
|
||||
"core-js": "3.18.2",
|
||||
"font-awesome": "4.7.0",
|
||||
"form-urlencoded": "4.0.1",
|
||||
"formdata-polyfill": "3.0.20",
|
||||
"formdata-polyfill": "4.0.10",
|
||||
"history": "4.10.1",
|
||||
"jslib-html5-camera-photo": "3.1.6",
|
||||
"jslib-html5-camera-photo": "3.1.8",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.findindex": "4.6.0",
|
||||
"lodash.get": "4.4.2",
|
||||
@@ -58,36 +58,35 @@
|
||||
"lodash.omit": "4.5.0",
|
||||
"lodash.pick": "4.4.0",
|
||||
"lodash.pickby": "4.6.0",
|
||||
"memoize-one": "5.1.1",
|
||||
"newrelic": "5.13.1",
|
||||
"memoize-one": "5.2.1",
|
||||
"prop-types": "15.7.2",
|
||||
"qs": "6.9.6",
|
||||
"react": "16.10.2",
|
||||
"react-dom": "16.10.2",
|
||||
"react-redux": "7.1.3",
|
||||
"react-router": "5.1.2",
|
||||
"react-router-dom": "5.1.2",
|
||||
"qs": "6.10.1",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react-redux": "7.2.5",
|
||||
"react-router": "5.2.1",
|
||||
"react-router-dom": "5.3.0",
|
||||
"react-router-hash-link": "1.2.2",
|
||||
"react-scrollspy": "3.4.3",
|
||||
"react-transition-group": "4.3.0",
|
||||
"redux": "4.0.5",
|
||||
"redux-devtools-extension": "2.13.8",
|
||||
"react-transition-group": "4.4.2",
|
||||
"redux": "4.1.1",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-saga": "1.1.3",
|
||||
"redux-thunk": "2.3.0",
|
||||
"regenerator-runtime": "0.13.9",
|
||||
"reselect": "4.0.0",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "5.6.9",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@testing-library/react": "10.4.9",
|
||||
"codecov": "3.7.2",
|
||||
"enzyme": "3.10.0",
|
||||
"@edx/frontend-build": "^9.0.5",
|
||||
"@testing-library/jest-dom": "5.14.1",
|
||||
"@testing-library/react": "12.1.2",
|
||||
"codecov": "3.8.3",
|
||||
"enzyme": "3.11.0",
|
||||
"enzyme-adapter-react-16": "1.15.6",
|
||||
"es-check": "5.0.0",
|
||||
"husky": "3.0.9",
|
||||
"react-test-renderer": "16.8.6",
|
||||
"es-check": "6.0.0",
|
||||
"react-test-renderer": "16.14.0",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "1.5.4"
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<title>Account | <%= process.env.SITE_NAME %></title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=webpackConfig.output.publicPath%>favicon.ico" type="image/x-icon" />
|
||||
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
|
||||
<% if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script
|
||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
{
|
||||
"extends": [
|
||||
"config:base",
|
||||
"schedule:weekly",
|
||||
":automergeLinters",
|
||||
":automergeTesters",
|
||||
":automergeMinor",
|
||||
":noUnscheduledUpdates",
|
||||
":semanticCommits"
|
||||
":automergeTesters",
|
||||
":enableVulnerabilityAlerts",
|
||||
":rebaseStalePrs",
|
||||
":semanticCommits",
|
||||
":updateNotScheduled"
|
||||
],
|
||||
"rebaseStalePrs": true,
|
||||
"schedule": [
|
||||
"every weekend"
|
||||
"packageRules": [
|
||||
{
|
||||
"matchDepTypes": [
|
||||
"devDependencies"
|
||||
],
|
||||
"matchUpdateTypes": [
|
||||
"lockFileMaintenance",
|
||||
"minor",
|
||||
"patch",
|
||||
"pin"
|
||||
],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"timezone": "America/New_York"
|
||||
}
|
||||
|
||||
@@ -13,20 +13,30 @@ import {
|
||||
getCountryList,
|
||||
getLanguageList,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
import {
|
||||
Button, Hyperlink, Icon, Alert,
|
||||
} from '@edx/paragon';
|
||||
import { CheckCircle, Error, WarningFilled } from '@edx/paragon/icons';
|
||||
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
import { fetchSettings, saveSettings, updateDraft } from './data/actions';
|
||||
import {
|
||||
fetchSettings,
|
||||
saveMultipleSettings,
|
||||
saveSettings,
|
||||
updateDraft,
|
||||
beginNameChange,
|
||||
} from './data/actions';
|
||||
import { accountSettingsPageSelector } from './data/selectors';
|
||||
import PageLoading from './PageLoading';
|
||||
import Alert from './Alert';
|
||||
import JumpNav from './JumpNav';
|
||||
import DeleteAccount from './delete-account';
|
||||
import EditableField from './EditableField';
|
||||
import ResetPassword from './reset-password';
|
||||
import NameChange from './name-change';
|
||||
import ThirdPartyAuth from './third-party-auth';
|
||||
import BetaLanguageBanner from './BetaLanguageBanner';
|
||||
import EmailField from './EmailField';
|
||||
import OneTimeDismissibleAlert from './OneTimeDismissibleAlert';
|
||||
import {
|
||||
YEAR_OF_BIRTH_OPTIONS,
|
||||
EDUCATION_LEVELS,
|
||||
@@ -136,14 +146,57 @@ class AccountSettingsPage extends React.Component {
|
||||
})),
|
||||
}));
|
||||
|
||||
sortDates = (a, b) => {
|
||||
const aTimeSinceEpoch = new Date(a).getTime();
|
||||
const bTimeSinceEpoch = new Date(b).getTime();
|
||||
|
||||
return bTimeSinceEpoch - aTimeSinceEpoch;
|
||||
}
|
||||
|
||||
sortVerifiedNameRecords = verifiedNameHistory => {
|
||||
if (Array.isArray(verifiedNameHistory)) {
|
||||
return [...verifiedNameHistory].sort(this.sortDates);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
};
|
||||
}
|
||||
|
||||
handleSubmit = (formId, values) => {
|
||||
this.props.saveSettings(formId, values);
|
||||
}
|
||||
|
||||
handleSubmitProfileName = (formId, values) => {
|
||||
if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) {
|
||||
this.props.saveMultipleSettings([
|
||||
{
|
||||
formId,
|
||||
commitValues: values,
|
||||
},
|
||||
{
|
||||
formId: 'useVerifiedNameForCerts',
|
||||
commitValues: this.props.formValues.useVerifiedNameForCerts,
|
||||
},
|
||||
], formId);
|
||||
} else {
|
||||
this.props.saveSettings(formId, values);
|
||||
}
|
||||
};
|
||||
|
||||
handleSubmitVerifiedName = (formId, values) => {
|
||||
if (Object.keys(this.props.drafts).includes('useVerifiedNameForCerts')) {
|
||||
this.props.saveSettings('useVerifiedNameForCerts', this.props.formValues.useVerifiedNameForCerts);
|
||||
}
|
||||
if (values !== this.props.committedValues?.verified_name) {
|
||||
this.props.beginNameChange(formId);
|
||||
} else {
|
||||
this.props.saveSettings(formId, values);
|
||||
}
|
||||
}
|
||||
|
||||
isEditable(fieldName) {
|
||||
return !this.props.staticFields.includes(fieldName);
|
||||
}
|
||||
@@ -161,7 +214,7 @@ class AccountSettingsPage extends React.Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Alert className="alert alert-danger" role="alert">
|
||||
<Alert variant="danger">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.duplicate.tpa.provider"
|
||||
defaultMessage="The {provider} account you selected is already linked to another {siteName} account."
|
||||
@@ -183,7 +236,7 @@ class AccountSettingsPage extends React.Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Alert className="alert alert-primary" role="alert">
|
||||
<Alert variant="info">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.managed.settings"
|
||||
defaultMessage="Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help."
|
||||
@@ -206,6 +259,156 @@ class AccountSettingsPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderFullNameHelpText = (status) => {
|
||||
if (
|
||||
!this.props.verifiedNameHistory
|
||||
|| !this.props.verifiedNameEnabled
|
||||
) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text']);
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 'submitted':
|
||||
if (this.props.committedValues.useVerifiedNameForCerts) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.submitted']);
|
||||
}
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.submitted.certificate']);
|
||||
default:
|
||||
if (this.props.committedValues.useVerifiedNameForCerts) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.non.certificate']);
|
||||
}
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text.certificate']);
|
||||
}
|
||||
}
|
||||
|
||||
renderVerifiedNameSuccessMessage = (verifiedName, created) => {
|
||||
const dateValue = new Date(created).valueOf();
|
||||
const id = `dismissedVerifiedNameSuccessMessage-${verifiedName}-${dateValue}`;
|
||||
|
||||
return (
|
||||
<OneTimeDismissibleAlert
|
||||
id={id}
|
||||
variant="success"
|
||||
icon={CheckCircle}
|
||||
header={this.props.intl.formatMessage(messages['account.settings.field.name.verified.success.message.header'])}
|
||||
body={this.props.intl.formatMessage(messages['account.settings.field.name.verified.success.message'])}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderVerifiedNameFailureMessage = (verifiedName, created) => {
|
||||
const dateValue = new Date(created).valueOf();
|
||||
const id = `dismissedVerifiedNameFailureMessage-${verifiedName}-${dateValue}`;
|
||||
|
||||
return (
|
||||
<OneTimeDismissibleAlert
|
||||
id={id}
|
||||
variant="danger"
|
||||
icon={Error}
|
||||
header={this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message.header'])}
|
||||
body={
|
||||
(
|
||||
<>
|
||||
<div className="d-flex flex-row">
|
||||
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message'])}
|
||||
</div>
|
||||
<div className="d-flex flex-row-reverse mt-3">
|
||||
<Button
|
||||
variant="primary"
|
||||
href="https://support.edx.org/hc/en-us/articles/360004381594-Why-was-my-ID-verification-denied"
|
||||
>
|
||||
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.failure.message.help.link'])}
|
||||
</Button>{' '}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderVerifiedNameSubmittedMessage = (willCertNameChange) => (
|
||||
<Alert
|
||||
variant="warning"
|
||||
icon={WarningFilled}
|
||||
>
|
||||
<Alert.Heading>
|
||||
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.submitted.message.header'])}
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
{this.props.intl.formatMessage(messages['account.settings.field.name.verified.submitted.message'])}{' '}
|
||||
{
|
||||
willCertNameChange
|
||||
&& this.props.intl.formatMessage(messages['account.settings.field.name.verified.submitted.message.certificate'])
|
||||
}
|
||||
</p>
|
||||
</Alert>
|
||||
)
|
||||
|
||||
renderVerifiedNameMessage = verifiedNameRecord => {
|
||||
const {
|
||||
created,
|
||||
status,
|
||||
profile_name: profileName,
|
||||
verified_name: verifiedName,
|
||||
} = verifiedNameRecord;
|
||||
let willCertNameChange = false;
|
||||
|
||||
if (
|
||||
(
|
||||
// User submitted a profile name change, and uses their profile name on certificates
|
||||
this.props.committedValues.name !== profileName
|
||||
&& !this.props.committedValues.useVerifiedNameForCerts
|
||||
)
|
||||
|| (
|
||||
// User submitted a verified name change, and uses their verified name on certificates
|
||||
this.props.committedValues.name === profileName
|
||||
&& this.props.committedValues.useVerifiedNameForCerts
|
||||
)
|
||||
) {
|
||||
willCertNameChange = true;
|
||||
}
|
||||
|
||||
switch (status) {
|
||||
case 'approved':
|
||||
return this.renderVerifiedNameSuccessMessage(verifiedName, created);
|
||||
case 'denied':
|
||||
return this.renderVerifiedNameFailureMessage(verifiedName, created);
|
||||
case 'submitted':
|
||||
return this.renderVerifiedNameSubmittedMessage(willCertNameChange);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
renderVerifiedNameIcon = (status) => {
|
||||
switch (status) {
|
||||
case 'approved':
|
||||
return (<Icon src={CheckCircle} className="ml-1" style={{ height: '18px', width: '18px', color: 'green' }} />);
|
||||
case 'submitted':
|
||||
return (<Icon src={WarningFilled} className="ml-1" style={{ height: '18px', width: '18px', color: 'yellow' }} />);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
renderVerifiedNameHelpText = (status) => {
|
||||
switch (status) {
|
||||
case 'approved':
|
||||
if (this.props.committedValues.useVerifiedNameForCerts) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.certificate']);
|
||||
}
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.verified']);
|
||||
case 'submitted':
|
||||
if (this.props.committedValues.useVerifiedNameForCerts) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.submitted.certificate']);
|
||||
}
|
||||
return this.props.intl.formatMessage(messages['account.settings.field.name.verified.help.text.submitted']);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
renderEmptyStaticFieldMessage() {
|
||||
if (this.isManagedProfile()) {
|
||||
return this.props.intl.formatMessage(messages['account.settings.static.field.empty'], {
|
||||
@@ -215,6 +418,13 @@ class AccountSettingsPage extends React.Component {
|
||||
return this.props.intl.formatMessage(messages['account.settings.static.field.empty.no.admin']);
|
||||
}
|
||||
|
||||
renderNameChangeModal() {
|
||||
if (this.props.nameChangeModal && this.props.nameChangeModal.formId) {
|
||||
return <NameChange targetFormId={this.props.nameChangeModal.formId} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderSecondaryEmailField(editableFieldProps) {
|
||||
if (!this.props.formValues.secondary_email_enabled) {
|
||||
return null;
|
||||
@@ -261,6 +471,8 @@ class AccountSettingsPage extends React.Component {
|
||||
// Show State field only if the country is US (could include Canada later)
|
||||
const showState = this.props.formValues.country === COUNTRY_WITH_STATES;
|
||||
|
||||
const { verifiedName, verifiedNameEnabled } = this.props;
|
||||
|
||||
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
|
||||
this.props.timeZoneOptions,
|
||||
this.props.countryTimeZoneOptions,
|
||||
@@ -268,16 +480,22 @@ class AccountSettingsPage extends React.Component {
|
||||
);
|
||||
|
||||
const hasLinkedTPA = findIndex(this.props.tpaProviders, provider => provider.connected) >= 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="account-section" id="basic-information" ref={this.navLinkRefs['#basic-information']}>
|
||||
{
|
||||
verifiedNameEnabled && this.props.mostRecentVerifiedName
|
||||
&& this.renderVerifiedNameMessage(this.props.mostRecentVerifiedName)
|
||||
}
|
||||
|
||||
<h2 className="section-heading">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.account.information'])}
|
||||
</h2>
|
||||
<p>{this.props.intl.formatMessage(messages['account.settings.section.account.information.description'])}</p>
|
||||
{this.renderManagedProfileMessage()}
|
||||
|
||||
{this.renderNameChangeModal()}
|
||||
|
||||
<EditableField
|
||||
name="username"
|
||||
type="text"
|
||||
@@ -293,17 +511,59 @@ class AccountSettingsPage extends React.Component {
|
||||
<EditableField
|
||||
name="name"
|
||||
type="text"
|
||||
value={this.props.formValues.name}
|
||||
value={
|
||||
verifiedNameEnabled
|
||||
&& verifiedName?.status === 'submitted'
|
||||
&& this.props.formValues.pending_name_change
|
||||
? this.props.formValues.pending_name_change
|
||||
: this.props.formValues.name
|
||||
}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
|
||||
emptyLabel={
|
||||
this.isEditable('name')
|
||||
? this.props.intl.formatMessage(messages['account.settings.field.full.name.empty'])
|
||||
: this.renderEmptyStaticFieldMessage()
|
||||
}
|
||||
helpText={this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])}
|
||||
isEditable={this.isEditable('name')}
|
||||
{...editableFieldProps}
|
||||
helpText={
|
||||
verifiedNameEnabled && verifiedName
|
||||
? this.renderFullNameHelpText(verifiedName.status)
|
||||
: this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])
|
||||
}
|
||||
isEditable={
|
||||
verifiedNameEnabled && verifiedName
|
||||
? this.isEditable('verifiedName') && this.isEditable('name')
|
||||
: this.isEditable('name')
|
||||
}
|
||||
isGrayedOut={
|
||||
verifiedNameEnabled && verifiedName && !this.isEditable('verifiedName')
|
||||
}
|
||||
onChange={this.handleEditableFieldChange}
|
||||
onSubmit={this.handleSubmitProfileName}
|
||||
/>
|
||||
{verifiedNameEnabled && verifiedName
|
||||
&& (
|
||||
<EditableField
|
||||
name="verified_name"
|
||||
type="text"
|
||||
value={this.props.formValues.verified_name}
|
||||
label={
|
||||
(
|
||||
<div className="d-flex">
|
||||
{this.props.intl.formatMessage(messages['account.settings.field.name.verified'])}
|
||||
{
|
||||
this.renderVerifiedNameIcon(verifiedName.status)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
helpText={this.renderVerifiedNameHelpText(verifiedName.status)}
|
||||
isEditable={this.isEditable('verifiedName')}
|
||||
isGrayedOut={!this.isEditable('verifiedName')}
|
||||
onChange={this.handleEditableFieldChange}
|
||||
onSubmit={this.handleSubmitVerifiedName}
|
||||
/>
|
||||
)}
|
||||
|
||||
<EmailField
|
||||
name="email"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.email'])}
|
||||
@@ -566,6 +826,7 @@ AccountSettingsPage.propTypes = {
|
||||
level_of_education: PropTypes.string,
|
||||
gender: PropTypes.string,
|
||||
language_proficiencies: PropTypes.string,
|
||||
pending_name_change: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
social_link_linkedin: PropTypes.string,
|
||||
social_link_facebook: PropTypes.string,
|
||||
@@ -578,7 +839,18 @@ AccountSettingsPage.propTypes = {
|
||||
}),
|
||||
state: PropTypes.string,
|
||||
shouldDisplayDemographicsSection: PropTypes.bool,
|
||||
useVerifiedNameForCerts: PropTypes.bool.isRequired,
|
||||
verified_name: PropTypes.string,
|
||||
}).isRequired,
|
||||
committedValues: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
useVerifiedNameForCerts: PropTypes.bool,
|
||||
verified_name: PropTypes.string,
|
||||
}),
|
||||
drafts: PropTypes.shape({}),
|
||||
formErrors: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
}),
|
||||
siteLanguage: PropTypes.shape({
|
||||
previousValue: PropTypes.string,
|
||||
draft: PropTypes.string,
|
||||
@@ -602,15 +874,43 @@ AccountSettingsPage.propTypes = {
|
||||
})),
|
||||
fetchSiteLanguages: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
saveMultipleSettings: PropTypes.func.isRequired,
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.arrayOf(PropTypes.object),
|
||||
beginNameChange: PropTypes.func.isRequired,
|
||||
tpaProviders: PropTypes.arrayOf(PropTypes.shape({
|
||||
connected: PropTypes.bool,
|
||||
})),
|
||||
nameChangeModal: PropTypes.shape({
|
||||
formId: PropTypes.string,
|
||||
}),
|
||||
verifiedNameEnabled: PropTypes.bool,
|
||||
verifiedName: PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
}),
|
||||
mostRecentVerifiedName: PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
}),
|
||||
verifiedNameHistory: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
verified_name: PropTypes.string,
|
||||
status: PropTypes.string,
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
AccountSettingsPage.defaultProps = {
|
||||
loading: false,
|
||||
loaded: false,
|
||||
loadingError: null,
|
||||
committedValues: {
|
||||
useVerifiedNameForCerts: false,
|
||||
verified_name: null,
|
||||
},
|
||||
drafts: {},
|
||||
formErrors: {},
|
||||
siteLanguage: null,
|
||||
siteLanguageOptions: [],
|
||||
timeZoneOptions: [],
|
||||
@@ -620,11 +920,18 @@ AccountSettingsPage.defaultProps = {
|
||||
tpaProviders: [],
|
||||
isActive: true,
|
||||
secondary_email_enabled: false,
|
||||
nameChangeModal: {},
|
||||
verifiedNameEnabled: false,
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: [],
|
||||
};
|
||||
|
||||
export default connect(accountSettingsPageSelector, {
|
||||
fetchSettings,
|
||||
saveSettings,
|
||||
saveMultipleSettings,
|
||||
updateDraft,
|
||||
fetchSiteLanguages,
|
||||
beginNameChange,
|
||||
})(injectIntl(AccountSettingsPage));
|
||||
|
||||
@@ -91,6 +91,96 @@ const messages = defineMessages({
|
||||
defaultMessage: 'The name that is used for ID verification and that appears on your certificates.',
|
||||
description: 'Help text for the account settings name field.',
|
||||
},
|
||||
'account.settings.field.full.name.help.text.non.certificate': {
|
||||
id: 'account.settings.field.full.name.help.text.non.certificate',
|
||||
defaultMessage: 'The name that appears on your public profile.',
|
||||
description: 'Help text for the account settings name field.',
|
||||
},
|
||||
'account.settings.field.full.name.help.text.certificate': {
|
||||
id: 'account.settings.field.full.name.help.text.certificate',
|
||||
defaultMessage: 'This name is selected to appear on your certificates and public-facing records.',
|
||||
description: 'Help text for the account settings name field.',
|
||||
},
|
||||
'account.settings.field.name.verified': {
|
||||
id: 'account.settings.field.name.verified',
|
||||
defaultMessage: 'Verified name',
|
||||
description: 'Label for account settings verified name field.',
|
||||
},
|
||||
'account.settings.field.name.verified.help.text.verified': {
|
||||
id: 'account.settings.field.name.verified.help.text.verified',
|
||||
defaultMessage: 'This name has been verified by government ID.',
|
||||
description: 'Help text for the account settings verified name field when the name is verified.',
|
||||
},
|
||||
'account.settings.field.name.verified.help.text.certificate': {
|
||||
id: 'account.settings.field.name.verified.help.text.certificate',
|
||||
defaultMessage: 'This name has been verified by government ID and selected to appear on your certificates and public-facing records.',
|
||||
description: 'Help text for the account settings verified name field when the name is selected for certificates.',
|
||||
},
|
||||
'account.settings.field.name.verified.help.text.submitted': {
|
||||
id: 'account.settings.field.name.verified.help.text.submitted',
|
||||
defaultMessage: 'Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.',
|
||||
description: 'Help text for the account settings verified name field when a verified name has been submitted.',
|
||||
},
|
||||
'account.settings.field.name.verified.help.text.submitted.certificate': {
|
||||
id: 'account.settings.field.name.verified.help.text.submitted.certificate',
|
||||
defaultMessage: 'When identity verification is successful, this name will appear on your certificates and public-facing records. Verified name cannot be changed at this time.',
|
||||
description: 'Help text for the account settings verified name field when a verified name has been submitted and will appear on certificates.',
|
||||
},
|
||||
'account.settings.field.name.verified.verification.alert': {
|
||||
id: 'account.settings.field.name.verified.verification.help',
|
||||
defaultMessage: 'Enter your name as it appears on your government-issued ID.',
|
||||
description: 'Form label instructing the user to enter the name on their ID.',
|
||||
},
|
||||
'account.settings.field.full.name.help.text.submitted': {
|
||||
id: 'account.settings.field.full.name.help.text.submitted',
|
||||
defaultMessage: 'Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.',
|
||||
description: 'Help text for the account settings full name field when a verified name has been submitted.',
|
||||
},
|
||||
'account.settings.field.full.name.help.text.submitted.certificate': {
|
||||
id: 'account.settings.field.full.name.help.text.submitted.certificate',
|
||||
defaultMessage: 'When identity verification is successful, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.',
|
||||
description: 'Help text for the account settings full name field when a full name has been submitted and will appear on certificates.',
|
||||
},
|
||||
'account.settings.field.name.verified.success.message': {
|
||||
id: 'account.settings.field.name.verified.success.message',
|
||||
defaultMessage: 'Your identity verification request has successfully completed. You now have the option of selecting which name you prefer to appear on your certificates and public-records.',
|
||||
description: 'The body of the success alert indicating that a user\'s name has been verified',
|
||||
},
|
||||
'account.settings.field.name.verified.success.message.header': {
|
||||
id: 'account.settings.field.name.verified.success.message.header',
|
||||
defaultMessage: 'Your name change request is complete!',
|
||||
description: 'The header of the success alert indicating that a user\'s name has been verified',
|
||||
},
|
||||
'account.settings.field.name.verified.failure.message': {
|
||||
id: 'account.settings.field.name.verified.failure.message',
|
||||
defaultMessage: 'Your most recent identity verification attempt did not pass. Related account settings have been restored.',
|
||||
description: 'The body of the failure alert indicating that a user\'s name was not able to be verified',
|
||||
},
|
||||
'account.settings.field.name.verified.failure.message.header': {
|
||||
id: 'account.settings.field.name.verified.failure.message.header',
|
||||
defaultMessage: 'We were not able to verify your identity.',
|
||||
description: 'The header of the failure alert indicating that a user\'s name was not able to be verified',
|
||||
},
|
||||
'account.settings.field.name.verified.failure.message.help.link': {
|
||||
id: 'account.settings.field.name.verified.failure.message.help.link',
|
||||
defaultMessage: 'Learn more about ID verification',
|
||||
description: 'The text of the button displayed when a user\'s name was not able to be verified, intended to direct the user to a help article about ID verification.',
|
||||
},
|
||||
'account.settings.field.name.verified.submitted.message': {
|
||||
id: 'account.settings.field.name.verified.submitted.message',
|
||||
defaultMessage: 'Your identity verification request has been submitted and usually takes between 24 and 48 hours to complete.',
|
||||
description: 'The body of the submitted alert indicating that a user\'s name has been submitted for verification',
|
||||
},
|
||||
'account.settings.field.name.verified.submitted.message.certificate': {
|
||||
id: 'account.settings.field.name.verified.submitted.message.certificate',
|
||||
defaultMessage: 'When your request is approved, your updated name will appear on all associated certificates and public-facing records.',
|
||||
description: 'The body of the submitted alert indicating that a user\'s name will be updated on certificates.',
|
||||
},
|
||||
'account.settings.field.name.verified.submitted.message.header': {
|
||||
id: 'account.settings.field.name.verified.submitted.message.header',
|
||||
defaultMessage: 'Your name change request is almost complete!',
|
||||
description: 'The header of the submitted alert indicating that a user\'s name has been submitted for verification',
|
||||
},
|
||||
'account.settings.field.email': {
|
||||
id: 'account.settings.field.email',
|
||||
defaultMessage: 'Email address (Sign in)',
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
closeForm,
|
||||
} from './data/actions';
|
||||
import { editableFieldSelector } from './data/selectors';
|
||||
import CertificatePreference from './certificate-preference/CertificatePreference';
|
||||
|
||||
function EditableField(props) {
|
||||
const {
|
||||
@@ -37,6 +38,7 @@ function EditableField(props) {
|
||||
onChange,
|
||||
isEditing,
|
||||
isEditable,
|
||||
isGrayedOut,
|
||||
intl,
|
||||
...others
|
||||
} = props;
|
||||
@@ -102,54 +104,57 @@ function EditableField(props) {
|
||||
expression={isEditing ? 'editing' : 'default'}
|
||||
cases={{
|
||||
editing: (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ValidationFormGroup
|
||||
for={id}
|
||||
invalid={error != null}
|
||||
invalidMessage={error}
|
||||
helpText={helpText}
|
||||
>
|
||||
<label className="h6 d-block" htmlFor={id}>{label}</label>
|
||||
<Input
|
||||
data-hj-suppress
|
||||
name={name}
|
||||
id={id}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
{...others}
|
||||
/>
|
||||
<>{others.children}</>
|
||||
</ValidationFormGroup>
|
||||
<p>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
className="mr-2"
|
||||
state={saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// Swallow clicks if the state is pending.
|
||||
// We do this instead of disabling the button to prevent
|
||||
// it from losing focus (disabled elements cannot have focus).
|
||||
// Disabling it would causes upstream issues in focus management.
|
||||
// Swallowing the onSubmit event on the form would be better, but
|
||||
// we would have to add that logic for every field given our
|
||||
// current structure of the application.
|
||||
if (saveState === 'pending') { e.preventDefault(); }
|
||||
}}
|
||||
disabledStates={[]}
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={handleCancel}
|
||||
<>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<ValidationFormGroup
|
||||
for={id}
|
||||
invalid={error != null}
|
||||
invalidMessage={error}
|
||||
helpText={helpText}
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
<label className="h6 d-block" htmlFor={id}>{label}</label>
|
||||
<Input
|
||||
data-hj-suppress
|
||||
name={name}
|
||||
id={id}
|
||||
type={type}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
options={options}
|
||||
{...others}
|
||||
/>
|
||||
<>{others.children}</>
|
||||
</ValidationFormGroup>
|
||||
<p>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
className="mr-2"
|
||||
state={saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
|
||||
}}
|
||||
onClick={(e) => {
|
||||
// Swallow clicks if the state is pending.
|
||||
// We do this instead of disabling the button to prevent
|
||||
// it from losing focus (disabled elements cannot have focus).
|
||||
// Disabling it would causes upstream issues in focus management.
|
||||
// Swallowing the onSubmit event on the form would be better, but
|
||||
// we would have to add that logic for every field given our
|
||||
// current structure of the application.
|
||||
if (saveState === 'pending') { e.preventDefault(); }
|
||||
}}
|
||||
disabledStates={[]}
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={handleCancel}
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
|
||||
</Button>
|
||||
</p>
|
||||
</form>
|
||||
{['name', 'verified_name'].includes(name) && <CertificatePreference fieldName={name} />}
|
||||
</>
|
||||
),
|
||||
default: (
|
||||
<div className="form-group">
|
||||
@@ -161,7 +166,7 @@ function EditableField(props) {
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<p data-hj-suppress>{renderValue(value)}</p>
|
||||
<p data-hj-suppress className={isGrayedOut ? 'grayed-out' : null}>{renderValue(value)}</p>
|
||||
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
|
||||
</div>
|
||||
),
|
||||
@@ -172,7 +177,7 @@ function EditableField(props) {
|
||||
|
||||
EditableField.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.node]),
|
||||
emptyLabel: PropTypes.node,
|
||||
type: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
@@ -196,6 +201,7 @@ EditableField.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
isEditing: PropTypes.bool,
|
||||
isEditable: PropTypes.bool,
|
||||
isGrayedOut: PropTypes.bool,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
@@ -211,6 +217,7 @@ EditableField.defaultProps = {
|
||||
helpText: undefined,
|
||||
isEditing: false,
|
||||
isEditable: true,
|
||||
isGrayedOut: false,
|
||||
userSuppliedValue: undefined,
|
||||
};
|
||||
|
||||
|
||||
43
src/account-settings/OneTimeDismissibleAlert.jsx
Normal file
43
src/account-settings/OneTimeDismissibleAlert.jsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert } from '@edx/paragon';
|
||||
|
||||
export default function OneTimeDismissibleAlert(props) {
|
||||
const [dismissed, setDismissed] = useState(localStorage.getItem(props.id) !== 'true');
|
||||
|
||||
const onClose = () => {
|
||||
localStorage.setItem(props.id, 'true');
|
||||
setDismissed(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert
|
||||
variant={props.variant}
|
||||
dismissible
|
||||
icon={props.icon}
|
||||
onClose={onClose}
|
||||
show={dismissed}
|
||||
>
|
||||
<Alert.Heading>{props.header}</Alert.Heading>
|
||||
<p>
|
||||
{props.body}
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
OneTimeDismissibleAlert.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
variant: PropTypes.string,
|
||||
icon: PropTypes.func,
|
||||
header: PropTypes.string,
|
||||
body: PropTypes.string,
|
||||
};
|
||||
|
||||
OneTimeDismissibleAlert.defaultProps = {
|
||||
variant: 'success',
|
||||
icon: undefined,
|
||||
header: undefined,
|
||||
body: undefined,
|
||||
};
|
||||
@@ -50,4 +50,8 @@
|
||||
line-height: 1.6rem;
|
||||
}
|
||||
}
|
||||
.grayed-out{
|
||||
opacity: 0.6; /* Real browsers */
|
||||
filter: alpha(opacity = 60); /* MSIE */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
ActionRow,
|
||||
Form,
|
||||
ModalDialog,
|
||||
StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import {
|
||||
closeForm,
|
||||
resetDrafts,
|
||||
saveSettings,
|
||||
updateDraft,
|
||||
} from '../data/actions';
|
||||
import { certPreferenceSelector } from '../data/selectors';
|
||||
|
||||
import commonMessages from '../AccountSettingsPage.messages';
|
||||
import messages from './messages';
|
||||
|
||||
function CertificatePreference({
|
||||
intl,
|
||||
fieldName,
|
||||
originalFullName,
|
||||
originalVerifiedName,
|
||||
saveState,
|
||||
useVerifiedNameForCerts,
|
||||
verifiedNameEnabled,
|
||||
}) {
|
||||
if (!verifiedNameEnabled || !originalVerifiedName) {
|
||||
// If the user doesn't have an approved verified name, do not display this component
|
||||
return null;
|
||||
}
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const [checked, setChecked] = useState(false);
|
||||
const [modalIsOpen, setModalIsOpen] = useState(false);
|
||||
const formId = 'useVerifiedNameForCerts';
|
||||
|
||||
function handleCheckboxChange() {
|
||||
if (!checked) {
|
||||
if (fieldName === 'verified_name') {
|
||||
dispatch(updateDraft(formId, true));
|
||||
} else {
|
||||
dispatch(updateDraft(formId, false));
|
||||
}
|
||||
} else {
|
||||
setModalIsOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
setModalIsOpen(false);
|
||||
dispatch(resetDrafts());
|
||||
}
|
||||
|
||||
function handleModalChange(e) {
|
||||
if (e.target.value === 'fullName') {
|
||||
dispatch(updateDraft(formId, false));
|
||||
} else {
|
||||
dispatch(updateDraft(formId, true));
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (saveState === 'pending') {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(saveSettings(formId, useVerifiedNameForCerts));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (fieldName === 'verified_name') {
|
||||
setChecked(useVerifiedNameForCerts);
|
||||
} else {
|
||||
setChecked(!useVerifiedNameForCerts);
|
||||
}
|
||||
}, [useVerifiedNameForCerts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (modalIsOpen && saveState === 'complete') {
|
||||
setModalIsOpen(false);
|
||||
dispatch(closeForm(fieldName));
|
||||
}
|
||||
}, [modalIsOpen, saveState]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Checkbox className="mt-1 mb-4" checked={checked} onChange={handleCheckboxChange}>
|
||||
{intl.formatMessage(messages['account.settings.field.name.checkbox.certificate.select'])}
|
||||
</Form.Checkbox>
|
||||
|
||||
<ModalDialog
|
||||
title={intl.formatMessage(messages['account.settings.field.name.modal.certificate.title'])}
|
||||
isOpen={modalIsOpen}
|
||||
onClose={handleCancel}
|
||||
size="lg"
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
{intl.formatMessage(messages['account.settings.field.name.modal.certificate.title'])}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
|
||||
<ModalDialog.Body className="overflow-hidden">
|
||||
<Form.Group className="mb-4">
|
||||
<Form.Label>
|
||||
{intl.formatMessage(messages['account.settings.field.name.modal.certificate.select'])}
|
||||
</Form.Label>
|
||||
<Form.RadioSet
|
||||
name={formId}
|
||||
value={useVerifiedNameForCerts ? 'verifiedName' : 'fullName'}
|
||||
onChange={handleModalChange}
|
||||
>
|
||||
<Form.Radio value="fullName">
|
||||
{originalFullName}{' '}
|
||||
({intl.formatMessage(messages['account.settings.field.name.modal.certificate.option.full'])})
|
||||
</Form.Radio>
|
||||
<Form.Radio value="verifiedName">
|
||||
{originalVerifiedName}{' '}
|
||||
({intl.formatMessage(messages['account.settings.field.name.modal.certificate.option.verified'])})
|
||||
</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
</Form.Group>
|
||||
</ModalDialog.Body>
|
||||
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="outline-primary" disabled={saveState === 'pending'}>
|
||||
{intl.formatMessage(commonMessages['account.settings.editable.field.action.cancel'])}
|
||||
</ModalDialog.CloseButton>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
state={saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.field.name.modal.certificate.button.choose']),
|
||||
}}
|
||||
disabledStates={[]}
|
||||
/>
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</Form>
|
||||
</ModalDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
CertificatePreference.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
fieldName: PropTypes.string.isRequired,
|
||||
originalFullName: PropTypes.string,
|
||||
originalVerifiedName: PropTypes.string,
|
||||
saveState: PropTypes.string,
|
||||
useVerifiedNameForCerts: PropTypes.bool,
|
||||
verifiedNameEnabled: PropTypes.bool,
|
||||
};
|
||||
|
||||
CertificatePreference.defaultProps = {
|
||||
originalFullName: '',
|
||||
originalVerifiedName: '',
|
||||
saveState: null,
|
||||
useVerifiedNameForCerts: false,
|
||||
verifiedNameEnabled: false,
|
||||
};
|
||||
|
||||
export default connect(certPreferenceSelector)(injectIntl(CertificatePreference));
|
||||
22
src/account-settings/certificate-preference/data/service.js
Normal file
22
src/account-settings/certificate-preference/data/service.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function postVerifiedNameConfig(username, commitValues) {
|
||||
const requestConfig = { headers: { Accept: 'application/json' } };
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name/config`;
|
||||
|
||||
const { useVerifiedNameForCerts } = commitValues;
|
||||
const postValues = {
|
||||
username,
|
||||
use_verified_name_for_certs: useVerifiedNameForCerts,
|
||||
};
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(requestUrl, postValues, requestConfig)
|
||||
.catch(error => handleRequestError(error));
|
||||
|
||||
return data;
|
||||
}
|
||||
36
src/account-settings/certificate-preference/messages.js
Normal file
36
src/account-settings/certificate-preference/messages.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.field.name.checkbox.certificate.select': {
|
||||
id: 'account.settings.field.name.certificate.select',
|
||||
defaultMessage: 'If checked, this name will appear on your certificates and public-facing records.',
|
||||
description: 'Label for checkbox describing that the selected name will appear on the user‘s certificates.',
|
||||
},
|
||||
'account.settings.field.name.modal.certificate.title': {
|
||||
id: 'account.settings.field.name.modal.certificate.title',
|
||||
defaultMessage: 'Choose a preferred name for certificates and public-facing records',
|
||||
description: 'Title instructing the user to choose a preferred name.',
|
||||
},
|
||||
'account.settings.field.name.modal.certificate.select': {
|
||||
id: 'account.settings.field.name.modal.certificate.select',
|
||||
defaultMessage: 'Select a name',
|
||||
description: 'Label instructing the user to select a name.',
|
||||
},
|
||||
'account.settings.field.name.modal.certificate.option.full': {
|
||||
id: 'account.settings.field.name.modal.certificate.option.full',
|
||||
defaultMessage: 'Full Name',
|
||||
description: 'Option representing the user’s full name.',
|
||||
},
|
||||
'account.settings.field.name.modal.certificate.option.verified': {
|
||||
id: 'account.settings.field.name.modal.certificate.option.verified',
|
||||
defaultMessage: 'Verified Name',
|
||||
description: 'Option representing the user’s verified name.',
|
||||
},
|
||||
'account.settings.field.name.modal.certificate.button.choose': {
|
||||
id: 'account.settings.field.name.modal.certificate.button.choose',
|
||||
defaultMessage: 'Choose name',
|
||||
description: 'Button to confirm the user’s name choice.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -0,0 +1,174 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
import CertificatePreference from '../CertificatePreference'; // eslint-disable-line import/first
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ certPreferenceSelector: () => ({}) })));
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
const IntlCertificatePreference = injectIntl(CertificatePreference);
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('NameChange', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
const formId = 'useVerifiedNameForCerts';
|
||||
const updateDraft = 'UPDATE_DRAFT';
|
||||
const labelText = 'If checked, this name will appear on your certificates and public-facing records.';
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
fieldName: 'name',
|
||||
originalFullName: 'Ed X',
|
||||
originalVerifiedName: 'edX Verified',
|
||||
saveState: null,
|
||||
useVerifiedNameForCerts: false,
|
||||
verifiedNameEnabled: true,
|
||||
intl: {},
|
||||
};
|
||||
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('does not render if there is no verified name', () => {
|
||||
props = {
|
||||
...props,
|
||||
originalVerifiedName: '',
|
||||
};
|
||||
|
||||
const wrapper = render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not trigger modal when checking empty checkbox, and updates draft immediately', () => {
|
||||
props = {
|
||||
...props,
|
||||
useVerifiedNameForCerts: true,
|
||||
};
|
||||
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
expect(checkbox.checked).toEqual(false);
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
expect(screen.queryByRole('radiogroup')).toBeNull();
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
payload: { name: formId, value: false },
|
||||
type: updateDraft,
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers modal when attempting to uncheck checkbox', () => {
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
expect(checkbox.checked).toEqual(true);
|
||||
|
||||
fireEvent.click(checkbox);
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
|
||||
screen.getByRole('radiogroup');
|
||||
});
|
||||
|
||||
it('updates draft when changing radio value', () => {
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
const fullNameOption = screen.getByLabelText('Ed X (Full Name)');
|
||||
const verifiedNameOption = screen.getByLabelText('edX Verified (Verified Name)');
|
||||
expect(fullNameOption.checked).toEqual(true);
|
||||
expect(verifiedNameOption.checked).toEqual(false);
|
||||
|
||||
fireEvent.click(verifiedNameOption);
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
payload: { name: formId, value: true },
|
||||
type: updateDraft,
|
||||
});
|
||||
});
|
||||
|
||||
it('clears draft on cancel', () => {
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
const cancelButton = screen.getByText('Cancel');
|
||||
fireEvent.click(cancelButton);
|
||||
|
||||
expect(mockDispatch).toHaveBeenCalledWith({ type: 'RESET_DRAFTS' });
|
||||
expect(screen.queryByRole('radiogroup')).toBeNull();
|
||||
});
|
||||
|
||||
it('submits', () => {
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
fireEvent.click(checkbox);
|
||||
|
||||
const submitButton = screen.getByText('Choose name');
|
||||
fireEvent.click(submitButton);
|
||||
expect(mockDispatch).toHaveBeenCalledWith({
|
||||
payload: { formId, commitValues: false },
|
||||
type: 'ACCOUNT_SETTINGS__SAVE_SETTINGS',
|
||||
});
|
||||
});
|
||||
|
||||
it('checks box for verified name', () => {
|
||||
props = {
|
||||
...props,
|
||||
fieldName: 'verified_name',
|
||||
useVerifiedNameForCerts: true,
|
||||
};
|
||||
|
||||
render(reduxWrapper(<IntlCertificatePreference {...props} />));
|
||||
|
||||
const checkbox = screen.getByLabelText(labelText);
|
||||
expect(checkbox.checked).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NameChange does not render if there is no verified name 1`] = `
|
||||
Object {
|
||||
"asFragment": [Function],
|
||||
"baseElement": <body>
|
||||
<div />
|
||||
</body>,
|
||||
"container": <div />,
|
||||
"debug": [Function],
|
||||
"findAllByAltText": [Function],
|
||||
"findAllByDisplayValue": [Function],
|
||||
"findAllByLabelText": [Function],
|
||||
"findAllByPlaceholderText": [Function],
|
||||
"findAllByRole": [Function],
|
||||
"findAllByTestId": [Function],
|
||||
"findAllByText": [Function],
|
||||
"findAllByTitle": [Function],
|
||||
"findByAltText": [Function],
|
||||
"findByDisplayValue": [Function],
|
||||
"findByLabelText": [Function],
|
||||
"findByPlaceholderText": [Function],
|
||||
"findByRole": [Function],
|
||||
"findByTestId": [Function],
|
||||
"findByText": [Function],
|
||||
"findByTitle": [Function],
|
||||
"getAllByAltText": [Function],
|
||||
"getAllByDisplayValue": [Function],
|
||||
"getAllByLabelText": [Function],
|
||||
"getAllByPlaceholderText": [Function],
|
||||
"getAllByRole": [Function],
|
||||
"getAllByTestId": [Function],
|
||||
"getAllByText": [Function],
|
||||
"getAllByTitle": [Function],
|
||||
"getByAltText": [Function],
|
||||
"getByDisplayValue": [Function],
|
||||
"getByLabelText": [Function],
|
||||
"getByPlaceholderText": [Function],
|
||||
"getByRole": [Function],
|
||||
"getByTestId": [Function],
|
||||
"getByText": [Function],
|
||||
"getByTitle": [Function],
|
||||
"queryAllByAltText": [Function],
|
||||
"queryAllByDisplayValue": [Function],
|
||||
"queryAllByLabelText": [Function],
|
||||
"queryAllByPlaceholderText": [Function],
|
||||
"queryAllByRole": [Function],
|
||||
"queryAllByTestId": [Function],
|
||||
"queryAllByText": [Function],
|
||||
"queryAllByTitle": [Function],
|
||||
"queryByAltText": [Function],
|
||||
"queryByDisplayValue": [Function],
|
||||
"queryByLabelText": [Function],
|
||||
"queryByPlaceholderText": [Function],
|
||||
"queryByRole": [Function],
|
||||
"queryByTestId": [Function],
|
||||
"queryByText": [Function],
|
||||
"queryByTitle": [Function],
|
||||
"rerender": [Function],
|
||||
"unmount": [Function],
|
||||
}
|
||||
`;
|
||||
@@ -9,6 +9,7 @@ export const OPEN_FORM = 'OPEN_FORM';
|
||||
export const CLOSE_FORM = 'CLOSE_FORM';
|
||||
export const UPDATE_DRAFT = 'UPDATE_DRAFT';
|
||||
export const RESET_DRAFTS = 'RESET_DRAFTS';
|
||||
export const BEGIN_NAME_CHANGE = 'BEGIN_NAME_CHANGE';
|
||||
|
||||
// FETCH SETTINGS ACTIONS
|
||||
|
||||
@@ -25,6 +26,7 @@ export const fetchSettingsSuccess = ({
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
verifiedNameHistory,
|
||||
}) => ({
|
||||
type: FETCH_SETTINGS.SUCCESS,
|
||||
payload: {
|
||||
@@ -32,6 +34,7 @@ export const fetchSettingsSuccess = ({
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
verifiedNameHistory,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -68,6 +71,10 @@ export const resetDrafts = () => ({
|
||||
type: RESET_DRAFTS,
|
||||
});
|
||||
|
||||
export const beginNameChange = (formId) => ({
|
||||
type: BEGIN_NAME_CHANGE,
|
||||
payload: { formId },
|
||||
});
|
||||
// SAVE SETTINGS ACTIONS
|
||||
|
||||
export const saveSettings = (formId, commitValues) => ({
|
||||
|
||||
@@ -8,11 +8,13 @@ import {
|
||||
UPDATE_DRAFT,
|
||||
RESET_DRAFTS,
|
||||
SAVE_MULTIPLE_SETTINGS,
|
||||
BEGIN_NAME_CHANGE,
|
||||
} from './actions';
|
||||
|
||||
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from '../delete-account';
|
||||
import { reducer as siteLanguageReducer, FETCH_SITE_LANGUAGES } from '../site-language';
|
||||
import { reducer as resetPasswordReducer, RESET_PASSWORD } from '../reset-password';
|
||||
import { reducer as nameChangeReducer, REQUEST_NAME_CHANGE } from '../name-change';
|
||||
import { reducer as thirdPartyAuthReducer, DISCONNECT_AUTH } from '../third-party-auth';
|
||||
|
||||
export const defaultState = {
|
||||
@@ -31,7 +33,13 @@ export const defaultState = {
|
||||
deleteAccount: deleteAccountReducer(),
|
||||
siteLanguage: siteLanguageReducer(),
|
||||
resetPassword: resetPasswordReducer(),
|
||||
nameChange: nameChangeReducer(),
|
||||
thirdPartyAuth: thirdPartyAuthReducer(),
|
||||
nameChangeModal: false,
|
||||
verifiedName: null,
|
||||
mostRecentVerifiedName: {},
|
||||
verifiedNameHistory: {},
|
||||
verifiedNameEnabled: false,
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action) => {
|
||||
@@ -56,6 +64,7 @@ const reducer = (state = defaultState, action) => {
|
||||
loading: false,
|
||||
loaded: true,
|
||||
loadingError: null,
|
||||
verifiedNameHistory: action.payload.verifiedNameHistory,
|
||||
};
|
||||
case FETCH_SETTINGS.FAILURE:
|
||||
return {
|
||||
@@ -89,6 +98,7 @@ const reducer = (state = defaultState, action) => {
|
||||
saveState: null,
|
||||
errors: {},
|
||||
drafts: {},
|
||||
nameChangeModal: false,
|
||||
};
|
||||
}
|
||||
return state;
|
||||
@@ -106,6 +116,15 @@ const reducer = (state = defaultState, action) => {
|
||||
drafts: {},
|
||||
};
|
||||
|
||||
case BEGIN_NAME_CHANGE:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'error',
|
||||
nameChangeModal: {
|
||||
formId: action.payload.formId,
|
||||
},
|
||||
};
|
||||
|
||||
case SAVE_SETTINGS.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
@@ -119,7 +138,6 @@ const reducer = (state = defaultState, action) => {
|
||||
values: { ...state.values, ...action.payload.values },
|
||||
errors: {},
|
||||
confirmationValues: {
|
||||
|
||||
...state.confirmationValues,
|
||||
...action.payload.confirmationValues,
|
||||
},
|
||||
@@ -198,6 +216,15 @@ const reducer = (state = defaultState, action) => {
|
||||
resetPassword: resetPasswordReducer(state.resetPassword, action),
|
||||
};
|
||||
|
||||
case REQUEST_NAME_CHANGE.BEGIN:
|
||||
case REQUEST_NAME_CHANGE.SUCCESS:
|
||||
case REQUEST_NAME_CHANGE.FAILURE:
|
||||
case REQUEST_NAME_CHANGE.RESET:
|
||||
return {
|
||||
...state,
|
||||
nameChange: nameChangeReducer(state.nameChange, action),
|
||||
};
|
||||
|
||||
case DISCONNECT_AUTH.BEGIN:
|
||||
case DISCONNECT_AUTH.SUCCESS:
|
||||
case DISCONNECT_AUTH.FAILURE:
|
||||
|
||||
@@ -25,11 +25,13 @@ import {
|
||||
saveMultipleSettingsBegin,
|
||||
saveMultipleSettingsSuccess,
|
||||
saveMultipleSettingsFailure,
|
||||
beginNameChange,
|
||||
} from './actions';
|
||||
|
||||
// Sub-modules
|
||||
import { saga as deleteAccountSaga } from '../delete-account';
|
||||
import { saga as resetPasswordSaga } from '../reset-password';
|
||||
import { saga as nameChangeSaga } from '../name-change';
|
||||
import {
|
||||
saga as siteLanguageSaga,
|
||||
patchPreferences,
|
||||
@@ -38,7 +40,12 @@ import {
|
||||
import { saga as thirdPartyAuthSaga } from '../third-party-auth';
|
||||
|
||||
// Services
|
||||
import { getSettings, patchSettings, getTimeZones } from './service';
|
||||
import {
|
||||
getSettings,
|
||||
patchSettings,
|
||||
getTimeZones,
|
||||
getVerifiedNameHistory,
|
||||
} from './service';
|
||||
|
||||
export function* handleFetchSettings() {
|
||||
try {
|
||||
@@ -54,6 +61,8 @@ export function* handleFetchSettings() {
|
||||
userId,
|
||||
);
|
||||
|
||||
const verifiedNameHistory = yield call(getVerifiedNameHistory);
|
||||
|
||||
if (values.country) { yield put(fetchTimeZones(values.country)); }
|
||||
|
||||
yield put(fetchSettingsSuccess({
|
||||
@@ -61,6 +70,7 @@ export function* handleFetchSettings() {
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
verifiedNameHistory,
|
||||
}));
|
||||
} catch (e) {
|
||||
yield put(fetchSettingsFailure(e.message));
|
||||
@@ -98,6 +108,9 @@ export function* handleSaveSettings(action) {
|
||||
yield put(closeForm(action.payload.formId));
|
||||
} catch (e) {
|
||||
if (e.fieldErrors) {
|
||||
if (e.fieldErrors.name?.includes('verification')) {
|
||||
yield put(beginNameChange('name'));
|
||||
}
|
||||
yield put(saveSettingsFailure({ fieldErrors: e.fieldErrors }));
|
||||
} else {
|
||||
yield put(saveSettingsFailure(e.message));
|
||||
@@ -126,6 +139,9 @@ export function* handleSaveMultipleSettings(action) {
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.fieldErrors) {
|
||||
if (e.fieldErrors.name?.includes('verification')) {
|
||||
yield put(beginNameChange('name'));
|
||||
}
|
||||
yield put(saveMultipleSettingsFailure({ fieldErrors: e.fieldErrors }));
|
||||
} else {
|
||||
yield put(saveMultipleSettingsFailure(e.message));
|
||||
@@ -148,6 +164,7 @@ export default function* saga() {
|
||||
deleteAccountSaga(),
|
||||
siteLanguageSaga(),
|
||||
resetPasswordSaga(),
|
||||
nameChangeSaga(),
|
||||
thirdPartyAuthSaga(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSelector, createStructuredSelector } from 'reselect';
|
||||
import { siteLanguageListSelector, siteLanguageOptionsSelector } from '../site-language';
|
||||
import { compareVerifiedNamesByCreatedDate } from '../../utils';
|
||||
|
||||
export const storeName = 'accountSettings';
|
||||
|
||||
@@ -7,9 +8,75 @@ export const accountSettingsSelector = state => ({ ...state[storeName] });
|
||||
|
||||
const editableFieldNameSelector = (state, props) => props.name;
|
||||
|
||||
const verifiedNameSettingsSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => ({
|
||||
history: accountSettings.verifiedNameHistory.results,
|
||||
verifiedNameEnabled: accountSettings?.verifiedNameHistory.verified_name_enabled,
|
||||
useVerifiedNameForCerts: accountSettings?.verifiedNameHistory.use_verified_name_for_certs,
|
||||
}),
|
||||
);
|
||||
|
||||
const sortedVerifiedNameHistorySelector = createSelector(
|
||||
verifiedNameSettingsSelector,
|
||||
verifiedNameSettings => {
|
||||
const { history } = verifiedNameSettings;
|
||||
|
||||
if (Array.isArray(history)) {
|
||||
return history.sort(compareVerifiedNamesByCreatedDate);
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
);
|
||||
|
||||
const mostRecentVerifiedNameSelector = createSelector(
|
||||
sortedVerifiedNameHistorySelector,
|
||||
sortedHistory => (sortedHistory.length > 0 ? sortedHistory[0] : null),
|
||||
);
|
||||
|
||||
const mostRecentApprovedVerifiedNameValueSelector = createSelector(
|
||||
sortedVerifiedNameHistorySelector,
|
||||
mostRecentVerifiedNameSelector,
|
||||
(sortedHistory, mostRecentVerifiedName) => {
|
||||
const approvedVerifiedNames = sortedHistory.filter(name => name.status === 'approved');
|
||||
const approvedVerifiedName = approvedVerifiedNames.length > 0 ? approvedVerifiedNames[0] : null;
|
||||
|
||||
let verifiedName = null;
|
||||
switch (mostRecentVerifiedName && mostRecentVerifiedName.status) {
|
||||
case 'approved':
|
||||
case 'denied':
|
||||
case 'pending':
|
||||
verifiedName = approvedVerifiedName;
|
||||
break;
|
||||
case 'submitted':
|
||||
verifiedName = mostRecentVerifiedName;
|
||||
break;
|
||||
default:
|
||||
verifiedName = null;
|
||||
}
|
||||
return verifiedName;
|
||||
},
|
||||
);
|
||||
|
||||
const valuesSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.values,
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
(accountSettings, mostRecentApprovedVerifiedNameValue) => {
|
||||
let useVerifiedNameForCerts = (
|
||||
accountSettings.verifiedNameHistory?.use_verified_name_for_certs || false
|
||||
);
|
||||
|
||||
if (Object.keys(accountSettings.confirmationValues).includes('useVerifiedNameForCerts')) {
|
||||
useVerifiedNameForCerts = accountSettings.confirmationValues.useVerifiedNameForCerts;
|
||||
}
|
||||
|
||||
return {
|
||||
...accountSettings.values,
|
||||
verified_name: mostRecentApprovedVerifiedNameValue?.verified_name,
|
||||
useVerifiedNameForCerts,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const draftsSelector = createSelector(
|
||||
@@ -50,6 +117,11 @@ const errorSelector = createSelector(
|
||||
accountSettings => accountSettings.errors,
|
||||
);
|
||||
|
||||
const nameChangeModalSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.nameChangeModal,
|
||||
);
|
||||
|
||||
const saveStateSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => accountSettings.saveState,
|
||||
@@ -69,7 +141,18 @@ export const profileDataManagerSelector = createSelector(
|
||||
|
||||
export const staticFieldsSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
accountSettings => (accountSettings.profileDataManager ? ['name', 'email', 'country'] : []),
|
||||
mostRecentVerifiedNameSelector,
|
||||
(accountSettings, verifiedName) => {
|
||||
const staticFields = [];
|
||||
if (accountSettings.profileDataManager) {
|
||||
staticFields.push('name', 'email', 'country');
|
||||
}
|
||||
if (verifiedName && ['submitted'].includes(verifiedName.status)) {
|
||||
staticFields.push('verifiedName');
|
||||
}
|
||||
|
||||
return staticFields;
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -85,7 +168,11 @@ const formValuesSelector = createSelector(
|
||||
(values, drafts) => {
|
||||
const formValues = {};
|
||||
Object.entries(values).forEach(([name, value]) => {
|
||||
formValues[name] = chooseFormValue(drafts[name], value) || '';
|
||||
if (typeof value === 'boolean') {
|
||||
formValues[name] = chooseFormValue(drafts[name], value);
|
||||
} else {
|
||||
formValues[name] = chooseFormValue(drafts[name], value) || '';
|
||||
}
|
||||
});
|
||||
return formValues;
|
||||
},
|
||||
@@ -130,21 +217,37 @@ export const accountSettingsPageSelector = createSelector(
|
||||
siteLanguageOptionsSelector,
|
||||
siteLanguageSelector,
|
||||
formValuesSelector,
|
||||
valuesSelector,
|
||||
draftsSelector,
|
||||
errorSelector,
|
||||
profileDataManagerSelector,
|
||||
staticFieldsSelector,
|
||||
timeZonesSelector,
|
||||
countryTimeZonesSelector,
|
||||
activeAccountSelector,
|
||||
nameChangeModalSelector,
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
mostRecentVerifiedNameSelector,
|
||||
sortedVerifiedNameHistorySelector,
|
||||
verifiedNameSettingsSelector,
|
||||
(
|
||||
accountSettings,
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
formValues,
|
||||
committedValues,
|
||||
drafts,
|
||||
formErrors,
|
||||
profileDataManager,
|
||||
staticFields,
|
||||
timeZoneOptions,
|
||||
countryTimeZoneOptions,
|
||||
activeAccount,
|
||||
nameChangeModal,
|
||||
verifiedName,
|
||||
mostRecentVerifiedName,
|
||||
verifiedNameHistory,
|
||||
verifiedNameSettings,
|
||||
) => ({
|
||||
siteLanguageOptions,
|
||||
siteLanguage,
|
||||
@@ -155,9 +258,41 @@ export const accountSettingsPageSelector = createSelector(
|
||||
countryTimeZoneOptions,
|
||||
isActive: activeAccount,
|
||||
formValues,
|
||||
committedValues,
|
||||
drafts,
|
||||
formErrors,
|
||||
profileDataManager,
|
||||
staticFields,
|
||||
tpaProviders: accountSettings.thirdPartyAuth.providers,
|
||||
nameChangeModal,
|
||||
verifiedName,
|
||||
mostRecentVerifiedName,
|
||||
verifiedNameHistory,
|
||||
verifiedNameEnabled: verifiedNameSettings?.verifiedNameEnabled,
|
||||
}),
|
||||
);
|
||||
|
||||
export const certPreferenceSelector = createSelector(
|
||||
verifiedNameSettingsSelector,
|
||||
valuesSelector,
|
||||
formValuesSelector,
|
||||
mostRecentApprovedVerifiedNameValueSelector,
|
||||
saveStateSelector,
|
||||
errorSelector,
|
||||
(
|
||||
verifiedNameSettings,
|
||||
committedValues,
|
||||
formValues,
|
||||
mostRecentApprovedVerifiedNameValue,
|
||||
saveState,
|
||||
errors,
|
||||
) => ({
|
||||
originalFullName: committedValues?.name || '',
|
||||
originalVerifiedName: mostRecentApprovedVerifiedNameValue?.verified_name || '',
|
||||
useVerifiedNameForCerts: formValues.useVerifiedNameForCerts || false,
|
||||
saveState,
|
||||
verifiedNameEnabled: verifiedNameSettings.verifiedNameEnabled || false,
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -204,3 +339,12 @@ export const demographicsSectionSelector = createSelector(
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
export const nameChangeSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
formValuesSelector,
|
||||
(accountSettings, formValues) => ({
|
||||
...accountSettings.nameChange,
|
||||
formValues,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -7,6 +7,7 @@ import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { handleRequestError, unpackFieldErrors } from './utils';
|
||||
import { getThirdPartyAuthProviders } from '../third-party-auth';
|
||||
import { postVerifiedNameConfig } from '../certificate-preference/data/service';
|
||||
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
|
||||
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
|
||||
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
|
||||
@@ -176,12 +177,70 @@ export async function shouldDisplayDemographicsQuestions() {
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function getVerifiedNameEnabled() {
|
||||
let data;
|
||||
const client = getAuthenticatedHttpClient();
|
||||
try {
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name_enabled`;
|
||||
({ data } = await client.get(requestUrl));
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getVerifiedName() {
|
||||
let data;
|
||||
const client = getAuthenticatedHttpClient();
|
||||
try {
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name`;
|
||||
({ data } = await client.get(requestUrl));
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getVerifiedNameHistory() {
|
||||
let data;
|
||||
const client = getAuthenticatedHttpClient();
|
||||
try {
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name/history`;
|
||||
({ data } = await client.get(requestUrl));
|
||||
} catch (error) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function postVerifiedName(data) {
|
||||
const requestConfig = { headers: { Accept: 'application/json' } };
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/edx_name_affirmation/v1/verified_name`;
|
||||
|
||||
await getAuthenticatedHttpClient()
|
||||
.post(requestUrl, data, requestConfig)
|
||||
.catch(error => handleRequestError(error));
|
||||
}
|
||||
|
||||
/**
|
||||
* A single function to GET everything considered a setting.
|
||||
* Currently encapsulates Account, Preferences, Coaching, ThirdPartyAuth, and Demographics
|
||||
*/
|
||||
export async function getSettings(username, userRoles, userId) {
|
||||
const results = await Promise.all([
|
||||
const [
|
||||
account,
|
||||
preferences,
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
coaching,
|
||||
shouldDisplayDemographicsQuestionsResponse,
|
||||
demographics,
|
||||
demographicsOptions,
|
||||
] = await Promise.all([
|
||||
getAccount(username),
|
||||
getPreferences(username),
|
||||
getThirdPartyAuthProviders(),
|
||||
@@ -194,15 +253,15 @@ export async function getSettings(username, userRoles, userId) {
|
||||
]);
|
||||
|
||||
return {
|
||||
...results[0],
|
||||
...results[1],
|
||||
thirdPartyAuthProviders: results[2],
|
||||
profileDataManager: results[3],
|
||||
timeZones: results[4],
|
||||
coaching: results[5],
|
||||
shouldDisplayDemographicsSection: results[6],
|
||||
...results[7], // demographics
|
||||
demographicsOptions: results[8],
|
||||
...account,
|
||||
...preferences,
|
||||
thirdPartyAuthProviders,
|
||||
profileDataManager,
|
||||
timeZones,
|
||||
coaching,
|
||||
shouldDisplayDemographicsSection: shouldDisplayDemographicsQuestionsResponse,
|
||||
...demographics,
|
||||
demographicsOptions,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -217,11 +276,19 @@ export async function patchSettings(username, commitValues, userId) {
|
||||
const preferenceKeys = ['time_zone'];
|
||||
const coachingKeys = ['coaching'];
|
||||
const demographicsKeys = DEMOGRAPHICS_FIELDS;
|
||||
const certificateKeys = ['useVerifiedNameForCerts'];
|
||||
const isDemographicsKey = (value, key) => key.includes('demographics');
|
||||
const accountCommitValues = omit(commitValues, preferenceKeys, coachingKeys, demographicsKeys);
|
||||
const accountCommitValues = omit(
|
||||
commitValues,
|
||||
preferenceKeys,
|
||||
coachingKeys,
|
||||
demographicsKeys,
|
||||
certificateKeys,
|
||||
);
|
||||
const preferenceCommitValues = pick(commitValues, preferenceKeys);
|
||||
const coachingCommitValues = pick(commitValues, coachingKeys);
|
||||
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
|
||||
const certCommitValues = pick(commitValues, certificateKeys);
|
||||
const patchRequests = [];
|
||||
|
||||
if (!isEmpty(accountCommitValues)) {
|
||||
@@ -236,6 +303,9 @@ export async function patchSettings(username, commitValues, userId) {
|
||||
if (!isEmpty(demographicsCommitValues)) {
|
||||
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
|
||||
}
|
||||
if (!isEmpty(certCommitValues)) {
|
||||
patchRequests.push(postVerifiedNameConfig(username, certCommitValues));
|
||||
}
|
||||
|
||||
const results = await Promise.all(patchRequests);
|
||||
// Assigns in order of requests. Preference keys
|
||||
|
||||
@@ -28,6 +28,7 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
@@ -76,6 +77,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
@@ -119,6 +121,7 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
<span>
|
||||
Before proceeding, please
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/articles/115000940568-How-do-I-activate-my-account-"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
@@ -160,6 +163,7 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
@@ -203,6 +207,7 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
<span>
|
||||
Before proceeding, please
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="https://support.edx.org/hc/en-us/articles/207206067"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
|
||||
@@ -12,20 +12,43 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
@@ -85,6 +108,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -148,6 +172,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
@@ -218,6 +243,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -281,6 +307,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -344,6 +371,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -407,6 +435,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -470,6 +499,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -533,6 +563,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -596,6 +627,7 @@ exports[`DemographicsSection should render 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -622,20 +654,43 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
@@ -709,6 +764,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -772,6 +828,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
@@ -842,6 +899,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -905,6 +963,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -968,6 +1027,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1031,6 +1091,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1094,6 +1155,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1157,6 +1219,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1220,6 +1283,7 @@ exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1246,20 +1310,43 @@ exports[`DemographicsSection should render an Alert when demographicsOptions pro
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
@@ -1292,20 +1379,43 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
@@ -1365,6 +1475,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1428,6 +1539,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Hispanic, Latin, or Spanish origin, White
|
||||
@@ -1491,6 +1603,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1554,6 +1667,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1617,6 +1731,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1680,6 +1795,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1743,6 +1859,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1806,6 +1923,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1869,6 +1987,7 @@ exports[`DemographicsSection should render ethnicity correctly when multiple opt
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -1895,20 +2014,43 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
@@ -1968,6 +2110,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2031,6 +2174,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Asian
|
||||
@@ -2094,6 +2238,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2157,6 +2302,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2220,6 +2366,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2283,6 +2430,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2346,6 +2494,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2409,6 +2558,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2472,6 +2622,7 @@ exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2498,20 +2649,43 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
@@ -2571,6 +2745,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2634,6 +2809,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
@@ -2704,6 +2880,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2767,6 +2944,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2830,6 +3008,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2893,6 +3072,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -2956,6 +3136,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Other: test
|
||||
@@ -3019,6 +3200,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3082,6 +3264,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3108,20 +3291,43 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
className="default-link standalone-link"
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
<span
|
||||
className="d-inline-block align-text-top"
|
||||
>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
className="pgn__icon"
|
||||
style={
|
||||
Object {
|
||||
"height": "1em",
|
||||
"width": "1em",
|
||||
}
|
||||
}
|
||||
>
|
||||
<svg
|
||||
aria-hidden={true}
|
||||
aria-label=""
|
||||
fill="none"
|
||||
focusable={false}
|
||||
height={24}
|
||||
role="img"
|
||||
viewBox="0 0 24 24"
|
||||
width={24}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
@@ -3181,6 +3387,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer to self describe: test
|
||||
@@ -3244,6 +3451,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
@@ -3314,6 +3522,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3377,6 +3586,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3440,6 +3650,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3503,6 +3714,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3566,6 +3778,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3629,6 +3842,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
@@ -3692,6 +3906,7 @@ exports[`DemographicsSection should set user input correctly when user provides
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className={null}
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
|
||||
203
src/account-settings/name-change/NameChange.jsx
Normal file
203
src/account-settings/name-change/NameChange.jsx
Normal file
@@ -0,0 +1,203 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { connect, useDispatch } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
Alert,
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
ModalDialog,
|
||||
StatefulButton,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { closeForm, saveSettingsReset } from '../data/actions';
|
||||
import { nameChangeSelector } from '../data/selectors';
|
||||
|
||||
import { requestNameChange, requestNameChangeFailure, requestNameChangeReset } from './data/actions';
|
||||
import messages from './messages';
|
||||
|
||||
function NameChangeModal({
|
||||
targetFormId,
|
||||
errors,
|
||||
formValues,
|
||||
intl,
|
||||
saveState,
|
||||
}) {
|
||||
const dispatch = useDispatch();
|
||||
const { push } = useHistory();
|
||||
const { username } = getAuthenticatedUser();
|
||||
const [verifiedNameInput, setVerifiedNameInput] = useState(formValues.verified_name || '');
|
||||
const [confirmedWarning, setConfirmedWarning] = useState(false);
|
||||
|
||||
function resetLocalState() {
|
||||
setConfirmedWarning(false);
|
||||
dispatch(requestNameChangeReset());
|
||||
}
|
||||
|
||||
function handleChange(e) {
|
||||
setVerifiedNameInput(e.target.value);
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
resetLocalState();
|
||||
dispatch(closeForm(targetFormId));
|
||||
dispatch(saveSettingsReset());
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (saveState === 'pending') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!verifiedNameInput) {
|
||||
dispatch(requestNameChangeFailure({
|
||||
verified_name: intl.formatMessage(messages['account.settings.name.change.error.valid.name']),
|
||||
}));
|
||||
} else {
|
||||
const draftProfileName = targetFormId === 'name' ? formValues.name : null;
|
||||
dispatch(requestNameChange(username, draftProfileName, verifiedNameInput));
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (saveState === 'complete') {
|
||||
handleClose();
|
||||
push('/id-verification');
|
||||
}
|
||||
}, [saveState]);
|
||||
|
||||
function renderErrors() {
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return (
|
||||
<>
|
||||
{Object.entries(errors).map(([key, value]) => (
|
||||
<Form.Control.Feedback type="invalid" key={key}>
|
||||
{
|
||||
key === 'general_error'
|
||||
? intl.formatMessage(messages['account.settings.name.change.error.general'])
|
||||
: value
|
||||
}
|
||||
</Form.Control.Feedback>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderTitle() {
|
||||
if (!confirmedWarning) {
|
||||
return intl.formatMessage(messages['account.settings.name.change.title.id']);
|
||||
}
|
||||
|
||||
return intl.formatMessage(messages['account.settings.name.change.title.begin']);
|
||||
}
|
||||
|
||||
function renderBody() {
|
||||
if (!confirmedWarning) {
|
||||
return (
|
||||
<Alert variant="warning">
|
||||
<p>
|
||||
{intl.formatMessage(messages['account.settings.name.change.warning.one'])}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(messages['account.settings.name.change.warning.two'])}
|
||||
</p>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group as={Col} isInvalid={Object.keys(errors).length > 0}>
|
||||
<Form.Label>
|
||||
{intl.formatMessage(messages['account.settings.name.change.id.name.label'])}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
name="verifiedName"
|
||||
placeholder={intl.formatMessage(messages['account.settings.name.change.id.name.placeholder'])}
|
||||
value={verifiedNameInput}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{renderErrors()}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
function renderContinueButton() {
|
||||
if (!confirmedWarning) {
|
||||
return (
|
||||
<Button variant="primary" onClick={() => setConfirmedWarning(true)}>
|
||||
{intl.formatMessage(messages['account.settings.name.change.continue'])}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
state={saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.name.change.continue']),
|
||||
}}
|
||||
disabledStates={[]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalDialog
|
||||
title={renderTitle()}
|
||||
isOpen
|
||||
hasCloseButton={false}
|
||||
onClose={handleClose}
|
||||
>
|
||||
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
{renderTitle()}
|
||||
</ModalDialog.Title>
|
||||
</ModalDialog.Header>
|
||||
|
||||
<ModalDialog.Body className="mb-3 overflow-hidden">
|
||||
{renderBody()}
|
||||
</ModalDialog.Body>
|
||||
|
||||
<ModalDialog.Footer>
|
||||
<ActionRow>
|
||||
<ModalDialog.CloseButton variant="tertiary">
|
||||
{intl.formatMessage(messages['account.settings.name.change.cancel'])}
|
||||
</ModalDialog.CloseButton>
|
||||
{renderContinueButton()}
|
||||
</ActionRow>
|
||||
</ModalDialog.Footer>
|
||||
</Form>
|
||||
|
||||
</ModalDialog>
|
||||
);
|
||||
}
|
||||
|
||||
NameChangeModal.propTypes = {
|
||||
targetFormId: PropTypes.string.isRequired,
|
||||
errors: PropTypes.shape({}).isRequired,
|
||||
formValues: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
verified_name: PropTypes.string,
|
||||
}).isRequired,
|
||||
saveState: PropTypes.string,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
NameChangeModal.defaultProps = {
|
||||
saveState: null,
|
||||
};
|
||||
|
||||
export default connect(nameChangeSelector)(injectIntl(NameChangeModal));
|
||||
25
src/account-settings/name-change/data/actions.js
Normal file
25
src/account-settings/name-change/data/actions.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import { AsyncActionType } from '../../data/utils';
|
||||
|
||||
export const REQUEST_NAME_CHANGE = new AsyncActionType('ACCOUNT_SETTINGS', 'REQUEST_NAME_CHANGE');
|
||||
|
||||
export const requestNameChange = (username, profileName, verifiedName) => ({
|
||||
type: REQUEST_NAME_CHANGE.BASE,
|
||||
payload: { username, profileName, verifiedName },
|
||||
});
|
||||
|
||||
export const requestNameChangeBegin = () => ({
|
||||
type: REQUEST_NAME_CHANGE.BEGIN,
|
||||
});
|
||||
|
||||
export const requestNameChangeSuccess = () => ({
|
||||
type: REQUEST_NAME_CHANGE.SUCCESS,
|
||||
});
|
||||
|
||||
export const requestNameChangeFailure = errors => ({
|
||||
type: REQUEST_NAME_CHANGE.FAILURE,
|
||||
payload: { errors },
|
||||
});
|
||||
|
||||
export const requestNameChangeReset = () => ({
|
||||
type: REQUEST_NAME_CHANGE.RESET,
|
||||
});
|
||||
44
src/account-settings/name-change/data/reducers.js
Normal file
44
src/account-settings/name-change/data/reducers.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import { REQUEST_NAME_CHANGE } from './actions';
|
||||
|
||||
export const defaultState = {
|
||||
saveState: null,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
const reducer = (state = defaultState, action = null) => {
|
||||
if (action !== null) {
|
||||
switch (action.type) {
|
||||
case REQUEST_NAME_CHANGE.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'pending',
|
||||
errors: {},
|
||||
};
|
||||
|
||||
case REQUEST_NAME_CHANGE.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'complete',
|
||||
};
|
||||
|
||||
case REQUEST_NAME_CHANGE.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'error',
|
||||
errors: action.payload.errors || { general_error: 'A technical error occurred. Please try again.' },
|
||||
};
|
||||
|
||||
case REQUEST_NAME_CHANGE.RESET:
|
||||
return {
|
||||
...state,
|
||||
saveState: null,
|
||||
errors: {},
|
||||
};
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
return state;
|
||||
};
|
||||
|
||||
export default reducer;
|
||||
40
src/account-settings/name-change/data/sagas.js
Normal file
40
src/account-settings/name-change/data/sagas.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { put, call, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { postVerifiedName } from '../../data/service';
|
||||
|
||||
import {
|
||||
REQUEST_NAME_CHANGE,
|
||||
requestNameChangeBegin,
|
||||
requestNameChangeSuccess,
|
||||
requestNameChangeFailure,
|
||||
} from './actions';
|
||||
import { postNameChange } from './service';
|
||||
|
||||
export function* handleRequestNameChange(action) {
|
||||
let { name: profileName } = getAuthenticatedUser();
|
||||
try {
|
||||
yield put(requestNameChangeBegin());
|
||||
if (action.payload.profileName) {
|
||||
yield call(postNameChange, action.payload.profileName);
|
||||
profileName = action.payload.profileName;
|
||||
}
|
||||
yield call(postVerifiedName, {
|
||||
username: action.payload.username,
|
||||
verified_name: action.payload.verifiedName,
|
||||
profile_name: profileName,
|
||||
});
|
||||
yield put(requestNameChangeSuccess());
|
||||
} catch (err) {
|
||||
if (err.customAttributes?.httpErrorResponseData) {
|
||||
yield put(requestNameChangeFailure(JSON.parse(err.customAttributes.httpErrorResponseData)));
|
||||
} else {
|
||||
yield put(requestNameChangeFailure());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function* saga() {
|
||||
yield takeEvery(REQUEST_NAME_CHANGE.BASE, handleRequestNameChange);
|
||||
}
|
||||
17
src/account-settings/name-change/data/service.js
Normal file
17
src/account-settings/name-change/data/service.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
import { handleRequestError } from '../../data/utils';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export async function postNameChange(name) {
|
||||
// Requests a pending name change, rather than saving the account name immediately
|
||||
const requestConfig = { headers: { Accept: 'application/json' } };
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/user/v1/accounts/name_change/`;
|
||||
|
||||
const { data } = await getAuthenticatedHttpClient()
|
||||
.post(requestUrl, { name }, requestConfig)
|
||||
.catch(error => handleRequestError(error));
|
||||
|
||||
return data;
|
||||
}
|
||||
4
src/account-settings/name-change/index.js
Normal file
4
src/account-settings/name-change/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default } from './NameChange';
|
||||
export { default as reducer } from './data/reducers';
|
||||
export { default as saga } from './data/sagas';
|
||||
export { REQUEST_NAME_CHANGE } from './data/actions';
|
||||
56
src/account-settings/name-change/messages.js
Normal file
56
src/account-settings/name-change/messages.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'account.settings.name.change.title.id': {
|
||||
id: 'account.settings.name.change.title.id',
|
||||
defaultMessage: 'This name change requires identity verification',
|
||||
description: 'Inform the user that changing their name requires identity verification',
|
||||
},
|
||||
'account.settings.name.change.title.begin': {
|
||||
id: 'account.settings.name.change.title.begin',
|
||||
defaultMessage: 'Before we begin',
|
||||
description: 'Title before beginning the ID verification process',
|
||||
},
|
||||
'account.settings.name.change.warning.one': {
|
||||
id: 'account.settings.name.change.warning.one',
|
||||
defaultMessage: 'Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.',
|
||||
description: 'Warning informing the user that a name change will update the name on all of their certificates.',
|
||||
},
|
||||
'account.settings.name.change.warning.two': {
|
||||
id: 'account.settings.name.change.warning.two',
|
||||
defaultMessage: 'This action cannot be undone without verifying your identity.',
|
||||
description: 'Warning informing the user that a name change cannot be undone without ID verification.',
|
||||
},
|
||||
'account.settings.name.change.id.name.label': {
|
||||
id: 'account.settings.name.change.id.name.label',
|
||||
defaultMessage: 'Enter your name as it appears on your government-issued ID.',
|
||||
description: 'Form label instructing the user to enter the name on their ID.',
|
||||
},
|
||||
'account.settings.name.change.id.name.placeholder': {
|
||||
id: 'account.settings.name.change.id.name.placeholder',
|
||||
defaultMessage: 'Enter the name on your government ID',
|
||||
description: 'Form label instructing the user to enter the name on their ID.',
|
||||
},
|
||||
'account.settings.name.change.error.valid.name': {
|
||||
id: 'account.settings.name.change.error.valid.name',
|
||||
defaultMessage: 'Please enter a valid name.',
|
||||
description: 'Error that appears when the user doesn’t enter a valid name.',
|
||||
},
|
||||
'account.settings.name.change.error.general': {
|
||||
id: 'account.settings.name.change.error.general',
|
||||
defaultMessage: 'A technical error occurred. Please try again.',
|
||||
description: 'Generic error message.',
|
||||
},
|
||||
'account.settings.name.change.continue': {
|
||||
id: 'account.settings.name.change.continue',
|
||||
defaultMessage: 'Continue',
|
||||
description: 'Continue button.',
|
||||
},
|
||||
'account.settings.name.change.cancel': {
|
||||
id: 'account.settings.name.change.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
description: 'Cancel button.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
172
src/account-settings/name-change/test/NameChange.test.jsx
Normal file
172
src/account-settings/name-change/test/NameChange.test.jsx
Normal file
@@ -0,0 +1,172 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
} from '@testing-library/react';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
// Modal creates a portal. Overriding ReactDOM.createPortal allows portals to be tested in jest.
|
||||
ReactDOM.createPortal = node => node;
|
||||
|
||||
import NameChange from '../NameChange'; // eslint-disable-line import/first
|
||||
|
||||
const mockDispatch = jest.fn();
|
||||
jest.mock('react-redux', () => ({
|
||||
...jest.requireActual('react-redux'),
|
||||
useDispatch: () => mockDispatch,
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ nameChangeSelector: () => ({}) })));
|
||||
|
||||
const history = createMemoryHistory();
|
||||
|
||||
const IntlNameChange = injectIntl(NameChange);
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('NameChange', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
targetFormId: 'test_form',
|
||||
errors: {},
|
||||
formValues: {
|
||||
name: 'edx edx',
|
||||
verified_name: 'edX Verified',
|
||||
},
|
||||
saveState: null,
|
||||
intl: {},
|
||||
};
|
||||
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'edx' }));
|
||||
});
|
||||
|
||||
afterEach(() => jest.clearAllMocks());
|
||||
|
||||
it('renders populated input after clicking continue if verified_name in form data', async () => {
|
||||
const getInput = () => screen.queryByPlaceholderText('Enter the name on your government ID');
|
||||
|
||||
render(reduxWrapper(<IntlNameChange {...props} />));
|
||||
expect(getInput()).toBeNull();
|
||||
|
||||
const continueButton = screen.getByText('Continue');
|
||||
fireEvent.click(continueButton);
|
||||
|
||||
expect(getInput().value).toBe('edX Verified');
|
||||
});
|
||||
|
||||
it('renders empty input after clicking continue if verified_name not in form data', async () => {
|
||||
const getInput = () => screen.queryByPlaceholderText('Enter the name on your government ID');
|
||||
const formProps = {
|
||||
...props,
|
||||
formValues: {
|
||||
name: 'edx edx',
|
||||
},
|
||||
};
|
||||
render(reduxWrapper(<IntlNameChange {...formProps} />));
|
||||
|
||||
const continueButton = screen.getByText('Continue');
|
||||
fireEvent.click(continueButton);
|
||||
|
||||
expect(getInput().value).toBe('');
|
||||
});
|
||||
|
||||
it('dispatches verifiedName on submit if targetForm is not "name"', async () => {
|
||||
const dispatchData = {
|
||||
payload: {
|
||||
profileName: null,
|
||||
username: 'edx',
|
||||
verifiedName: 'Verified Name',
|
||||
},
|
||||
type: 'ACCOUNT_SETTINGS__REQUEST_NAME_CHANGE',
|
||||
};
|
||||
|
||||
render(reduxWrapper(<IntlNameChange {...props} />));
|
||||
|
||||
const continueButton = screen.getByText('Continue');
|
||||
fireEvent.click(continueButton);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter the name on your government ID');
|
||||
fireEvent.change(input, { target: { value: 'Verified Name' } });
|
||||
|
||||
const submitButton = screen.getByText('Continue');
|
||||
fireEvent.click(submitButton);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(dispatchData);
|
||||
});
|
||||
|
||||
it('dispatches both profileName and verifiedName on submit if the targetForm is "name"', async () => {
|
||||
const dispatchData = {
|
||||
payload: {
|
||||
profileName: 'edx edx',
|
||||
username: 'edx',
|
||||
verifiedName: 'Verified Name',
|
||||
},
|
||||
type: 'ACCOUNT_SETTINGS__REQUEST_NAME_CHANGE',
|
||||
};
|
||||
const formProps = {
|
||||
...props,
|
||||
targetFormId: 'name',
|
||||
};
|
||||
|
||||
render(reduxWrapper(<IntlNameChange {...formProps} />));
|
||||
|
||||
const continueButton = screen.getByText('Continue');
|
||||
fireEvent.click(continueButton);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter the name on your government ID');
|
||||
fireEvent.change(input, { target: { value: 'Verified Name' } });
|
||||
|
||||
const submitButton = screen.getByText('Continue');
|
||||
fireEvent.click(submitButton);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(dispatchData);
|
||||
});
|
||||
|
||||
it('does not dispatch action while pending', async () => {
|
||||
props.saveState = 'pending';
|
||||
|
||||
render(reduxWrapper(<IntlNameChange {...props} />));
|
||||
|
||||
const continueButton = screen.getByText('Continue');
|
||||
fireEvent.click(continueButton);
|
||||
|
||||
const input = screen.getByPlaceholderText('Enter the name on your government ID');
|
||||
fireEvent.change(input, { target: { value: 'Verified Name' } });
|
||||
|
||||
const submitButton = screen.getByText('Continue');
|
||||
fireEvent.click(submitButton);
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('routes to IDV when name change request is successful', async () => {
|
||||
props.saveState = 'complete';
|
||||
|
||||
render(reduxWrapper(<IntlNameChange {...props} />));
|
||||
expect(history.location.pathname).toEqual('/id-verification');
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
23
src/hooks.js
Normal file
23
src/hooks.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export function useAsyncCall(asyncFunc) {
|
||||
const [isLoading, setIsLoading] = useState();
|
||||
const [data, setData] = useState();
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
setIsLoading(true);
|
||||
const response = await asyncFunc();
|
||||
setIsLoading(false);
|
||||
if (response) {
|
||||
setData(response);
|
||||
}
|
||||
})();
|
||||
},
|
||||
[asyncFunc],
|
||||
);
|
||||
|
||||
return [isLoading, data];
|
||||
}
|
||||
@@ -20,6 +20,24 @@
|
||||
"account.settings.field.full.name": "الاسم الكامل",
|
||||
"account.settings.field.full.name.empty": "إضافة اسم",
|
||||
"account.settings.field.full.name.help.text": "الاسم المستخدم للتحقق من هويتك والذي سوف يظهر على الشهادات الخاصة بك.",
|
||||
"account.settings.field.full.name.help.text.non.certificate": "The name that appears on your public profile.",
|
||||
"account.settings.field.full.name.help.text.certificate": "This name is selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified": "Verified name",
|
||||
"account.settings.field.name.verified.help.text.verified": "This name has been verified by government ID.",
|
||||
"account.settings.field.name.verified.help.text.certificate": "This name has been verified by government ID and selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Verified name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.verification.help": "Enter your name as it appears on your government-issued ID.",
|
||||
"account.settings.field.full.name.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.",
|
||||
"account.settings.field.full.name.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.success.message": "Your identity verification request has successfully completed. You now have the option of selecting which name you prefer to appear on your certificates and public-records.",
|
||||
"account.settings.field.name.verified.success.message.header": "Your name change request is complete!",
|
||||
"account.settings.field.name.verified.failure.message": "Your most recent identity verification attempt did not pass. Related account settings have been restored.",
|
||||
"account.settings.field.name.verified.failure.message.header": "We were not able to verify your identity.",
|
||||
"account.settings.field.name.verified.failure.message.help.link": "Learn more about ID verification",
|
||||
"account.settings.field.name.verified.submitted.message": "Your identity verification request has been submitted and usually takes between 24 and 48 hours to complete.",
|
||||
"account.settings.field.name.verified.submitted.message.certificate": "When your request is approved, your updated name will appear on all associated certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.submitted.message.header": "Your name change request is almost complete!",
|
||||
"account.settings.field.email": "البريد الالكتروني (الدخول)",
|
||||
"account.settings.field.email.empty": "إضافة عنوان البريد الإلكتروني",
|
||||
"account.settings.field.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر فوق الرابط في الرسالة لتحديث عنوان بريدك الإلكتروني.",
|
||||
@@ -80,6 +98,12 @@
|
||||
"account.settings.editable.field.action.edit": "تحرير",
|
||||
"account.settings.static.field.empty": "لم يتم تحديد قيمة، فضلًا اتصل بمدير {enterprise} لتعيين بعض التغييرات.",
|
||||
"account.settings.static.field.empty.no.admin": "لم يتم تحديد قيمة",
|
||||
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
|
||||
"account.settings.field.name.modal.certificate.select": "Select a name",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Full Name",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
|
||||
"account.settings.coaching.consent.welcome.header": "لنبدأ",
|
||||
"account.settings.coaching.consent.welcome.subheader": "نحن هنا لأجلك من البداية حتى النهاية",
|
||||
"account.settings.coaching.consent.description": "تتضمن برامج البكالوريوس التدريب الذي يركز على مهنتك وتعليمك وكيفية تحقيق نتائج مبهرة من خلال التواصل الشخصي مع خبراء متمرسين. إذا كنت مهتمًا، فقدّم المعلومات أدناه وانقر فوق \"إرسال\"، وسيتصل بك شريكنا في التدريب عبر البريد الإلكتروني و/أو الرسائل النصية لمساعدتك على المضي قدمًا. تنطبق الشروط والأحكام.*",
|
||||
@@ -157,6 +181,16 @@
|
||||
"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?",
|
||||
"account.settings.name.change.title.id": "This name change requires identity verification",
|
||||
"account.settings.name.change.title.begin": "Before we begin",
|
||||
"account.settings.name.change.warning.one": "Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.",
|
||||
"account.settings.name.change.warning.two": "This action cannot be undone without verifying your identity.",
|
||||
"account.settings.name.change.id.name.label": "Enter your name as it appears on your government-issued ID.",
|
||||
"account.settings.name.change.id.name.placeholder": "Enter the name on your government ID",
|
||||
"account.settings.name.change.error.valid.name": "Please enter a valid name.",
|
||||
"account.settings.name.change.error.general": "A technical error occurred. Please try again.",
|
||||
"account.settings.name.change.continue": "Continue",
|
||||
"account.settings.name.change.cancel": "Cancel",
|
||||
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في نص الرابط. الرجاء التحقق من الرابط والمحاولة مجددا.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "الدعم الفني",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "لقد أرسلنا رسالة إلى {email}. انقر فوق الرابط في الرسالة لإعادة تعيين كلمة المرور. إذا لم يتم استلام الرسالة؟ اتصل بـ {technicalSupportLink}.",
|
||||
@@ -174,11 +208,11 @@
|
||||
"id.verification.continue.upload": "Continue with Upload",
|
||||
"id.verification.example.card.alt": "مثال على بطاقة هوية صحيحة بالاسم الكامل وصورة.",
|
||||
"id.verification.requirements.title": "متطلبات التحقق من الصورة",
|
||||
"id.verification.requirements.description": "يجب عليك اتباع الآتي لإكمال عملية التحقق الإلكتروني من هويتك:",
|
||||
"id.verification.requirements.description": "In order to complete Photo Verification, you will need the following:",
|
||||
"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.requirements.card.id.title": "Photo Identification Card",
|
||||
"id.verification.requirements.card.id.text": "You need a valid identification card that contains your full name and photo, such as a driver’s license or passport.",
|
||||
"id.verification.privacy.title": "بيانات الخصوصية.",
|
||||
"id.verification.privacy.need.photo.question": "Why does {siteName} need my photo?",
|
||||
"id.verification.privacy.need.photo.answer": "نستخدم صور التحقق الخاصة بك لتأكيد هويتك والتأكد من صحة شهادتك.",
|
||||
@@ -261,27 +295,34 @@
|
||||
"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.photo.unclear.question": "Is your ID card image not clear or too blurry?",
|
||||
"id.verification.id.tips.title": "Helpful Identification Card Tips",
|
||||
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid identification card that includes your full name and photo, such as a driver’s license or passport. Please have your ID ready.",
|
||||
"id.verification.id.tips.list.well.lit": "Your identification card is well-lit.",
|
||||
"id.verification.id.tips.list.clear": "تأكد من قدرتك على رؤية صورتك وقراءة اسمك بوضوح.",
|
||||
"id.verification.id.photo.title.camera": "التقط صورة لبطاقتك الشخصية",
|
||||
"id.verification.id.photo.title.upload": "حمّل صورة لهويتك",
|
||||
"id.verification.id.photo.title.camera": "Take a Photo of Your Identification Card",
|
||||
"id.verification.id.photo.title.upload": "Upload a Photo of Your Identification Card",
|
||||
"id.verification.id.photo.preview.alt": "معاينة صورة الهوية.",
|
||||
"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.camera": "When your ID is in position, use the Take Photo button below to take your photo. Please use a passport, driver’s license, or another identification card that includes your full name and a picture of your face.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload a photo of your identification card. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
|
||||
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
|
||||
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
|
||||
"id.verification.account.name.title": "التحقق من اسم الحساب",
|
||||
"id.verification.name.check.title": "Double-Check Your Name",
|
||||
"id.verification.account.name.instructions": "يجب أن يكون الاسم الموجود على حسابك والاسم الموجود على المعرّف الخاص بك متطابقًا تمامًا. إذا لم يكن الأمر كذلك، فيرجى النقر فوق \"لا\" لتحديث اسم حسابك.",
|
||||
"id.verification.name.check.instructions": "Does the name below match the name on your government-issued ID? If not, update the name below to match your goverment-issued ID.",
|
||||
"id.verification.name.check.mismatch.information": "If the name below does not match your government-issued ID, your identity verification will be denied.",
|
||||
"id.verification.account.name.radio.label": "هل يتطابق الاسم الموجود على هويتك مع اسم الحساب أدناه؟",
|
||||
"id.verification.name.check.radio.label": "Select an option",
|
||||
"id.verification.account.name.radio.yes": "نعم",
|
||||
"id.verification.name.check.radio.yes": "Yes, the name below matches my ID",
|
||||
"id.verification.account.name.radio.no": "لا",
|
||||
"id.verification.name.check.radio.no": "No, the name below does not match my ID",
|
||||
"id.verification.account.name.error": "يرجى تحديث اسم الحساب لمطابقة الاسم على بطاقة الهوية.",
|
||||
"id.verification.account.name.warning.prefix": "يُرجى الملاحظة:",
|
||||
"id.verification.account.name.settings": "إعدادات الحساب",
|
||||
"id.verification.account.name.label": "اسم الحساب",
|
||||
"id.verification.name.label": "Name",
|
||||
"id.verification.account.name.photo.alt": "صورة من هويتك للتقديم.",
|
||||
"id.verification.account.name.save": "حفظ ثم التالي",
|
||||
"id.verification.review.title": "مراجعة صورك",
|
||||
@@ -289,8 +330,8 @@
|
||||
"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.label": "Your Identification Card",
|
||||
"id.verification.review.id.alt": "Photo of your identification card to be submitted.",
|
||||
"id.verification.review.id.retake": "إعادة التقاط صورة الهوية",
|
||||
"id.verification.review.confirm": "إرسال",
|
||||
"id.verification.submission.alert.error.face": "مطلوب صورة لوجهك. يرجى إعادة التقاط الصورة الشخصية.",
|
||||
|
||||
@@ -20,6 +20,24 @@
|
||||
"account.settings.field.full.name": "Nombre completo",
|
||||
"account.settings.field.full.name.empty": "Añade nombre",
|
||||
"account.settings.field.full.name.help.text": "El nombre que es usado para la verificación de identidad y aparece en sus certificados.",
|
||||
"account.settings.field.full.name.help.text.non.certificate": "The name that appears on your public profile.",
|
||||
"account.settings.field.full.name.help.text.certificate": "This name is selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified": "Verified name",
|
||||
"account.settings.field.name.verified.help.text.verified": "This name has been verified by government ID.",
|
||||
"account.settings.field.name.verified.help.text.certificate": "This name has been verified by government ID and selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Verified name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.verification.help": "Enter your name as it appears on your government-issued ID.",
|
||||
"account.settings.field.full.name.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.",
|
||||
"account.settings.field.full.name.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.success.message": "Your identity verification request has successfully completed. You now have the option of selecting which name you prefer to appear on your certificates and public-records.",
|
||||
"account.settings.field.name.verified.success.message.header": "Your name change request is complete!",
|
||||
"account.settings.field.name.verified.failure.message": "Your most recent identity verification attempt did not pass. Related account settings have been restored.",
|
||||
"account.settings.field.name.verified.failure.message.header": "We were not able to verify your identity.",
|
||||
"account.settings.field.name.verified.failure.message.help.link": "Más información sobre la verificación de ID",
|
||||
"account.settings.field.name.verified.submitted.message": "Your identity verification request has been submitted and usually takes between 24 and 48 hours to complete.",
|
||||
"account.settings.field.name.verified.submitted.message.certificate": "When your request is approved, your updated name will appear on all associated certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.submitted.message.header": "Your name change request is almost complete!",
|
||||
"account.settings.field.email": "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.",
|
||||
@@ -80,6 +98,12 @@
|
||||
"account.settings.editable.field.action.edit": "Editar",
|
||||
"account.settings.static.field.empty": "No hay valor establecido. Contacte su administrador {enterprise} para hacer cambios.",
|
||||
"account.settings.static.field.empty.no.admin": "No hay valor establecido.",
|
||||
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
|
||||
"account.settings.field.name.modal.certificate.select": "Select a name",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Full Name",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
|
||||
"account.settings.coaching.consent.welcome.header": "Empecemos",
|
||||
"account.settings.coaching.consent.welcome.subheader": "Estamos aquí para ustede desde el inicio hasta el final",
|
||||
"account.settings.coaching.consent.description": "Los programas de MicroBachelors incluyen entrenamiento que se enfoca en su carrera, educación y cómo logrará resultados a través de la comunicación individual con un profesional experimentado. Si está interesado, proporcione la información a continuación y haga clic en \"Enviar\", y nuestro socio asesor se comunicará con usted por correo electrónico y / o mensaje de texto para ayudarlo a avanzar. Los términos y Condiciones aplican.*",
|
||||
@@ -103,7 +127,7 @@
|
||||
"account.settings.delete.account.subheader": "¡Sentimos que te vayas!",
|
||||
"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 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.text.2.edX": "Una vez su cuenta haya sido eliminada, no la podrá usar para tomar cursos en la app de edX, edx.org o en cualquier otro sitio administrado por edX. Esto incluye el acceso a edx.org desde el sistema de su empleador o universidad y el acceso a páginas privadas ofrecidas por MIT Open Learning, Wharton Executive Education y Harvard Medical School.",
|
||||
"account.settings.delete.account.text.3.link": "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?",
|
||||
@@ -113,7 +137,7 @@
|
||||
"account.settings.delete.account.modal.header": "¿Está seguro?",
|
||||
"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.text.2.edX": "Si procedes, no será posible usar esta cuenta para tomar cursos ni en la aplicación móvil de edX, ni en edx.org, ni en cualquier otro sitio hospedado por edX. Esto incluye el acceso a edx.org desde el sistema de tu empleador o universidad, y el acceso a sitios privados ofrecidos por MIT Open Learning, Wharton Executive Education, y Harvard Medical School.",
|
||||
"account.settings.delete.account.modal.enter.password": "Si deseas continuar y eliminar tu cuenta, por favor introduce la contraseña de tu cuenta:",
|
||||
"account.settings.delete.account.modal.confirm.delete": "Si, Eliminar",
|
||||
"account.settings.delete.account.modal.confirm.cancel": "Cancelar",
|
||||
@@ -157,6 +181,16 @@
|
||||
"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": "Why does {siteName} collect this information?",
|
||||
"account.settings.name.change.title.id": "This name change requires identity verification",
|
||||
"account.settings.name.change.title.begin": "Before we begin",
|
||||
"account.settings.name.change.warning.one": "Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.",
|
||||
"account.settings.name.change.warning.two": "This action cannot be undone without verifying your identity.",
|
||||
"account.settings.name.change.id.name.label": "Enter your name as it appears on your government-issued ID.",
|
||||
"account.settings.name.change.id.name.placeholder": "Enter the name on your government ID",
|
||||
"account.settings.name.change.error.valid.name": "Please enter a valid name.",
|
||||
"account.settings.name.change.error.general": "A technical error occurred. Please try again.",
|
||||
"account.settings.name.change.continue": "Continue",
|
||||
"account.settings.name.change.cancel": "Cancel",
|
||||
"error.notfound.message": "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}.",
|
||||
@@ -170,15 +204,15 @@
|
||||
"account.settings.sso.no.providers": "No se pueden vincular cuentas en este momento.",
|
||||
"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.support": "soporte",
|
||||
"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.description": "In order to complete Photo Verification, you will need the following:",
|
||||
"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 documento de identidad válido que contenga tu foto y nombre completo.",
|
||||
"id.verification.requirements.card.id.title": "Photo Identification Card",
|
||||
"id.verification.requirements.card.id.text": "You need a valid identification card that contains your full name and photo, such as a driver’s license or passport.",
|
||||
"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.",
|
||||
@@ -188,7 +222,7 @@
|
||||
"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": "Retake Photo?",
|
||||
"id.verification.photo.retake": "¿Tomar nuevamente la foto?",
|
||||
"id.verification.photo.enable.detection": "Habilitar la detección de rostro",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "Si está marcada, aparecerá un cuadro alrededor de tu cara. Tu rostro se puede ver claramente si el cuadro que lo rodea es azul. Si Tu cara no está en una buena posición o es indetectable, el cuadro será rojo.",
|
||||
"id.verification.photo.enable.detection.id.help.text": "Si está marcada, aparecerá una casilla alrededor de la cara de tu documento de identificación. La cara se puede ver claramente si la caja que la rodea es azul. Si la cara no está en una buena posición o es indetectable, el cuadro será rojo.",
|
||||
@@ -261,27 +295,34 @@
|
||||
"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.photo.unclear.question": "Is your ID card image not clear or too blurry?",
|
||||
"id.verification.id.tips.title": "Helpful Identification Card Tips",
|
||||
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid identification card that includes your full name and photo, such as a driver’s license or passport. Please have your ID ready.",
|
||||
"id.verification.id.tips.list.well.lit": "Your identification card is well-lit.",
|
||||
"id.verification.id.tips.list.clear": "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.title.camera": "Take a Photo of Your Identification Card",
|
||||
"id.verification.id.photo.title.upload": "Upload a Photo of Your Identification Card",
|
||||
"id.verification.id.photo.preview.alt": "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.camera": "When your ID is in position, use the Take Photo button below to take your photo. Please use a passport, driver’s license, or another identification card that includes your full name and a picture of your face.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload a photo of your identification card. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
|
||||
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
|
||||
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
|
||||
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "El archivo que has seleccionado es demasiado grande. Vuelve a intentarlo con un archivo de menos de 10 MB.",
|
||||
"id.verification.account.name.title": "Verificación de nombre de cuenta",
|
||||
"id.verification.name.check.title": "Double-Check Your Name",
|
||||
"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.name.check.instructions": "Does the name below match the name on your government-issued ID? If not, update the name below to match your goverment-issued ID.",
|
||||
"id.verification.name.check.mismatch.information": "If the name below does not match your government-issued ID, your identity verification will be denied.",
|
||||
"id.verification.account.name.radio.label": "¿El nombre de tu identificación coincide con el nombre de la cuenta a continuación?",
|
||||
"id.verification.name.check.radio.label": "Select an option",
|
||||
"id.verification.account.name.radio.yes": "Si",
|
||||
"id.verification.name.check.radio.yes": "Yes, the name below matches my ID",
|
||||
"id.verification.account.name.radio.no": "No",
|
||||
"id.verification.name.check.radio.no": "No, the name below does not match my ID",
|
||||
"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.name.label": "Name",
|
||||
"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",
|
||||
@@ -289,8 +330,8 @@
|
||||
"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.label": "Your Identification Card",
|
||||
"id.verification.review.id.alt": "Photo of your identification card to be submitted.",
|
||||
"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.",
|
||||
@@ -318,5 +359,5 @@
|
||||
"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}"
|
||||
"id.verification.account.name.edit": "Editar {sr}"
|
||||
}
|
||||
@@ -1,322 +1,363 @@
|
||||
{
|
||||
"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.duplicate.tpa.provider": "Le compte {provider} que vous avez sélectionné est déjà lié à un autre compte {siteName}.",
|
||||
"account.settings.message.managed.settings": "Vos paramètres de profile sont gérés par {managerTitle}. Contactez votre administrateur ou {support} en cas de besoin.",
|
||||
"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 {siteName}.",
|
||||
"account.settings.field.username": "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": "We’ve 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 {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": "We’ve 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.page.heading": "Paramètres du compte",
|
||||
"account.settings.loading.message": "Chargement...",
|
||||
"account.settings.loading.error": "Erreur : {error}",
|
||||
"account.settings.banner.beta.language": "Vous avez réglé votre langue sur {beta_language}, qui n'est actuellement pas entièrement traduite. Vous pouvez nous aider à traduire complètement cette langue en rejoignant la communauté Transifex et en ajoutant des traductions de l'anglais pour les apprenants qui parlent {beta_language}.",
|
||||
"account.settings.banner.beta.language.action.switch.back": "Revenir au {previous_language}",
|
||||
"account.settings.banner.beta.language.action.help.translate": "Aidez-nous à traduire en {beta_language}",
|
||||
"account.settings.section.account.information": "Information du compte",
|
||||
"account.settings.section.account.information.description": "Ces paramètres comprennent des informations de base à propos de votre compte.",
|
||||
"account.settings.section.profile.information": "Informations de profil",
|
||||
"account.settings.section.demographics.information": "Informations optionnelles",
|
||||
"account.settings.section.site.preferences": "Préférences de site",
|
||||
"account.settings.section.linked.accounts": "Comptes liés",
|
||||
"account.settings.section.linked.accounts.description": "Vous pouvez associer vos comptes d'identité pour simplifier la connexion à {siteName}.",
|
||||
"account.settings.field.username": "Nom d’utilisateur",
|
||||
"account.settings.field.username.help.text": "Le nom qui vous identifie sur {siteName}. Vous ne pouvez pas changer votre nom d'utilisateur.",
|
||||
"account.settings.field.full.name": "Nom complet",
|
||||
"account.settings.field.full.name.empty": "Ajouter un nom",
|
||||
"account.settings.field.full.name.help.text": "Le nom qui apparaîtra sur vos certificats et dans le cadre de toute vérification d'identité.",
|
||||
"account.settings.field.full.name.help.text.non.certificate": "Le nom qui apparaît sur votre profile public.",
|
||||
"account.settings.field.full.name.help.text.certificate": "Ce nom va apparaître sur vos attestations et dossiers publics.",
|
||||
"account.settings.field.name.verified": "Nom vérifié",
|
||||
"account.settings.field.name.verified.help.text.verified": "Ce nom a été vérifié par une pièce d'identité gouvernementale.",
|
||||
"account.settings.field.name.verified.help.text.certificate": "This name has been verified by government ID and selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.help.text.submitted": "La vérification a été soumise. Cela prend généralement 48 heures ou moins. Le nom vérifié ne peut pas être modifié pour le moment.",
|
||||
"account.settings.field.name.verified.help.text.submitted.certificate": "Une fois la vérification d'identité réussie, ce nom apparaîtra sur vos attestations et vos dossiers publics. Le nom complet ne peut pas être modifié pour le moment.",
|
||||
"account.settings.field.name.verified.verification.help": "Enter your name as it appears on your government-issued ID.",
|
||||
"account.settings.field.full.name.help.text.submitted": "La vérification a été soumise. Cela prend généralement 48 heures ou moins. Le nom vérifié ne peut pas être modifié pour le moment.",
|
||||
"account.settings.field.full.name.help.text.submitted.certificate": "Une fois la vérification d'identité réussie, ce nom apparaîtra sur vos attestations et vos dossiers publics. Le nom complet ne peut pas être modifié pour le moment.",
|
||||
"account.settings.field.name.verified.success.message": "Votre demande de vérification d'identité a été complétée avec succès. Vous avez maintenant la possibilité de sélectionner le nom que vous préférez voir apparaître sur vos attestations et documents publics.",
|
||||
"account.settings.field.name.verified.success.message.header": "Votre demande de changement de nom est terminée!",
|
||||
"account.settings.field.name.verified.failure.message": "Votre dernière tentative de vérification d'identité n'a pas abouti. Les paramètres du compte correspondant ont été restaurés.",
|
||||
"account.settings.field.name.verified.failure.message.header": "Nous n'avons pas pu vérifier votre identité.",
|
||||
"account.settings.field.name.verified.failure.message.help.link": "En savoir plus sur la vérification d'identité",
|
||||
"account.settings.field.name.verified.submitted.message": "Votre demande de vérification d'identité a bien été envoyée et prend généralement entre 24 et 48 heures afin d'être validée.",
|
||||
"account.settings.field.name.verified.submitted.message.certificate": "Lorsque votre demande est approuvée, votre nouveau nom apparaîtra sur tous les certificats associés et les enregistrements publics.",
|
||||
"account.settings.field.name.verified.submitted.message.header": "Votre demande de changement de nom est presque terminée!",
|
||||
"account.settings.field.email": "Adresse e-mail (Connexion)",
|
||||
"account.settings.field.email.empty": "Ajouter l'adresse e-mail",
|
||||
"account.settings.field.email.confirmation": "Un message de confirmation a été envoyé à {value}. Cliquer le lien dans le message afin de mettre à jour votre adresse email.",
|
||||
"account.settings.field.email.help.text": "Vous recevrez des messages de {siteName} et des équipes pédagogiques à cette adresse.",
|
||||
"account.settings.field.secondary.email": "Adresse électronique de récupération",
|
||||
"account.settings.field.secondary.email.empty": "Ajouter une adresse électronique de récupération",
|
||||
"account.settings.field.secondary.email.confirmation": "Un message de confirmation a été envoyé à {value}. Cliquer le lien dans le message afin de mettre à jour votre adresse email.",
|
||||
"account.settings.email.field.confirmation.header": "En attente de confirmation",
|
||||
"account.settings.field.dob": "Année de naissance",
|
||||
"account.settings.field.dob.empty": "Ajouter année de naissance",
|
||||
"account.settings.field.year_of_birth.options.empty": "Sélectionnez une année de naissance",
|
||||
"account.settings.field.country": "Pays",
|
||||
"account.settings.field.country.empty": "Ajouter un pays",
|
||||
"account.settings.field.country.options.empty": "Sélectionnez un pays",
|
||||
"account.settings.field.state": "État",
|
||||
"account.settings.field.state.empty": "Ajouter un état",
|
||||
"account.settings.field.state.options.empty": "Choisir un état",
|
||||
"account.settings.field.site.language": "Langue du site",
|
||||
"account.settings.field.site.language.help.text": "La langue utilisée au travers du site. Le site est actuellement disponible dans un nombre limité de langues.",
|
||||
"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 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",
|
||||
"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 browser’s 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 {siteName} profile.",
|
||||
"account.settings.field.education.empty": "Ajouter le niveau d'éducation",
|
||||
"account.settings.field.education.levels.empty": "Sélectionnez un niveau d'éducation",
|
||||
"account.settings.field.education.levels.p": "Doctorat",
|
||||
"account.settings.field.education.levels.m": "Master ou diplôme professionnel",
|
||||
"account.settings.field.education.levels.b": "Diplôme de licence",
|
||||
"account.settings.field.education.levels.a": "Grade de l'associé",
|
||||
"account.settings.field.education.levels.hs": "Lycée / enseignement secondaire",
|
||||
"account.settings.field.education.levels.jhs": "Collège / enseignement secondaire inférieur",
|
||||
"account.settings.field.education.levels.el": "Enseignement primaire",
|
||||
"account.settings.field.education.levels.none": "Sans diplôme",
|
||||
"account.settings.field.education.levels.o": "Autre niveau d'étude",
|
||||
"account.settings.field.gender": "Genre",
|
||||
"account.settings.field.gender.empty": "Ajouter le genre",
|
||||
"account.settings.field.gender.options.empty": "Sélectionner un genre",
|
||||
"account.settings.field.gender.options.f": "Femme",
|
||||
"account.settings.field.gender.options.m": "Homme",
|
||||
"account.settings.field.gender.options.o": "Autre",
|
||||
"account.settings.field.language.proficiencies": "Langue parlée",
|
||||
"account.settings.field.language.proficiencies.empty": "Ajouter une langue parlée",
|
||||
"account.settings.field.language_proficiencies.options.empty": "Sélectionnez une langue",
|
||||
"account.settings.field.time.zone": "Fuseau horaire",
|
||||
"account.settings.field.time.zone.empty": "Définir le fuseau horaire",
|
||||
"account.settings.field.time.zone.description": "Sélectionnez le fuseau horaire pour l'affichage des dates de cours. Si vous n'indiquez aucun fuseau horaire, les dates de cours, y compris les échéances de devoirs, seront affichés en fonction du fuseau horaire local de votre navigateur. ",
|
||||
"account.settings.field.time.zone.default": "Défaut (fuseau horaire local)",
|
||||
"account.settings.field.time.zone.all": "Tous les fuseaux horaires",
|
||||
"account.settings.field.time.zone.country": "Fuseaux horaires des pays",
|
||||
"account.settings.section.social.media": "Liens vers les réseaux sociaux",
|
||||
"account.settings.section.social.media.description": "Optionnellement, liez vos comptes personnels aux icônes des médias sociaux sur votre profil {siteName}.",
|
||||
"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.linkedin.empty": "Ajouter un profil LinkedIn",
|
||||
"account.settings.jump.nav.delete.account": "Supprimer mon compte",
|
||||
"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.twitter.empty": "Ajouter un profil Twitter",
|
||||
"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": "Let’s 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 you’re 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.field.social.platform.name.facebook.empty": "Ajouter un profil Facebook",
|
||||
"account.settings.editable.field.action.save": "Enregistrer",
|
||||
"account.settings.editable.field.action.cancel": "Annuler",
|
||||
"account.settings.editable.field.action.edit": "Modifier",
|
||||
"account.settings.static.field.empty": "Aucune valeur n'a été fixée. Contactez l'administrateur de votre {entreprise} pour effectuer des modifications.",
|
||||
"account.settings.static.field.empty.no.admin": "Pas de valeur définie.",
|
||||
"account.settings.field.name.certificate.select": "Si sélectionné, ce nom va apparaître sur vos attestations et dossiers publics.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choisissez le nom qui va apparaîtres sur vos attestations et dossiers publics.",
|
||||
"account.settings.field.name.modal.certificate.select": "Sélectionnez un nom",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Nom complet",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Nom vérifié",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choisissez un nom",
|
||||
"account.settings.coaching.consent.welcome.header": "Commençons.",
|
||||
"account.settings.coaching.consent.welcome.subheader": "Nous sommes là pour vous du début à la fin",
|
||||
"account.settings.coaching.consent.description": "Les programmes MicroBachelors comprennent un coaching axé sur votre carrière, vos études et la façon dont vous obtiendrez des résultats grâce à une communication individuelle avec un professionnel expérimenté. Si vous êtes intéressé, fournissez les informations ci-dessous et cliquez sur \"Soumettre\". Notre partenaire de coaching vous contactera par courrier électronique et/ou par SMS pour vous aider à progresser. Les conditions générales s'appliquent.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Les services de coaching sont inclus sans frais supplémentaires pour les apprenants ayant un numéro de téléphone aux États-Unis. Le coaching comprend des messages textes récurrents. Les tarifs des messages et des données peuvent s'appliquer. Text STOP pour se désengager.",
|
||||
"account.settings.coaching.consent.accept-coaching": "S'inscrire pour le coaching.",
|
||||
"account.settings.coaching.consent.decline-coaching": "Je préfère ne pas être contacté par les services de coaching gratuit.",
|
||||
"account.settings.coaching.consent.label.name": "Veuillez confirmer votre nom",
|
||||
"account.settings.coaching.consent.label.phone-number": "Entrer un numéro de cellulaire",
|
||||
"account.settings.coaching.consent.success.header": "Opération réussie!",
|
||||
"account.settings.coaching.consent.success.message": "Vous êtes inscrit au coaching. Vous pouvez vous attendre à un message par courriel ou SMS dans les prochains jours.",
|
||||
"account.settings.coaching.consent.success.continue": "Démarrer mon cours",
|
||||
"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. {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 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.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. {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",
|
||||
"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.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",
|
||||
"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 {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.coaching.managed.alert": "Votre nom est géré par {managerTitle}. Contactez votre administrateur pour obtenir de l'aide.",
|
||||
"account.settings.field.phone_number": "Numéro de téléphone",
|
||||
"account.settings.field.phone_number.empty": "Ajouter un numéro de téléphone",
|
||||
"account.settings.field.coaching_consent": "Consentement pour le coaching",
|
||||
"account.settings.field.coaching_consent.tooltip": "Un parcours MicroBachelors inclus du coaching par texto avec un professionnel expériementé qui vous aidera à combiner votre expérience éducationnelle et vos objectifs de carrière. Les services de coaching sont inclus sans coût additionnel aux apprenant ayant un numéro de téléphone US. Le coût standard des textos s'applique. Texter 'STOP' en tout temp pour arrêter les messages.",
|
||||
"account.settings.field.coaching_consent.error": "Un numéro de téléphone US valide est requis pour s'inscrire pour du coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Avant de poursuivre, veuillez {actionLink}.",
|
||||
"account.settings.delete.account.header": "Supprimer mon compte",
|
||||
"account.settings.delete.account.subheader": "Nous sommes désolés de vous voir quitter!",
|
||||
"account.settings.delete.account.text.1": "Veuillez noter que la suppression de votre compte et de vos données personnelles est permanente et ne peut être annulée. {siteName} ne pourra pas récupérer votre compte ou les données supprimées.",
|
||||
"account.settings.delete.account.text.2": "Une fois votre compte supprimé, vous ne pourrez plus l'utiliser pour suivre des cours sur {siteName}.",
|
||||
"account.settings.delete.account.text.2.edX": "Une fois votre compte supprimé, vous ne pourrez plus l'utiliser pour prendre des cours sur l'application edX, edx.org, ou tout autre site hébergé par edX. Cela comprend l'accès à edx.org à partir du système de votre employeur ou de votre université et l'accès aux sites privés offerts par MIT Open Learning, Wharton Executive Education, et Harvard Medical School.",
|
||||
"account.settings.delete.account.text.3.link": "Suivez ces instructions pour imprimer ou télécharger une attestation",
|
||||
"account.settings.delete.account.text.warning": "Avertissement : la suppression d'un compte est permanente. Veuillez lire attentivement ce qui précède avant de continuer. Cette action est irréversible et vous ne pourrez plus utiliser la même adresse courriel sur {siteName}.",
|
||||
"account.settings.delete.account.text.change.instead": "Vous souhaitez modifier votre adresse électronique, votre nom ou votre mot de passe?",
|
||||
"account.settings.delete.account.button": "Supprimer mon compte",
|
||||
"account.settings.delete.account.please.activate": "activez votre compte",
|
||||
"account.settings.delete.account.please.unlink": "dissocier tous les comptes de médias sociaux",
|
||||
"account.settings.delete.account.modal.header": "Êtes-vous certain ?",
|
||||
"account.settings.delete.account.modal.text.1": "Vous avez sélectionné \"Supprimer mon compte\". La suppression de votre compte et de vos données personnelles est permanente et ne peut être annulée. {siteName} ne pourra ni récupérer votre compte ni les données supprimées.",
|
||||
"account.settings.delete.account.modal.text.2": "Si vous continuez, vous ne pourrez plus utiliser ce compte pour suivre des cours sur {siteName}.",
|
||||
"account.settings.delete.account.modal.text.2.edX": "Si vous procédez ainsi, vous ne pourrez pas utiliser ce compte pour suivre des cours sur l'application edX, edx.org ou tout autre site hébergé par edX. Cela inclut l'accès à edx.org depuis le système de votre employeur ou de votre université et l'accès aux sites privés proposés par MIT Open Learning, Wharton Executive Education et Harvard Medical School.",
|
||||
"account.settings.delete.account.modal.enter.password": "Si vous souhaitez toujours supprimer votre compte, veuillez saisir le mot de passe de votre compte :",
|
||||
"account.settings.delete.account.modal.confirm.delete": "Oui, supprimer",
|
||||
"account.settings.delete.account.modal.confirm.cancel": "Annuler",
|
||||
"account.settings.delete.account.error.unable.to.delete": "Impossible de supprimer le compte",
|
||||
"account.settings.delete.account.error.no.password": "Un mot de passe est requis",
|
||||
"account.settings.delete.account.error.invalid.password": "Mot de passe incorrect.",
|
||||
"account.settings.delete.account.error.unable.to.delete.details": "Nous sommes désolés, il y a eu une erreur dans le suivi de votre demande. Veuillez réessayer plus tard.",
|
||||
"account.settings.delete.account.modal.after.header": "Nous sommes désolés de vous voir quitter! Votre compte sera bientôt supprimé.",
|
||||
"account.settings.delete.account.modal.after.text": "La suppression d'un compte, comprenant le retrait des listes d'e-mail, peu prendre quelques semaines afin de traiter entièrement notre système. Si vous souhaitez renoncer aux e-mails ultérieurement, veuillez vous désinscrire en consultant le pied de page de n'importe quel e-mail.",
|
||||
"account.settings.delete.account.modal.after.button": "Fermer",
|
||||
"account.settings.delete.account.text.3.edX": "Vous pouvez également perdre l'accès aux attestations vérifiées et à d'autres informations d'identification de programme, comme les attestations MicroMasters. Vous pouvez en faire une copie pour vos dossiers avant de procéder à la suppression. {actionLink}.",
|
||||
"account.settings.delete.account.text.3": "Vous pouvez également perdre accès aux attestations vérifiées et à d'autres informations d'identification de programme. Vous pouvez en faire une copie pour vos dossiers avant de procéder à la suppression.",
|
||||
"account.settings.message.demographics.service.issue": "Une erreur s'est produite lors de la tentative de récupération ou d'enregistrement des informations de votre compte. Veuillez réessayer plus tard.",
|
||||
"account.settings.field.demographics.gender": "Identité de genre",
|
||||
"account.settings.field.demographics.gender.empty": "Ajouter une identité de genre",
|
||||
"account.settings.field.demographics.gender.options.empty": "Sélectionnez une identité de genre",
|
||||
"account.settings.field.demographics.gender_description": "Description de l'identité de genre",
|
||||
"account.settings.field.demographics.gender_description.empty": "Entrez la description",
|
||||
"account.settings.field.demographics.ethnicity": "Identité raciale / ethnique",
|
||||
"account.settings.field.demographics.ethnicity.empty": "Ajouter une identité raciale / ethnique",
|
||||
"account.settings.field.demographics.ethnicity.options.empty": "Sélectionnez tout ce qui s'y rapporte",
|
||||
"account.settings.field.demographics.income": "Revenu familial",
|
||||
"account.settings.field.demographics.income.empty": "Ajouter le revenu familial",
|
||||
"account.settings.field.demographics.income.options.empty": "Sélectionnez une fourchette de revenu familial",
|
||||
"account.settings.field.demographics.military_history": "Statut militaire U.S.",
|
||||
"account.settings.field.demographics.military_history.empty": "Ajouter un statut militaire",
|
||||
"account.settings.field.demographics.military_history.options.empty": "Sélectionnez un statut militaire",
|
||||
"account.settings.field.demographics.learner_education_level": "Votre niveau d'éducation",
|
||||
"account.settings.field.demographics.learner_education_level.empty": "Ajouter un niveau d'éducation",
|
||||
"account.settings.field.demographics.parent_education_level": "Niveau d'éducation des parents / tuteurs",
|
||||
"account.settings.field.demographics.parent_education_level.empty": "Ajouter un niveau d'éducation",
|
||||
"account.settings.field.demographics.education_level.options.empty": "Sélectionnez le niveau d'éducation",
|
||||
"account.settings.field.demographics.work_status": "Statut d'emploi",
|
||||
"account.settings.field.demographics.work_status.empty": "Ajouter un statut d'emploi",
|
||||
"account.settings.field.demographics.work_status.options.empty": "Sélectionnez le statut d'emploi",
|
||||
"account.settings.field.demographics.work_status_description": "Description du statut d'emploi",
|
||||
"account.settings.field.demographics.work_status_description.empty": "Entrez la description",
|
||||
"account.settings.field.demographics.current_work_sector": "Secteur d'emploi actuel",
|
||||
"account.settings.field.demographics.current_work_sector.empty": "Ajouter le secteur d'emploi",
|
||||
"account.settings.field.demographics.future_work_sector": "Secteur d'emploi futur",
|
||||
"account.settings.field.demographics.future_work_sector.empty": "Ajouter le secteur d'emploi",
|
||||
"account.settings.field.demographics.work_sector.options.empty": "Sélectionnez un secteur d'emploi",
|
||||
"account.settings.section.demographics.why": "Pourquoi est-ce que {siteName} collecte ces informations ?",
|
||||
"account.settings.name.change.title.id": "Ce changement de nom requiert une vérification d'identité.",
|
||||
"account.settings.name.change.title.begin": "Avant que l'on commençons",
|
||||
"account.settings.name.change.warning.one": "Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.",
|
||||
"account.settings.name.change.warning.two": "Cette action ne peut pas être renversée sans vérification d'identité.",
|
||||
"account.settings.name.change.id.name.label": "Enter your name as it appears on your government-issued ID.",
|
||||
"account.settings.name.change.id.name.placeholder": "Enter the name on your government ID",
|
||||
"account.settings.name.change.error.valid.name": "Entrez un nom valide",
|
||||
"account.settings.name.change.error.general": "Une erreur est survenue. Veuillez réessayer.",
|
||||
"account.settings.name.change.continue": "Continuer",
|
||||
"account.settings.name.change.cancel": "Annuler",
|
||||
"error.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
|
||||
"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.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",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "Nous avons envoyé un message à {email}. Cliquez sur le lien dans le message pour réinitialiser votre mot de passe. Vous n'avez pas reçu le message? Contactez {technicalSupportLink}.",
|
||||
"account.settings.editable.field.password.reset.button": "Réinitialiser le mot de passe",
|
||||
"account.settings.editable.field.password.reset.button.forbidden": "Votre demande précédente est en cours, veuillez réessayer dans quelques instants.",
|
||||
"account.settings.editable.field.password.reset.label": "Mot de passe",
|
||||
"account.settings.sso.link.account": "Se connecter avec {name}",
|
||||
"account.settings.sso.account.connected": "Lié",
|
||||
"account.settings.sso.account.disconnect.error": "Un problème est survenu lors de la déconnexion de ce compte. Contactez le support si le problème persiste.",
|
||||
"account.settings.sso.unlink.account": "Dissocier le compte {name}",
|
||||
"account.settings.sso.no.providers": "Aucun compte ne peut être lié pour le moment.",
|
||||
"id.verification.access.blocked.denied": "Vous ne pouvez pas vérifier votre identité pour le moment. Si vous n'avez pas encore activé votre compte, veuillez vérifier votre dossier de pourriels pour le courriel d'activation de {email}.",
|
||||
"id.verification.next": "Suivant",
|
||||
"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 identification card that contains your full name and photo.",
|
||||
"id.verification.privacy.title": "Privacy Information",
|
||||
"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 {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.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.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",
|
||||
"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: ",
|
||||
"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 {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 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: ",
|
||||
"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?",
|
||||
"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.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}.",
|
||||
"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}"
|
||||
"id.verification.continue.upload": "Continuer avec le téléchargement",
|
||||
"id.verification.example.card.alt": "Exemple de carte d'identité valide avec un nom complet et une photo.",
|
||||
"id.verification.requirements.title": "Exigences de vérification des photos",
|
||||
"id.verification.requirements.description": "Afin de procéder à la vérification des photos, vous aurez besoin des éléments suivants :",
|
||||
"id.verification.requirements.card.device.title": "Appareil avec caméra",
|
||||
"id.verification.requirements.card.device.allow": "Autoriser",
|
||||
"id.verification.requirements.card.id.title": "Carte d'identité avec photo",
|
||||
"id.verification.requirements.card.id.text": "Vous avez besoin d'une carte d'identité valide contenant votre nom complet et votre photo, comme un permis de conduire ou un passeport.",
|
||||
"id.verification.privacy.title": "Information sur la confidentialité",
|
||||
"id.verification.privacy.need.photo.question": "Pourquoi est-ce que {siteName} a besoin de ma photo?",
|
||||
"id.verification.privacy.need.photo.answer": "Nous utilisons vos photos de vérification pour confirmer votre identité et assurer la validité de votre certificat.",
|
||||
"id.verification.privacy.do.with.photo.question": "Qu'est-ce que {siteName} fait avec cette photo?",
|
||||
"id.verification.privacy.do.with.photo.answer": "Nous chiffrons votre photo de manière sécurisée et l'envoyons à notre service d'autorisation pour vérification. Votre photo et vos informations ne sont pas enregistrées ni visibles nulle part sur {siteName} une fois le processus de vérification terminé.",
|
||||
"id.verification.access.blocked.title": "Vérification d'identité",
|
||||
"id.verification.access.blocked.enrollment": "Vous n'êtes actuellement pas inscrit à un cours nécessitant une vérification d'identité.",
|
||||
"id.verification.access.blocked.pending": "Vous avez déjà soumis vos informations de vérification. Vous verrez un message sur votre tableau de bord lorsque le processus de vérification sera terminé (généralement dans les 5 jours).",
|
||||
"id.verification.photo.take": "Prendre une photo",
|
||||
"id.verification.photo.retake": "Reprendre la photo ?",
|
||||
"id.verification.photo.enable.detection": "Activer la détection des visages",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "Si coché, une case apparaîtra autour de votre visage. Votre visage peut être vu clairement si la boîte qui l'entoure est bleue. Si votre visage n'est pas dans une bonne position ou indétectable, la case sera rouge.",
|
||||
"id.verification.photo.enable.detection.id.help.text": "Si coché, une case apparaîtra autour du visage sur votre pièce d'identité. Le visage peut être vu clairement si la boîte qui l'entoure est bleue. Si le visage n'est pas dans une bonne position ou indétectable, la case sera rouge.",
|
||||
"id.verification.photo.feedback.correct": "Le visage est en bonne position.",
|
||||
"id.verification.photo.feedback.two.faces": "Plus d'un visage détecté.",
|
||||
"id.verification.photo.feedback.no.faces": "Aucun visage détecté.",
|
||||
"id.verification.photo.feedback.top.left": "Position incorrecte. En haut à gauche.",
|
||||
"id.verification.photo.feedback.top.center": "Position incorrecte. En haut au centre.",
|
||||
"id.verification.photo.feedback.top.right": "Position incorrecte. En haut à droite.",
|
||||
"id.verification.photo.feedback.center.left": "Position incorrecte. Centre gauche.",
|
||||
"id.verification.photo.feedback.center.center": "Position incorrecte. Trop près de la caméra.",
|
||||
"id.verification.photo.feedback.center.right": "Position incorrecte. Centre droit.",
|
||||
"id.verification.photo.feedback.bottom.left": "Position incorrecte. En bas à gauche.",
|
||||
"id.verification.photo.feedback.bottom.center": "Position incorrecte. En bas au centre.",
|
||||
"id.verification.photo.feedback.bottom.right": "Position incorrecte. En bas à droite.",
|
||||
"id.verification.camera.access.title": "Autorisations de la caméra",
|
||||
"id.verification.camera.access.title.success": "Accès à la caméra activé",
|
||||
"id.verification.camera.access.title.failed": "L'accès à la caméra a échoué",
|
||||
"id.verification.camera.access.click.allow": "Veuillez vous assurer de cliquer sur \"Autoriser\"",
|
||||
"id.verification.camera.access.enable": "Activer la caméra",
|
||||
"id.verification.camera.access.problems": "Vous avez des problèmes?",
|
||||
"id.verification.camera.access.skip": "Ignorer et télécharger les fichiers image au lieu",
|
||||
"id.verification.camera.access.success": "Il semble que votre appareil photo fonctionne et est prêt.",
|
||||
"id.verification.camera.access.failure": "Il semble que nous ne puissions pas accéder à votre caméra. Vous devrez télécharger des fichiers image de vous et de votre pièce d'identité.",
|
||||
"id.verification.camera.access.failure.temporary": "Il semblerait que nous ne pouvons pas accéder à votre caméra. Vérifiez qu'elle est bien connectée et que votre navigateur est autorisé à y accéder.",
|
||||
"id.verification.camera.access.failure.temporary.chrome": "Pour activer l'accès à la caméra dans Chrome:",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step1": "Ouvrir Chrome.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2": "Accédez à Plus> Paramètres.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "Pour Windows: Alt+F, Alt+E ou F10 suivi de la barre d'espace",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "Pour Mac: Command+,",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step3": "Sous l'onglet «Confidentialité et sécurité», sélectionnez «Paramètres du site», puis «Appareil photo».",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step4": "Sous \"Bloqué\", trouvez \"edx.org\" et sélectionnez-le.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step5": "Dans la section \"Autorisations\", mettez à jour les autorisations de la caméra pour \"Autoriser\".",
|
||||
"id.verification.camera.access.failure.temporary.ie11": "Pour activer l'accès à la caméra dans Internet Explorer:",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step1": "Ouvrez le Gestionnaire des paramètres de Flash Player en accédant à Paramètres Windows> Panneau de configuration> Flash Player.",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step2": "Sélectionnez l'onglet \"Caméra et micro\", puis sélectionnez le bouton \"Paramètres de la caméra et du microphone par site\".",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step3": "Choisissez \"edx.org\" dans la liste des sites Web et modifiez les autorisations en sélectionnant \"Autoriser\" dans le menu déroulant.",
|
||||
"id.verification.camera.access.failure.temporary.firefox": "Pour activer l'accès à la caméra dans Firefox:",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step1": "Ouvrez Firefox.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step2": "Saisissez \"about:preferences\" dans la barre d'URL.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step3": "Sélectionnez l'onglet \"Confidentialité et sécurité\" et accédez à la section \"Autorisations\".",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step4": "À côté de \"Appareil photo\", sélectionnez le bouton \"Paramètres…\".",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step5": "Dans la barre de recherche, entrez \"edx.org\".",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step6": "Dans la colonne d'état de \"edx.org\", sélectionnez \"Autoriser\" dans la liste déroulante.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step7": "Sélectionnez \"Enregistrer les modifications\".",
|
||||
"id.verification.camera.access.failure.temporary.safari": "Pour activer l'accès à la caméra dans Safari:",
|
||||
"id.verification.camera.access.failure.temporary.safari.step1": "Ouvrir Safari.",
|
||||
"id.verification.camera.access.failure.temporary.safari.step2": "Cliquez sur le menu de l'application Safari, puis sélectionnez \"Préférences\". Vous pouvez également utiliser Command+ comme raccourci clavier.",
|
||||
"id.verification.camera.access.failure.temporary.safari.step3": "Sélectionnez l'onglet \"Sites Web\", puis sélectionnez \"Appareil photo\".",
|
||||
"id.verification.camera.access.failure.temporary.safari.step4": "Sélectionnez \"edx.org\" et changez les autorisations de la caméra en \"Allow\".",
|
||||
"id.verification.camera.access.failure.unsupported": "Il semble que votre fureteur ne prend pas en charge l'accès à la caméra.",
|
||||
"id.verification.camera.access.failure.unsupported.chrome.explanation": "Le fureteur Chrome ne prend actuellement pas en charge l'accès à la caméra sur les appareils iOS, tels que les iPhones et les iPad.",
|
||||
"id.verification.camera.access.failure.unsupported.instructions": "Veuillez utiliser un autre navigateur pour terminer la vérification d'identité.",
|
||||
"id.verification.photo.tips.title": "Conseils photo utiles",
|
||||
"id.verification.photo.tips.description": "Ensuite, nous aurons besoin de vous preniez une photo de votre visage. Veuillez consulter les conseils utiles ci-dessous.",
|
||||
"id.verification.photo.tips.list.title": "Conseils photo",
|
||||
"id.verification.photo.tips.list.description": "Pour prendre une bonne photo, assurez-vous que: ",
|
||||
"id.verification.photo.tips.list.well.lit": "Votre visage est bien éclairé",
|
||||
"id.verification.photo.tips.list.inside.frame": " Votre visage est entièrement dans le cadre.",
|
||||
"id.verification.portrait.photo.title.camera": "Prenez une photo de vous",
|
||||
"id.verification.portrait.photo.title.upload": "Téléchargez une photo de vous",
|
||||
"id.verification.portrait.photo.preview.alt": "Aperçu de la photo du visage de l'utilisateur.",
|
||||
"id.verification.portrait.photo.instructions.camera": "Lorsque votre visage est en position, utilisez le bouton Prendre une photo ci-dessous pour prendre votre photo.",
|
||||
"id.verification.portrait.photo.instructions.upload": "Veuillez télécharger une photo de portrait. Assurez-vous que tout votre visage s'insère dans le cadre et est bien éclairé. Formats pris en charge :",
|
||||
"id.verification.camera.help.sight.question": "Que faire si je ne peux pas voir l'image de la caméra ou si je ne peux pas voir ma photo pour déterminer quel côté est visible?",
|
||||
"id.verification.camera.help.sight.answer.portrait": "Vous pourrez peut-être terminer la procédure de capture d'image sans aide, mais cela peut prendre quelques tentatives de soumission pour obtenir le bon positionnement de la caméra. Le positionnement optimal de la caméra varie avec chaque ordinateur, mais généralement la meilleure position pour une prise de vue de la tête est d'environ 12-18 pouces (30-45 centimètres) de la caméra, la tête étant centrée par rapport à l'écran de l'ordinateur. Si les photos que vous soumettez sont rejetées, essayez de déplacer l’orientation de l’ordinateur ou de l’appareil photo pour modifier l’angle d’éclairage.",
|
||||
"id.verification.camera.help.sight.answer.id": "Vous pourrez peut-être terminer la procédure de capture d'image sans aide, mais cela peut prendre quelques tentatives de soumission pour obtenir le bon positionnement de la caméra. Le positionnement optimal de la caméra varie avec chaque ordinateur, mais généralement, la meilleure position pour une photo d'une carte d'identité est de 8-12 pouces (20-30 centimètres) de la caméra, la carte d'identité étant centrée par rapport à la caméra. Si les photos que vous soumettez sont rejetées, essayez de déplacer l’orientation de l’ordinateur ou de l’appareil photo pour modifier l’angle d’éclairage. La raison la plus courante de rejet est l'incapacité de lire le texte sur la carte d'identité.",
|
||||
"id.verification.camera.help.difficulty.question.portrait": "Que faire si j'ai des difficultés à maintenir ma tête en position par rapport à la caméra ?",
|
||||
"id.verification.camera.help.difficulty.question.id": "Que faire si j'ai des difficultés à tenir ma carte d'identité en position par rapport à la caméra ?",
|
||||
"id.verification.camera.help.difficulty.answer": "Si vous avez besoin d'aide pour prendre une photo à soumettre, contactez le support {siteName} pour des suggestions supplémentaires.",
|
||||
"id.verification.camera.help.upload.question": "Et si je souhaite télécharger une photo à la place ?",
|
||||
"id.verification.camera.help.upload.answer": "Sur la page suivante, vous aurez la possibilité de passer en mode téléchargement. En sélectionnant cette option, vous pourrez télécharger une photo à la place.",
|
||||
"id.verification.id.photo.unclear.question": "L'image de votre carte d'identité n'est pas claire ou trop floue ?",
|
||||
"id.verification.id.tips.title": "Conseils utiles pour les cartes d'identité",
|
||||
"id.verification.id.tips.description": "Ensuite, nous vous demanderons de prendre une photo d'une carte d'identité valide comportant votre nom complet et votre photo, comme un permis de conduire ou un passeport. Veuillez préparer votre carte d'identité.",
|
||||
"id.verification.id.tips.list.well.lit": "Votre carte d'identité est bien éclairée.",
|
||||
"id.verification.id.tips.list.clear": "Assurez-vous que vous pouvez voir votre photo et lire clairement votre nom.",
|
||||
"id.verification.id.photo.title.camera": "Prenez une photo de votre carte d'identité",
|
||||
"id.verification.id.photo.title.upload": "Téléversez une photo de votre carte d'identité",
|
||||
"id.verification.id.photo.preview.alt": "Aperçu de la pièce d'identité avec photo.",
|
||||
"id.verification.id.photo.instructions.camera": "Lorsque votre pièce d'identité est en place, utilisez le bouton Prendre une photo ci-dessous pour prendre votre photo. Veuillez utiliser un passeport, un permis de conduire ou une autre carte d'identité comportant votre nom complet et une photo de votre visage.",
|
||||
"id.verification.id.photo.instructions.upload": "Veuillez téléverser une photo de votre carte d'identité. Assurez-vous que la totalité de la carte d'identité rentre dans le cadre et qu'elle est bien éclairée. La taille du fichier doit être inférieure à 10 Mo. Formats pris en charge : ",
|
||||
"id.verification.id.photo.instructions.upload.error.invalidFileType": "Le fichier que vous avez sélectionné n'est pas un type d'image pris en charge. Veuillez choisir parmi les formats suivants :",
|
||||
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "Le fichier que vous avez sélectionné est trop volumineux. Veuillez réessayer avec un fichier de moins de 10 Mo.",
|
||||
"id.verification.account.name.title": "Vérification du nom du compte",
|
||||
"id.verification.name.check.title": "Vérifiez votre nom",
|
||||
"id.verification.account.name.instructions": "Le nom de votre compte et le nom de votre pièce d'identité doivent correspondre exactement. Sinon, cliquez sur \"Non\" pour mettre à jour le nom de votre compte.",
|
||||
"id.verification.name.check.instructions": "Le nom ci-dessous correspond-il au nom figurant sur votre pièce d'identité officielle ? Si ce n'est pas le cas, mettez à jour le nom ci-dessous afin qu'il corresponde à votre pièce d'identité.",
|
||||
"id.verification.name.check.mismatch.information": "Si le nom ci-dessous ne correspond pas à votre pièce d'identité, la vérification de votre identité sera refusée.",
|
||||
"id.verification.account.name.radio.label": "Le nom sur votre pièce d'identité correspond-il au nom du compte ci-dessous?",
|
||||
"id.verification.name.check.radio.label": "Sélectionnez une option",
|
||||
"id.verification.account.name.radio.yes": "Oui",
|
||||
"id.verification.name.check.radio.yes": "Oui, le nom ci-dessous correspond à mon ID",
|
||||
"id.verification.account.name.radio.no": "Non",
|
||||
"id.verification.name.check.radio.no": "Non, le nom ci-dessous ne correspond pas à mon ID",
|
||||
"id.verification.account.name.error": "Veuillez mettre à jour le nom de votre compte pour qu'il corresponde au nom sur votre pièce d'identité.",
|
||||
"id.verification.account.name.warning.prefix": "Veuillez noter:",
|
||||
"id.verification.account.name.settings": "Paramètres du compte",
|
||||
"id.verification.account.name.label": "Nom du compte",
|
||||
"id.verification.name.label": "Nom",
|
||||
"id.verification.account.name.photo.alt": "Photo de votre pièce d'identité à soumettre.",
|
||||
"id.verification.account.name.save": "Enregistrer et suivant",
|
||||
"id.verification.review.title": "Vérifiez vos photos",
|
||||
"id.verification.review.description": "Assurez-vous que nous pourrons vérifier votre identité avec les photos et les informations fournies.",
|
||||
"id.verification.review.portrait.label": "Votre portrait",
|
||||
"id.verification.review.portrait.alt": "Photo de votre visage à soumettre.",
|
||||
"id.verification.review.portrait.retake": "Reprendre la photo de portrait",
|
||||
"id.verification.review.id.label": "Votre carte d'identité",
|
||||
"id.verification.review.id.alt": "Photo de votre carte d'identité à présenter.",
|
||||
"id.verification.review.id.retake": "Reprendre la photo de la pièce d'identité",
|
||||
"id.verification.review.confirm": "Envoyez",
|
||||
"id.verification.submission.alert.error.face": "Une photo de votre visage est requise. Veuillez reprendre votre photo de portrait.",
|
||||
"id.verification.submission.alert.error.id": "Une photo de votre pièce d'identité est requise. Veuillez reprendre votre photo d'identité.",
|
||||
"id.verification.submission.alert.error.name": "Un nom de compte valide est requis. Veuillez mettre à jour le nom de votre compte pour qu'il corresponde au nom sur votre pièce d'identité.",
|
||||
"id.verification.submission.alert.error.unsupported": "Un ou plusieurs des fichiers que vous avez téléchargés sont dans un format non pris en charge. Veuillez choisir parmi les formats suivants :",
|
||||
"id.verification.review.error": "Page de Support {siteName}",
|
||||
"id.verification.submitted.title": "Vérification d'identité en cours",
|
||||
"id.verification.submitted.text": "Nous avons reçu vos informations et vérifions votre identité. Vous verrez un message sur votre tableau de bord lorsque le processus de vérification sera terminé (généralement dans les 5 jours). En attendant, vous pouvez toujours accéder à tous les contenus de cours disponibles.",
|
||||
"id.verification.return.dashboard": "Retour au Tableau de bord",
|
||||
"id.verification.return.course": "Revenir au cours",
|
||||
"id.verification.photo.upload.help.title": "Téléchargez une photo à la place",
|
||||
"id.verification.photo.camera.help.title": "Utilisez votre appareil photo à la place",
|
||||
"id.verification.photo.upload.help.text": "Si vous rencontrez des difficultés lors de l'utilisation de la capture de photo ci-dessus, vous souhaiterez peut-être télécharger une photo à la place. Pour télécharger une photo, cliquez sur le bouton ci-dessous.",
|
||||
"id.verification.photo.camera.help.text": "Si vous rencontrez des difficultés pour télécharger une photo ci-dessus, vous pouvez utiliser votre appareil photo à la place. Pour utiliser votre appareil photo, cliquez sur le bouton ci-dessous.",
|
||||
"id.verification.upload.help.button": "Passer en mode de téléchargement",
|
||||
"id.verification.camera.help.button": "Passer en mode caméra",
|
||||
"id.verification.choose.mode.title": "Options d'exigences relatives aux photos",
|
||||
"id.verification.choose.mode.hep.text": "Pour terminer la vérification, veuillez sélectionner l'une des options suivantes pour soumettre des photos. Vous pourrez basculer entre ces options tout au long du processus si nécessaire.",
|
||||
"id.verification.choose.mode.radio.upload": "Télécharger des photos depuis mon appareil",
|
||||
"id.verification.choose.mode.radio.camera": "Prendre des photos avec mon appareil photo",
|
||||
"id.verification.account.name.managed.alert": "Les paramètres de votre profil sont gérés par {managerTitle}, vous n'êtes donc pas autorisé à mettre à jour votre nom. Veuillez contacter votre administrateur {profileDataManager} ou {support} pour obtenir de l'aide.",
|
||||
"id.verification.request.camera.access.instructions": "Afin de prendre une photo à l'aide de votre webcam, vous pouvez recevoir une invite du navigateur pour accéder à votre caméra. {clickAllow}",
|
||||
"id.verification.requirements.account.managed.alert": "Les paramètres de votre compte sont gérés par {managerTitle}. Si le nom sur votre pièce d'identité avec photo ne correspond pas au nom de votre compte, veuillez contacter votre administrateur {profileDataManager} ou {support} pour obtenir de l'aide avant de terminer le processus de vérification des photos.",
|
||||
"id.verification.requirements.card.device.text": "Vous avez besoin d'un appareil équipé d'une caméra. Si vous recevez une invite du navigateur pour accéder à votre caméra, assurez-vous de cliquer sur {allow}.",
|
||||
"id.verification.account.name.summary.alert": "Les paramètres de votre compte sont gérés par {managerTitle}. Si le nom sur votre pièce d'identité avec photo ne correspond pas au nom de votre compte, veuillez contacter votre administrateur {profileDataManager} ou {support} pour obtenir de l'aide.",
|
||||
"idv.submission.alert.error": "\n Nous avons rencontré une erreur technique en essayant de soumettre la vérification d'identité.\n Il peut s'agir d'un problème temporaire. Veuillez réessayer dans quelques minutes.\n Si le problème persiste, veuillez consulter {support_link} pour obtenir de l'aide.\n ",
|
||||
"id.verification.account.name.edit": "Modifier {sr}"
|
||||
}
|
||||
@@ -20,6 +20,24 @@
|
||||
"account.settings.field.full.name": "Full name",
|
||||
"account.settings.field.full.name.empty": "Add name",
|
||||
"account.settings.field.full.name.help.text": "The name that is used for ID verification and that appears on your certificates.",
|
||||
"account.settings.field.full.name.help.text.non.certificate": "The name that appears on your public profile.",
|
||||
"account.settings.field.full.name.help.text.certificate": "This name is selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified": "Verified name",
|
||||
"account.settings.field.name.verified.help.text.verified": "This name has been verified by government ID.",
|
||||
"account.settings.field.name.verified.help.text.certificate": "This name has been verified by government ID and selected to appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Verified name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Verified name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.verification.help": "Enter your name as it appears on your government-issued ID.",
|
||||
"account.settings.field.full.name.help.text.submitted": "Verification has been submitted. This usually takes 48 hours or less. Full name cannot be changed at this time.",
|
||||
"account.settings.field.full.name.help.text.submitted.certificate": "When identity verification is successful, this name will appear on your certificates and public-facing records. Full name cannot be changed at this time.",
|
||||
"account.settings.field.name.verified.success.message": "Your identity verification request has successfully completed. You now have the option of selecting which name you prefer to appear on your certificates and public-records.",
|
||||
"account.settings.field.name.verified.success.message.header": "Your name change request is complete!",
|
||||
"account.settings.field.name.verified.failure.message": "Your most recent identity verification attempt did not pass. Related account settings have been restored.",
|
||||
"account.settings.field.name.verified.failure.message.header": "We were not able to verify your identity.",
|
||||
"account.settings.field.name.verified.failure.message.help.link": "Learn more about ID verification",
|
||||
"account.settings.field.name.verified.submitted.message": "Your identity verification request has been submitted and usually takes between 24 and 48 hours to complete.",
|
||||
"account.settings.field.name.verified.submitted.message.certificate": "When your request is approved, your updated name will appear on all associated certificates and public-facing records.",
|
||||
"account.settings.field.name.verified.submitted.message.header": "Your name change request is almost complete!",
|
||||
"account.settings.field.email": "Email address (Sign in)",
|
||||
"account.settings.field.email.empty": "Add email address",
|
||||
"account.settings.field.email.confirmation": "We’ve sent a confirmation message to {value}. Click the link in the message to update your email address.",
|
||||
@@ -80,6 +98,12 @@
|
||||
"account.settings.editable.field.action.edit": "Edit",
|
||||
"account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
|
||||
"account.settings.static.field.empty.no.admin": "No value set.",
|
||||
"account.settings.field.name.certificate.select": "If checked, this name will appear on your certificates and public-facing records.",
|
||||
"account.settings.field.name.modal.certificate.title": "Choose a preferred name for certificates and public-facing records",
|
||||
"account.settings.field.name.modal.certificate.select": "Select a name",
|
||||
"account.settings.field.name.modal.certificate.option.full": "Full Name",
|
||||
"account.settings.field.name.modal.certificate.option.verified": "Verified Name",
|
||||
"account.settings.field.name.modal.certificate.button.choose": "Choose name",
|
||||
"account.settings.coaching.consent.welcome.header": "Let’s 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 you’re 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.*",
|
||||
@@ -157,6 +181,16 @@
|
||||
"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 {siteName} collect this information?",
|
||||
"account.settings.name.change.title.id": "This name change requires identity verification",
|
||||
"account.settings.name.change.title.begin": "Before we begin",
|
||||
"account.settings.name.change.warning.one": "Warning: This action updates the name that appears on all certificates that have been earned on this account in the past and any certificates you are currently earning or will earn in the future.",
|
||||
"account.settings.name.change.warning.two": "This action cannot be undone without verifying your identity.",
|
||||
"account.settings.name.change.id.name.label": "Enter your name as it appears on your government-issued ID.",
|
||||
"account.settings.name.change.id.name.placeholder": "Enter the name on your government ID",
|
||||
"account.settings.name.change.error.valid.name": "Please enter a valid name.",
|
||||
"account.settings.name.change.error.general": "A technical error occurred. Please try again.",
|
||||
"account.settings.name.change.continue": "Continue",
|
||||
"account.settings.name.change.cancel": "Cancel",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
|
||||
@@ -174,11 +208,11 @@
|
||||
"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.description": "In order to complete Photo Verification, you will need the following:",
|
||||
"id.verification.requirements.card.device.title": "Device with Camera",
|
||||
"id.verification.requirements.card.device.allow": "Allow",
|
||||
"id.verification.requirements.card.id.title": "Photo Identification",
|
||||
"id.verification.requirements.card.id.text": "You need a valid identification card that contains your full name and photo.",
|
||||
"id.verification.requirements.card.id.title": "Photo Identification Card",
|
||||
"id.verification.requirements.card.id.text": "You need a valid identification card that contains your full name and photo, such as a driver’s license or passport.",
|
||||
"id.verification.privacy.title": "Privacy Information",
|
||||
"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.",
|
||||
@@ -261,27 +295,34 @@
|
||||
"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 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.photo.unclear.question": "Is your ID card image not clear or too blurry?",
|
||||
"id.verification.id.tips.title": "Helpful Identification Card Tips",
|
||||
"id.verification.id.tips.description": "Next, we'll need you to take a photo of a valid identification card that includes your full name and photo, such as a driver’s license or passport. Please have your ID ready.",
|
||||
"id.verification.id.tips.list.well.lit": "Your identification card is well-lit.",
|
||||
"id.verification.id.tips.list.clear": "Ensure that you can see your photo and clearly read your name.",
|
||||
"id.verification.id.photo.title.camera": "Take a Photo of Your ID",
|
||||
"id.verification.id.photo.title.upload": "Upload a Photo of Your ID",
|
||||
"id.verification.id.photo.title.camera": "Take a Photo of Your Identification Card",
|
||||
"id.verification.id.photo.title.upload": "Upload a Photo of Your Identification Card",
|
||||
"id.verification.id.photo.preview.alt": "Preview of photo ID.",
|
||||
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
|
||||
"id.verification.id.photo.instructions.camera": "When your ID is in position, use the Take Photo button below to take your photo. Please use a passport, driver’s license, or another identification card that includes your full name and a picture of your face.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload a photo of your identification card. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
|
||||
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
|
||||
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
|
||||
"id.verification.account.name.title": "Account Name Check",
|
||||
"id.verification.name.check.title": "Double-Check Your Name",
|
||||
"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.name.check.instructions": "Does the name below match the name on your government-issued ID? If not, update the name below to match your goverment-issued ID.",
|
||||
"id.verification.name.check.mismatch.information": "If the name below does not match your government-issued ID, your identity verification will be denied.",
|
||||
"id.verification.account.name.radio.label": "Does the name on your ID match the Account Name below?",
|
||||
"id.verification.name.check.radio.label": "Select an option",
|
||||
"id.verification.account.name.radio.yes": "Yes",
|
||||
"id.verification.name.check.radio.yes": "Yes, the name below matches my ID",
|
||||
"id.verification.account.name.radio.no": "No",
|
||||
"id.verification.name.check.radio.no": "No, the name below does not match my ID",
|
||||
"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.name.label": "Name",
|
||||
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
|
||||
"id.verification.account.name.save": "Save and Next",
|
||||
"id.verification.review.title": "Review Your Photos",
|
||||
@@ -289,8 +330,8 @@
|
||||
"id.verification.review.portrait.label": "Your Portrait",
|
||||
"id.verification.review.portrait.alt": "Photo of your face to be submitted.",
|
||||
"id.verification.review.portrait.retake": "Retake Portrait Photo",
|
||||
"id.verification.review.id.label": "Your Photo ID",
|
||||
"id.verification.review.id.alt": "Photo of your ID to be submitted.",
|
||||
"id.verification.review.id.label": "Your Identification Card",
|
||||
"id.verification.review.id.alt": "Photo of your identification card to be submitted.",
|
||||
"id.verification.review.id.retake": "Retake ID Photo",
|
||||
"id.verification.review.confirm": "Submit",
|
||||
"id.verification.submission.alert.error.face": "A photo of your face is required. Please retake your portrait photo.",
|
||||
|
||||
@@ -28,7 +28,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.requirements.description': {
|
||||
id: 'id.verification.requirements.description',
|
||||
defaultMessage: 'In order to complete Photo Verification online, you will need the following:',
|
||||
defaultMessage: 'In order to complete Photo Verification, you will need the following:',
|
||||
description: 'Description for the Photo Verification Requirements page.',
|
||||
},
|
||||
'id.verification.requirements.card.device.title': {
|
||||
@@ -43,12 +43,12 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.requirements.card.id.title': {
|
||||
id: 'id.verification.requirements.card.id.title',
|
||||
defaultMessage: 'Photo Identification',
|
||||
defaultMessage: 'Photo Identification Card',
|
||||
description: 'Title for the Photo Identification requirement card.',
|
||||
},
|
||||
'id.verification.requirements.card.id.text': {
|
||||
id: 'id.verification.requirements.card.id.text',
|
||||
defaultMessage: 'You need a valid identification card that contains your full name and photo.',
|
||||
defaultMessage: 'You need a valid identification card that contains your full name and photo, such as a driver’s license or passport.',
|
||||
description: 'Text that explains that the user needs a photo ID.',
|
||||
},
|
||||
'id.verification.privacy.title': {
|
||||
@@ -463,22 +463,22 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.id.photo.unclear.question': {
|
||||
id: 'id.verification.id.photo.unclear.question',
|
||||
defaultMessage: 'Is your ID image not clear or too blurry?',
|
||||
defaultMessage: 'Is your ID card image not clear or too blurry?',
|
||||
description: 'Question on what to do if the user\'s ID image is unclear',
|
||||
},
|
||||
'id.verification.id.tips.title': {
|
||||
id: 'id.verification.id.tips.title',
|
||||
defaultMessage: 'Helpful ID Tips',
|
||||
defaultMessage: 'Helpful Identification Card Tips',
|
||||
description: 'Title for the ID Tips page.',
|
||||
},
|
||||
'id.verification.id.tips.description': {
|
||||
id: 'id.verification.id.tips.description',
|
||||
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.',
|
||||
defaultMessage: 'Next, we\'ll need you to take a photo of a valid identification card that includes your full name and photo, such as a driver’s license or passport. Please have your ID ready.',
|
||||
description: 'Description for the ID Tips page.',
|
||||
},
|
||||
'id.verification.id.tips.list.well.lit': {
|
||||
id: 'id.verification.id.tips.list.well.lit',
|
||||
defaultMessage: 'Your ID is well-lit.',
|
||||
defaultMessage: 'Your identification card is well-lit.',
|
||||
description: 'Tip to ensure ID is well lit.',
|
||||
},
|
||||
'id.verification.id.tips.list.clear': {
|
||||
@@ -488,12 +488,12 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.id.photo.title.camera': {
|
||||
id: 'id.verification.id.photo.title.camera',
|
||||
defaultMessage: 'Take a Photo of Your ID',
|
||||
defaultMessage: 'Take a Photo of Your Identification Card',
|
||||
description: 'Title for the ID Photo page if camera access is enabled.',
|
||||
},
|
||||
'id.verification.id.photo.title.upload': {
|
||||
id: 'id.verification.id.photo.title.upload',
|
||||
defaultMessage: 'Upload a Photo of Your ID',
|
||||
defaultMessage: 'Upload a Photo of Your Identification Card',
|
||||
description: 'Title for the ID Photo page if camera access is disabled.',
|
||||
},
|
||||
'id.verification.id.photo.preview.alt': {
|
||||
@@ -503,12 +503,12 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.id.photo.instructions.camera': {
|
||||
id: 'id.verification.id.photo.instructions.camera',
|
||||
defaultMessage: 'When your ID is in position, use the Take Photo button below to take your photo.',
|
||||
defaultMessage: 'When your ID is in position, use the Take Photo button below to take your photo. Please use a passport, driver’s license, or another identification card that includes your full name and a picture of your face.',
|
||||
description: 'Instructions to use the camera to take an ID photo.',
|
||||
},
|
||||
'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: ',
|
||||
defaultMessage: 'Please upload a photo of your identification card. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ',
|
||||
description: 'Instructions for ID photo upload.',
|
||||
},
|
||||
'id.verification.id.photo.instructions.upload.error.invalidFileType': {
|
||||
@@ -526,26 +526,56 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Account Name Check',
|
||||
description: 'Title for the Account Name Check page.',
|
||||
},
|
||||
'id.verification.name.check.title': {
|
||||
id: 'id.verification.name.check.title',
|
||||
defaultMessage: 'Double-Check Your Name',
|
||||
description: 'Title for the page where a user double-checks that their name is correct.',
|
||||
},
|
||||
'id.verification.account.name.instructions': {
|
||||
id: 'id.verification.account.name.instructions',
|
||||
defaultMessage: '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.',
|
||||
description: 'Text to verify that the account name matches the name on the ID photo.',
|
||||
},
|
||||
'id.verification.name.check.instructions': {
|
||||
id: 'id.verification.name.check.instructions',
|
||||
defaultMessage: 'Does the name below match the name on your government-issued ID? If not, update the name below to match your goverment-issued ID.',
|
||||
description: 'Text to instruct the user to check that the name displayed on the page matches what is on their government-issued ID.',
|
||||
},
|
||||
'id.verification.name.check.mismatch.information': {
|
||||
id: 'id.verification.name.check.mismatch.information',
|
||||
defaultMessage: 'If the name below does not match your government-issued ID, your identity verification will be denied.',
|
||||
description: 'Text to inform the user that if the name displayed on the page does not match what is on their government-issued ID, identity verification will be denied.',
|
||||
},
|
||||
'id.verification.account.name.radio.label': {
|
||||
id: 'id.verification.account.name.radio.label',
|
||||
defaultMessage: 'Does the name on your ID match the Account Name below?',
|
||||
description: 'Question to ask the user whether their account name match the name on their ID card.',
|
||||
},
|
||||
'id.verification.name.check.radio.label': {
|
||||
id: 'id.verification.name.check.radio.label',
|
||||
defaultMessage: 'Select an option',
|
||||
description: 'Label for a radio button group where the user needs to choose one of two options.',
|
||||
},
|
||||
'id.verification.account.name.radio.yes': {
|
||||
id: 'id.verification.account.name.radio.yes',
|
||||
defaultMessage: 'Yes',
|
||||
description: 'The radio button that says the account name matches.',
|
||||
},
|
||||
'id.verification.name.check.radio.yes': {
|
||||
id: 'id.verification.name.check.radio.yes',
|
||||
defaultMessage: 'Yes, the name below matches my ID',
|
||||
description: 'Label for a radio button that indicates that the name displayed on the page matches the name on the user\'s ID.',
|
||||
},
|
||||
'id.verification.account.name.radio.no': {
|
||||
id: 'id.verification.account.name.radio.no',
|
||||
defaultMessage: 'No',
|
||||
description: 'The radio button that says the account name does not match.',
|
||||
},
|
||||
'id.verification.name.check.radio.no': {
|
||||
id: 'id.verification.name.check.radio.no',
|
||||
defaultMessage: 'No, the name below does not match my ID',
|
||||
description: 'Label for a radio button that indicates that the name displayed on the page does not match the name on the user\'s ID.',
|
||||
},
|
||||
'id.verification.account.name.error': {
|
||||
id: 'id.verification.account.name.error',
|
||||
defaultMessage: 'Please update account name to match the name on your ID.',
|
||||
@@ -566,6 +596,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Account Name',
|
||||
description: 'Label for account name input.',
|
||||
},
|
||||
'id.verification.name.label': {
|
||||
id: 'id.verification.name.label',
|
||||
defaultMessage: 'Name',
|
||||
description: 'Label for name input.',
|
||||
},
|
||||
'id.verification.account.name.photo.alt': {
|
||||
id: 'id.verification.account.name.photo.alt',
|
||||
defaultMessage: 'Photo of your ID to be submitted.',
|
||||
@@ -603,12 +638,12 @@ const messages = defineMessages({
|
||||
},
|
||||
'id.verification.review.id.label': {
|
||||
id: 'id.verification.review.id.label',
|
||||
defaultMessage: 'Your Photo ID',
|
||||
defaultMessage: 'Your Identification Card',
|
||||
description: 'Label for the Photo ID card.',
|
||||
},
|
||||
'id.verification.review.id.alt': {
|
||||
id: 'id.verification.review.id.alt',
|
||||
defaultMessage: 'Photo of your ID to be submitted.',
|
||||
defaultMessage: 'Photo of your identification card to be submitted.',
|
||||
description: 'Alt text for the ID photo.',
|
||||
},
|
||||
'id.verification.review.id.retake': {
|
||||
|
||||
@@ -4,23 +4,27 @@ import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import { getProfileDataManager } from '../account-settings/data/service';
|
||||
import PageLoading from '../account-settings/PageLoading';
|
||||
import { useAsyncCall } from '../hooks';
|
||||
|
||||
import { getExistingIdVerification, getEnrollments } from './data/service';
|
||||
import AccessBlocked from './AccessBlocked';
|
||||
import { hasGetUserMediaSupport } from './getUserMediaShim';
|
||||
import IdVerificationContext, { MEDIA_ACCESS, ERROR_REASONS, VERIFIED_MODES } from './IdVerificationContext';
|
||||
import { VerifiedNameContext } from './VerifiedNameContext';
|
||||
|
||||
export default function IdVerificationContextProvider({ children }) {
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
const { isVerifiedNameHistoryLoading, verifiedName, verifiedNameEnabled } = useContext(VerifiedNameContext);
|
||||
|
||||
// Call verification status endpoint to check whether we can verify.
|
||||
const [existingIdVerification, setExistingIdVerification] = useState(null);
|
||||
const [isIDVerificationLoading, idVerificationData] = useAsyncCall(getExistingIdVerification);
|
||||
const [isEnrollmentsLoading, enrollmentsData] = useAsyncCall(getEnrollments);
|
||||
useEffect(() => {
|
||||
// Call verification status endpoint to check whether we can verify.
|
||||
(async () => {
|
||||
const existingIdV = await getExistingIdVerification();
|
||||
setExistingIdVerification(existingIdV);
|
||||
})();
|
||||
}, []);
|
||||
if (idVerificationData) {
|
||||
setExistingIdVerification(idVerificationData);
|
||||
}
|
||||
}, [idVerificationData]);
|
||||
|
||||
const [facePhotoFile, setFacePhotoFile] = useState(null);
|
||||
const [idPhotoFile, setIdPhotoFile] = useState(null);
|
||||
@@ -33,8 +37,9 @@ export default function IdVerificationContextProvider({ children }) {
|
||||
const [canVerify, setCanVerify] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
useEffect(() => {
|
||||
// Check for an existing verification attempt
|
||||
if (existingIdVerification && !existingIdVerification.canVerify) {
|
||||
// With verified name we can redo verification multiple times
|
||||
// if not a successful request prevents re-verification
|
||||
if (!verifiedNameEnabled && existingIdVerification && !existingIdVerification.canVerify) {
|
||||
const { status } = existingIdVerification;
|
||||
setCanVerify(false);
|
||||
if (status === 'pending' || status === 'approved') {
|
||||
@@ -42,22 +47,22 @@ export default function IdVerificationContextProvider({ children }) {
|
||||
} else {
|
||||
setError(ERROR_REASONS.CANNOT_VERIFY);
|
||||
}
|
||||
} else if (verifiedNameEnabled) {
|
||||
setCanVerify(true);
|
||||
}
|
||||
}, [existingIdVerification]);
|
||||
}, [existingIdVerification, verifiedNameEnabled]);
|
||||
|
||||
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 (!isEnrollmentsLoading && enrollmentsData) {
|
||||
const verifiedEnrollments = enrollmentsData.filter((enrollment) => (
|
||||
VERIFIED_MODES.includes(enrollment.mode)
|
||||
));
|
||||
if (verifiedEnrollments.length === 0) {
|
||||
setCanVerify(false);
|
||||
setError(ERROR_REASONS.COURSE_ENROLLMENT);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
}
|
||||
}, [enrollmentsData]);
|
||||
|
||||
const [profileDataManager, setProfileDataManager] = useState(null);
|
||||
useEffect(() => {
|
||||
@@ -95,7 +100,9 @@ export default function IdVerificationContextProvider({ children }) {
|
||||
mediaStream,
|
||||
mediaAccess,
|
||||
userId: authenticatedUser.userId,
|
||||
nameOnAccount: authenticatedUser.name,
|
||||
// If the learner has an applicable verified name, then this should override authenticatedUser.name
|
||||
// when determining the context value nameOnAccount.
|
||||
nameOnAccount: verifiedName || authenticatedUser.name,
|
||||
profileDataManager,
|
||||
optimizelyExperimentName,
|
||||
shouldUseCamera,
|
||||
@@ -135,7 +142,7 @@ export default function IdVerificationContextProvider({ children }) {
|
||||
};
|
||||
|
||||
// If we are waiting for verification status endpoint, show spinner.
|
||||
if (!existingIdVerification) {
|
||||
if (isIDVerificationLoading || isVerifiedNameHistoryLoading) {
|
||||
return <PageLoading srMessage="Loading verification status" />;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { idVerificationSelector } from './data/selectors';
|
||||
import './getUserMediaShim';
|
||||
|
||||
import IdVerificationContextProvider from './IdVerificationContextProvider';
|
||||
import { VerifiedNameContextProvider } from './VerifiedNameContext';
|
||||
import ReviewRequirementsPanel from './panels/ReviewRequirementsPanel';
|
||||
import ChooseModePanel from './panels/ChooseModePanel';
|
||||
import RequestCameraAccessPanel from './panels/RequestCameraAccessPanel';
|
||||
@@ -51,20 +52,22 @@ function IdVerificationPage(props) {
|
||||
<div className="page__id-verification container-fluid py-5">
|
||||
<div className="row">
|
||||
<div className="col-lg-6 col-md-8">
|
||||
<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} />
|
||||
<Route path={`${path}/id-context`} component={IdContextPanel} />
|
||||
<Route path={`${path}/get-name-id`} component={GetNameIdPanel} />
|
||||
<Route path={`${path}/take-id-photo`} component={TakeIdPhotoPanel} />
|
||||
<Route path={`${path}/summary`} component={SummaryPanel} />
|
||||
<Route path={`${path}/submitted`} component={SubmittedPanel} />
|
||||
</Switch>
|
||||
</IdVerificationContextProvider>
|
||||
<VerifiedNameContextProvider>
|
||||
<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} />
|
||||
<Route path={`${path}/id-context`} component={IdContextPanel} />
|
||||
<Route path={`${path}/get-name-id`} component={GetNameIdPanel} />
|
||||
<Route path={`${path}/take-id-photo`} component={TakeIdPhotoPanel} />
|
||||
<Route path={`${path}/summary`} component={SummaryPanel} />
|
||||
<Route path={`${path}/submitted`} component={SubmittedPanel} />
|
||||
</Switch>
|
||||
</IdVerificationContextProvider>
|
||||
</VerifiedNameContextProvider>
|
||||
</div>
|
||||
<div className="col-lg-6 col-md-4 pt-md-0 pt-4 text-right">
|
||||
<Button variant="link" className="px-0" onClick={() => setIsModalOpen(true)}>
|
||||
|
||||
38
src/id-verification/VerifiedNameContext.jsx
Normal file
38
src/id-verification/VerifiedNameContext.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React, { createContext, useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { getVerifiedNameHistory } from '../account-settings/data/service';
|
||||
import { getMostRecentApprovedOrPendingVerifiedName } from '../utils';
|
||||
import { useAsyncCall } from '../hooks';
|
||||
|
||||
export const VerifiedNameContext = createContext();
|
||||
|
||||
export function VerifiedNameContextProvider({ children }) {
|
||||
const [verifiedNameEnabled, setVerifiedNameEnabled] = useState(false);
|
||||
const [verifiedName, setVerifiedName] = useState('');
|
||||
const [isVerifiedNameHistoryLoading, verifiedNameHistory] = useAsyncCall(getVerifiedNameHistory);
|
||||
|
||||
useEffect(() => {
|
||||
if (verifiedNameHistory) {
|
||||
const { verified_name_enabled: verifiedNameFeatureEnabled, results } = verifiedNameHistory;
|
||||
setVerifiedNameEnabled(verifiedNameFeatureEnabled);
|
||||
|
||||
if (verifiedNameFeatureEnabled) {
|
||||
const applicableVerifiedName = getMostRecentApprovedOrPendingVerifiedName(results);
|
||||
setVerifiedName(applicableVerifiedName);
|
||||
}
|
||||
}
|
||||
}, [verifiedNameHistory]);
|
||||
|
||||
const value = {
|
||||
isVerifiedNameHistoryLoading,
|
||||
verifiedNameEnabled,
|
||||
verifiedName,
|
||||
};
|
||||
|
||||
return (<VerifiedNameContext.Provider value={value}>{children}</VerifiedNameContext.Provider>);
|
||||
}
|
||||
|
||||
VerifiedNameContextProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
@@ -14,14 +14,6 @@
|
||||
max-height: 10rem;
|
||||
}
|
||||
}
|
||||
.form-check {
|
||||
padding: 0.5rem 0.5rem 1rem;
|
||||
.form-check-label {
|
||||
margin-left: 0.5rem;
|
||||
padding-top: 0.2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
.action-row {
|
||||
display: flex;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import IdVerificationContext from '../IdVerificationContext';
|
||||
import { VerifiedNameContext } from '../VerifiedNameContext';
|
||||
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
@@ -23,6 +24,7 @@ function GetNameIdPanel(props) {
|
||||
const {
|
||||
nameOnAccount, userId, profileDataManager, idPhotoName, setIdPhotoName,
|
||||
} = useContext(IdVerificationContext);
|
||||
const { verifiedNameEnabled } = useContext(VerifiedNameContext);
|
||||
const nameOnAccountValue = nameOnAccount || '';
|
||||
const invalidName = !nameMatches && (!idPhotoName || idPhotoName === nameOnAccount);
|
||||
const blankName = !nameOnAccount && !idPhotoName;
|
||||
@@ -91,64 +93,85 @@ function GetNameIdPanel(props) {
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={props.intl.formatMessage(messages['id.verification.account.name.title'])}
|
||||
title={
|
||||
verifiedNameEnabled
|
||||
? props.intl.formatMessage(messages['id.verification.name.check.title'])
|
||||
: props.intl.formatMessage(messages['id.verification.account.name.title'])
|
||||
}
|
||||
>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.instructions'])}
|
||||
{
|
||||
verifiedNameEnabled
|
||||
? props.intl.formatMessage(messages['id.verification.name.check.instructions'])
|
||||
: props.intl.formatMessage(messages['id.verification.account.name.instructions'])
|
||||
}
|
||||
</p>
|
||||
<p>
|
||||
{verifiedNameEnabled && props.intl.formatMessage(messages['id.verification.name.check.mismatch.information'])}
|
||||
</p>
|
||||
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group>
|
||||
<Form.Label htmlFor="nameMatchesYes">
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.radio.label'])}
|
||||
<Form.Label className="font-weight-bold" htmlFor="nameMatchesYes">
|
||||
{
|
||||
verifiedNameEnabled
|
||||
? props.intl.formatMessage(messages['id.verification.name.check.radio.label'])
|
||||
: props.intl.formatMessage(messages['id.verification.account.name.radio.label'])
|
||||
}
|
||||
</Form.Label>
|
||||
<Form.Row>
|
||||
<Form.Check
|
||||
type="radio"
|
||||
id="nameMatchesYes"
|
||||
name="nameMatches"
|
||||
data-testid="name-matches-yes"
|
||||
label={props.intl.formatMessage(messages['id.verification.account.name.radio.yes'])}
|
||||
checked={nameMatches}
|
||||
disabled={!nameOnAccount}
|
||||
inline
|
||||
onChange={() => {
|
||||
setNameMatches(true);
|
||||
setIdPhotoName(null);
|
||||
}}
|
||||
/>
|
||||
<Form.Check
|
||||
type="radio"
|
||||
id="nameMatchesNo"
|
||||
name="nameMatches"
|
||||
data-testid="name-matches-no"
|
||||
label={props.intl.formatMessage(messages['id.verification.account.name.radio.no'])}
|
||||
checked={!nameMatches}
|
||||
disabled={!nameOnAccount}
|
||||
inline
|
||||
onChange={() => setNameMatches(false)}
|
||||
/>
|
||||
</Form.Row>
|
||||
<Form.Check
|
||||
type="radio"
|
||||
id="nameMatchesYes"
|
||||
name="nameMatches"
|
||||
data-testid="name-matches-yes"
|
||||
label={verifiedNameEnabled ? props.intl.formatMessage(messages['id.verification.name.check.radio.yes']) : props.intl.formatMessage(messages['id.verification.account.name.radio.yes'])}
|
||||
checked={nameMatches}
|
||||
disabled={!nameOnAccount}
|
||||
onChange={() => {
|
||||
setNameMatches(true);
|
||||
setIdPhotoName(null);
|
||||
}}
|
||||
/>
|
||||
<Form.Check
|
||||
type="radio"
|
||||
id="nameMatchesNo"
|
||||
name="nameMatches"
|
||||
data-testid="name-matches-no"
|
||||
label={verifiedNameEnabled ? props.intl.formatMessage(messages['id.verification.name.check.radio.no']) : props.intl.formatMessage(messages['id.verification.account.name.radio.no'])}
|
||||
checked={!nameMatches}
|
||||
disabled={!nameOnAccount}
|
||||
onChange={() => setNameMatches(false)}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Label htmlFor="photo-id-name">
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.label'])}
|
||||
<Form.Label className="font-weight-bold" htmlFor="photo-id-name">
|
||||
{
|
||||
verifiedNameEnabled
|
||||
? props.intl.formatMessage(messages['id.verification.name.label'])
|
||||
: props.intl.formatMessage(messages['id.verification.account.name.label'])
|
||||
}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
controlId="photo-id-name"
|
||||
size="lg"
|
||||
type="text"
|
||||
ref={nameInputRef}
|
||||
readOnly={nameMatches || profileDataManager}
|
||||
readOnly={nameMatches || !!profileDataManager}
|
||||
isInvalid={invalidName || blankName}
|
||||
aria-describedby="photo-id-name-feedback"
|
||||
value={getNameValue()}
|
||||
onChange={e => setIdPhotoName(e.target.value)}
|
||||
data-testid="name-input"
|
||||
/>
|
||||
<Form.Control.Feedback id="photo-id-name-feedback" type="invalid">
|
||||
{getErrorMessage()}
|
||||
</Form.Control.Feedback>
|
||||
{(invalidName || !!profileDataManager) && (
|
||||
<Form.Control.Feedback
|
||||
id="photo-id-name-feedback"
|
||||
data-testid="id-name-feedback-message"
|
||||
type="invalid"
|
||||
>
|
||||
{getErrorMessage()}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
</Form>
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import IdVerificationContext from '../IdVerificationContext';
|
||||
import ImagePreview from '../ImagePreview';
|
||||
import { VerifiedNameContext } from '../VerifiedNameContext';
|
||||
|
||||
import messages from '../IdVerification.messages';
|
||||
import CameraHelpWithUpload from '../CameraHelpWithUpload';
|
||||
@@ -31,6 +32,7 @@ function SummaryPanel(props) {
|
||||
portraitPhotoMode,
|
||||
idPhotoMode,
|
||||
} = useContext(IdVerificationContext);
|
||||
const { verifiedNameEnabled } = useContext(VerifiedNameContext);
|
||||
const nameToBeUsed = idPhotoName || nameOnAccount || '';
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submissionError, setSubmissionError] = useState(null);
|
||||
@@ -72,6 +74,19 @@ function SummaryPanel(props) {
|
||||
};
|
||||
if (idPhotoName) {
|
||||
verificationData.idPhotoName = idPhotoName;
|
||||
} else if (verifiedNameEnabled) {
|
||||
/**
|
||||
* If learner has not entered an idPhotoName on the GetNameIdPanel,
|
||||
* and the verified name feature is enabled, use the current nameOnAccount
|
||||
* when submitting IDV. The reason we only do this if the feature is enabled
|
||||
* is that, when the feature is off, the server will change the learner's
|
||||
* profile name to this value. If we send the idPhotoName on all requests,
|
||||
* even ones where the learner does not change the idPhotoName, then the
|
||||
* server will record that the full name on the learner's profile has
|
||||
* a requested change, even if the name is the same. This will pollute
|
||||
* the history.
|
||||
*/
|
||||
verificationData.idPhotoName = nameOnAccount;
|
||||
}
|
||||
if (optimizelyExperimentName) {
|
||||
verificationData.optimizelyExperimentName = optimizelyExperimentName;
|
||||
@@ -205,14 +220,16 @@ function SummaryPanel(props) {
|
||||
{!optimizelyExperimentName && <CameraHelpWithUpload />}
|
||||
<div className="form-group">
|
||||
<label htmlFor="name-to-be-used" className="font-weight-bold">
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.label'])}
|
||||
{verifiedNameEnabled
|
||||
? props.intl.formatMessage(messages['id.verification.name.label'])
|
||||
: props.intl.formatMessage(messages['id.verification.account.name.label'])}
|
||||
</label>
|
||||
{renderManagedProfileMessage()}
|
||||
<div className="d-flex">
|
||||
<Input
|
||||
id="name-to-be-used"
|
||||
type="text"
|
||||
readOnly
|
||||
disabled
|
||||
value={nameToBeUsed}
|
||||
onChange={() => {}}
|
||||
aria-describedby={profileDataManager ? 'profile-manager-warning' : null}
|
||||
@@ -225,14 +242,29 @@ function SummaryPanel(props) {
|
||||
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>,
|
||||
}}
|
||||
/>
|
||||
{
|
||||
verifiedNameEnabled
|
||||
? (
|
||||
<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">Name</span>,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<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>
|
||||
|
||||
@@ -4,7 +4,6 @@ import { createMemoryHistory } from 'history';
|
||||
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';
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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';
|
||||
@@ -9,9 +8,11 @@ import { getProfileDataManager } from '../../account-settings/data/service';
|
||||
|
||||
import { getExistingIdVerification, getEnrollments } from '../data/service';
|
||||
import IdVerificationContextProvider from '../IdVerificationContextProvider';
|
||||
import { VerifiedNameContext } from '../VerifiedNameContext';
|
||||
|
||||
jest.mock('../../account-settings/data/service', () => ({
|
||||
getProfileDataManager: jest.fn(),
|
||||
getVerifiedNameHistory: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../data/service', () => ({
|
||||
@@ -30,12 +31,15 @@ describe('IdVerificationContextProvider', () => {
|
||||
});
|
||||
|
||||
it('renders correctly and calls getExistingIdVerification + getEnrollments', async () => {
|
||||
const context = { authenticatedUser: { userId: 3, roles: [] } };
|
||||
const appContext = { authenticatedUser: { userId: 3, roles: [] } };
|
||||
const verifiedNameContext = { verifiedName: '', verifiedNameEnabled: false };
|
||||
await act(async () => render((
|
||||
<AppContext.Provider value={context}>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContextProvider {...defaultProps} />
|
||||
</IntlProvider>
|
||||
<AppContext.Provider value={appContext}>
|
||||
<VerifiedNameContext.Provider value={verifiedNameContext}>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContextProvider {...defaultProps} />
|
||||
</IntlProvider>
|
||||
</VerifiedNameContext.Provider>
|
||||
</AppContext.Provider>
|
||||
)));
|
||||
expect(getExistingIdVerification).toHaveBeenCalled();
|
||||
@@ -43,23 +47,26 @@ describe('IdVerificationContextProvider', () => {
|
||||
});
|
||||
|
||||
it('calls getProfileDataManager if the user has any roles', async () => {
|
||||
const context = {
|
||||
const appContext = {
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'testname',
|
||||
roles: ['enterprise_learner'],
|
||||
},
|
||||
};
|
||||
const verifiedNameContext = { verifiedName: '', verifiedNameEnabled: false };
|
||||
await act(async () => render((
|
||||
<AppContext.Provider value={context}>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContextProvider {...defaultProps} />
|
||||
</IntlProvider>
|
||||
<AppContext.Provider value={appContext}>
|
||||
<VerifiedNameContext.Provider value={verifiedNameContext}>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContextProvider {...defaultProps} />
|
||||
</IntlProvider>
|
||||
</VerifiedNameContext.Provider>
|
||||
</AppContext.Provider>
|
||||
)));
|
||||
expect(getProfileDataManager).toHaveBeenCalledWith(
|
||||
context.authenticatedUser.username,
|
||||
context.authenticatedUser.roles,
|
||||
appContext.authenticatedUser.username,
|
||||
appContext.authenticatedUser.roles,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,13 @@ import * as selectors from '../data/selectors';
|
||||
|
||||
jest.mock('../data/selectors', () => jest.fn().mockImplementation(() => ({ idVerificationSelector: () => ({}) })));
|
||||
jest.mock('../IdVerificationContextProvider', () => jest.fn(({ children }) => children));
|
||||
jest.mock('../VerifiedNameContext', () => {
|
||||
const originalModule = jest.requireActual('../VerifiedNameContext');
|
||||
return {
|
||||
...originalModule,
|
||||
VerifiedNameContextProvider: jest.fn(({ children }) => children),
|
||||
};
|
||||
});
|
||||
jest.mock('../panels/ReviewRequirementsPanel');
|
||||
jest.mock('../panels/RequestCameraAccessPanel');
|
||||
jest.mock('../panels/PortraitPhotoContextPanel');
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { render, cleanup, waitFor } from '@testing-library/react';
|
||||
|
||||
import { getVerifiedNameHistory } from '../../account-settings/data/service';
|
||||
import { VerifiedNameContext, VerifiedNameContextProvider } from '../VerifiedNameContext';
|
||||
|
||||
const VerifiedNameContextTestComponent = () => {
|
||||
const { verifiedName, verifiedNameEnabled } = useContext(VerifiedNameContext);
|
||||
return (
|
||||
<>
|
||||
{verifiedNameEnabled && (<div data-testid="verified-name">{verifiedName}</div>)}
|
||||
<div data-testid="verified-name-enabled">{verifiedNameEnabled ? 'true' : 'false'}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
jest.mock('../../account-settings/data/service', () => ({
|
||||
getVerifiedNameHistory: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('VerifiedNameContextProvider', () => {
|
||||
const defaultProps = {
|
||||
children: <div />,
|
||||
intl: {},
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls getVerifiedNameHistory', async () => {
|
||||
jest.mock('../../account-settings/data/service', () => ({
|
||||
getVerifiedNameHistory: jest.fn(),
|
||||
}));
|
||||
|
||||
render(<VerifiedNameContextProvider {...defaultProps} />);
|
||||
expect(getVerifiedNameHistory).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('sets verifiedName and verifiedNameEnabled correctly when verified name feature enabled', async () => {
|
||||
const mockReturnValue = {
|
||||
verified_name_enabled: true,
|
||||
results: [{
|
||||
verified_name: 'Michael',
|
||||
status: 'approved',
|
||||
created: '2021-08-31T18:33:32.489200Z',
|
||||
}],
|
||||
};
|
||||
getVerifiedNameHistory.mockReturnValueOnce(mockReturnValue);
|
||||
|
||||
const { getByTestId } = render((
|
||||
<VerifiedNameContextProvider {...defaultProps}>
|
||||
<VerifiedNameContextTestComponent />
|
||||
</VerifiedNameContextProvider>
|
||||
));
|
||||
|
||||
await waitFor(() => expect(getVerifiedNameHistory).toHaveBeenCalledTimes(1));
|
||||
expect(getByTestId('verified-name')).toHaveTextContent('Michael');
|
||||
expect(getByTestId('verified-name-enabled')).toHaveTextContent('true');
|
||||
});
|
||||
|
||||
it('sets verifiedName and verifiedNameEnabled correctly when verified name feature not enabled', async () => {
|
||||
const mockReturnValue = {
|
||||
verified_name_enabled: false,
|
||||
results: [{
|
||||
verified_name: 'Michael',
|
||||
status: 'approved',
|
||||
created: '2021-08-31T18:33:32.489200Z',
|
||||
}],
|
||||
};
|
||||
getVerifiedNameHistory.mockReturnValueOnce(mockReturnValue);
|
||||
|
||||
const { queryByTestId } = render((
|
||||
<VerifiedNameContextProvider {...defaultProps}>
|
||||
<VerifiedNameContextTestComponent />
|
||||
</VerifiedNameContextProvider>
|
||||
));
|
||||
|
||||
await waitFor(() => expect(getVerifiedNameHistory).toHaveBeenCalledTimes(1));
|
||||
expect(queryByTestId('verified-name')).toBeNull();
|
||||
expect(queryByTestId('verified-name-enabled')).toHaveTextContent('false');
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ 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';
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import IdVerificationContext from '../../IdVerificationContext';
|
||||
import { VerifiedNameContext } from '../../VerifiedNameContext';
|
||||
import GetNameIdPanel from '../../panels/GetNameIdPanel';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
@@ -22,7 +23,7 @@ describe('GetNameIdPanel', () => {
|
||||
intl: {},
|
||||
};
|
||||
|
||||
const contextValue = {
|
||||
const IDVerificationContextValue = {
|
||||
nameOnAccount: 'test',
|
||||
userId: 3,
|
||||
idPhotoName: '',
|
||||
@@ -31,13 +32,19 @@ describe('GetNameIdPanel', () => {
|
||||
idPhotoFile: 'test.jpg',
|
||||
};
|
||||
|
||||
const verifiedNameContextValue = {
|
||||
verifiedNameEnabled: false,
|
||||
};
|
||||
|
||||
const getPanel = async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlGetNameIdPanel {...defaultProps} />
|
||||
</IdVerificationContext.Provider>
|
||||
<VerifiedNameContext.Provider value={verifiedNameContextValue}>
|
||||
<IdVerificationContext.Provider value={IDVerificationContextValue}>
|
||||
<IntlGetNameIdPanel {...defaultProps} />
|
||||
</IdVerificationContext.Provider>
|
||||
</VerifiedNameContext.Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
)));
|
||||
@@ -54,38 +61,46 @@ describe('GetNameIdPanel', () => {
|
||||
const noButton = await screen.findByTestId('name-matches-no');
|
||||
const input = await screen.findByTestId('name-input');
|
||||
const nextButton = await screen.findByTestId('next-button');
|
||||
const errorMessageQuery = await screen.queryByTestId('id-name-feedback-message');
|
||||
|
||||
expect(input).toHaveProperty('readOnly');
|
||||
expect(input).toHaveAttribute('readonly');
|
||||
expect(errorMessageQuery).toBeNull();
|
||||
|
||||
fireEvent.click(noButton);
|
||||
expect(input).toHaveProperty('readOnly', false);
|
||||
expect(input).not.toHaveAttribute('readonly');
|
||||
expect(nextButton.classList.contains('disabled')).toBe(true);
|
||||
expect(nextButton).toHaveAttribute('aria-disabled');
|
||||
|
||||
fireEvent.change(input, { target: { value: 'test change' } });
|
||||
expect(contextValue.setIdPhotoName).toHaveBeenCalled();
|
||||
expect(IDVerificationContextValue.setIdPhotoName).toHaveBeenCalled();
|
||||
// Ensure the feedback message on name shows when the user says the name does not match ID
|
||||
expect(await screen.queryByTestId('id-name-feedback-message')).toBeTruthy();
|
||||
|
||||
fireEvent.click(yesButton);
|
||||
expect(input).toHaveProperty('readOnly');
|
||||
expect(contextValue.setIdPhotoName).toHaveBeenCalled();
|
||||
expect(input).toHaveAttribute('readonly');
|
||||
expect(IDVerificationContextValue.setIdPhotoName).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('disables radio buttons + next button and enables input if account name is blank', async () => {
|
||||
contextValue.nameOnAccount = '';
|
||||
IDVerificationContextValue.nameOnAccount = '';
|
||||
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');
|
||||
const errorMessageQuery = await screen.queryByTestId('id-name-feedback-message');
|
||||
|
||||
expect(yesButton).toHaveProperty('disabled');
|
||||
expect(noButton).toHaveProperty('disabled');
|
||||
expect(input).toHaveProperty('readOnly', false);
|
||||
expect(yesButton).toBeDisabled();
|
||||
expect(noButton).toBeDisabled();
|
||||
expect(input).not.toHaveAttribute('readonly');
|
||||
expect(nextButton.classList.contains('disabled')).toBe(true);
|
||||
expect(nextButton).toHaveAttribute('aria-disabled');
|
||||
expect(errorMessageQuery).toBeTruthy();
|
||||
});
|
||||
|
||||
it('blocks the user from changing account name if managed by a third party', async () => {
|
||||
contextValue.profileDataManager = 'test-org';
|
||||
IDVerificationContextValue.profileDataManager = 'test-org';
|
||||
await getPanel();
|
||||
|
||||
const noButton = await screen.findByTestId('name-matches-no');
|
||||
@@ -93,8 +108,9 @@ describe('GetNameIdPanel', () => {
|
||||
const nextButton = await screen.findByTestId('next-button');
|
||||
|
||||
fireEvent.click(noButton);
|
||||
expect(input).toHaveProperty('readOnly');
|
||||
expect(input).toHaveAttribute('readonly');
|
||||
expect(nextButton.classList.contains('disabled')).toBe(true);
|
||||
expect(nextButton).toHaveAttribute('aria-disabled');
|
||||
const warning = await screen.getAllByText('test-org');
|
||||
expect(warning.length).toEqual(1);
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ import { createMemoryHistory } from 'history';
|
||||
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';
|
||||
|
||||
@@ -4,7 +4,6 @@ import { createMemoryHistory } from 'history';
|
||||
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 PortraitPhotoContextPanel from '../../panels/PortraitPhotoContextPanel';
|
||||
|
||||
@@ -5,7 +5,6 @@ import { createMemoryHistory } from 'history';
|
||||
import {
|
||||
render, screen, cleanup, act, 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 RequestCameraAccessPanel from '../../panels/RequestCameraAccessPanel';
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import {
|
||||
render, cleanup, act, screen,
|
||||
} from '@testing-library/react';
|
||||
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
render, cleanup, act, screen, fireEvent, waitFor,
|
||||
} from '@testing-library/react';
|
||||
import '@edx/frontend-platform/analytics';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import * as dataService from '../../data/service';
|
||||
import IdVerificationContext from '../../IdVerificationContext';
|
||||
import SummaryPanel from '../../panels/SummaryPanel';
|
||||
import { VerifiedNameContext } from '../../VerifiedNameContext';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics', () => ({
|
||||
sendTrackEvent: jest.fn(),
|
||||
@@ -27,7 +27,7 @@ describe('SummaryPanel', () => {
|
||||
intl: {},
|
||||
};
|
||||
|
||||
const contextValue = {
|
||||
const appContextValue = {
|
||||
facePhotoFile: 'test.jpg',
|
||||
idPhotoFile: 'test.jpg',
|
||||
nameOnAccount: 'test name',
|
||||
@@ -39,13 +39,19 @@ describe('SummaryPanel', () => {
|
||||
setReachedSummary: jest.fn(),
|
||||
};
|
||||
|
||||
const verifiedNameContextValue = {
|
||||
verifiedNameEnabled: false,
|
||||
};
|
||||
|
||||
const getPanel = async () => {
|
||||
await act(async () => render((
|
||||
<Router history={history}>
|
||||
<IntlProvider locale="en">
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
<IntlSummaryPanel {...defaultProps} />
|
||||
</IdVerificationContext.Provider>
|
||||
<VerifiedNameContext.Provider value={verifiedNameContextValue}>
|
||||
<IdVerificationContext.Provider value={appContextValue}>
|
||||
<IntlSummaryPanel {...defaultProps} />
|
||||
</IdVerificationContext.Provider>
|
||||
</VerifiedNameContext.Provider>
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
)));
|
||||
@@ -72,17 +78,17 @@ describe('SummaryPanel', () => {
|
||||
});
|
||||
|
||||
it('allows user to upload ID photo', async () => {
|
||||
contextValue.optimizelyExperimentName = '';
|
||||
appContextValue.optimizelyExperimentName = '';
|
||||
await getPanel();
|
||||
const collapsible = await screen.getAllByRole('button', { 'aria-expanded': false })[0];
|
||||
fireEvent.click(collapsible);
|
||||
const uploadButton = await screen.getByTestId('fileUpload');
|
||||
expect(uploadButton).toBeVisible();
|
||||
contextValue.optimizelyExperimentName = 'test-experiment';
|
||||
appContextValue.optimizelyExperimentName = 'test-experiment';
|
||||
});
|
||||
|
||||
it('displays warning if account is managed by a third party', async () => {
|
||||
contextValue.profileDataManager = 'test-org';
|
||||
appContextValue.profileDataManager = 'test-org';
|
||||
await getPanel();
|
||||
const warning = await screen.getAllByText('test-org');
|
||||
expect(warning.length).toEqual(1);
|
||||
@@ -90,29 +96,29 @@ describe('SummaryPanel', () => {
|
||||
|
||||
it('submits', async () => {
|
||||
const verificationData = {
|
||||
facePhotoFile: contextValue.facePhotoFile,
|
||||
idPhotoFile: contextValue.idPhotoFile,
|
||||
idPhotoName: contextValue.idPhotoName,
|
||||
optimizelyExperimentName: contextValue.optimizelyExperimentName,
|
||||
portraitPhotoMode: contextValue.portraitPhotoMode,
|
||||
idPhotoMode: contextValue.idPhotoMode,
|
||||
facePhotoFile: appContextValue.facePhotoFile,
|
||||
idPhotoFile: appContextValue.idPhotoFile,
|
||||
idPhotoName: appContextValue.idPhotoName,
|
||||
optimizelyExperimentName: appContextValue.optimizelyExperimentName,
|
||||
portraitPhotoMode: appContextValue.portraitPhotoMode,
|
||||
idPhotoMode: appContextValue.idPhotoMode,
|
||||
courseRunKey: null,
|
||||
};
|
||||
await getPanel();
|
||||
const button = await screen.findByTestId('submit-button');
|
||||
fireEvent.click(button);
|
||||
expect(dataService.submitIdVerification).toHaveBeenCalledWith(verificationData);
|
||||
await waitFor(() => expect(contextValue.stopUserMedia).toHaveBeenCalled());
|
||||
await waitFor(() => expect(appContextValue.stopUserMedia).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('does not submit a name if name is blank', async () => {
|
||||
contextValue.idPhotoName = '';
|
||||
appContextValue.idPhotoName = '';
|
||||
const verificationData = {
|
||||
facePhotoFile: contextValue.facePhotoFile,
|
||||
idPhotoFile: contextValue.idPhotoFile,
|
||||
portraitPhotoMode: contextValue.portraitPhotoMode,
|
||||
idPhotoMode: contextValue.idPhotoMode,
|
||||
optimizelyExperimentName: contextValue.optimizelyExperimentName,
|
||||
facePhotoFile: appContextValue.facePhotoFile,
|
||||
idPhotoFile: appContextValue.idPhotoFile,
|
||||
portraitPhotoMode: appContextValue.portraitPhotoMode,
|
||||
idPhotoMode: appContextValue.idPhotoMode,
|
||||
optimizelyExperimentName: appContextValue.optimizelyExperimentName,
|
||||
courseRunKey: null,
|
||||
};
|
||||
await getPanel();
|
||||
@@ -122,13 +128,13 @@ describe('SummaryPanel', () => {
|
||||
});
|
||||
|
||||
it('does not submit a name if name is unchanged', async () => {
|
||||
contextValue.idPhotoName = null;
|
||||
appContextValue.idPhotoName = null;
|
||||
const verificationData = {
|
||||
facePhotoFile: contextValue.facePhotoFile,
|
||||
idPhotoFile: contextValue.idPhotoFile,
|
||||
portraitPhotoMode: contextValue.portraitPhotoMode,
|
||||
idPhotoMode: contextValue.idPhotoMode,
|
||||
optimizelyExperimentName: contextValue.optimizelyExperimentName,
|
||||
facePhotoFile: appContextValue.facePhotoFile,
|
||||
idPhotoFile: appContextValue.idPhotoFile,
|
||||
portraitPhotoMode: appContextValue.portraitPhotoMode,
|
||||
idPhotoMode: appContextValue.idPhotoMode,
|
||||
optimizelyExperimentName: appContextValue.optimizelyExperimentName,
|
||||
courseRunKey: null,
|
||||
};
|
||||
await getPanel();
|
||||
@@ -137,6 +143,24 @@ describe('SummaryPanel', () => {
|
||||
expect(dataService.submitIdVerification).toHaveBeenCalledWith(verificationData);
|
||||
});
|
||||
|
||||
it('submits a name if a name is unchanged if verified name feature is enabled', async () => {
|
||||
appContextValue.idPhotoName = null;
|
||||
verifiedNameContextValue.verifiedNameEnabled = true;
|
||||
const verificationData = {
|
||||
facePhotoFile: appContextValue.facePhotoFile,
|
||||
idPhotoFile: appContextValue.idPhotoFile,
|
||||
portraitPhotoMode: appContextValue.portraitPhotoMode,
|
||||
idPhotoMode: appContextValue.idPhotoMode,
|
||||
optimizelyExperimentName: appContextValue.optimizelyExperimentName,
|
||||
courseRunKey: null,
|
||||
idPhotoName: appContextValue.nameOnAccount,
|
||||
};
|
||||
await getPanel();
|
||||
const button = await screen.findByTestId('submit-button');
|
||||
fireEvent.click(button);
|
||||
expect(dataService.submitIdVerification).toHaveBeenCalledWith(verificationData);
|
||||
});
|
||||
|
||||
it('shows error when cannot submit', async () => {
|
||||
dataService.submitIdVerification = jest.fn().mockReturnValue({ success: false });
|
||||
await getPanel();
|
||||
@@ -207,6 +231,6 @@ describe('SummaryPanel', () => {
|
||||
await getPanel();
|
||||
const collapsible = await screen.queryByTestId('collapsible');
|
||||
expect(collapsible).not.toBeInTheDocument();
|
||||
contextValue.optimizelyExperimentName = 'test-experiment';
|
||||
appContextValue.optimizelyExperimentName = 'test-experiment';
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ 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 TakeIdPhotoPanel from '../../panels/TakeIdPhotoPanel';
|
||||
@@ -98,10 +97,10 @@ describe('TakeIdPhotoPanel', () => {
|
||||
)));
|
||||
|
||||
// check that upload title and text are correct
|
||||
const title = await screen.findByText('Upload a Photo of Your ID');
|
||||
const title = await screen.findByText('Upload a Photo of Your Identification Card');
|
||||
expect(title).toBeVisible();
|
||||
|
||||
const text = await screen.findByTestId('upload-text');
|
||||
expect(text.textContent).toContain('Please upload an ID photo');
|
||||
expect(text.textContent).toContain('Please upload a photo of your identification card');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,6 @@ 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 TakePortraitPhotoPanel from '../../panels/TakePortraitPhotoPanel';
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'babel-polyfill';
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
|
||||
import 'formdata-polyfill';
|
||||
import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
|
||||
import {
|
||||
@@ -18,7 +20,6 @@ import CoachingConsent from './account-settings/coaching/CoachingConsent';
|
||||
import appMessages from './i18n';
|
||||
|
||||
import './index.scss';
|
||||
import './assets/favicon.ico';
|
||||
|
||||
subscribe(APP_READY, () => {
|
||||
ReactDOM.render(
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
import 'core-js/stable';
|
||||
import 'regenerator-runtime/runtime';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
|
||||
46
src/tests/hooks.test.jsx
Normal file
46
src/tests/hooks.test.jsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { render, waitFor } from '@testing-library/react';
|
||||
|
||||
import { useAsyncCall } from '../hooks';
|
||||
|
||||
const TestUseAsyncCallHookComponent = (props) => {
|
||||
const { asyncFunc } = props;
|
||||
const [isCallLoading, callData] = useAsyncCall(asyncFunc);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ isCallLoading && <div>loading</div> }
|
||||
<div>{ callData }</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
TestUseAsyncCallHookComponent.propTypes = {
|
||||
asyncFunc: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
describe('useAsyncCall mock', () => {
|
||||
it('returns data correctly for response', async () => {
|
||||
const mockAsyncFunc = jest.fn(async () => ('data'));
|
||||
|
||||
const { queryByText } = render(<TestUseAsyncCallHookComponent asyncFunc={mockAsyncFunc} />);
|
||||
|
||||
await waitFor(() => (expect(mockAsyncFunc).toHaveBeenCalledTimes(1)));
|
||||
expect(queryByText('data')).not.toBeNull();
|
||||
});
|
||||
it('returns data correctly for no response', async () => {
|
||||
const mockAsyncFunc = jest.fn(async () => {});
|
||||
|
||||
const { queryByText } = render(<TestUseAsyncCallHookComponent asyncFunc={mockAsyncFunc} />);
|
||||
|
||||
await waitFor(() => (expect(mockAsyncFunc).toHaveBeenCalledTimes(1)));
|
||||
expect(queryByText('data')).toBeNull();
|
||||
});
|
||||
it('returns isLoading correctly', async () => {
|
||||
const mockAsyncFunc = jest.fn(async () => {});
|
||||
|
||||
const { queryByText } = render(<TestUseAsyncCallHookComponent asyncFunc={mockAsyncFunc} />);
|
||||
expect(queryByText('loading')).not.toBeNull();
|
||||
expect(queryByText('data')).toBeNull();
|
||||
});
|
||||
});
|
||||
83
src/tests/utils.test.js
Normal file
83
src/tests/utils.test.js
Normal file
@@ -0,0 +1,83 @@
|
||||
import { compareVerifiedNamesByCreatedDate, getMostRecentApprovedOrPendingVerifiedName } from '../utils';
|
||||
|
||||
describe('getMostRecentApprovedOrPendingVerifiedName', () => {
|
||||
it('returns correct verified name if one exists', () => {
|
||||
const verifiedNames = [
|
||||
{
|
||||
created: '2021-08-31T18:33:32.489200Z',
|
||||
verified_name: 'Mike',
|
||||
status: 'denied',
|
||||
},
|
||||
{
|
||||
created: '2021-09-03T18:33:32.489200Z',
|
||||
verified_name: 'Michelangelo',
|
||||
status: 'approved',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getMostRecentApprovedOrPendingVerifiedName(verifiedNames)).toEqual(verifiedNames[1].verified_name);
|
||||
});
|
||||
it('returns no verified name if one does not exist', () => {
|
||||
const verifiedNames = [
|
||||
{
|
||||
created: '2021-08-31T18:33:32.489200Z',
|
||||
verified_name: 'Mike',
|
||||
status: 'denied',
|
||||
},
|
||||
{
|
||||
created: '2021-09-03T18:33:32.489200Z',
|
||||
verified_name: 'Michelangelo',
|
||||
status: 'submitted',
|
||||
},
|
||||
];
|
||||
|
||||
expect(getMostRecentApprovedOrPendingVerifiedName(verifiedNames)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('compareVerifiedNamesByCreatedDate', () => {
|
||||
it('returns 0 when equal', () => {
|
||||
const a = {
|
||||
created: '2021-08-31T18:33:32.489200Z',
|
||||
verified_name: 'Mike',
|
||||
status: 'denied',
|
||||
};
|
||||
const b = {
|
||||
created: '2021-08-31T18:33:32.489200Z',
|
||||
verified_name: 'Michael',
|
||||
status: 'denied',
|
||||
};
|
||||
|
||||
expect(compareVerifiedNamesByCreatedDate(a, b)).toEqual(0);
|
||||
});
|
||||
|
||||
it('returns negative number when first argument is greater than second argument', () => {
|
||||
const a = {
|
||||
created: '2021-09-30T18:33:32.489200Z',
|
||||
verified_name: 'Mike',
|
||||
status: 'denied',
|
||||
};
|
||||
const b = {
|
||||
created: '2021-08-31T18:33:32.489200Z',
|
||||
verified_name: 'Michael',
|
||||
status: 'denied',
|
||||
};
|
||||
|
||||
expect(compareVerifiedNamesByCreatedDate(a, b)).toBeLessThan(0);
|
||||
});
|
||||
|
||||
it('returns positive number when first argument is less than second argument', () => {
|
||||
const a = {
|
||||
created: '2021-08-31T18:33:32.489200Z',
|
||||
verified_name: 'Mike',
|
||||
status: 'denied',
|
||||
};
|
||||
const b = {
|
||||
created: '2021-09-30T18:33:32.489200Z',
|
||||
verified_name: 'Michael',
|
||||
status: 'denied',
|
||||
};
|
||||
|
||||
expect(compareVerifiedNamesByCreatedDate(a, b)).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
40
src/utils.js
Normal file
40
src/utils.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Compare two dates.
|
||||
* @param {*} a the first date
|
||||
* @param {*} b the second date
|
||||
* @returns a negative integer if a > b, a positive integer if a < b, or 0 if a = b
|
||||
*/
|
||||
export function compareVerifiedNamesByCreatedDate(a, b) {
|
||||
const aTimeSinceEpoch = new Date(a.created).getTime();
|
||||
const bTimeSinceEpoch = new Date(b.created).getTime();
|
||||
return bTimeSinceEpoch - aTimeSinceEpoch;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} verifiedNames a list of verified name objects, where each object has at least the
|
||||
* following keys: created, status, and verified_name.
|
||||
* @returns the most recent verified name object from the list parameter with the 'pending' or
|
||||
* 'accepted' status, if one exists; otherwise, null
|
||||
*/
|
||||
export function getMostRecentApprovedOrPendingVerifiedName(verifiedNames) {
|
||||
// clone array so as not to modify original array
|
||||
const names = [...verifiedNames];
|
||||
|
||||
if (Array.isArray(names)) {
|
||||
names.sort(compareVerifiedNamesByCreatedDate);
|
||||
}
|
||||
|
||||
// We only want to consider a subset of verified names when determining the value of nameOnAccount.
|
||||
// approved: consider this status, as the name has been verified by IDV and should supersede the full name
|
||||
// (profile name).
|
||||
// pending: consider this status, as the learner has started the name change process through the
|
||||
// Account Settings page, and has been navigated to IDV to complete the name change process.
|
||||
// submitted: do not consider this status, as the name has already been submitted for verification through
|
||||
// IDV but has not yet been verified
|
||||
// denied: do not consider this status because the name was already denied via the IDV process
|
||||
const applicableNames = names.filter(name => ['approved', 'pending'].includes(name.status));
|
||||
const applicableName = applicableNames.length > 0 ? applicableNames[0].verified_name : null;
|
||||
|
||||
return applicableName;
|
||||
}
|
||||
Reference in New Issue
Block a user