Compare commits
4 Commits
open-relea
...
kdmccormic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de2a751e36 | ||
|
|
0375ebecda | ||
|
|
f088e2783b | ||
|
|
abc29bbcee |
10
.env
10
.env
@@ -5,7 +5,6 @@ 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
|
||||
@@ -13,13 +12,6 @@ NODE_ENV=null
|
||||
ORDER_HISTORY_URL=null
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT=null
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME=''
|
||||
SITE_NAME=null
|
||||
SUPPORT_URL=null
|
||||
USER_INFO_COOKIE_NAME=null
|
||||
LOGO_URL=''
|
||||
LOGO_TRADEMARK_URL=''
|
||||
LOGO_WHITE_URL=''
|
||||
FAVICON_URL=''
|
||||
PUBLISHER_BASE_URL=
|
||||
STUDIO_BASE_URL=
|
||||
DISCOVERY_API_BASE_URL=
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
BASE_URL='localhost:1997'
|
||||
BASE_URL='localhost:19000/account/'
|
||||
CREDENTIALS_BASE_URL='http://localhost:18150'
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
@@ -7,23 +7,14 @@ LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:5335'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
NODE_ENV='development'
|
||||
ORDER_HISTORY_URL='localhost:1996/orders'
|
||||
PORT=1997
|
||||
ORDER_HISTORY_URL='localhost:19000/ecommerce/orders'
|
||||
PORT=1997 # For standalone dev server only.
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME=localhost
|
||||
SITE_NAME='edX'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
# Temporary, Remove this once we are ready to release the feature.
|
||||
COACHING_ENABLED=true
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=true
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
PUBLISHER_BASE_URL=
|
||||
STUDIO_BASE_URL=
|
||||
DISCOVERY_API_BASE_URL=
|
||||
COACHING_ENABLED=''
|
||||
|
||||
15
.env.test
15
.env.test
@@ -6,22 +6,13 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
LMS_BASE_URL='http://localhost:18000'
|
||||
LOGIN_URL='http://localhost:18000/login'
|
||||
LOGOUT_URL='http://localhost:18000/logout'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:5335'
|
||||
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
|
||||
LOGOUT_URL='http://localhost:18000/login'
|
||||
MARKETING_SITE_BASE_URL='http://localhost:18000'
|
||||
NODE_ENV=null
|
||||
ORDER_HISTORY_URL='localhost:1996/orders'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
SEGMENT_KEY=null
|
||||
SITE_NAME=localhost
|
||||
SITE_NAME='edX'
|
||||
SUPPORT_URL='http://localhost:18000/support'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
COACHING_ENABLED=''
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION=''
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
PUBLISHER_BASE_URL=
|
||||
STUDIO_BASE_URL=
|
||||
DISCOVERY_API_BASE_URL=
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,4 +16,3 @@ temp/babel-plugin-react-intl
|
||||
*~
|
||||
/temp
|
||||
/.vscode
|
||||
/module.config.js
|
||||
|
||||
@@ -41,8 +41,13 @@ This MFE is configured via node environment variables supplied at build time. Se
|
||||
For more information see the document: `Micro-frontend applications in Open
|
||||
edX <https://github.com/edx/edx-developer-docs/blob/5191e800bf16cf42f25c58c58f983bdaf7f9305d/docs/micro-frontends-in-open-edx.rst>`__.
|
||||
|
||||
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-account.svg?branch=master
|
||||
:target: https://travis-ci.com/edx/frontend-app-account
|
||||
Notes
|
||||
-----
|
||||
|
||||
The production Webpack configuration for this repo uses `Purgecss <https://www.purgecss.com/>`__ to remove unused CSS from the production css file. In ``webpack.prod.config.js`` the Purgecss plugin is configured to scan directories to determine what css selectors should remain. Currently the src/ directory is scanned along with all ``@edx/frontend-component*`` node modules and ``@edx/paragon``. **If you add and use a component in this repo that relies on HTML classes or ids for styling you must add it to the Purgecss configuration or it will be unstyled in the production build.**
|
||||
|
||||
.. |Build Status| image:: https://api.travis-ci.org/edx/frontend-app-account.svg?branch=master
|
||||
:target: https://travis-ci.org/edx/frontend-app-account
|
||||
.. |Codecov| image:: https://img.shields.io/codecov/c/github/edx/frontend-app-account
|
||||
:target: https://codecov.io/gh/edx/frontend-app-account
|
||||
.. |npm_version| image:: https://img.shields.io/npm/v/@edx/frontend-app-account.svg
|
||||
|
||||
13173
package-lock.json
generated
13173
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file → Executable file
41
package.json
Normal file → Executable file
@@ -10,9 +10,10 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"dev-build": "fedx-scripts webpack-dev",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"is-es5": "es-check es5 ./dist/*.js",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx .",
|
||||
"lint": "fedx-scripts eslint",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"test": "fedx-scripts jest --coverage --passWithNoTests"
|
||||
@@ -29,27 +30,22 @@
|
||||
"ie 11"
|
||||
],
|
||||
"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",
|
||||
"@edx/frontend-component-footer": "10.0.9",
|
||||
"@edx/frontend-component-header": "2.0.5",
|
||||
"@edx/frontend-platform": "1.1.14",
|
||||
"@edx/paragon": "7.1.5",
|
||||
"@fortawesome/fontawesome-svg-core": "1.2.28",
|
||||
"@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",
|
||||
"@tensorflow-models/blazeface": "0.0.7",
|
||||
"@tensorflow/tfjs-converter": "1.6.1",
|
||||
"@tensorflow/tfjs-core": "1.6.1",
|
||||
"@fortawesome/react-fontawesome": "0.1.9",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"bowser": "2.10.0",
|
||||
"classnames": "2.2.6",
|
||||
"font-awesome": "4.7.0",
|
||||
"form-urlencoded": "4.0.1",
|
||||
"formdata-polyfill": "3.0.20",
|
||||
"formdata-polyfill": "3.0.19",
|
||||
"history": "4.10.1",
|
||||
"jslib-html5-camera-photo": "3.1.6",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.findindex": "4.6.0",
|
||||
"lodash.get": "4.4.2",
|
||||
@@ -57,18 +53,17 @@
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.omit": "4.5.0",
|
||||
"lodash.pick": "4.4.0",
|
||||
"lodash.pickby": "4.6.0",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"memoize-one": "5.1.1",
|
||||
"newrelic": "5.13.1",
|
||||
"prop-types": "15.7.2",
|
||||
"qs": "6.9.6",
|
||||
"react": "16.10.2",
|
||||
"react-dom": "16.10.2",
|
||||
"react-redux": "7.1.3",
|
||||
"react-router": "5.1.2",
|
||||
"react-router-dom": "5.1.2",
|
||||
"react-router-hash-link": "1.2.2",
|
||||
"react-scrollspy": "3.4.3",
|
||||
"react-scrollspy": "3.4.2",
|
||||
"react-transition-group": "4.3.0",
|
||||
"redux": "4.0.5",
|
||||
"redux-devtools-extension": "2.13.8",
|
||||
@@ -76,17 +71,17 @@
|
||||
"redux-saga": "1.1.3",
|
||||
"redux-thunk": "2.3.0",
|
||||
"reselect": "4.0.0",
|
||||
"universal-cookie": "4.0.4"
|
||||
"universal-cookie": "4.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "5.6.9",
|
||||
"@testing-library/jest-dom": "5.11.9",
|
||||
"@testing-library/react": "10.4.9",
|
||||
"codecov": "3.7.2",
|
||||
"@edx/frontend-build": "github:edx/frontend-build#kdmccormick/devstack-frontends",
|
||||
"codecov": "3.6.5",
|
||||
"enzyme": "3.10.0",
|
||||
"enzyme-adapter-react-16": "1.15.6",
|
||||
"enzyme-adapter-react-16": "1.15.2",
|
||||
"es-check": "5.0.0",
|
||||
"glob": "7.1.6",
|
||||
"husky": "3.0.9",
|
||||
"purgecss-webpack-plugin": "1.6.0",
|
||||
"react-test-renderer": "16.8.6",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "1.5.4"
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>Account | <%= process.env.SITE_NAME %></title>
|
||||
<title>Account | edX</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" href="<%=webpackConfig.output.publicPath%>favicon.ico" type="image/x-icon" />
|
||||
<% if (process.env.OPTIMIZELY_PROJECT_ID) { %>
|
||||
<script
|
||||
src="<%= process.env.MARKETING_SITE_BASE_URL %>/optimizelyjs/<%= process.env.OPTIMIZELY_PROJECT_ID %>.js"
|
||||
></script>
|
||||
<% } %>
|
||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -5,13 +5,5 @@
|
||||
"patch": {
|
||||
"automerge": true
|
||||
},
|
||||
"rebaseStalePrs": true,
|
||||
"packageRules": [
|
||||
{
|
||||
"matchPackagePatterns": ["^@edx/paragon"],
|
||||
"matchUpdateTypes": ["minor", "patch", "pin", "digest"],
|
||||
"automerge": true,
|
||||
"stabilityDays": 3
|
||||
}
|
||||
]
|
||||
"rebaseStalePrs": true
|
||||
}
|
||||
|
||||
@@ -31,12 +31,9 @@ import {
|
||||
YEAR_OF_BIRTH_OPTIONS,
|
||||
EDUCATION_LEVELS,
|
||||
GENDER_OPTIONS,
|
||||
COUNTRY_WITH_STATES,
|
||||
getStatesList,
|
||||
} from './data/constants';
|
||||
import { fetchSiteLanguages } from './site-language';
|
||||
import CoachingToggle from './coaching/CoachingToggle';
|
||||
import DemographicsSection from './demographics/DemographicsSection';
|
||||
|
||||
class AccountSettingsPage extends React.Component {
|
||||
constructor(props, context) {
|
||||
@@ -44,8 +41,8 @@ class AccountSettingsPage extends React.Component {
|
||||
|
||||
// If there is a "duplicate_provider" query parameter, that's the backend's
|
||||
// way of telling us that the provider account the user tried to link is already linked
|
||||
// to another user account on the platform. We use this to display a message to that effect,
|
||||
// and remove the parameter from the URL.
|
||||
// to another Open edX account. We use this to display a message to that effect, and remove the
|
||||
// parameter from the URL.
|
||||
const duplicateTpaProvider = getQueryParameters().duplicate_provider;
|
||||
if (duplicateTpaProvider !== undefined) {
|
||||
history.replace(history.location.pathname);
|
||||
@@ -53,16 +50,6 @@ class AccountSettingsPage extends React.Component {
|
||||
this.state = {
|
||||
duplicateTpaProvider,
|
||||
};
|
||||
|
||||
this.navLinkRefs = {
|
||||
'#basic-information': React.createRef(),
|
||||
'#profile-information': React.createRef(),
|
||||
'#demographics-information': React.createRef(),
|
||||
'#social-media': React.createRef(),
|
||||
'#site-preferences': React.createRef(),
|
||||
'#linked-accounts': React.createRef(),
|
||||
'#delete-account': React.createRef(),
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
@@ -75,20 +62,6 @@ class AccountSettingsPage extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.loading && !prevProps.loaded && this.props.loaded) {
|
||||
const locationHash = global.location.hash;
|
||||
// Check for the locationHash in the URL and then scroll to it if it is in the
|
||||
// NavLinks list
|
||||
if (typeof locationHash !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (Object.keys(this.navLinkRefs).includes(locationHash) && this.navLinkRefs[locationHash].current) {
|
||||
window.scrollTo(0, this.navLinkRefs[locationHash].current.offsetTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: We need 'locale' for the memoization in getLocalizedTimeZoneOptions. Don't remove it!
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
getLocalizedTimeZoneOptions = memoize((timeZoneOptions, countryTimeZoneOptions, locale) => {
|
||||
@@ -109,15 +82,11 @@ class AccountSettingsPage extends React.Component {
|
||||
return concatTimeZoneOptions;
|
||||
});
|
||||
|
||||
getLocalizedOptions = memoize((locale, country) => ({
|
||||
getLocalizedOptions = memoize(locale => ({
|
||||
countryOptions: [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.country.options.empty']),
|
||||
}].concat(getCountryList(locale).map(({ code, name }) => ({ value: code, label: name }))),
|
||||
stateOptions: [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.state.options.empty']),
|
||||
}].concat(getStatesList(country)),
|
||||
languageProficiencyOptions: [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages['account.settings.field.language_proficiencies.options.empty']),
|
||||
@@ -136,14 +105,6 @@ class AccountSettingsPage extends React.Component {
|
||||
})),
|
||||
}));
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
};
|
||||
|
||||
handleSubmit = (formId, values) => {
|
||||
this.props.saveSettings(formId, values);
|
||||
};
|
||||
|
||||
isEditable(fieldName) {
|
||||
return !this.props.staticFields.includes(fieldName);
|
||||
}
|
||||
@@ -154,6 +115,14 @@ class AccountSettingsPage extends React.Component {
|
||||
return Boolean(this.props.profileDataManager);
|
||||
}
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
};
|
||||
|
||||
handleSubmit = (formId, values) => {
|
||||
this.props.saveSettings(formId, values);
|
||||
};
|
||||
|
||||
renderDuplicateTpaProviderMessage() {
|
||||
if (!this.state.duplicateTpaProvider) {
|
||||
return null;
|
||||
@@ -164,11 +133,10 @@ class AccountSettingsPage extends React.Component {
|
||||
<Alert className="alert alert-danger" role="alert">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.duplicate.tpa.provider"
|
||||
defaultMessage="The {provider} account you selected is already linked to another {siteName} account."
|
||||
description="alert message informing the user that the third-party account they attempted to link is already linked to another account"
|
||||
defaultMessage="The {provider} account you selected is already linked to another edX account."
|
||||
description="alert message informing the user that the third-party account they attempted to link is already linked to another edX account"
|
||||
values={{
|
||||
provider: <b>{this.state.duplicateTpaProvider}</b>,
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
@@ -216,7 +184,7 @@ class AccountSettingsPage extends React.Component {
|
||||
}
|
||||
|
||||
renderSecondaryEmailField(editableFieldProps) {
|
||||
if (!this.props.formValues.secondary_email_enabled) {
|
||||
if (!Boolean(this.props.formValues.secondary_email_enabled)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -232,16 +200,6 @@ class AccountSettingsPage extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderDemographicsSection() {
|
||||
// check the result of an LMS API call to determine if we should render the DemographicsSection component
|
||||
if (this.props.formValues.shouldDisplayDemographicsSection) {
|
||||
return (
|
||||
<DemographicsSection forwardRef={this.navLinkRefs['#demographics-information']} />
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const editableFieldProps = {
|
||||
onChange: this.handleEditableFieldChange,
|
||||
@@ -251,15 +209,11 @@ class AccountSettingsPage extends React.Component {
|
||||
// Memoized options lists
|
||||
const {
|
||||
countryOptions,
|
||||
stateOptions,
|
||||
languageProficiencyOptions,
|
||||
yearOfBirthOptions,
|
||||
educationLevelOptions,
|
||||
genderOptions,
|
||||
} = this.getLocalizedOptions(this.context.locale, this.props.formValues.country);
|
||||
|
||||
// Show State field only if the country is US (could include Canada later)
|
||||
const showState = this.props.formValues.country === COUNTRY_WITH_STATES;
|
||||
} = this.getLocalizedOptions(this.context.locale);
|
||||
|
||||
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
|
||||
this.props.timeZoneOptions,
|
||||
@@ -270,8 +224,8 @@ 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']}>
|
||||
<React.Fragment>
|
||||
<div className="account-section" id="basic-information">
|
||||
<h2 className="section-heading">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.account.information'])}
|
||||
</h2>
|
||||
@@ -283,10 +237,7 @@ class AccountSettingsPage extends React.Component {
|
||||
type="text"
|
||||
value={this.props.formValues.username}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.username'])}
|
||||
helpText={this.props.intl.formatMessage(
|
||||
messages['account.settings.field.username.help.text'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
helpText={this.props.intl.formatMessage(messages['account.settings.field.username.help.text'])}
|
||||
isEditable={false}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
@@ -296,9 +247,9 @@ class AccountSettingsPage extends React.Component {
|
||||
value={this.props.formValues.name}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.full.name'])}
|
||||
emptyLabel={
|
||||
this.isEditable('name')
|
||||
? this.props.intl.formatMessage(messages['account.settings.field.full.name.empty'])
|
||||
: this.renderEmptyStaticFieldMessage()
|
||||
this.isEditable('name') ?
|
||||
this.props.intl.formatMessage(messages['account.settings.field.full.name.empty']) :
|
||||
this.renderEmptyStaticFieldMessage()
|
||||
}
|
||||
helpText={this.props.intl.formatMessage(messages['account.settings.field.full.name.help.text'])}
|
||||
isEditable={this.isEditable('name')}
|
||||
@@ -308,16 +259,13 @@ class AccountSettingsPage extends React.Component {
|
||||
name="email"
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.email'])}
|
||||
emptyLabel={
|
||||
this.isEditable('email')
|
||||
? this.props.intl.formatMessage(messages['account.settings.field.email.empty'])
|
||||
: this.renderEmptyStaticFieldMessage()
|
||||
this.isEditable('email') ?
|
||||
this.props.intl.formatMessage(messages['account.settings.field.email.empty']) :
|
||||
this.renderEmptyStaticFieldMessage()
|
||||
}
|
||||
value={this.props.formValues.email}
|
||||
confirmationMessageDefinition={messages['account.settings.field.email.confirmation']}
|
||||
helpText={this.props.intl.formatMessage(
|
||||
messages['account.settings.field.email.help.text'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
helpText={this.props.intl.formatMessage(messages['account.settings.field.email.help.text'])}
|
||||
isEditable={this.isEditable('email')}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
@@ -339,33 +287,16 @@ class AccountSettingsPage extends React.Component {
|
||||
options={countryOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.country'])}
|
||||
emptyLabel={
|
||||
this.isEditable('country')
|
||||
? this.props.intl.formatMessage(messages['account.settings.field.country.empty'])
|
||||
: this.renderEmptyStaticFieldMessage()
|
||||
this.isEditable('country') ?
|
||||
this.props.intl.formatMessage(messages['account.settings.field.country.empty']) :
|
||||
this.renderEmptyStaticFieldMessage()
|
||||
}
|
||||
isEditable={this.isEditable('country')}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{showState
|
||||
&& (
|
||||
<EditableField
|
||||
name="state"
|
||||
type="select"
|
||||
value={this.props.formValues.state}
|
||||
options={stateOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.state'])}
|
||||
emptyLabel={
|
||||
this.isEditable('state')
|
||||
? this.props.intl.formatMessage(messages['account.settings.field.state.empty'])
|
||||
: this.renderEmptyStaticFieldMessage()
|
||||
}
|
||||
isEditable={this.isEditable('state')}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="account-section" id="profile-information" ref={this.navLinkRefs['#profile-information']}>
|
||||
<div className="account-section" id="profile-information">
|
||||
<h2 className="section-heading">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.profile.information'])}
|
||||
</h2>
|
||||
@@ -397,27 +328,21 @@ class AccountSettingsPage extends React.Component {
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.language.proficiencies.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
{getConfig().COACHING_ENABLED
|
||||
&& this.props.formValues.coaching.eligible_for_coaching
|
||||
&& (
|
||||
{getConfig().COACHING_ENABLED &&
|
||||
this.props.formValues.coaching.eligible_for_coaching &&
|
||||
<CoachingToggle
|
||||
name="coaching"
|
||||
phone_number={this.props.formValues.phone_number}
|
||||
coaching={this.props.formValues.coaching}
|
||||
/>
|
||||
)}
|
||||
}
|
||||
</div>
|
||||
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && this.renderDemographicsSection()}
|
||||
|
||||
<div className="account-section" id="social-media">
|
||||
<h2 className="section-heading">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
|
||||
</h2>
|
||||
<p>
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.section.social.media.description'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>{this.props.intl.formatMessage(messages['account.settings.section.social.media.description'])}</p>
|
||||
|
||||
<EditableField
|
||||
name="social_link_linkedin"
|
||||
@@ -445,7 +370,7 @@ class AccountSettingsPage extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="account-section" id="site-preferences" ref={this.navLinkRefs['#site-preferences']}>
|
||||
<div className="account-section" id="site-preferences">
|
||||
<h2 className="section-heading">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.site.preferences'])}
|
||||
</h2>
|
||||
@@ -476,25 +401,20 @@ class AccountSettingsPage extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="account-section" id="linked-accounts" ref={this.navLinkRefs['#linked-accounts']}>
|
||||
<div className="account-section" id="linked-accounts">
|
||||
<h2 className="section-heading">{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts'])}</h2>
|
||||
<p>
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.section.linked.accounts.description'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>{this.props.intl.formatMessage(messages['account.settings.section.linked.accounts.description'])}</p>
|
||||
<ThirdPartyAuth />
|
||||
</div>
|
||||
|
||||
<div className="account-section" id="delete-account" ref={this.navLinkRefs['#delete-account']}>
|
||||
<div className="account-section" id="delete-account">
|
||||
<DeleteAccount
|
||||
isVerifiedAccount={this.props.isActive}
|
||||
hasLinkedTPA={hasLinkedTPA}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -530,9 +450,7 @@ class AccountSettingsPage extends React.Component {
|
||||
<div>
|
||||
<div className="row">
|
||||
<div className="col-md-3">
|
||||
<JumpNav
|
||||
displayDemographicsLink={this.props.formValues.shouldDisplayDemographicsSection}
|
||||
/>
|
||||
<JumpNav />
|
||||
</div>
|
||||
<div className="col-md-9">
|
||||
{loading ? this.renderLoading() : null}
|
||||
@@ -560,7 +478,6 @@ AccountSettingsPage.propTypes = {
|
||||
name: PropTypes.string,
|
||||
email: PropTypes.string,
|
||||
secondary_email: PropTypes.string,
|
||||
secondary_email_enabled: PropTypes.bool,
|
||||
year_of_birth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
country: PropTypes.string,
|
||||
level_of_education: PropTypes.string,
|
||||
@@ -571,13 +488,11 @@ AccountSettingsPage.propTypes = {
|
||||
social_link_facebook: PropTypes.string,
|
||||
social_link_twitter: PropTypes.string,
|
||||
time_zone: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
coaching: PropTypes.objectOf(PropTypes.shape({
|
||||
coaching_consent: PropTypes.string.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
}),
|
||||
state: PropTypes.string,
|
||||
shouldDisplayDemographicsSection: PropTypes.bool,
|
||||
})),
|
||||
}).isRequired,
|
||||
siteLanguage: PropTypes.shape({
|
||||
previousValue: PropTypes.string,
|
||||
|
||||
@@ -46,11 +46,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Profile Information',
|
||||
description: 'The profile information section heading.',
|
||||
},
|
||||
'account.settings.section.demographics.information': {
|
||||
id: 'account.settings.section.demographics.information',
|
||||
defaultMessage: 'Optional Information',
|
||||
description: 'The optional information section heading.',
|
||||
},
|
||||
'account.settings.section.site.preferences': {
|
||||
id: 'account.settings.section.site.preferences',
|
||||
defaultMessage: 'Site Preferences',
|
||||
@@ -63,7 +58,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.section.linked.accounts.description': {
|
||||
id: 'account.settings.section.linked.accounts.description',
|
||||
defaultMessage: 'You can link your identity accounts to simplify signing in to {siteName}.',
|
||||
defaultMessage: 'You can link your identity accounts to simplify signing in to edX.',
|
||||
description: 'The linked accounts section heading description.',
|
||||
},
|
||||
'account.settings.field.username': {
|
||||
@@ -73,7 +68,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.field.username.help.text': {
|
||||
id: 'account.settings.field.username.help.text',
|
||||
defaultMessage: 'The name that identifies you on {siteName}. You cannot change your username.',
|
||||
defaultMessage: 'The name that identifies you on edX. You cannot change your username.',
|
||||
description: 'Help text for the account settings username field.',
|
||||
},
|
||||
'account.settings.field.full.name': {
|
||||
@@ -108,7 +103,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.field.email.help.text': {
|
||||
id: 'account.settings.field.email.help.text',
|
||||
defaultMessage: 'You receive messages from {siteName} and course teams at this address.',
|
||||
defaultMessage: 'You receive messages from edX and course teams at this address.',
|
||||
description: 'Help text for the account settings email field.',
|
||||
},
|
||||
'account.settings.field.secondary.email': {
|
||||
@@ -161,21 +156,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Select a Country',
|
||||
description: 'Option for empty value on account settings country field.',
|
||||
},
|
||||
'account.settings.field.state': {
|
||||
id: 'account.settings.field.state',
|
||||
defaultMessage: 'State',
|
||||
description: 'Label for account settings state field.',
|
||||
},
|
||||
'account.settings.field.state.empty': {
|
||||
id: 'account.settings.field.state.empty',
|
||||
defaultMessage: 'Add state',
|
||||
description: 'Placeholder for empty account settings state field.',
|
||||
},
|
||||
'account.settings.field.state.options.empty': {
|
||||
id: 'account.settings.field.state.options.empty',
|
||||
defaultMessage: 'Select a State',
|
||||
description: 'Option for empty value on account settings state field.',
|
||||
},
|
||||
'account.settings.field.site.language': {
|
||||
id: 'account.settings.field.site.language',
|
||||
defaultMessage: 'Site language',
|
||||
@@ -279,20 +259,19 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.field.language.proficiencies': {
|
||||
id: 'account.settings.field.language.proficiencies',
|
||||
defaultMessage: 'Spoken language',
|
||||
description: 'Label for account settings spoken language field.',
|
||||
defaultMessage: 'Spoken languages',
|
||||
description: 'Label for account settings spoken languages field.',
|
||||
},
|
||||
'account.settings.field.language.proficiencies.empty': {
|
||||
id: 'account.settings.field.language.proficiencies.empty',
|
||||
defaultMessage: 'Add a spoken language',
|
||||
description: 'Placeholder for empty account settings spoken language field.',
|
||||
description: 'Placeholder for empty account settings spoken languages field.',
|
||||
},
|
||||
'account.settings.field.language_proficiencies.options.empty': {
|
||||
id: 'account.settings.field.language_proficiencies.options.empty',
|
||||
defaultMessage: 'Select a Language',
|
||||
description: 'Option for an empty value on account settings spoken language field.',
|
||||
description: 'Option for an empty value on account settings spoken languages field.',
|
||||
},
|
||||
|
||||
'account.settings.field.time.zone': {
|
||||
id: 'account.settings.field.time.zone',
|
||||
defaultMessage: 'Time zone',
|
||||
@@ -331,7 +310,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.section.social.media.description': {
|
||||
id: 'account.settings.section.social.media.description',
|
||||
defaultMessage: 'Optionally, link your personal accounts to the social media icons on your {siteName} profile.',
|
||||
defaultMessage: 'Optionally, link your personal accounts to the social media icons on your edX profile.',
|
||||
description: 'Section subheader for social media links settings',
|
||||
},
|
||||
'account.settings.field.social.platform.name.linkedin': {
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
||||
function Alert(props) {
|
||||
return (
|
||||
<div className={classNames('alert d-flex align-items-start', props.className)}>
|
||||
@@ -15,6 +16,7 @@ function Alert(props) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Alert.propTypes = {
|
||||
className: PropTypes.string,
|
||||
icon: PropTypes.node,
|
||||
@@ -27,4 +29,5 @@ Alert.defaultProps = {
|
||||
children: undefined,
|
||||
};
|
||||
|
||||
|
||||
export default Alert;
|
||||
|
||||
@@ -65,7 +65,7 @@ class BetaLanguageBanner extends React.Component {
|
||||
})}
|
||||
</p>
|
||||
<div>
|
||||
<Button onClick={this.handleRevertLanguage} className="mr-2">
|
||||
<Button onClick={this.handleRevertLanguage} className="btn btn-primary mr-2">
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.banner.beta.language.action.switch.back'],
|
||||
{ previous_language: previousLanguage.name },
|
||||
|
||||
@@ -2,9 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, Input, StatefulButton, ValidationFormGroup,
|
||||
} from '@edx/paragon';
|
||||
import { Button, Input, StatefulButton, ValidationFormGroup } from '@edx/paragon';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
@@ -17,6 +15,7 @@ import {
|
||||
} from './data/actions';
|
||||
import { editableFieldSelector } from './data/selectors';
|
||||
|
||||
|
||||
function EditableField(props) {
|
||||
const {
|
||||
name,
|
||||
@@ -24,7 +23,6 @@ function EditableField(props) {
|
||||
emptyLabel,
|
||||
type,
|
||||
value,
|
||||
userSuppliedValue,
|
||||
options,
|
||||
saveState,
|
||||
error,
|
||||
@@ -61,37 +59,26 @@ function EditableField(props) {
|
||||
|
||||
const renderEmptyLabel = () => {
|
||||
if (isEditable) {
|
||||
return <Button variant="link" onClick={handleEdit} className="p-0">{emptyLabel}</Button>;
|
||||
return <Button onClick={handleEdit} className="btn-link p-0">{emptyLabel}</Button>;
|
||||
}
|
||||
return <span className="text-muted">{emptyLabel}</span>;
|
||||
};
|
||||
|
||||
const renderValue = (rawValue) => {
|
||||
if (!rawValue) {
|
||||
return renderEmptyLabel();
|
||||
}
|
||||
let finalValue = rawValue;
|
||||
if (!rawValue) return renderEmptyLabel();
|
||||
|
||||
if (options) {
|
||||
// Use == instead of === to prevent issues when HTML casts numbers as strings
|
||||
// eslint-disable-next-line eqeqeq
|
||||
const selectedOption = options.find(option => option.value == rawValue);
|
||||
if (selectedOption) {
|
||||
finalValue = selectedOption.label;
|
||||
}
|
||||
if (selectedOption) return selectedOption.label;
|
||||
}
|
||||
|
||||
if (userSuppliedValue) {
|
||||
finalValue += `: ${userSuppliedValue}`;
|
||||
}
|
||||
|
||||
return finalValue;
|
||||
return rawValue;
|
||||
};
|
||||
|
||||
const renderConfirmationMessage = () => {
|
||||
if (!confirmationMessageDefinition || !confirmationValue) {
|
||||
return null;
|
||||
}
|
||||
if (!confirmationMessageDefinition || !confirmationValue) return null;
|
||||
return intl.formatMessage(confirmationMessageDefinition, {
|
||||
value: confirmationValue,
|
||||
});
|
||||
@@ -111,7 +98,6 @@ function EditableField(props) {
|
||||
>
|
||||
<label className="h6 d-block" htmlFor={id}>{label}</label>
|
||||
<Input
|
||||
data-hj-suppress
|
||||
name={name}
|
||||
id={id}
|
||||
type={type}
|
||||
@@ -120,12 +106,11 @@ function EditableField(props) {
|
||||
options={options}
|
||||
{...others}
|
||||
/>
|
||||
<>{others.children}</>
|
||||
</ValidationFormGroup>
|
||||
<p>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
className="mr-2"
|
||||
className="btn-primary mr-2"
|
||||
state={saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
|
||||
@@ -138,13 +123,13 @@ function EditableField(props) {
|
||||
// Swallowing the onSubmit event on the form would be better, but
|
||||
// we would have to add that logic for every field given our
|
||||
// current structure of the application.
|
||||
if (saveState === 'pending') { e.preventDefault(); }
|
||||
if (saveState === 'pending') e.preventDefault();
|
||||
}}
|
||||
disabledStates={[]}
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={handleCancel}
|
||||
className="btn-outline-primary"
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
|
||||
</Button>
|
||||
@@ -156,12 +141,12 @@ function EditableField(props) {
|
||||
<div className="d-flex align-items-start">
|
||||
<h6 aria-level="3">{label}</h6>
|
||||
{isEditable ? (
|
||||
<Button variant="link" onClick={handleEdit} className="ml-3">
|
||||
<Button onClick={handleEdit} className="ml-3 btn-link">
|
||||
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />{intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<p data-hj-suppress>{renderValue(value)}</p>
|
||||
<p>{renderValue(value)}</p>
|
||||
<p className="small text-muted mt-n2">{renderConfirmationMessage() || helpText}</p>
|
||||
</div>
|
||||
),
|
||||
@@ -170,13 +155,13 @@ function EditableField(props) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
EditableField.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
emptyLabel: PropTypes.node,
|
||||
type: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
userSuppliedValue: PropTypes.string,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
@@ -211,9 +196,9 @@ EditableField.defaultProps = {
|
||||
helpText: undefined,
|
||||
isEditing: false,
|
||||
isEditable: true,
|
||||
userSuppliedValue: undefined,
|
||||
};
|
||||
|
||||
|
||||
export default connect(editableFieldSelector, {
|
||||
onEdit: openForm,
|
||||
onCancel: closeForm,
|
||||
|
||||
@@ -2,9 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
Button, StatefulButton, Input, ValidationFormGroup,
|
||||
} from '@edx/paragon';
|
||||
import { Button, StatefulButton, Input, ValidationFormGroup } from '@edx/paragon';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExclamationTriangle, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
@@ -18,6 +16,7 @@ import {
|
||||
} from './data/actions';
|
||||
import { editableFieldSelector } from './data/selectors';
|
||||
|
||||
|
||||
function EmailField(props) {
|
||||
const {
|
||||
name,
|
||||
@@ -57,9 +56,7 @@ function EmailField(props) {
|
||||
};
|
||||
|
||||
const renderConfirmationMessage = () => {
|
||||
if (!confirmationMessageDefinition || !confirmationValue) {
|
||||
return null;
|
||||
}
|
||||
if (!confirmationMessageDefinition || !confirmationValue) return null;
|
||||
return (
|
||||
<Alert
|
||||
className="alert-warning mt-n2"
|
||||
@@ -88,15 +85,13 @@ function EmailField(props) {
|
||||
|
||||
const renderEmptyLabel = () => {
|
||||
if (isEditable) {
|
||||
return <Button variant="link" onClick={handleEdit} className="p-0">{emptyLabel}</Button>;
|
||||
return <Button onClick={handleEdit} className="btn-link p-0">{emptyLabel}</Button>;
|
||||
}
|
||||
return <span className="text-muted">{emptyLabel}</span>;
|
||||
};
|
||||
|
||||
const renderValue = () => {
|
||||
if (confirmationValue) {
|
||||
return renderConfirmationValue();
|
||||
}
|
||||
if (confirmationValue) return renderConfirmationValue();
|
||||
return value || renderEmptyLabel();
|
||||
};
|
||||
|
||||
@@ -114,7 +109,6 @@ function EmailField(props) {
|
||||
>
|
||||
<label className="h6 d-block" htmlFor={id}>{label}</label>
|
||||
<Input
|
||||
data-hj-suppress
|
||||
name={name}
|
||||
id={id}
|
||||
type="email"
|
||||
@@ -125,7 +119,7 @@ function EmailField(props) {
|
||||
<p>
|
||||
<StatefulButton
|
||||
type="submit"
|
||||
className="mr-2"
|
||||
className="btn-primary mr-2"
|
||||
state={saveState}
|
||||
labels={{
|
||||
default: intl.formatMessage(messages['account.settings.editable.field.action.save']),
|
||||
@@ -138,13 +132,13 @@ function EmailField(props) {
|
||||
// Swallowing the onSubmit event on the form would be better, but
|
||||
// we would have to add that logic for every field given our
|
||||
// current structure of the application.
|
||||
if (saveState === 'pending') { e.preventDefault(); }
|
||||
if (saveState === 'pending') e.preventDefault();
|
||||
}}
|
||||
disabledStates={[]}
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
onClick={handleCancel}
|
||||
className="btn-outline-primary"
|
||||
>
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.cancel'])}
|
||||
</Button>
|
||||
@@ -156,13 +150,13 @@ function EmailField(props) {
|
||||
<div className="d-flex align-items-start">
|
||||
<h6 aria-level="3">{label}</h6>
|
||||
{isEditable ? (
|
||||
<Button variant="link" onClick={handleEdit} className="ml-3">
|
||||
<Button onClick={handleEdit} className="ml-3 btn-link">
|
||||
<FontAwesomeIcon className="mr-1" icon={faPencilAlt} />
|
||||
{intl.formatMessage(messages['account.settings.editable.field.action.edit'])}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<p data-hj-suppress>{renderValue()}</p>
|
||||
<p>{renderValue()}</p>
|
||||
{renderConfirmationMessage() || <p className="small text-muted mt-n2">{helpText}</p>}
|
||||
</div>
|
||||
),
|
||||
@@ -171,6 +165,7 @@ function EmailField(props) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
EmailField.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||
@@ -207,6 +202,7 @@ EmailField.defaultProps = {
|
||||
isEditable: true,
|
||||
};
|
||||
|
||||
|
||||
export default connect(editableFieldSelector, {
|
||||
onEdit: openForm,
|
||||
onCancel: closeForm,
|
||||
|
||||
@@ -3,18 +3,16 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { NavHashLink } from 'react-router-hash-link';
|
||||
import Scrollspy from 'react-scrollspy';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import PropTypes from 'prop-types';
|
||||
import messages from './AccountSettingsPage.messages';
|
||||
|
||||
function JumpNav({ intl, displayDemographicsLink }) {
|
||||
|
||||
function JumpNav({ intl }) {
|
||||
return (
|
||||
<div className="jump-nav">
|
||||
<Scrollspy
|
||||
items={[
|
||||
'basic-information',
|
||||
'profile-information',
|
||||
'demographics-information',
|
||||
'social-media',
|
||||
'site-preferences',
|
||||
'linked-accounts',
|
||||
@@ -33,14 +31,6 @@ function JumpNav({ intl, displayDemographicsLink }) {
|
||||
{intl.formatMessage(messages['account.settings.section.profile.information'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && displayDemographicsLink
|
||||
&& (
|
||||
<li>
|
||||
<NavHashLink to="#demographics-information">
|
||||
{intl.formatMessage(messages['account.settings.section.demographics.information'])}
|
||||
</NavHashLink>
|
||||
</li>
|
||||
)}
|
||||
<li>
|
||||
<NavHashLink to="#social-media">
|
||||
{intl.formatMessage(messages['account.settings.section.social.media'])}
|
||||
@@ -66,9 +56,10 @@ function JumpNav({ intl, displayDemographicsLink }) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
JumpNav.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
displayDemographicsLink: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
|
||||
export default injectIntl(JumpNav);
|
||||
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { TransitionReplace } from '@edx/paragon';
|
||||
|
||||
|
||||
const onChildExit = (htmlNode) => {
|
||||
// If the leaving child has focus, take control and redirect it
|
||||
if (htmlNode.contains(document.activeElement)) {
|
||||
@@ -10,9 +11,7 @@ const onChildExit = (htmlNode) => {
|
||||
const enteringChild = htmlNode.previousSibling || htmlNode.nextSibling;
|
||||
|
||||
// There's no replacement, do nothing.
|
||||
if (!enteringChild) {
|
||||
return;
|
||||
}
|
||||
if (!enteringChild) return;
|
||||
|
||||
// Get all the focusable elements in the entering child and focus the first one
|
||||
const focusableElements = enteringChild.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
@@ -22,6 +21,7 @@ const onChildExit = (htmlNode) => {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function SwitchContent({ expression, cases, className }) {
|
||||
const getContent = (caseKey) => {
|
||||
if (cases[caseKey]) {
|
||||
@@ -29,8 +29,7 @@ function SwitchContent({ expression, cases, className }) {
|
||||
return getContent(cases[caseKey]);
|
||||
}
|
||||
return React.cloneElement(cases[caseKey], { key: caseKey });
|
||||
}
|
||||
if (cases.default) {
|
||||
} else if (cases.default) {
|
||||
if (typeof cases.default === 'string') {
|
||||
return getContent(cases.default);
|
||||
}
|
||||
@@ -50,6 +49,7 @@ function SwitchContent({ expression, cases, className }) {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
SwitchContent.propTypes = {
|
||||
expression: PropTypes.string,
|
||||
cases: PropTypes.objectOf(PropTypes.node).isRequired,
|
||||
@@ -61,4 +61,5 @@ SwitchContent.defaultProps = {
|
||||
className: null,
|
||||
};
|
||||
|
||||
|
||||
export default SwitchContent;
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h6, .h6 {
|
||||
margin-bottom: .25rem;
|
||||
}
|
||||
|
||||
.btn-link {
|
||||
line-height: 1.2;
|
||||
border: none;
|
||||
@@ -20,10 +18,8 @@
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: .5rem;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -34,17 +30,15 @@
|
||||
@extend .h4;
|
||||
margin-bottom: map-get($spacers, 3);
|
||||
}
|
||||
|
||||
.account-section {
|
||||
// These properties together will shift the hashlink position
|
||||
margin-bottom: map-get($spacers, 5);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
|
||||
.custom-switch {
|
||||
padding: 0;
|
||||
max-width: 500px;
|
||||
|
||||
.custom-control-label {
|
||||
left: 2.25rem;
|
||||
line-height: 1.6rem;
|
||||
|
||||
@@ -7,14 +7,12 @@ import { faCheck } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash.get';
|
||||
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import PageLoading from '../PageLoading';
|
||||
import CoachingConsentForm from './CoachingConsentForm';
|
||||
import messages from './CoachingConsent.messages';
|
||||
import LogoSVG from '../../logo.svg';
|
||||
import { fetchSettings } from '../data/actions';
|
||||
import { fetchSettings, saveSettings } from '../data/actions';
|
||||
import { coachingConsentPageSelector } from '../data/selectors';
|
||||
|
||||
const Logo = ({ src, alt, ...attributes }) => (
|
||||
@@ -59,74 +57,104 @@ class CoachingConsent extends React.Component {
|
||||
formErrors: {},
|
||||
formSubmitted: false,
|
||||
declineSubmitted: false,
|
||||
submissionSuccess: false,
|
||||
allSubmissionsComplete: false,
|
||||
};
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.declineCoaching = this.declineCoaching.bind(this);
|
||||
this.patchUsingCoachingConsentForm = this.patchUsingCoachingConsentForm.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchSettings();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
/*
|
||||
When we are submitting the form, we're calling saveSettings 3 times, which causes
|
||||
multiple parallel redux flows. Because of this we can't rely on just the redux states
|
||||
being sent in through props. For instance if the coaching submission and name
|
||||
submission happen in near parallel, the coaching flow could return errors in
|
||||
formErrors and the name flow could overwrite the formErrors with an empty object.
|
||||
|
||||
To minimize disruption to the rest of the app, we're going to manage flow state from
|
||||
within this component.
|
||||
*/
|
||||
|
||||
// If a new error comes in, store it before the next redux call overwrites it.
|
||||
let allFormErrors = {};
|
||||
let allSubmissionsComplete = false;
|
||||
|
||||
// Collect new errors and add to state (will be cleared on new submission)
|
||||
const newErrorsFound = (
|
||||
this.props.formErrors !== prevProps.formErrors
|
||||
&& Object.keys(this.props.formErrors).length > 0
|
||||
);
|
||||
if (newErrorsFound) {
|
||||
allFormErrors = Object.assign({}, this.state.formErrors, this.props.formErrors);
|
||||
}
|
||||
|
||||
// Check if all values from the form have confirmation values
|
||||
if (
|
||||
this.state.formSubmitted &&
|
||||
this.props.confirmationValues.coaching &&
|
||||
this.props.confirmationValues.name &&
|
||||
this.props.confirmationValues.phone_number
|
||||
) {
|
||||
allSubmissionsComplete = true;
|
||||
}
|
||||
|
||||
// Check if all values from the decline link have confirmation values
|
||||
if (this.props.confirmationValues.coaching && this.state.declineSubmitted) {
|
||||
allSubmissionsComplete = true;
|
||||
}
|
||||
if (newErrorsFound || (allSubmissionsComplete !== prevState.allSubmissionsComplete)) {
|
||||
this.setState({
|
||||
formErrors: allFormErrors,
|
||||
allSubmissionsComplete,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
sanitizeForwardingUrl(url) {
|
||||
// Redirect to root of MFE if invalid next param is sent
|
||||
return url && url.startsWith(getConfig().LMS_BASE_URL) ? url : `${getConfig().LMS_BASE_URL}/dashboard/`;
|
||||
}
|
||||
|
||||
async patchUsingCoachingConsentForm(body) {
|
||||
const { userId } = getAuthenticatedUser();
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/coaching_consent/${userId}/`;
|
||||
let formErrors = {};
|
||||
const data = await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, body)
|
||||
.catch((error) => {
|
||||
if (get(error, 'customAttributes.httpErrorResponseData')) {
|
||||
formErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
} else {
|
||||
formErrors = { full_name: 'Something went wrong. Please try again.' };
|
||||
}
|
||||
this.setState({
|
||||
submissionSuccess: false,
|
||||
formErrors,
|
||||
formSubmitted: false,
|
||||
});
|
||||
});
|
||||
if (get(data, 'status') === 200) {
|
||||
this.setState({ submissionSuccess: true });
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
async handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const fullName = e.target.fullName.value;
|
||||
const phoneNumber = e.target.phoneNumber.value;
|
||||
const body = {
|
||||
coaching_consent: true,
|
||||
consent_form_seen: true,
|
||||
phone_number: phoneNumber,
|
||||
full_name: fullName,
|
||||
};
|
||||
this.setState({
|
||||
formErrors: {},
|
||||
formSubmitted: true,
|
||||
declineSubmitted: false,
|
||||
}, () => this.patchUsingCoachingConsentForm(body));
|
||||
});
|
||||
// Must store target values or they disappear before the async function can use them.
|
||||
const fullName = e.target.fullName.value;
|
||||
const phoneNumber = e.target.phoneNumber.value;
|
||||
const coachingValues = this.props.formValues.coaching;
|
||||
|
||||
// These will overwrite each other's redux states (see componentDidUpdate note)
|
||||
this.props.saveSettings('name', fullName);
|
||||
this.props.saveSettings('phone_number', phoneNumber);
|
||||
this.props.saveSettings('coaching', {
|
||||
...coachingValues,
|
||||
phone_number: phoneNumber,
|
||||
coaching_consent: true,
|
||||
consent_form_seen: true,
|
||||
});
|
||||
}
|
||||
|
||||
declineCoaching(e) {
|
||||
async declineCoaching(e) {
|
||||
e.preventDefault();
|
||||
const body = {
|
||||
coaching_consent: false,
|
||||
consent_form_seen: true,
|
||||
};
|
||||
this.setState({
|
||||
formErrors: {},
|
||||
formSubmitted: false,
|
||||
declineSubmitted: true,
|
||||
}, () => this.patchUsingCoachingConsentForm(body));
|
||||
});
|
||||
// Must store target values or they disappear before the async function can use them.
|
||||
const coachingValues = this.props.formValues.coaching;
|
||||
this.props.saveSettings('coaching', {
|
||||
...coachingValues,
|
||||
coaching_consent: false,
|
||||
consent_form_seen: true,
|
||||
});
|
||||
}
|
||||
|
||||
renderView(currentView) {
|
||||
@@ -134,27 +162,22 @@ class CoachingConsent extends React.Component {
|
||||
case VIEWS.NOT_LOADED:
|
||||
return <PageLoading srMessage="" />;
|
||||
case VIEWS.LOADED:
|
||||
return (
|
||||
<CoachingConsentForm
|
||||
onSubmit={this.handleSubmit}
|
||||
declineCoaching={this.declineCoaching}
|
||||
formErrors={this.state.formErrors}
|
||||
formValues={this.props.formValues}
|
||||
redirectUrl={this.state.redirectUrl}
|
||||
profileDataManager={this.props.profileDataManager}
|
||||
/>
|
||||
);
|
||||
return (<CoachingConsentForm
|
||||
onSubmit={this.handleSubmit}
|
||||
declineCoaching={this.declineCoaching}
|
||||
formErrors={this.state.formErrors}
|
||||
formValues={this.props.formValues}
|
||||
redirectUrl={this.state.redirectUrl}
|
||||
/>);
|
||||
case VIEWS.SUCCESS_PENDING:
|
||||
return <PageLoading srMessage="Submitting..." />;
|
||||
case VIEWS.SUCCESS:
|
||||
return (
|
||||
<SuccessMessage
|
||||
continueUrl={this.state.redirectUrl}
|
||||
header={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.header'])}
|
||||
message={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.message'])}
|
||||
continue={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.continue'])}
|
||||
/>
|
||||
);
|
||||
return (<SuccessMessage
|
||||
continueUrl={this.state.redirectUrl}
|
||||
header={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.header'])}
|
||||
message={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.message'])}
|
||||
continue={this.props.intl.formatMessage(messages['account.settings.coaching.consent.success.continue'])}
|
||||
/>);
|
||||
case VIEWS.DECLINE_PENDING:
|
||||
return <PageLoading srMessage="Redirecting..." />;
|
||||
case VIEWS.DECLINED:
|
||||
@@ -168,17 +191,18 @@ class CoachingConsent extends React.Component {
|
||||
const { loaded } = this.props;
|
||||
const formHasErrors = Object.keys(this.state.formErrors).length > 0;
|
||||
let currentView = null;
|
||||
|
||||
// This amount of logic was making the template very hard to read, so I broke it out into views.
|
||||
if (!loaded) {
|
||||
currentView = VIEWS.NOT_LOADED;
|
||||
} else if (this.state.formSubmitted && !formHasErrors) {
|
||||
if (this.state.submissionSuccess) {
|
||||
if (this.state.allSubmissionsComplete) {
|
||||
currentView = VIEWS.SUCCESS;
|
||||
} else {
|
||||
currentView = VIEWS.SUCCESS_PENDING;
|
||||
}
|
||||
} else if (this.state.declineSubmitted && !formHasErrors) {
|
||||
if (this.state.submissionSuccess) {
|
||||
if (this.state.allSubmissionsComplete) {
|
||||
currentView = VIEWS.DECLINED;
|
||||
} else {
|
||||
currentView = VIEWS.DECLINE_PENDING;
|
||||
@@ -236,7 +260,6 @@ AutoRedirect.propTypes = {
|
||||
|
||||
CoachingConsent.defaultProps = {
|
||||
loaded: false,
|
||||
profileDataManager: null,
|
||||
};
|
||||
|
||||
CoachingConsent.propTypes = {
|
||||
@@ -261,9 +284,10 @@ CoachingConsent.propTypes = {
|
||||
phone_number: PropTypes.object,
|
||||
}).isRequired,
|
||||
fetchSettings: PropTypes.func.isRequired,
|
||||
profileDataManager: PropTypes.string,
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(coachingConsentPageSelector, {
|
||||
fetchSettings,
|
||||
saveSettings,
|
||||
})(injectIntl(CoachingConsent));
|
||||
|
||||
@@ -48,7 +48,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.coaching.consent.success.message': {
|
||||
id: 'account.settings.coaching.consent.success.message',
|
||||
defaultMessage: "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
|
||||
defaultMessage: "You're signed up for coaching. You will receive a text message confirmation.",
|
||||
description: 'Text announcing that you have signed up and will receive texts',
|
||||
},
|
||||
'account.settings.coaching.consent.success.continue': {
|
||||
@@ -56,11 +56,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Start my course',
|
||||
description: 'Text that the user will be sent back to the courseware',
|
||||
},
|
||||
'account.settings.coaching.managed.support': {
|
||||
id: 'account.settings.coaching.managed.support',
|
||||
defaultMessage: 'support',
|
||||
description: 'website support',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -1,29 +1,15 @@
|
||||
import React from 'react';
|
||||
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Input, Button, Hyperlink } from '@edx/paragon';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Alert from '../Alert';
|
||||
import messages from './CoachingConsent.messages';
|
||||
|
||||
const ErrorMessage = props => (
|
||||
<div className="alert-warning mb-2">{props.message}</div>
|
||||
);
|
||||
|
||||
const ManagedProfileAlert = ({ profileDataManager }) => (
|
||||
<Alert className="alert alert-primary" role="alert">
|
||||
<FormattedMessage
|
||||
id="account.settings.coaching.managed.alert"
|
||||
defaultMessage="Your name is managed by {managerTitle}. Contact your administrator for help."
|
||||
description="alert message informing the user their account data is managed by a third party"
|
||||
values={{
|
||||
managerTitle: <b>{profileDataManager}</b>,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
const CoachingForm = props => (
|
||||
<div className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg">
|
||||
<h2 className="h2">
|
||||
@@ -33,29 +19,21 @@ const CoachingForm = props => (
|
||||
<div>
|
||||
<form onSubmit={props.onSubmit}>
|
||||
<div className="py-3">
|
||||
{!!props.profileDataManager && (
|
||||
<ManagedProfileAlert profileDataManager={props.profileDataManager} />
|
||||
)}
|
||||
<ErrorMessage message={props.formErrors.full_name} />
|
||||
<label className="h6" htmlFor="fullName">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}
|
||||
</label>
|
||||
<ErrorMessage message={props.formErrors.name} />
|
||||
<label className="h6" htmlFor="fullName">{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}</label>
|
||||
<Input
|
||||
type="text"
|
||||
name="full-name"
|
||||
id="fullName"
|
||||
disabled={!!props.profileDataManager}
|
||||
defaultValue={props.formValues.name}
|
||||
/>
|
||||
</div>
|
||||
<div className="py-3">
|
||||
<ErrorMessage message={props.formErrors.phone_number} />
|
||||
<label className="h6" htmlFor="phoneNumber">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}
|
||||
</label>
|
||||
<label className="h6" htmlFor="phoneNumber">{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}</label>
|
||||
<Input
|
||||
type="text"
|
||||
name="phone_number"
|
||||
name="full-name"
|
||||
id="phoneNumber"
|
||||
defaultValue={props.formValues.phone_number}
|
||||
/>
|
||||
@@ -67,7 +45,7 @@ const CoachingForm = props => (
|
||||
</div>
|
||||
<ErrorMessage message={props.formErrors.coaching} />
|
||||
<div className="d-flex flex-column align-items-center">
|
||||
<Button variant="outline-primary" className="w-100" type="submit">
|
||||
<Button className="w-100 btn-outline-primary" type="submit">
|
||||
{props.intl.formatMessage(messages['account.settings.coaching.consent.accept-coaching'])}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -109,11 +87,10 @@ CoachingForm.propTypes = {
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
coaching: PropTypes.string,
|
||||
full_name: PropTypes.string,
|
||||
name: PropTypes.string,
|
||||
phone_number: PropTypes.string,
|
||||
}),
|
||||
redirectUrl: PropTypes.string.isRequired,
|
||||
profileDataManager: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
ErrorMessage.defaultProps = {
|
||||
@@ -124,8 +101,4 @@ ErrorMessage.propTypes = {
|
||||
message: PropTypes.string,
|
||||
};
|
||||
|
||||
ManagedProfileAlert.propTypes = {
|
||||
profileDataManager: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CoachingForm);
|
||||
|
||||
@@ -5,9 +5,10 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { ValidationFormGroup, Input } from '@edx/paragon';
|
||||
import messages from './CoachingToggle.messages';
|
||||
import { editableFieldSelector } from '../data/selectors';
|
||||
import { saveSettings, updateDraft, saveMultipleSettings } from '../data/actions';
|
||||
import { saveSettings, updateDraft } from '../data/actions';
|
||||
import EditableField from '../EditableField';
|
||||
|
||||
|
||||
const CoachingToggle = props => (
|
||||
<>
|
||||
<EditableField
|
||||
@@ -17,25 +18,7 @@ const CoachingToggle = props => (
|
||||
label={props.intl.formatMessage(messages['account.settings.field.phone_number'])}
|
||||
emptyLabel={props.intl.formatMessage(messages['account.settings.field.phone_number.empty'])}
|
||||
onChange={props.updateDraft}
|
||||
onSubmit={() => {
|
||||
const { coaching } = props;
|
||||
if (coaching.coaching_consent === true) {
|
||||
return props.saveMultipleSettings([
|
||||
{
|
||||
formId: 'coaching',
|
||||
commitValues: {
|
||||
...coaching,
|
||||
phone_number: props.phone_number,
|
||||
},
|
||||
},
|
||||
{
|
||||
formId: 'phone_number',
|
||||
commitValues: props.phone_number,
|
||||
},
|
||||
], 'phone_number');
|
||||
}
|
||||
return props.saveSettings('phone_number', props.phone_number);
|
||||
}}
|
||||
onSubmit={props.saveSettings}
|
||||
/>
|
||||
<ValidationFormGroup
|
||||
for="coachingConsent"
|
||||
@@ -54,11 +37,9 @@ const CoachingToggle = props => (
|
||||
value={props.coaching.coaching_consent}
|
||||
onChange={async (e) => {
|
||||
const { name } = e.target;
|
||||
// eslint-disable-next-line camelcase
|
||||
const { user, eligible_for_coaching } = props.coaching;
|
||||
const value = {
|
||||
user,
|
||||
eligible_for_coaching,
|
||||
...props.coaching,
|
||||
phone_number: props.phone_number,
|
||||
coaching_consent: e.target.checked,
|
||||
};
|
||||
props.saveSettings(name, value);
|
||||
@@ -72,20 +53,18 @@ const CoachingToggle = props => (
|
||||
CoachingToggle.defaultProps = {
|
||||
phone_number: '',
|
||||
error: '',
|
||||
saveState: undefined,
|
||||
};
|
||||
|
||||
CoachingToggle.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
error: PropTypes.string,
|
||||
coaching: PropTypes.shape({
|
||||
coaching_consent: PropTypes.bool.isRequired,
|
||||
coaching: PropTypes.objectOf(PropTypes.shape({
|
||||
coaching_consent: PropTypes.string.isRequired,
|
||||
user: PropTypes.number.isRequired,
|
||||
eligible_for_coaching: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
|
||||
})).isRequired,
|
||||
saveState: PropTypes.func.isRequired,
|
||||
saveSettings: PropTypes.func.isRequired,
|
||||
saveMultipleSettings: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
phone_number: PropTypes.string,
|
||||
@@ -94,5 +73,4 @@ CoachingToggle.propTypes = {
|
||||
export default connect(editableFieldSelector, {
|
||||
saveSettings,
|
||||
updateDraft,
|
||||
saveMultipleSettings,
|
||||
})(injectIntl(CoachingToggle));
|
||||
|
||||
@@ -18,7 +18,7 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.field.coaching_consent.tooltip': {
|
||||
id: 'account.settings.field.coaching_consent.tooltip',
|
||||
defaultMessage: '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.',
|
||||
defaultMessage: '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 in English and Spanish languages. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.',
|
||||
description: 'A tooltip explaining what coaching is and who it is for',
|
||||
},
|
||||
'account.settings.field.coaching_consent.error': {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import get from 'lodash.get';
|
||||
|
||||
/**
|
||||
* get all settings related to the coaching plugin. Settings used
|
||||
@@ -8,20 +7,21 @@ import get from 'lodash.get';
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
*/
|
||||
export async function getCoachingPreferences(userId) {
|
||||
let data = {};
|
||||
let data = null;
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.get(`${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`));
|
||||
} catch (error) {
|
||||
// If a user isn't active the API call will fail with a lack of credentials.
|
||||
// Default values so the client doesn't fail if the user doesn't have an entry in the
|
||||
// UserCoaching model yet, with the assumption that they'll be eligible for coaching
|
||||
// when they hit this form.
|
||||
data = {
|
||||
coaching_consent: false,
|
||||
user: userId,
|
||||
eligible_for_coaching: false,
|
||||
eligible_for_coaching: true,
|
||||
consent_form_seen: false,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -40,11 +40,9 @@ export async function patchCoachingPreferences(userId, commitValues) {
|
||||
.catch((error) => {
|
||||
const apiError = Object.create(error);
|
||||
apiError.fieldErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
if (get(apiError, 'fieldErrors.phone_number')) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.coaching = apiError.fieldErrors.phone_number[0];
|
||||
delete apiError.fieldErrors.phone_number;
|
||||
}
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.coaching = apiError.fieldErrors.phone_number[0];
|
||||
delete apiError.fieldErrors.phone_number;
|
||||
throw apiError;
|
||||
});
|
||||
return commitValues;
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
|
||||
import CoachingConsent from '../CoachingConsent';
|
||||
import * as selectors from '../../data/selectors';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const IntlCoachingConsent = injectIntl(CoachingConsent);
|
||||
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ coachingConsentPageSelector: () => ({}) })));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('CoachingConsent', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
selectors.mockClear();
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
fetchSettings: jest.fn(),
|
||||
loaded: true,
|
||||
saveState: undefined,
|
||||
formValues: {
|
||||
name: 'edx edx',
|
||||
phone_number: '1234567890',
|
||||
coaching: {
|
||||
coaching_consent: true,
|
||||
consent_form_seen: false,
|
||||
eligible_for_coaching: true,
|
||||
user: 1,
|
||||
},
|
||||
},
|
||||
formErrors: {},
|
||||
confirmationValues: {},
|
||||
profileDataManager: '',
|
||||
intl: {},
|
||||
};
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3 }));
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlCoachingConsent {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('disables name field on enterprise user', () => {
|
||||
props = {
|
||||
...props,
|
||||
profileDataManager: 'test person',
|
||||
};
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlCoachingConsent {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('display completed box when successfully submitted', async () => {
|
||||
const fakeEvent = {
|
||||
preventDefault: () => {},
|
||||
target: {
|
||||
fullName: { value: 'edx edx' },
|
||||
phoneNumber: { value: '9783028731' },
|
||||
},
|
||||
};
|
||||
const wrapper = renderer.create(
|
||||
reduxWrapper(<IntlCoachingConsent {...props} />),
|
||||
{
|
||||
// bypass the forward-ref. we don't care about focus for this one test
|
||||
createNodeMock: (element) => {
|
||||
if (element.type === 'button') {
|
||||
// mock a focus function
|
||||
return {
|
||||
focus: async () => wrapper.root.findByType('form').props.onSubmit(fakeEvent),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
);
|
||||
const form = wrapper.root.findByType('form');
|
||||
await act(async () => { await form.props.onSubmit(fakeEvent); });
|
||||
expect(wrapper.toJSON()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,278 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CoachingConsent disables name field on enterprise user 1`] = `
|
||||
<main>
|
||||
<div
|
||||
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
className="logo"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg"
|
||||
>
|
||||
<h2
|
||||
className="h2"
|
||||
>
|
||||
Let’s get started.
|
||||
</h2>
|
||||
<p>
|
||||
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.*
|
||||
</p>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert alert-primary"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<span>
|
||||
Your name is managed by
|
||||
<b>
|
||||
test person
|
||||
</b>
|
||||
. Contact your administrator for help.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="fullName"
|
||||
>
|
||||
Please confirm your name
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
defaultValue="edx edx"
|
||||
disabled={true}
|
||||
id="fullName"
|
||||
name="full-name"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="phoneNumber"
|
||||
>
|
||||
Enter your mobile number
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
defaultValue="1234567890"
|
||||
id="phoneNumber"
|
||||
name="phone_number"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=" py-3"
|
||||
>
|
||||
<p
|
||||
className="small font-italic"
|
||||
>
|
||||
* 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.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-column align-items-center"
|
||||
>
|
||||
<button
|
||||
className="w-100 btn btn-outline-primary"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
Sign up for coaching
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="mt-3"
|
||||
>
|
||||
<a
|
||||
className="mt-3 text-dark btn-link small"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
I prefer not to be contacted with free coaching services
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
exports[`CoachingConsent display completed box when successfully submitted 1`] = `
|
||||
<main>
|
||||
<div
|
||||
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
className="logo"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="d-flex justify-content-center align-items-center flex-column"
|
||||
style={
|
||||
Object {
|
||||
"height": "50vh",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="spinner-border text-primary"
|
||||
role="status"
|
||||
>
|
||||
<span
|
||||
className="sr-only"
|
||||
>
|
||||
Submitting...
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
|
||||
exports[`CoachingConsent should render 1`] = `
|
||||
<main>
|
||||
<div
|
||||
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
|
||||
>
|
||||
<img
|
||||
alt="Logo"
|
||||
className="logo"
|
||||
src="icon/mock/path"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg"
|
||||
>
|
||||
<h2
|
||||
className="h2"
|
||||
>
|
||||
Let’s get started.
|
||||
</h2>
|
||||
<p>
|
||||
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.*
|
||||
</p>
|
||||
<div>
|
||||
<form
|
||||
onSubmit={[Function]}
|
||||
>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="fullName"
|
||||
>
|
||||
Please confirm your name
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
defaultValue="edx edx"
|
||||
disabled={false}
|
||||
id="fullName"
|
||||
name="full-name"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="py-3"
|
||||
>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<label
|
||||
className="h6"
|
||||
htmlFor="phoneNumber"
|
||||
>
|
||||
Enter your mobile number
|
||||
</label>
|
||||
<input
|
||||
className="form-control"
|
||||
defaultValue="1234567890"
|
||||
id="phoneNumber"
|
||||
name="phone_number"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className=" py-3"
|
||||
>
|
||||
<p
|
||||
className="small font-italic"
|
||||
>
|
||||
* 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.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className="alert-warning mb-2"
|
||||
>
|
||||
|
||||
</div>
|
||||
<div
|
||||
className="d-flex flex-column align-items-center"
|
||||
>
|
||||
<button
|
||||
className="w-100 btn btn-outline-primary"
|
||||
disabled={false}
|
||||
type="submit"
|
||||
>
|
||||
Sign up for coaching
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
className="mt-3"
|
||||
>
|
||||
<a
|
||||
className="mt-3 text-dark btn-link small"
|
||||
href="http://localhost:18000/dashboard/"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
I prefer not to be contacted with free coaching services
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
@@ -2,7 +2,6 @@ import { AsyncActionType } from './utils';
|
||||
|
||||
export const FETCH_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'FETCH_SETTINGS');
|
||||
export const SAVE_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'SAVE_SETTINGS');
|
||||
export const SAVE_MULTIPLE_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'SAVE_MULTIPLE_SETTINGS');
|
||||
export const FETCH_TIME_ZONES = new AsyncActionType('ACCOUNT_SETTINGS', 'FETCH_TIME_ZONES');
|
||||
export const SAVE_PREVIOUS_SITE_LANGUAGE = 'SAVE_PREVIOUS_SITE_LANGUAGE';
|
||||
export const OPEN_FORM = 'OPEN_FORM';
|
||||
@@ -44,6 +43,7 @@ export const fetchSettingsReset = () => ({
|
||||
type: FETCH_SETTINGS.RESET,
|
||||
});
|
||||
|
||||
|
||||
// FORM STATE ACTIONS
|
||||
|
||||
export const openForm = formId => ({
|
||||
@@ -68,6 +68,7 @@ export const resetDrafts = () => ({
|
||||
type: RESET_DRAFTS,
|
||||
});
|
||||
|
||||
|
||||
// SAVE SETTINGS ACTIONS
|
||||
|
||||
export const saveSettings = (formId, commitValues) => ({
|
||||
@@ -98,25 +99,6 @@ export const savePreviousSiteLanguage = previousSiteLanguage => ({
|
||||
payload: { previousSiteLanguage },
|
||||
});
|
||||
|
||||
export const saveMultipleSettings = (settingsArray, form = null) => ({
|
||||
type: SAVE_MULTIPLE_SETTINGS.BASE,
|
||||
payload: { settingsArray, form },
|
||||
});
|
||||
|
||||
export const saveMultipleSettingsBegin = () => ({
|
||||
type: SAVE_MULTIPLE_SETTINGS.BEGIN,
|
||||
});
|
||||
|
||||
export const saveMultipleSettingsSuccess = settingsArray => ({
|
||||
type: SAVE_MULTIPLE_SETTINGS.SUCCESS,
|
||||
payload: { settingsArray },
|
||||
});
|
||||
|
||||
export const saveMultipleSettingsFailure = ({ fieldErrors, message }) => ({
|
||||
type: SAVE_MULTIPLE_SETTINGS.FAILURE,
|
||||
payload: { errors: fieldErrors, message },
|
||||
});
|
||||
|
||||
// FETCH TIME_ZONE ACTIONS
|
||||
|
||||
export const fetchTimeZones = country => ({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
|
||||
export const YEAR_OF_BIRTH_OPTIONS = (() => {
|
||||
const currentYear = new Date().getFullYear();
|
||||
const years = [];
|
||||
@@ -30,88 +31,4 @@ export const GENDER_OPTIONS = [
|
||||
'o',
|
||||
];
|
||||
|
||||
export const COUNTRY_WITH_STATES = 'US';
|
||||
|
||||
export const TRANSIFEX_LANGUAGE_BASE_URL = 'https://www.transifex.com/open-edx/edx-platform/language/';
|
||||
|
||||
const COUNTRY_STATES_MAP = {
|
||||
CA: [
|
||||
{ value: 'AB', label: 'Alberta' },
|
||||
{ value: 'BC', label: 'British Columbia' },
|
||||
{ value: 'MB', label: 'Manitoba' },
|
||||
{ value: 'NB', label: 'New Brunswick' },
|
||||
{ value: 'NL', label: 'Newfoundland and Labrador' },
|
||||
{ value: 'NS', label: 'Nova Scotia' },
|
||||
{ value: 'NT', label: 'Northwest Territories' },
|
||||
{ value: 'NU', label: 'Nunavut' },
|
||||
{ value: 'ON', label: 'Ontario' },
|
||||
{ value: 'PE', label: 'Prince Edward Island' },
|
||||
{ value: 'QC', label: 'Québec' },
|
||||
{ value: 'SK', label: 'Saskatchewan' },
|
||||
{ value: 'YT', label: 'Yukon' },
|
||||
],
|
||||
US: [
|
||||
{ value: 'AL', label: 'Alabama' },
|
||||
{ value: 'AK', label: 'Alaska' },
|
||||
{ value: 'AZ', label: 'Arizona' },
|
||||
{ value: 'AR', label: 'Arkansas' },
|
||||
{ value: 'AA', label: 'Armed Forces Americas' },
|
||||
{ value: 'AE', label: 'Armed Forces Europe' },
|
||||
{ value: 'AP', label: 'Armed Forces Pacific' },
|
||||
{ value: 'CA', label: 'California' },
|
||||
{ value: 'CO', label: 'Colorado' },
|
||||
{ value: 'CT', label: 'Connecticut' },
|
||||
{ value: 'DE', label: 'Delaware' },
|
||||
{ value: 'DC', label: 'District Of Columbia' },
|
||||
{ value: 'FL', label: 'Florida' },
|
||||
{ value: 'GA', label: 'Georgia' },
|
||||
{ value: 'HI', label: 'Hawaii' },
|
||||
{ value: 'ID', label: 'Idaho' },
|
||||
{ value: 'IL', label: 'Illinois' },
|
||||
{ value: 'IN', label: 'Indiana' },
|
||||
{ value: 'IA', label: 'Iowa' },
|
||||
{ value: 'KS', label: 'Kansas' },
|
||||
{ value: 'KY', label: 'Kentucky' },
|
||||
{ value: 'LA', label: 'Louisiana' },
|
||||
{ value: 'ME', label: 'Maine' },
|
||||
{ value: 'MD', label: 'Maryland' },
|
||||
{ value: 'MA', label: 'Massachusetts' },
|
||||
{ value: 'MI', label: 'Michigan' },
|
||||
{ value: 'MN', label: 'Minnesota' },
|
||||
{ value: 'MS', label: 'Mississippi' },
|
||||
{ value: 'MO', label: 'Missouri' },
|
||||
{ value: 'MT', label: 'Montana' },
|
||||
{ value: 'NE', label: 'Nebraska' },
|
||||
{ value: 'NV', label: 'Nevada' },
|
||||
{ value: 'NH', label: 'New Hampshire' },
|
||||
{ value: 'NJ', label: 'New Jersey' },
|
||||
{ value: 'NM', label: 'New Mexico' },
|
||||
{ value: 'NY', label: 'New York' },
|
||||
{ value: 'NC', label: 'North Carolina' },
|
||||
{ value: 'ND', label: 'North Dakota' },
|
||||
{ value: 'OH', label: 'Ohio' },
|
||||
{ value: 'OK', label: 'Oklahoma' },
|
||||
{ value: 'OR', label: 'Oregon' },
|
||||
{ value: 'PA', label: 'Pennsylvania' },
|
||||
{ value: 'RI', label: 'Rhode Island' },
|
||||
{ value: 'SC', label: 'South Carolina' },
|
||||
{ value: 'SD', label: 'South Dakota' },
|
||||
{ value: 'TN', label: 'Tennessee' },
|
||||
{ value: 'TX', label: 'Texas' },
|
||||
{ value: 'UT', label: 'Utah' },
|
||||
{ value: 'VT', label: 'Vermont' },
|
||||
{ value: 'VA', label: 'Virginia' },
|
||||
{ value: 'WA', label: 'Washington' },
|
||||
{ value: 'WV', label: 'West Virginia' },
|
||||
{ value: 'WI', label: 'Wisconsin' },
|
||||
{ value: 'WY', label: 'Wyoming' },
|
||||
],
|
||||
};
|
||||
|
||||
export function getStatesList(country) {
|
||||
return country && COUNTRY_STATES_MAP[country.toUpperCase()];
|
||||
}
|
||||
|
||||
export const DECLINED = 'declined';
|
||||
export const SELF_DESCRIBE = 'self-describe';
|
||||
export const OTHER = 'other';
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
SAVE_PREVIOUS_SITE_LANGUAGE,
|
||||
UPDATE_DRAFT,
|
||||
RESET_DRAFTS,
|
||||
SAVE_MULTIPLE_SETTINGS,
|
||||
} from './actions';
|
||||
|
||||
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from '../delete-account';
|
||||
@@ -48,9 +47,11 @@ const reducer = (state = defaultState, action) => {
|
||||
case FETCH_SETTINGS.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
values: { ...state.values, ...action.payload.values },
|
||||
values: Object.assign({}, state.values, action.payload.values),
|
||||
// Dump the providers into thirdPartyAuth.
|
||||
thirdPartyAuth: { ...state.thirdPartyAuth, providers: action.payload.thirdPartyAuthProviders },
|
||||
thirdPartyAuth: Object.assign({}, state.thirdPartyAuth, {
|
||||
providers: action.payload.thirdPartyAuthProviders,
|
||||
}),
|
||||
profileDataManager: action.payload.profileDataManager,
|
||||
timeZones: action.payload.timeZones,
|
||||
loading: false,
|
||||
@@ -95,7 +96,9 @@ const reducer = (state = defaultState, action) => {
|
||||
case UPDATE_DRAFT:
|
||||
return {
|
||||
...state,
|
||||
drafts: { ...state.drafts, [action.payload.name]: action.payload.value },
|
||||
drafts: Object.assign({}, state.drafts, {
|
||||
[action.payload.name]: action.payload.value,
|
||||
}),
|
||||
saveState: null,
|
||||
errors: {},
|
||||
};
|
||||
@@ -116,19 +119,19 @@ const reducer = (state = defaultState, action) => {
|
||||
return {
|
||||
...state,
|
||||
saveState: 'complete',
|
||||
values: { ...state.values, ...action.payload.values },
|
||||
values: Object.assign({}, state.values, action.payload.values),
|
||||
errors: {},
|
||||
confirmationValues: {
|
||||
|
||||
...state.confirmationValues,
|
||||
...action.payload.confirmationValues,
|
||||
},
|
||||
confirmationValues: Object.assign(
|
||||
{},
|
||||
state.confirmationValues,
|
||||
action.payload.confirmationValues,
|
||||
),
|
||||
};
|
||||
case SAVE_SETTINGS.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'error',
|
||||
errors: { ...state.errors, ...action.payload.errors },
|
||||
errors: Object.assign({}, state.errors, action.payload.errors),
|
||||
};
|
||||
case SAVE_SETTINGS.RESET:
|
||||
return {
|
||||
@@ -141,24 +144,6 @@ const reducer = (state = defaultState, action) => {
|
||||
...state,
|
||||
previousSiteLanguage: action.payload.previousSiteLanguage,
|
||||
};
|
||||
case SAVE_MULTIPLE_SETTINGS.BEGIN:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'pending',
|
||||
};
|
||||
|
||||
case SAVE_MULTIPLE_SETTINGS.SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'complete',
|
||||
};
|
||||
|
||||
case SAVE_MULTIPLE_SETTINGS.FAILURE:
|
||||
return {
|
||||
...state,
|
||||
saveState: 'error',
|
||||
errors: { ...state.errors, ...action.payload.errors },
|
||||
};
|
||||
|
||||
case FETCH_TIME_ZONES.SUCCESS:
|
||||
return {
|
||||
@@ -192,7 +177,6 @@ const reducer = (state = defaultState, action) => {
|
||||
|
||||
case RESET_PASSWORD.BEGIN:
|
||||
case RESET_PASSWORD.SUCCESS:
|
||||
case RESET_PASSWORD.FORBIDDEN:
|
||||
return {
|
||||
...state,
|
||||
resetPassword: resetPasswordReducer(state.resetPassword, action),
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import {
|
||||
call, put, delay, takeEvery, all,
|
||||
} from 'redux-saga/effects';
|
||||
import { call, put, delay, takeEvery, all } from 'redux-saga/effects';
|
||||
|
||||
import { publish } from '@edx/frontend-platform';
|
||||
import { getLocale, handleRtl, LOCALE_CHANGED } from '@edx/frontend-platform/i18n';
|
||||
@@ -14,7 +12,6 @@ import {
|
||||
fetchSettingsFailure,
|
||||
closeForm,
|
||||
SAVE_SETTINGS,
|
||||
SAVE_MULTIPLE_SETTINGS,
|
||||
saveSettingsBegin,
|
||||
saveSettingsSuccess,
|
||||
saveSettingsFailure,
|
||||
@@ -22,9 +19,6 @@ import {
|
||||
FETCH_TIME_ZONES,
|
||||
fetchTimeZones,
|
||||
fetchTimeZonesSuccess,
|
||||
saveMultipleSettingsBegin,
|
||||
saveMultipleSettingsSuccess,
|
||||
saveMultipleSettingsFailure,
|
||||
} from './actions';
|
||||
|
||||
// Sub-modules
|
||||
@@ -54,7 +48,7 @@ export function* handleFetchSettings() {
|
||||
userId,
|
||||
);
|
||||
|
||||
if (values.country) { yield put(fetchTimeZones(values.country)); }
|
||||
if (values.country) yield put(fetchTimeZones(values.country));
|
||||
|
||||
yield put(fetchSettingsSuccess({
|
||||
values,
|
||||
@@ -93,7 +87,7 @@ export function* handleSaveSettings(action) {
|
||||
savedValues = yield call(patchSettings, username, commitData, userId);
|
||||
}
|
||||
yield put(saveSettingsSuccess(savedValues, commitData));
|
||||
if (savedValues.country) { yield put(fetchTimeZones(savedValues.country)); }
|
||||
if (savedValues.country) yield put(fetchTimeZones(savedValues.country));
|
||||
yield delay(1000);
|
||||
yield put(closeForm(action.payload.formId));
|
||||
} catch (e) {
|
||||
@@ -106,43 +100,15 @@ export function* handleSaveSettings(action) {
|
||||
}
|
||||
}
|
||||
|
||||
// handles mutiple settings saved at once, in order, and stops executing on first failure.
|
||||
export function* handleSaveMultipleSettings(action) {
|
||||
try {
|
||||
yield put(saveMultipleSettingsBegin());
|
||||
const { username, userId } = getAuthenticatedUser();
|
||||
const { settingsArray, form } = action.payload;
|
||||
for (let i = 0; i < settingsArray.length; i += 1) {
|
||||
const { formId, commitValues } = settingsArray[i];
|
||||
yield put(saveSettingsBegin());
|
||||
const commitData = { [formId]: commitValues };
|
||||
const savedSettings = yield call(patchSettings, username, commitData, userId);
|
||||
yield put(saveSettingsSuccess(savedSettings, commitData));
|
||||
}
|
||||
yield put(saveMultipleSettingsSuccess(action));
|
||||
if (form) {
|
||||
yield delay(1000);
|
||||
yield put(closeForm(form));
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.fieldErrors) {
|
||||
yield put(saveMultipleSettingsFailure({ fieldErrors: e.fieldErrors }));
|
||||
} else {
|
||||
yield put(saveMultipleSettingsFailure(e.message));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* handleFetchTimeZones(action) {
|
||||
const response = yield call(getTimeZones, action.payload.country);
|
||||
yield put(fetchTimeZonesSuccess(response, action.payload.country));
|
||||
}
|
||||
|
||||
|
||||
export default function* saga() {
|
||||
yield takeEvery(FETCH_SETTINGS.BASE, handleFetchSettings);
|
||||
yield takeEvery(SAVE_SETTINGS.BASE, handleSaveSettings);
|
||||
yield takeEvery(SAVE_MULTIPLE_SETTINGS.BASE, handleSaveMultipleSettings);
|
||||
yield takeEvery(FETCH_TIME_ZONES.BASE, handleFetchTimeZones);
|
||||
yield all([
|
||||
deleteAccountSaga(),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createSelector, createStructuredSelector } from 'reselect';
|
||||
import { siteLanguageListSelector, siteLanguageOptionsSelector } from '../site-language';
|
||||
|
||||
import { siteLanguageOptionsSelector, siteLanguageListSelector } from '../site-language';
|
||||
|
||||
export const storeName = 'accountSettings';
|
||||
|
||||
@@ -72,6 +73,7 @@ export const staticFieldsSelector = createSelector(
|
||||
accountSettings => (accountSettings.profileDataManager ? ['name', 'email', 'country'] : []),
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* If there's no draft present at all (undefined), use the original committed value.
|
||||
*/
|
||||
@@ -165,7 +167,6 @@ export const coachingConsentPageSelector = createSelector(
|
||||
accountSettingsSelector,
|
||||
formValuesSelector,
|
||||
activeAccountSelector,
|
||||
profileDataManagerSelector,
|
||||
saveStateSelector,
|
||||
confirmationValuesSelector,
|
||||
errorSelector,
|
||||
@@ -173,7 +174,6 @@ export const coachingConsentPageSelector = createSelector(
|
||||
accountSettings,
|
||||
formValues,
|
||||
activeAccount,
|
||||
profileDataManager,
|
||||
saveState,
|
||||
confirmationValues,
|
||||
errors,
|
||||
@@ -182,25 +182,9 @@ export const coachingConsentPageSelector = createSelector(
|
||||
loaded: accountSettings.loaded,
|
||||
loadingError: accountSettings.loadingError,
|
||||
isActive: activeAccount,
|
||||
profileDataManager,
|
||||
formValues,
|
||||
saveState,
|
||||
confirmationValues,
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
export const demographicsSectionSelector = createSelector(
|
||||
formValuesSelector,
|
||||
draftsSelector,
|
||||
errorSelector,
|
||||
(
|
||||
formValues,
|
||||
drafts,
|
||||
errors,
|
||||
) => ({
|
||||
formValues,
|
||||
drafts,
|
||||
formErrors: errors,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import pick from 'lodash.pick';
|
||||
import pickBy from 'lodash.pickby';
|
||||
import omit from 'lodash.omit';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
||||
import { handleRequestError, unpackFieldErrors } from './utils';
|
||||
import { getThirdPartyAuthProviders } from '../third-party-auth';
|
||||
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
|
||||
import { getDemographics, getDemographicsOptions, patchDemographics } from '../demographics/data/service';
|
||||
import { DEMOGRAPHICS_FIELDS } from '../demographics/data/utils';
|
||||
|
||||
const SOCIAL_PLATFORMS = [
|
||||
{ id: 'twitter', key: 'social_link_twitter' },
|
||||
@@ -44,9 +41,7 @@ function packAccountCommitData(commitData) {
|
||||
|
||||
SOCIAL_PLATFORMS.forEach(({ id, key }) => {
|
||||
// Skip missing values. Empty strings are valid values and should be preserved.
|
||||
if (commitData[key] === undefined) {
|
||||
return;
|
||||
}
|
||||
if (commitData[key] === undefined) return;
|
||||
|
||||
packedData.social_links = [{ platform: id, social_link: commitData[key] }];
|
||||
delete packedData[key];
|
||||
@@ -142,11 +137,12 @@ export async function getProfileDataManager(username, userRoles) {
|
||||
const url = `${getConfig().LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/?username=${username}`;
|
||||
const { data } = await getAuthenticatedHttpClient().get(url).catch(handleRequestError);
|
||||
|
||||
if (data.results.length > 0) {
|
||||
const enterprise = data.results[0] && data.results[0].enterprise_customer;
|
||||
// To ensure that enterprise returned is current enterprise & it manages profile settings
|
||||
if (enterprise && enterprise.sync_learner_profile_data) {
|
||||
return enterprise.name;
|
||||
if ('results' in data) {
|
||||
for (let i = 0; i < data.results.length; i += 1) {
|
||||
const enterprise = data.results[i].enterprise_customer;
|
||||
if (enterprise.sync_learner_profile_data) {
|
||||
return enterprise.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,31 +150,9 @@ export async function getProfileDataManager(username, userRoles) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function to determine if the Demographics questions should be displayed to the user. For the
|
||||
* MVP release of Demographics we are limiting the Demographics question visibility only to
|
||||
* MicroBachelors learners.
|
||||
*/
|
||||
export async function shouldDisplayDemographicsQuestions() {
|
||||
const requestUrl = `${getConfig().LMS_BASE_URL}/api/demographics/v1/demographics/status/`;
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient().get(requestUrl));
|
||||
if (data.display) {
|
||||
return data.display;
|
||||
}
|
||||
} catch (error) {
|
||||
// if there was an error then we just hide the section
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single function to GET everything considered a setting.
|
||||
* Currently encapsulates Account, Preferences, Coaching, ThirdPartyAuth, and Demographics
|
||||
* Currently encapsulates Account, Preferences, Coaching, and ThirdPartyAuth
|
||||
*/
|
||||
export async function getSettings(username, userRoles, userId) {
|
||||
const results = await Promise.all([
|
||||
@@ -188,9 +162,6 @@ export async function getSettings(username, userRoles, userId) {
|
||||
getProfileDataManager(username, userRoles),
|
||||
getTimeZones(),
|
||||
getConfig().COACHING_ENABLED && getCoachingPreferences(userId),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && shouldDisplayDemographicsQuestions(),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId),
|
||||
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographicsOptions(),
|
||||
]);
|
||||
|
||||
return {
|
||||
@@ -200,9 +171,6 @@ export async function getSettings(username, userRoles, userId) {
|
||||
profileDataManager: results[3],
|
||||
timeZones: results[4],
|
||||
coaching: results[5],
|
||||
shouldDisplayDemographicsSection: results[6],
|
||||
...results[7], // demographics
|
||||
demographicsOptions: results[8],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -216,12 +184,9 @@ export async function patchSettings(username, commitValues, userId) {
|
||||
// user/v1/preferences where it does update. This is the one we use.
|
||||
const preferenceKeys = ['time_zone'];
|
||||
const coachingKeys = ['coaching'];
|
||||
const demographicsKeys = DEMOGRAPHICS_FIELDS;
|
||||
const isDemographicsKey = (value, key) => key.includes('demographics');
|
||||
const accountCommitValues = omit(commitValues, preferenceKeys, coachingKeys, demographicsKeys);
|
||||
const accountCommitValues = omit(commitValues, preferenceKeys);
|
||||
const preferenceCommitValues = pick(commitValues, preferenceKeys);
|
||||
const coachingCommitValues = pick(commitValues, coachingKeys);
|
||||
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
|
||||
const patchRequests = [];
|
||||
|
||||
if (!isEmpty(accountCommitValues)) {
|
||||
@@ -233,9 +198,6 @@ export async function patchSettings(username, commitValues, userId) {
|
||||
if (!isEmpty(coachingCommitValues)) {
|
||||
patchRequests.push(patchCoachingPreferences(userId, coachingCommitValues));
|
||||
}
|
||||
if (!isEmpty(demographicsCommitValues)) {
|
||||
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
|
||||
}
|
||||
|
||||
const results = await Promise.all(patchRequests);
|
||||
// Assigns in order of requests. Preference keys
|
||||
|
||||
38
src/account-settings/data/utils/dataUtils.js
Normal file
38
src/account-settings/data/utils/dataUtils.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import camelCase from 'lodash.camelcase';
|
||||
import snakeCase from 'lodash.snakecase';
|
||||
|
||||
export function modifyObjectKeys(object, modify) {
|
||||
// If the passed in object is not an object, return it.
|
||||
if (
|
||||
object === undefined ||
|
||||
object === null ||
|
||||
(typeof object !== 'object' && !Array.isArray(object))
|
||||
) {
|
||||
return object;
|
||||
}
|
||||
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(value => modifyObjectKeys(value, modify));
|
||||
}
|
||||
|
||||
// Otherwise, process all its keys.
|
||||
const result = {};
|
||||
Object.entries(object).forEach(([key, value]) => {
|
||||
result[modify(key)] = modifyObjectKeys(value, modify);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function camelCaseObject(object) {
|
||||
return modifyObjectKeys(object, camelCase);
|
||||
}
|
||||
|
||||
export function snakeCaseObject(object) {
|
||||
return modifyObjectKeys(object, snakeCase);
|
||||
}
|
||||
|
||||
export function convertKeyNames(object, nameMap) {
|
||||
const transformer = key => (nameMap[key] === undefined ? key : nameMap[key]);
|
||||
|
||||
return modifyObjectKeys(object, transformer);
|
||||
}
|
||||
90
src/account-settings/data/utils/dataUtils.test.js
Normal file
90
src/account-settings/data/utils/dataUtils.test.js
Normal file
@@ -0,0 +1,90 @@
|
||||
import {
|
||||
modifyObjectKeys,
|
||||
camelCaseObject,
|
||||
snakeCaseObject,
|
||||
convertKeyNames,
|
||||
} from './dataUtils';
|
||||
|
||||
describe('modifyObjectKeys', () => {
|
||||
it('should use the provided modify function to change all keys in and object and its children', () => {
|
||||
function meowKeys(key) {
|
||||
return `${key}Meow`;
|
||||
}
|
||||
|
||||
const result = modifyObjectKeys(
|
||||
{
|
||||
one: undefined,
|
||||
two: null,
|
||||
three: '',
|
||||
four: 0,
|
||||
five: NaN,
|
||||
six: [1, 2, { seven: 'woof' }],
|
||||
eight: { nine: { ten: 'bark' }, eleven: true },
|
||||
},
|
||||
meowKeys,
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
oneMeow: undefined,
|
||||
twoMeow: null,
|
||||
threeMeow: '',
|
||||
fourMeow: 0,
|
||||
fiveMeow: NaN,
|
||||
sixMeow: [1, 2, { sevenMeow: 'woof' }],
|
||||
eightMeow: { nineMeow: { tenMeow: 'bark' }, elevenMeow: true },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('camelCaseObject', () => {
|
||||
it('should make everything camelCase', () => {
|
||||
const result = camelCaseObject({
|
||||
what_now: 'brown cow',
|
||||
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
|
||||
'dot.dot.dot': 123,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
whatNow: 'brown cow',
|
||||
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
|
||||
dotDotDot: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('snakeCaseObject', () => {
|
||||
it('should make everything snake_case', () => {
|
||||
const result = snakeCaseObject({
|
||||
whatNow: 'brown cow',
|
||||
butWho: { saysYouPeople: 'okay then', butHow: { willWeEvenKnow: 'the song is over' } },
|
||||
'dot.dot.dot': 123,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
what_now: 'brown cow',
|
||||
but_who: { says_you_people: 'okay then', but_how: { will_we_even_know: 'the song is over' } },
|
||||
dot_dot_dot: 123,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertKeyNames', () => {
|
||||
it('should replace the specified keynames', () => {
|
||||
const result = convertKeyNames(
|
||||
{
|
||||
one: { two: { three: 'four' } },
|
||||
five: 'six',
|
||||
},
|
||||
{
|
||||
two: 'blue',
|
||||
five: 'alive',
|
||||
seven: 'heaven',
|
||||
},
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
one: { blue: { three: 'four' } },
|
||||
alive: 'six',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,9 @@
|
||||
export {
|
||||
camelCaseObject,
|
||||
convertKeyNames,
|
||||
modifyObjectKeys,
|
||||
snakeCaseObject,
|
||||
} from './dataUtils';
|
||||
export {
|
||||
AsyncActionType,
|
||||
getModuleState,
|
||||
|
||||
@@ -27,10 +27,6 @@ export class AsyncActionType {
|
||||
get RESET() {
|
||||
return `${this.topic}__${this.name}__RESET`;
|
||||
}
|
||||
|
||||
get FORBIDDEN() {
|
||||
return `${this.topic}__${this.name}__FORBIDDEN`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,6 @@ describe('AsyncActionType', () => {
|
||||
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
|
||||
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
|
||||
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
|
||||
expect(actionType.FORBIDDEN).toBe('HOUSE_CATS__START_THE_RACE__FORBIDDEN');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
// Messages
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
|
||||
// Components
|
||||
@@ -23,14 +22,13 @@ const BeforeProceedingBanner = (props) => {
|
||||
<FormattedMessage
|
||||
id="account.settings.delete.account.before.proceeding"
|
||||
defaultMessage="Before proceeding, please {actionLink}."
|
||||
description="Error that appears if you are trying to delete your account, but something about your account needs attention first. The actionLink will be instructions, such as 'unlink your Facebook account'."
|
||||
description="Error that appears if you are trying to delete your edX account, but something about your account needs attention first. The actionLink will be instructions, such as 'unlink your Facebook account'."
|
||||
values={{
|
||||
actionLink: (
|
||||
<Hyperlink destination={supportArticleUrl}>
|
||||
{intl.formatMessage(messages[instructionMessageId])}
|
||||
</Hyperlink>
|
||||
),
|
||||
siteName: getConfig().SITE_NAME,
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Button, Input, Modal, ValidationFormGroup,
|
||||
} from '@edx/paragon';
|
||||
import { Button, Input, Modal, ValidationFormGroup } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { faExclamationCircle, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
import Alert from '../Alert';
|
||||
import PrintingInstructions from './PrintingInstructions';
|
||||
@@ -22,8 +19,6 @@ export class ConfirmationModal extends Component {
|
||||
switch (reason) {
|
||||
case 'empty-password':
|
||||
return 'account.settings.delete.account.error.no.password';
|
||||
case 'invalid-password':
|
||||
return 'account.settings.delete.account.error.invalid.password';
|
||||
default:
|
||||
return 'account.settings.delete.account.error.unable.to.delete';
|
||||
}
|
||||
@@ -36,9 +31,10 @@ export class ConfirmationModal extends Component {
|
||||
return null;
|
||||
}
|
||||
const headerMessageId = this.getShortErrorMessageId(errorType);
|
||||
const detailsMessageId = reason === 'empty-password'
|
||||
? null
|
||||
: 'account.settings.delete.account.error.unable.to.delete.details';
|
||||
const detailsMessageId =
|
||||
reason === 'empty-password'
|
||||
? null
|
||||
: 'account.settings.delete.account.error.unable.to.delete.details';
|
||||
|
||||
return (
|
||||
<Alert
|
||||
@@ -66,18 +62,11 @@ export class ConfirmationModal extends Component {
|
||||
const open = ['confirming', 'pending', 'failed'].includes(status);
|
||||
const passwordFieldId = 'passwordFieldId';
|
||||
const invalidMessage = messages[this.getShortErrorMessageId(errorType)];
|
||||
|
||||
// TODO: We lack a good way of providing custom language for a particular site. This is a hack
|
||||
// to allow edx.org to fulfill its business requirements.
|
||||
const deleteAccountModalText2MessageKey = getConfig().SITE_NAME === 'edX'
|
||||
? 'account.settings.delete.account.modal.text.2.edX'
|
||||
: 'account.settings.delete.account.modal.text.2';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={open}
|
||||
title={intl.formatMessage(messages['account.settings.delete.account.modal.header'])}
|
||||
body={(
|
||||
body={
|
||||
<div>
|
||||
{this.renderError()}
|
||||
<Alert
|
||||
@@ -85,17 +74,9 @@ export class ConfirmationModal extends Component {
|
||||
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
|
||||
>
|
||||
<h6>
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.modal.text.1'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
{intl.formatMessage(messages['account.settings.delete.account.modal.text.1'])}
|
||||
</h6>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages[deleteAccountModalText2MessageKey],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.modal.text.2'])}</p>
|
||||
<p>
|
||||
<PrintingInstructions />
|
||||
</p>
|
||||
@@ -117,9 +98,9 @@ export class ConfirmationModal extends Component {
|
||||
/>
|
||||
</ValidationFormGroup>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
buttons={[
|
||||
<Button variant="danger" onClick={onSubmit}>
|
||||
<Button className="btn-danger" onClick={onSubmit}>
|
||||
{intl.formatMessage(messages['account.settings.delete.account.modal.confirm.delete'])}
|
||||
</Button>,
|
||||
]}
|
||||
|
||||
@@ -24,13 +24,9 @@ import ConnectedSuccessModal from './SuccessModal';
|
||||
import BeforeProceedingBanner from './BeforeProceedingBanner';
|
||||
|
||||
export class DeleteAccount extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
password: '',
|
||||
};
|
||||
}
|
||||
state = {
|
||||
password: '',
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
if (this.state.password === '') {
|
||||
@@ -60,36 +56,19 @@ export class DeleteAccount extends React.Component {
|
||||
} = this.props;
|
||||
const canDelete = isVerifiedAccount && !hasLinkedTPA;
|
||||
|
||||
// TODO: We lack a good way of providing custom language for a particular site. This is a hack
|
||||
// to allow edx.org to fulfill its business requirements.
|
||||
const deleteAccountText2MessageKey = getConfig().SITE_NAME === 'edX'
|
||||
? 'account.settings.delete.account.text.2.edX'
|
||||
: 'account.settings.delete.account.text.2';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="section-heading">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.header'])}
|
||||
</h2>
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.subheader'])}</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages['account.settings.delete.account.text.1'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{intl.formatMessage(
|
||||
messages[deleteAccountText2MessageKey],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.text.1'])}</p>
|
||||
<p>{intl.formatMessage(messages['account.settings.delete.account.text.2'])}</p>
|
||||
<p>
|
||||
<PrintingInstructions />
|
||||
</p>
|
||||
<p className="text-danger h6">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.text.warning'],
|
||||
{ siteName: getConfig().SITE_NAME })}
|
||||
{intl.formatMessage(messages['account.settings.delete.account.text.warning'])}
|
||||
</p>
|
||||
<p>
|
||||
<Hyperlink destination="https://support.edx.org/hc/en-us/sections/115004139268-Manage-Your-Account-Settings">
|
||||
@@ -98,7 +77,7 @@ export class DeleteAccount extends React.Component {
|
||||
</p>
|
||||
<p>
|
||||
<Button
|
||||
variant="outline-danger"
|
||||
className="btn-outline-danger"
|
||||
onClick={canDelete ? this.props.deleteAccountConfirmation : null}
|
||||
disabled={!canDelete}
|
||||
>
|
||||
|
||||
@@ -2,39 +2,22 @@ import React from 'react';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './messages';
|
||||
|
||||
const PrintingInstructions = (props) => {
|
||||
const actionLink = (
|
||||
<Hyperlink
|
||||
// TODO: What would a generic version of this link look like? Should
|
||||
// CERTIFICATE_SHARING_HELP_URL really be a configuration variable? In the meantime,
|
||||
// We've removed the link from the default message.
|
||||
destination="https://support.edx.org/hc/en-us/sections/115004173027-Receive-and-Share-edX-Certificates"
|
||||
destination="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
|
||||
>
|
||||
{props.intl.formatMessage(messages['account.settings.delete.account.text.3.link'])}
|
||||
</Hyperlink>
|
||||
);
|
||||
|
||||
// TODO: We lack a good way of providing custom language for a particular site. This is a hack
|
||||
// to allow edx.org to mention MicroMasters certificates to fulfill its business requirements.
|
||||
if (getConfig().SITE_NAME === 'edX') {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="account.settings.delete.account.text.3.edX"
|
||||
defaultMessage="You may also lose access to verified certificates and other program credentials like MicroMasters certificates. You can make a copy of these for your records before proceeding with deletion. {actionLink}."
|
||||
description="A message in the user account deletion area warning users that deleting their account will prevent them from accessing their certificates. 'actionLink' is a HTML link with a full sentence that describes how to print a certificate."
|
||||
values={{ actionLink }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="account.settings.delete.account.text.3"
|
||||
defaultMessage="You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion."
|
||||
description="A message in the user account deletion area warning users that deleting their account will prevent them from accessing their certificates."
|
||||
defaultMessage="You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}."
|
||||
description="A message in the user account deletion area"
|
||||
values={{ actionLink }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -11,13 +11,13 @@ export const SuccessModal = (props) => {
|
||||
<Modal
|
||||
open={status === 'deleted'}
|
||||
title={intl.formatMessage(messages['account.settings.delete.account.modal.after.header'])}
|
||||
body={(
|
||||
body={
|
||||
<div>
|
||||
<p className="h6">
|
||||
{intl.formatMessage(messages['account.settings.delete.account.modal.after.text'])}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
closeText={intl.formatMessage(messages['account.settings.delete.account.modal.after.button'])}
|
||||
renderHeaderCloseButton={false}
|
||||
onClose={onClose}
|
||||
|
||||
@@ -1,566 +1,439 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConfirmationModal should match default closed confirmation modal snapshot 1`] = `
|
||||
Array [
|
||||
<div>
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>,
|
||||
/>
|
||||
<div
|
||||
className="modal fade"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id1"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled="disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id1"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
</h6>
|
||||
<p>
|
||||
If you proceed, you will be unable to use this account to take courses on localhost.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="d-block"
|
||||
htmlFor="passwordFieldId"
|
||||
>
|
||||
If you still wish to continue and delete your account, please enter your account password:
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
className="form-control"
|
||||
id="passwordFieldId"
|
||||
name="password"
|
||||
onChange={[MockFunction]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="passwordFieldId-invalid-feedback"
|
||||
>
|
||||
Unable to delete account
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Yes, Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ConfirmationModal should match empty password confirmation modal snapshot 1`] = `
|
||||
Array [
|
||||
<div
|
||||
className="modal-backdrop show"
|
||||
role="presentation"
|
||||
/>,
|
||||
<div
|
||||
className="modal show d-block"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id3"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id3"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-danger mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-circle fa-w-16 mr-2"
|
||||
data-icon="exclamation-circle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
A password is required
|
||||
</h6>
|
||||
<p
|
||||
className="text-danger"
|
||||
>
|
||||
Sorry, there was an error trying to process your request. Please try again later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
</h6>
|
||||
<p>
|
||||
If you proceed, you will be unable to use this account to take courses on localhost.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="d-block"
|
||||
htmlFor="passwordFieldId"
|
||||
>
|
||||
If you still wish to continue and delete your account, please enter your account password:
|
||||
</label>
|
||||
<input
|
||||
aria-describedby="passwordFieldId-invalid-feedback"
|
||||
className="form-control is-invalid"
|
||||
id="passwordFieldId"
|
||||
name="password"
|
||||
onChange={[MockFunction]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="passwordFieldId-invalid-feedback"
|
||||
>
|
||||
A password is required
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Yes, Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
|
||||
Array [
|
||||
<div
|
||||
className="modal-backdrop show"
|
||||
role="presentation"
|
||||
/>,
|
||||
<div
|
||||
className="modal show d-block"
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id2"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
className=""
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
className="modal-header"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id2"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id2"
|
||||
Are you sure?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
</h6>
|
||||
<p>
|
||||
If you proceed, you will be unable to use this account to take courses on localhost.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="d-block"
|
||||
htmlFor="passwordFieldId"
|
||||
>
|
||||
If you still wish to continue and delete your account, please enter your account password:
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
className="form-control"
|
||||
id="passwordFieldId"
|
||||
name="password"
|
||||
onChange={[MockFunction]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="passwordFieldId-invalid-feedback"
|
||||
>
|
||||
Unable to delete account
|
||||
</strong>
|
||||
<div>
|
||||
<h6>
|
||||
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
|
||||
</h6>
|
||||
<p>
|
||||
If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
|
||||
<a
|
||||
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
follow the instructions for printing or downloading a certificate
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
type="button"
|
||||
>
|
||||
Yes, Delete
|
||||
</button>
|
||||
<label
|
||||
className="d-block"
|
||||
htmlFor="passwordFieldId"
|
||||
>
|
||||
If you still wish to continue and delete your account, please enter your account password:
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
className="form-control"
|
||||
id="passwordFieldId"
|
||||
name="password"
|
||||
onChange={[MockFunction]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="passwordFieldId-invalid-feedback"
|
||||
>
|
||||
Unable to delete account
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Yes, Delete
|
||||
</button>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton1"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfirmationModal should match empty password confirmation modal snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="modal-backdrop show"
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click show d-block"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id6"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id6"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-danger mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-circle fa-w-16 mr-2"
|
||||
data-icon="exclamation-circle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M504 256c0 136.997-111.043 248-248 248S8 392.997 8 256C8 119.083 119.043 8 256 8s248 111.083 248 248zm-248 50c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
A password is required
|
||||
</h6>
|
||||
<p
|
||||
className="text-danger"
|
||||
>
|
||||
Sorry, there was an error trying to process your request. Please try again later.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
|
||||
</h6>
|
||||
<p>
|
||||
If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
|
||||
<a
|
||||
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
follow the instructions for printing or downloading a certificate
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="d-block"
|
||||
htmlFor="passwordFieldId"
|
||||
>
|
||||
If you still wish to continue and delete your account, please enter your account password:
|
||||
</label>
|
||||
<input
|
||||
aria-describedby="passwordFieldId-invalid-feedback"
|
||||
className="form-control is-invalid"
|
||||
id="passwordFieldId"
|
||||
name="password"
|
||||
onChange={[MockFunction]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="passwordFieldId-invalid-feedback"
|
||||
>
|
||||
A password is required
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Yes, Delete
|
||||
</button>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton5"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ConfirmationModal should match open confirmation modal snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="modal-backdrop show"
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click show d-block"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id4"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id4"
|
||||
>
|
||||
Are you sure?
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert-warning mt-n2"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-exclamation-triangle fa-w-18 mr-2"
|
||||
data-icon="exclamation-triangle"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 576 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M569.517 440.013C587.975 472.007 564.806 512 527.94 512H48.054c-36.937 0-59.999-40.055-41.577-71.987L246.423 23.985c18.467-32.009 64.72-31.951 83.154 0l239.94 416.028zM288 354c-25.405 0-46 20.595-46 46s20.595 46 46 46 46-20.595 46-46-20.595-46-46-46zm-43.673-165.346l7.418 136c.347 6.364 5.609 11.346 11.982 11.346h48.546c6.373 0 11.635-4.982 11.982-11.346l7.418-136c.375-6.874-5.098-12.654-11.982-12.654h-63.383c-6.884 0-12.356 5.78-11.981 12.654z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h6>
|
||||
You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
|
||||
</h6>
|
||||
<p>
|
||||
If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
|
||||
<a
|
||||
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
follow the instructions for printing or downloading a certificate
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<label
|
||||
className="d-block"
|
||||
htmlFor="passwordFieldId"
|
||||
>
|
||||
If you still wish to continue and delete your account, please enter your account password:
|
||||
</label>
|
||||
<input
|
||||
aria-describedby=""
|
||||
className="form-control"
|
||||
id="passwordFieldId"
|
||||
name="password"
|
||||
onChange={[MockFunction]}
|
||||
type="password"
|
||||
value="fluffy bunnies"
|
||||
/>
|
||||
<strong
|
||||
className="invalid-feedback"
|
||||
id="passwordFieldId-invalid-feedback"
|
||||
>
|
||||
Unable to delete account
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-danger"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Yes, Delete
|
||||
</button>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton3"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -11,20 +11,28 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
|
||||
We're sorry to see you go!
|
||||
</p>
|
||||
<p>
|
||||
Please note: Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
|
||||
</p>
|
||||
<p>
|
||||
Once your account is deleted, you cannot use it to take courses on localhost.
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
|
||||
<a
|
||||
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
follow the instructions for printing or downloading a certificate
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
className="text-danger h6"
|
||||
>
|
||||
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on localhost.
|
||||
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
@@ -39,7 +47,9 @@ exports[`DeleteAccount should match default section snapshot 1`] = `
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Delete My Account
|
||||
@@ -59,20 +69,28 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
We're sorry to see you go!
|
||||
</p>
|
||||
<p>
|
||||
Please note: Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
|
||||
</p>
|
||||
<p>
|
||||
Once your account is deleted, you cannot use it to take courses on localhost.
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
|
||||
<a
|
||||
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
follow the instructions for printing or downloading a certificate
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
className="text-danger h6"
|
||||
>
|
||||
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on localhost.
|
||||
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
@@ -87,7 +105,9 @@ exports[`DeleteAccount should match unverified account section snapshot 1`] = `
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
disabled={true}
|
||||
onClick={null}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Delete My Account
|
||||
@@ -143,20 +163,28 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
We're sorry to see you go!
|
||||
</p>
|
||||
<p>
|
||||
Please note: Deletion of your account and personal data is permanent and cannot be undone. localhost will not be able to recover your account or the data that is deleted.
|
||||
Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.
|
||||
</p>
|
||||
<p>
|
||||
Once your account is deleted, you cannot use it to take courses on localhost.
|
||||
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.
|
||||
</p>
|
||||
<p>
|
||||
<span>
|
||||
You may also lose access to verified certificates and other program credentials. You can make a copy of these for your records before proceeding with deletion.
|
||||
You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion,
|
||||
<a
|
||||
href="https://edx.readthedocs.io/projects/edx-guide-for-students/en/latest/SFD_certificates.html#printing-a-certificate"
|
||||
onClick={[Function]}
|
||||
target="_self"
|
||||
>
|
||||
follow the instructions for printing or downloading a certificate
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
</p>
|
||||
<p
|
||||
className="text-danger h6"
|
||||
>
|
||||
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on localhost.
|
||||
Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.
|
||||
</p>
|
||||
<p>
|
||||
<a
|
||||
@@ -171,7 +199,9 @@ exports[`DeleteAccount should match unverified account section snapshot 2`] = `
|
||||
<button
|
||||
className="btn btn-outline-danger"
|
||||
disabled={true}
|
||||
onClick={null}
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Delete My Account
|
||||
|
||||
@@ -1,125 +1,14 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 1`] = `
|
||||
Array [
|
||||
<div>
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>,
|
||||
/>
|
||||
<div
|
||||
className="modal fade"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id1"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled="disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id1"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 2`] = `
|
||||
Array [
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>,
|
||||
<div
|
||||
className="modal fade"
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
@@ -127,223 +16,61 @@ Array [
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled="disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
className="modal-header"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id2"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id2"
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton1"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 3`] = `
|
||||
Array [
|
||||
exports[`SuccessModal should match default closed success modal snapshot 2`] = `
|
||||
<div>
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>,
|
||||
/>
|
||||
<div
|
||||
className="modal fade"
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id3"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled="disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id3"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 4`] = `
|
||||
Array [
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>,
|
||||
<div
|
||||
className="modal fade"
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
@@ -351,213 +78,234 @@ Array [
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled="disabled"
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
className="modal-header"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id4"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id4"
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton3"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SuccessModal should match open success modal snapshot 1`] = `
|
||||
Array [
|
||||
exports[`SuccessModal should match default closed success modal snapshot 3`] = `
|
||||
<div>
|
||||
<div
|
||||
className="modal-backdrop show"
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>,
|
||||
/>
|
||||
<div
|
||||
className="modal show d-block"
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id5"
|
||||
aria-labelledby="id6"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id6"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton5"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SuccessModal should match default closed success modal snapshot 4`] = `
|
||||
<div>
|
||||
<div
|
||||
className="fade"
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click fade"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id8"
|
||||
aria-modal={true}
|
||||
className=""
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id8"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton7"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SuccessModal should match open success modal snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="modal-backdrop show"
|
||||
role="presentation"
|
||||
/>
|
||||
<div
|
||||
className="modal js-close-modal-on-click show d-block"
|
||||
onMouseDown={[Function]}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
aria-labelledby="id10"
|
||||
aria-modal={true}
|
||||
className="modal-dialog"
|
||||
role="dialog"
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={1}
|
||||
/>
|
||||
<div
|
||||
data-focus-lock-disabled={false}
|
||||
onBlur={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseDown={[Function]}
|
||||
onScrollCapture={[Function]}
|
||||
onTouchMoveCapture={[Function]}
|
||||
onTouchStart={[Function]}
|
||||
onWheelCapture={[Function]}
|
||||
className="modal-content"
|
||||
>
|
||||
<div
|
||||
className="modal-content"
|
||||
className="modal-header"
|
||||
>
|
||||
<div
|
||||
className="modal-header"
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id10"
|
||||
>
|
||||
<h2
|
||||
className="modal-title"
|
||||
id="id5"
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
We're sorry to see you go! Your account will be deleted shortly.
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
className="modal-body"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="h6"
|
||||
>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="modal-footer"
|
||||
>
|
||||
<button
|
||||
className="btn js-close-modal-on-click btn-secondary"
|
||||
id="paragonCloseModalButton9"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onKeyDown={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
data-focus-guard={true}
|
||||
style={
|
||||
Object {
|
||||
"height": "0px",
|
||||
"left": "1px",
|
||||
"overflow": "hidden",
|
||||
"padding": 0,
|
||||
"position": "fixed",
|
||||
"top": "1px",
|
||||
"width": "1px",
|
||||
}
|
||||
}
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
</div>,
|
||||
]
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -15,9 +15,7 @@ export function* handleDeleteAccount(action) {
|
||||
const response = yield call(postDeleteAccount, action.payload.password);
|
||||
yield put(deleteAccountSuccess(response));
|
||||
} catch (e) {
|
||||
if (e.response.status === 403) {
|
||||
yield put(deleteAccountFailure('invalid-password'));
|
||||
} else if (typeof e.response.data === 'string') {
|
||||
if (typeof e.response.data === 'string') {
|
||||
yield put(deleteAccountFailure());
|
||||
} else {
|
||||
throw e;
|
||||
|
||||
@@ -13,27 +13,22 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.delete.account.text.1': {
|
||||
id: 'account.settings.delete.account.text.1',
|
||||
defaultMessage: 'Please note: Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.',
|
||||
defaultMessage: 'Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.',
|
||||
description: 'A message in the user account deletion area',
|
||||
},
|
||||
'account.settings.delete.account.text.2': {
|
||||
id: 'account.settings.delete.account.text.2',
|
||||
defaultMessage: 'Once your account is deleted, you cannot use it to take courses on {siteName}.',
|
||||
description: 'A message in the user account deletion area',
|
||||
},
|
||||
'account.settings.delete.account.text.2.edX': {
|
||||
id: 'account.settings.delete.account.text.2.edX',
|
||||
defaultMessage: 'Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer’s or university’s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.',
|
||||
description: 'A message in the user account deletion area',
|
||||
},
|
||||
'account.settings.delete.account.text.3.link': {
|
||||
id: 'account.settings.delete.account.text.3.link',
|
||||
defaultMessage: 'Follow these instructions for printing or downloading a certificate',
|
||||
description: 'This text is a link to a technical support page where users can learn how to print or download their certificates.',
|
||||
defaultMessage: 'follow the instructions for printing or downloading a certificate',
|
||||
description: 'This text will be a link to a technical support page; it will go in the phrase If you want to make a copy of these for your records, ______ .',
|
||||
},
|
||||
'account.settings.delete.account.text.warning': {
|
||||
id: 'account.settings.delete.account.text.warning',
|
||||
defaultMessage: 'Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on {siteName}.',
|
||||
defaultMessage: 'Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.',
|
||||
description: 'A message in the user account deletion area',
|
||||
},
|
||||
'account.settings.delete.account.text.change.instead': {
|
||||
@@ -44,7 +39,7 @@ const messages = defineMessages({
|
||||
'account.settings.delete.account.button': {
|
||||
id: 'account.settings.delete.account.button',
|
||||
defaultMessage: 'Delete My Account',
|
||||
description: 'Button label to permanently delete your platform account',
|
||||
description: 'Button label to permanently delete your edX account',
|
||||
},
|
||||
'account.settings.delete.account.please.activate': {
|
||||
id: 'account.settings.delete.account.please.activate',
|
||||
@@ -63,16 +58,11 @@ const messages = defineMessages({
|
||||
},
|
||||
'account.settings.delete.account.modal.text.1': {
|
||||
id: 'account.settings.delete.account.modal.text.1',
|
||||
defaultMessage: 'You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. {siteName} will not be able to recover your account or the data that is deleted.',
|
||||
defaultMessage: 'You have selected "Delete My Account". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.',
|
||||
description: 'Messaging in the dialog asking user to confirm that they want to delete their entire account',
|
||||
},
|
||||
'account.settings.delete.account.modal.text.2': {
|
||||
id: 'account.settings.delete.account.modal.text.2',
|
||||
defaultMessage: 'If you proceed, you will be unable to use this account to take courses on {siteName}.',
|
||||
description: 'Messaging in the dialog asking user to confirm that they want to delete their entire account',
|
||||
},
|
||||
'account.settings.delete.account.modal.text.2.edX': {
|
||||
id: 'account.settings.delete.account.modal.text.2.edX',
|
||||
defaultMessage: 'If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer\'s or university\'s system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.',
|
||||
description: 'Messaging in the dialog asking user to confirm that they want to delete their entire account',
|
||||
},
|
||||
@@ -101,11 +91,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'A password is required',
|
||||
description: 'Error message when user has not entered their password',
|
||||
},
|
||||
'account.settings.delete.account.error.invalid.password': {
|
||||
id: 'account.settings.delete.account.error.invalid.password',
|
||||
defaultMessage: 'Password is incorrect',
|
||||
description: 'Error message when user has entered incorrect password',
|
||||
},
|
||||
'account.settings.delete.account.error.unable.to.delete.details': {
|
||||
id: 'account.settings.delete.account.error.unable.to.delete.details',
|
||||
defaultMessage: 'Sorry, there was an error trying to process your request. Please try again later.',
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CheckBox } from '@edx/paragon';
|
||||
import { DECLINED } from '../data/constants';
|
||||
|
||||
const Checkboxes = (props) => {
|
||||
const {
|
||||
id,
|
||||
options,
|
||||
values,
|
||||
onChange,
|
||||
} = props;
|
||||
|
||||
const [selected, setSelected] = useState(values);
|
||||
useEffect(() => {
|
||||
onChange(id, selected);
|
||||
}, [selected]);
|
||||
|
||||
const handleToggle = (value, option) => {
|
||||
// If the user checked 'declined', uncheck all other options
|
||||
if (value && option === DECLINED) {
|
||||
setSelected([DECLINED]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If option checked, make sure this option is in `selected` (and remove 'declined')
|
||||
if (value && !selected.includes(option)) {
|
||||
const newSelected = selected.filter(i => i !== DECLINED).concat(option);
|
||||
setSelected(newSelected);
|
||||
}
|
||||
|
||||
// If unchecked, make sure this option is NOT in `selected`
|
||||
if (!value) {
|
||||
setSelected(selected.filter(i => i !== option));
|
||||
}
|
||||
};
|
||||
|
||||
const renderCheckboxes = () => options.map((option, index) => {
|
||||
const isFirst = index === 0;
|
||||
const isChecked = selected.includes(option.value);
|
||||
return (
|
||||
<div key={option.value} className="checkboxOption">
|
||||
<CheckBox
|
||||
type="checkbox"
|
||||
id={option.value}
|
||||
name={option.value}
|
||||
value={option.value}
|
||||
checked={isChecked}
|
||||
autoFocus={isFirst}
|
||||
label={option.label}
|
||||
onChange={(value) => handleToggle(value, option.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div role="group">
|
||||
{renderCheckboxes()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Checkboxes.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
options: PropTypes.arrayOf(PropTypes.shape({
|
||||
value: PropTypes.string,
|
||||
label: PropTypes.string,
|
||||
})),
|
||||
values: PropTypes.arrayOf(PropTypes.string),
|
||||
onChange: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
Checkboxes.defaultProps = {
|
||||
options: [],
|
||||
values: [],
|
||||
};
|
||||
|
||||
export default Checkboxes;
|
||||
@@ -1,357 +0,0 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Hyperlink, Input } from '@edx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import get from 'lodash.get';
|
||||
import isEmpty from 'lodash.isempty';
|
||||
import memoize from 'memoize-one';
|
||||
import { demographicsSectionSelector } from '../data/selectors';
|
||||
import EditableField from '../EditableField';
|
||||
import Checkboxes from './Checkboxes';
|
||||
import Alert from '../Alert';
|
||||
import { saveMultipleSettings, updateDraft } from '../data/actions';
|
||||
import {
|
||||
OTHER,
|
||||
SELF_DESCRIBE,
|
||||
} from '../data/constants';
|
||||
import messages from './DemographicsSection.messages';
|
||||
|
||||
class DemographicsSection extends React.Component {
|
||||
// We check the `demographicsOptions` prop to see if it is empty before we attempt to extract and
|
||||
// format the available options for each question from the API response.
|
||||
getApiOptions = memoize((demographicsOptions) => (this.hasRetrievedDemographicsOptions() && {
|
||||
demographicsGenderOptions: this.addDefaultOption('account.settings.field.demographics.gender.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.gender.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
/* Ethnicity options don't need the blank/default option */
|
||||
demographicsEthnicityOptions: demographicsOptions.actions.POST.user_ethnicity.child.children.ethnicity.choices.map(
|
||||
key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}),
|
||||
),
|
||||
demographicsIncomeOptions: this.addDefaultOption('account.settings.field.demographics.income.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.income.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsMilitaryHistoryOptions: this.addDefaultOption('account.settings.field.demographics.military_history.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.military_history.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsEducationLevelOptions: this.addDefaultOption('account.settings.field.demographics.education_level.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.learner_education_level.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsWorkStatusOptions: this.addDefaultOption('account.settings.field.demographics.work_status.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.work_status.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
demographicsWorkSectorOptions: this.addDefaultOption('account.settings.field.demographics.work_sector.options.empty')
|
||||
.concat(demographicsOptions.actions.POST.current_work_sector.choices.map(key => ({
|
||||
value: key.value,
|
||||
label: key.display_name,
|
||||
}))),
|
||||
}));
|
||||
|
||||
ethnicityFieldDisplay = (demographicsEthnicityOptions) => {
|
||||
let ethnicities = [];
|
||||
if (get(this, 'props.formValues.demographics_user_ethnicity')) {
|
||||
ethnicities = this.props.formValues.demographics_user_ethnicity;
|
||||
}
|
||||
return ethnicities.map((e) => {
|
||||
const matchingOption = demographicsEthnicityOptions.filter(option => option.value === e)[0];
|
||||
return matchingOption && matchingOption.label;
|
||||
}).join(', ');
|
||||
}
|
||||
|
||||
handleEditableFieldChange = (name, value) => {
|
||||
this.props.updateDraft(name, value);
|
||||
};
|
||||
|
||||
handleSubmit = (formId) => {
|
||||
// We have some custom fields in this section. Instead of relying on the
|
||||
// submitted values, submit the values stored in 'drafts'.
|
||||
const { drafts } = this.props;
|
||||
const settingsArray = Object.entries(drafts).map(([field, value]) => ({
|
||||
formId: field,
|
||||
commitValues: value,
|
||||
}));
|
||||
|
||||
this.props.saveMultipleSettings(settingsArray, formId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility method that adds the specified message as a default option to the list of available
|
||||
* choices.
|
||||
*
|
||||
* @param {*} messageId id of message matching desired default label text
|
||||
*/
|
||||
addDefaultOption(messageId) {
|
||||
return [{
|
||||
value: '',
|
||||
label: this.props.intl.formatMessage(messages[messageId]),
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method that helps determine if we were able to retrieve the available options for
|
||||
* the Demographics questions. Returns true if the `demographicsOptions` prop is _not_ empty,
|
||||
* otherwise false. This prop being empty is indicative of a failure communicating with the
|
||||
* Demographics IDA's API.
|
||||
*/
|
||||
hasRetrievedDemographicsOptions() {
|
||||
return !isEmpty(this.props.formValues.demographicsOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* If an error is encountered when trying to communicate with the Demographics IDA then we will
|
||||
* display an Alert letting the user know that their info will not be displayed and temporarily
|
||||
* cannot be updated.
|
||||
*/
|
||||
renderDemographicsServiceIssueWarning() {
|
||||
if (!isEmpty(this.props.formErrors.demographicsError)
|
||||
|| !this.hasRetrievedDemographicsOptions()) {
|
||||
return (
|
||||
<div
|
||||
tabIndex="-1"
|
||||
ref={this.alertRef}
|
||||
>
|
||||
<Alert className="alert alert-danger" role="alert">
|
||||
<FormattedMessage
|
||||
id="account.settings.message.demographics.service.issue"
|
||||
defaultMessage="An error occurred attempting to retrieve or save your account information. Please try again later."
|
||||
description="alert message informing the user that the there is a problem retrieving or updating information from the Demographics microservice"
|
||||
/>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const editableFieldProps = {
|
||||
onChange: this.handleEditableFieldChange,
|
||||
onSubmit: this.handleSubmit,
|
||||
};
|
||||
|
||||
const {
|
||||
demographicsGenderOptions,
|
||||
demographicsEthnicityOptions,
|
||||
demographicsIncomeOptions,
|
||||
demographicsMilitaryHistoryOptions,
|
||||
demographicsEducationLevelOptions,
|
||||
demographicsWorkStatusOptions,
|
||||
demographicsWorkSectorOptions,
|
||||
} = this.getApiOptions(this.props.formValues.demographicsOptions);
|
||||
|
||||
const showSelfDescribe = this.props.formValues.demographics_gender === SELF_DESCRIBE;
|
||||
const showWorkStatusDescribe = this.props.formValues.demographics_work_status === OTHER;
|
||||
|
||||
return (
|
||||
<div className="account-section" id="demographics-information" ref={this.props.forwardRef}>
|
||||
<h2 className="section-heading">
|
||||
{this.props.intl.formatMessage(messages['account.settings.section.demographics.information'])}
|
||||
</h2>
|
||||
<p>
|
||||
<Hyperlink
|
||||
destination={`${getConfig().MARKETING_SITE_BASE_URL}/demographics`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{this.props.intl.formatMessage(
|
||||
messages['account.settings.section.demographics.why'],
|
||||
{
|
||||
siteName: getConfig().SITE_NAME,
|
||||
},
|
||||
)}
|
||||
</Hyperlink>
|
||||
</p>
|
||||
{this.renderDemographicsServiceIssueWarning()}
|
||||
{/*
|
||||
If the demographicsOptions props are empty then there is no need to display the fields as
|
||||
the user will not have any choices available to select, nor will they be able to update
|
||||
their answers.
|
||||
*/}
|
||||
{this.hasRetrievedDemographicsOptions() && (
|
||||
<div id="demographics-fields">
|
||||
<EditableField
|
||||
name="demographics_gender"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_gender}
|
||||
userSuppliedValue={showSelfDescribe ? this.props.formValues.demographics_gender_description : null}
|
||||
options={demographicsGenderOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
{showSelfDescribe && (
|
||||
<Input
|
||||
name="demographics_gender_description"
|
||||
id="field-demographics_gender_description"
|
||||
type="text"
|
||||
placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description.empty'])}
|
||||
value={this.props.formValues.demographics_gender_description}
|
||||
onChange={(e) => this.handleEditableFieldChange('demographics_gender_description', e.target.value)}
|
||||
aria-label={this.props.intl.formatMessage(messages['account.settings.field.demographics.gender_description'])}
|
||||
className="mt-1"
|
||||
/>
|
||||
)}
|
||||
</EditableField>
|
||||
<EditableField
|
||||
name="demographics_user_ethnicity"
|
||||
type="select"
|
||||
hidden
|
||||
value={this.ethnicityFieldDisplay(demographicsEthnicityOptions)}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.ethnicity'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.ethnicity.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
<Checkboxes
|
||||
id="demographics_user_ethnicity"
|
||||
options={demographicsEthnicityOptions}
|
||||
values={this.props.formValues.demographics_user_ethnicity}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</EditableField>
|
||||
<EditableField
|
||||
name="demographics_income"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_income}
|
||||
options={demographicsIncomeOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.income'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.income.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="demographics_military_history"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_military_history}
|
||||
options={demographicsMilitaryHistoryOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.military_history'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.military_history.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="demographics_learner_education_level"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_learner_education_level}
|
||||
options={demographicsEducationLevelOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.learner_education_level'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.learner_education_level.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="demographics_parent_education_level"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_parent_education_level}
|
||||
options={demographicsEducationLevelOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.parent_education_level'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.parent_education_level.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="demographics_work_status"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_work_status}
|
||||
userSuppliedValue={showWorkStatusDescribe
|
||||
? this.props.formValues.demographics_work_status_description
|
||||
: null}
|
||||
options={demographicsWorkStatusOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status.empty'])}
|
||||
{...editableFieldProps}
|
||||
>
|
||||
{showWorkStatusDescribe && (
|
||||
<Input
|
||||
name="demographics_work_status_description"
|
||||
id="field-demographics_work_status_description"
|
||||
type="text"
|
||||
placeholder={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status_description.empty'])}
|
||||
value={this.props.formValues.demographics_work_status_description}
|
||||
onChange={(e) => this.handleEditableFieldChange('demographics_work_status_description', e.target.value)}
|
||||
aria-label={this.props.intl.formatMessage(messages['account.settings.field.demographics.work_status_description'])}
|
||||
className="mt-1"
|
||||
/>
|
||||
)}
|
||||
</EditableField>
|
||||
<EditableField
|
||||
name="demographics_current_work_sector"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_current_work_sector}
|
||||
options={demographicsWorkSectorOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.current_work_sector'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.current_work_sector.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
<EditableField
|
||||
name="demographics_future_work_sector"
|
||||
type="select"
|
||||
value={this.props.formValues.demographics_future_work_sector}
|
||||
options={demographicsWorkSectorOptions}
|
||||
label={this.props.intl.formatMessage(messages['account.settings.field.demographics.future_work_sector'])}
|
||||
emptyLabel={this.props.intl.formatMessage(messages['account.settings.field.demographics.future_work_sector.empty'])}
|
||||
{...editableFieldProps}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DemographicsSection.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
formValues: PropTypes.shape({
|
||||
demographics_gender: PropTypes.string,
|
||||
demographics_user_ethnicity: PropTypes.array,
|
||||
demographics_income: PropTypes.string,
|
||||
demographics_military_history: PropTypes.string,
|
||||
demographics_learner_education_level: PropTypes.string,
|
||||
demographics_parent_education_level: PropTypes.string,
|
||||
demographics_work_status: PropTypes.string,
|
||||
demographics_current_work_sector: PropTypes.string,
|
||||
demographics_future_work_sector: PropTypes.string,
|
||||
demographics_work_status_description: PropTypes.string,
|
||||
demographics_gender_description: PropTypes.string,
|
||||
demographicsOptions: PropTypes.object,
|
||||
}).isRequired,
|
||||
drafts: PropTypes.shape({
|
||||
demographics_gender: PropTypes.string,
|
||||
demographics_user_ethnicity: PropTypes.array,
|
||||
demographics_income: PropTypes.string,
|
||||
demographics_military_history: PropTypes.string,
|
||||
demographics_learner_education_level: PropTypes.string,
|
||||
demographics_parent_education_level: PropTypes.string,
|
||||
demographics_work_status: PropTypes.string,
|
||||
demographics_current_work_sector: PropTypes.string,
|
||||
demographics_future_work_sector: PropTypes.string,
|
||||
demographics_work_status_description: PropTypes.string,
|
||||
demographics_gender_description: PropTypes.string,
|
||||
demographicsOptions: PropTypes.object,
|
||||
}).isRequired,
|
||||
formErrors: PropTypes.shape({
|
||||
demographicsError: PropTypes.string,
|
||||
}).isRequired,
|
||||
forwardRef: PropTypes.func.isRequired,
|
||||
updateDraft: PropTypes.func.isRequired,
|
||||
saveMultipleSettings: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(demographicsSectionSelector, {
|
||||
saveMultipleSettings,
|
||||
updateDraft,
|
||||
})(injectIntl(DemographicsSection));
|
||||
@@ -1,170 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
/* Demographics section heading */
|
||||
'account.settings.section.demographics.information': {
|
||||
id: 'account.settings.section.demographics.information',
|
||||
defaultMessage: 'Optional Information',
|
||||
description: 'The optional information section heading.',
|
||||
},
|
||||
/* Gender identity */
|
||||
'account.settings.field.demographics.gender': {
|
||||
id: 'account.settings.field.demographics.gender',
|
||||
defaultMessage: 'Gender identity',
|
||||
description: 'Label for account settings gender identity field.',
|
||||
},
|
||||
'account.settings.field.demographics.gender.empty': {
|
||||
id: 'account.settings.field.demographics.gender.empty',
|
||||
defaultMessage: 'Add gender identity',
|
||||
description: 'Placeholder for empty account settings gender identity field.',
|
||||
},
|
||||
'account.settings.field.demographics.gender.options.empty': {
|
||||
id: 'account.settings.field.demographics.gender.options.empty',
|
||||
defaultMessage: 'Select a gender identity',
|
||||
description: 'Placeholder for the gender identity options dropdown.',
|
||||
},
|
||||
'account.settings.field.demographics.gender_description': {
|
||||
id: 'account.settings.field.demographics.gender_description',
|
||||
defaultMessage: 'Gender identity description',
|
||||
description: 'Label for account settings gender identity description field.',
|
||||
},
|
||||
'account.settings.field.demographics.gender_description.empty': {
|
||||
id: 'account.settings.field.demographics.gender_description.empty',
|
||||
defaultMessage: 'Enter description',
|
||||
description: 'Placeholder for empty account settings gender identity field.',
|
||||
},
|
||||
/* Ethnicity */
|
||||
'account.settings.field.demographics.ethnicity': {
|
||||
id: 'account.settings.field.demographics.ethnicity',
|
||||
defaultMessage: 'Race/Ethnicity identity',
|
||||
description: 'Label for account settings ethnic background field.',
|
||||
},
|
||||
'account.settings.field.demographics.ethnicity.empty': {
|
||||
id: 'account.settings.field.demographics.ethnicity.empty',
|
||||
defaultMessage: 'Add race/ethnicity identity',
|
||||
description: 'Placeholder for empty account settings ethnic background field.',
|
||||
},
|
||||
'account.settings.field.demographics.ethnicity.options.empty': {
|
||||
id: 'account.settings.field.demographics.ethnicity.options.empty',
|
||||
defaultMessage: 'Select all that apply', // TODO: Is this the desired text?
|
||||
description: 'Placeholder for the ethnic background options field.',
|
||||
},
|
||||
/* Income */
|
||||
'account.settings.field.demographics.income': {
|
||||
id: 'account.settings.field.demographics.income',
|
||||
defaultMessage: 'Family income',
|
||||
description: 'Label for account settings household income field.',
|
||||
},
|
||||
'account.settings.field.demographics.income.empty': {
|
||||
id: 'account.settings.field.demographics.income.empty',
|
||||
defaultMessage: 'Add family income',
|
||||
description: 'Placeholder for empty account settings household income field.',
|
||||
},
|
||||
'account.settings.field.demographics.income.options.empty': {
|
||||
id: 'account.settings.field.demographics.income.options.empty',
|
||||
defaultMessage: 'Select a family income range',
|
||||
description: 'Placeholder for the household income dropdown.',
|
||||
},
|
||||
/* Military history */
|
||||
'account.settings.field.demographics.military_history': {
|
||||
id: 'account.settings.field.demographics.military_history',
|
||||
defaultMessage: 'U.S. Military status',
|
||||
description: 'Label for account settings military history field.',
|
||||
},
|
||||
'account.settings.field.demographics.military_history.empty': {
|
||||
id: 'account.settings.field.demographics.military_history.empty',
|
||||
defaultMessage: 'Add military status',
|
||||
description: 'Placeholder for empty account settings military history field.',
|
||||
},
|
||||
'account.settings.field.demographics.military_history.options.empty': {
|
||||
id: 'account.settings.field.demographics.military_history.options.empty',
|
||||
defaultMessage: 'Select military status',
|
||||
description: 'Placeholder for the military history dropdown.',
|
||||
},
|
||||
/* Learner and family education level */
|
||||
'account.settings.field.demographics.learner_education_level': {
|
||||
id: 'account.settings.field.demographics.learner_education_level',
|
||||
defaultMessage: 'Your education level',
|
||||
description: 'Label for account settings learner education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.learner_education_level.empty': {
|
||||
id: 'account.settings.field.demographics.learner_education_level.empty',
|
||||
defaultMessage: 'Add education level',
|
||||
description: 'Placeholder for empty account settings learner education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.parent_education_level': {
|
||||
id: 'account.settings.field.demographics.parent_education_level',
|
||||
defaultMessage: 'Parents/Guardians education level',
|
||||
description: 'Label for account settings parent education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.parent_education_level.empty': {
|
||||
id: 'account.settings.field.demographics.parent_education_level.empty',
|
||||
defaultMessage: 'Add education level',
|
||||
description: 'Placeholder for empty account settings parent education level field.',
|
||||
},
|
||||
'account.settings.field.demographics.education_level.options.empty': {
|
||||
id: 'account.settings.field.demographics.education_level.options.empty',
|
||||
defaultMessage: 'Select education level',
|
||||
description: 'Placeholder for the education level options dropdown.',
|
||||
},
|
||||
/* Work status */
|
||||
'account.settings.field.demographics.work_status': {
|
||||
id: 'account.settings.field.demographics.work_status',
|
||||
defaultMessage: 'Employment status',
|
||||
description: 'Label for account settings work status field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status.empty': {
|
||||
id: 'account.settings.field.demographics.work_status.empty',
|
||||
defaultMessage: 'Add employment status',
|
||||
description: 'Placeholder for empty account settings work status field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status.options.empty': {
|
||||
id: 'account.settings.field.demographics.work_status.options.empty',
|
||||
defaultMessage: 'Select employment status',
|
||||
description: 'Placeholder for the work status options dropdown.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status_description': {
|
||||
id: 'account.settings.field.demographics.work_status_description',
|
||||
defaultMessage: 'Employment status description',
|
||||
description: 'Label for account settings work status description field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_status_description.empty': {
|
||||
id: 'account.settings.field.demographics.work_status_description.empty',
|
||||
defaultMessage: 'Enter description',
|
||||
description: 'Placeholder for empty account settings work status description field.',
|
||||
},
|
||||
/* Work sector */
|
||||
'account.settings.field.demographics.current_work_sector': {
|
||||
id: 'account.settings.field.demographics.current_work_sector',
|
||||
defaultMessage: 'Current work industry',
|
||||
description: 'Label for account settings current work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.current_work_sector.empty': {
|
||||
id: 'account.settings.field.demographics.current_work_sector.empty',
|
||||
defaultMessage: 'Add work industry',
|
||||
description: 'Placeholder for empty account settings current work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.future_work_sector': {
|
||||
id: 'account.settings.field.demographics.future_work_sector',
|
||||
defaultMessage: 'Future work industry',
|
||||
description: 'Label for account settings future work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.future_work_sector.empty': {
|
||||
id: 'account.settings.field.demographics.future_work_sector.empty',
|
||||
defaultMessage: 'Add work industry',
|
||||
description: 'Placeholder for empty account settings future work sector field.',
|
||||
},
|
||||
'account.settings.field.demographics.work_sector.options.empty': {
|
||||
id: 'account.settings.field.demographics.work_sector.options.empty',
|
||||
defaultMessage: 'Select work industry',
|
||||
description: 'Placeholder for the work sector options dropdown.',
|
||||
},
|
||||
/* Legal copy link text */
|
||||
'account.settings.section.demographics.why': {
|
||||
id: 'account.settings.section.demographics.why',
|
||||
defaultMessage: 'Why does {siteName} collect this information?',
|
||||
description: 'Link text for a link to external legal text',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,140 +0,0 @@
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import get from 'lodash.get';
|
||||
import { convertData, TO, FROM } from './utils';
|
||||
|
||||
/**
|
||||
* Utility method that attempts to extract errors from the response of a PATCH request in order to
|
||||
* display a warning or otherwise meaningful message to the user.
|
||||
*
|
||||
* @param {Error} error
|
||||
*/
|
||||
export function createDemographicsError(error) {
|
||||
const apiError = Object.create(error);
|
||||
// If the error received has the `httpResponseData` field in it, then we should have reason to
|
||||
// believe the Demographics service is alive and responding. Extract errors from fields where
|
||||
// appropriate so we can display them to the user.
|
||||
if (get(error, 'customAttributes.httpErrorResponseData')) {
|
||||
apiError.fieldErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
|
||||
if (get(apiError, 'fieldErrors.gender_description')) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.demographics_gender = apiError.fieldErrors.gender_description[0];
|
||||
delete apiError.fieldErrors.gender_description;
|
||||
} else if (get(apiError, 'fieldErrors.work_status_description')) {
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
apiError.fieldErrors.demographics_work_status = apiError.fieldErrors.work_status_description[0];
|
||||
delete apiError.fieldErrors.work_status_description;
|
||||
}
|
||||
// Otherwise, when the service is down, the error response will not contain a
|
||||
// `httpErrorResponseData` field. Add a generic 'demographicsError' field to the fieldErrors that
|
||||
// will trigger showing an Alert to the user to them them know the update was unsuccessful.
|
||||
} else {
|
||||
apiError.fieldErrors = {
|
||||
demographicsError: error.customAttributes.httpErrorType,
|
||||
};
|
||||
}
|
||||
|
||||
return apiError;
|
||||
}
|
||||
|
||||
/**
|
||||
* post all of the data related to demographics.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
* @param {Object} commitValues { demographics }
|
||||
*/
|
||||
export async function postDemographics(userId) {
|
||||
const requestConfig = { headers: { 'Content-Type': 'application/json' } };
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/`;
|
||||
const commitValues = { user: userId };
|
||||
let data = {};
|
||||
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.post(requestUrl, commitValues, requestConfig)
|
||||
.catch((error) => {
|
||||
const apiError = createDemographicsError(error);
|
||||
throw apiError;
|
||||
}));
|
||||
|
||||
return convertData(data, FROM);
|
||||
}
|
||||
|
||||
/**
|
||||
* get all data related to the demographics.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
*/
|
||||
export async function getDemographics(userId) {
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/${userId}/`;
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.get(requestUrl));
|
||||
|
||||
data = convertData(data, FROM);
|
||||
} catch (error) {
|
||||
const apiError = Object.create(error);
|
||||
// if the API called resulted in this user receiving a 404 then follow up with a POST call to
|
||||
// try and create the demographics entity on the backend
|
||||
if (apiError.customAttributes.httpErrorStatus) {
|
||||
if (apiError.customAttributes.httpErrorStatus === 404) {
|
||||
data = await postDemographics(userId);
|
||||
}
|
||||
} else {
|
||||
data = {
|
||||
user: userId,
|
||||
demographics_gender: '',
|
||||
demographics_gender_description: '',
|
||||
demographics_income: '',
|
||||
demographics_learner_education_level: '',
|
||||
demographics_parent_education_level: '',
|
||||
demographics_military_history: '',
|
||||
demographics_work_status: '',
|
||||
demographics_work_status_description: '',
|
||||
demographics_current_work_sector: '',
|
||||
demographics_future_work_sector: '',
|
||||
demographics_user_ethnicity: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* patch all of the data related to demographics.
|
||||
* @param {Number} userId users are identified in the api by LMS id
|
||||
* @param {Object} commitValues { demographics }
|
||||
*/
|
||||
export async function patchDemographics(userId, commitValues) {
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/${userId}/`;
|
||||
const convertedCommitValues = convertData(commitValues, TO);
|
||||
let data = {};
|
||||
|
||||
({ data } = await getAuthenticatedHttpClient()
|
||||
.patch(requestUrl, convertedCommitValues)
|
||||
.catch((error) => {
|
||||
const apiError = createDemographicsError(error);
|
||||
throw apiError;
|
||||
}));
|
||||
|
||||
return convertData(data, FROM);
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieve the options for each field from the Demographics API
|
||||
*/
|
||||
export async function getDemographicsOptions() {
|
||||
const requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/`;
|
||||
let data = {};
|
||||
|
||||
try {
|
||||
({ data } = await getAuthenticatedHttpClient().options(requestUrl));
|
||||
} catch (error) {
|
||||
// We are catching and suppressing errors here on purpose. If an error occurs during the
|
||||
// getDemographicsOptions call we will pass back an empty `data` object. Downstream we make
|
||||
// the assumption that if the demographicsOptions object is empty that there was an issue or
|
||||
// error communicating with the service/API.
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
export const TO = 'to';
|
||||
export const FROM = 'from';
|
||||
export const DEMOGRAPHICS_FIELDS = [
|
||||
'demographics_gender',
|
||||
'demographics_gender_description',
|
||||
'demographics_income',
|
||||
'demographics_learner_education_level',
|
||||
'demographics_parent_education_level',
|
||||
'demographics_military_history',
|
||||
'demographics_work_status',
|
||||
'demographics_work_status_description',
|
||||
'demographics_current_work_sector',
|
||||
'demographics_future_work_sector',
|
||||
'demographics_user_ethnicity',
|
||||
];
|
||||
|
||||
// Frontend wants (example):
|
||||
// demographics_user_ethnicity: ["asian", "white", "other"]
|
||||
//
|
||||
// Demographics wants (example):
|
||||
// user_ethnicity: [
|
||||
// { ethnicity: "asian" },
|
||||
// { ethnicity: "white" },
|
||||
// { ethnicity: "other" }
|
||||
// ]
|
||||
function convertEthnicity(ethnicityData, direction) {
|
||||
if (direction === FROM) {
|
||||
return ethnicityData.map(e => e.ethnicity);
|
||||
}
|
||||
|
||||
if (direction === TO) {
|
||||
return ethnicityData.map(e => ({ ethnicity: e }));
|
||||
}
|
||||
|
||||
return ethnicityData;
|
||||
}
|
||||
|
||||
// Handles conversion of data to/from Demographics IDA to/from format needed for
|
||||
// frontend
|
||||
// * handles ethnicity field
|
||||
// * adds/removes 'demographics' to/from key
|
||||
// * replace `null` with empty string or empty string with null
|
||||
export function convertData(dataObject, direction) {
|
||||
const converted = {};
|
||||
|
||||
Object.entries(dataObject).forEach(([key, value]) => {
|
||||
let newValue = value;
|
||||
|
||||
if (key.includes('ethnicity')) {
|
||||
newValue = convertEthnicity(value, direction);
|
||||
}
|
||||
|
||||
if (direction === TO) {
|
||||
converted[key.replace('demographics_', '')] = newValue || null;
|
||||
}
|
||||
|
||||
if (direction === FROM) {
|
||||
converted[`demographics_${key}`] = newValue || '';
|
||||
}
|
||||
});
|
||||
|
||||
return converted;
|
||||
}
|
||||
@@ -1,583 +0,0 @@
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
import React from 'react';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import renderer from 'react-test-renderer';
|
||||
import DemographicsSection from '../DemographicsSection';
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth');
|
||||
|
||||
const IntlDemographicsSection = injectIntl(DemographicsSection);
|
||||
|
||||
jest.mock('../../data/selectors', () => jest.fn().mockImplementation(() => ({ demographicsSectionSelector: () => ({}) })));
|
||||
|
||||
const mockStore = configureStore();
|
||||
|
||||
describe('DemographicsSection', () => {
|
||||
let props = {};
|
||||
let store = {};
|
||||
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
store = mockStore();
|
||||
props = {
|
||||
updateDraft: jest.fn(),
|
||||
formValues: {
|
||||
demographics_gender: 'declined',
|
||||
demographics_gender_description: '',
|
||||
demographics_user_ethnicity: [],
|
||||
demographics_income: 'declined',
|
||||
demographics_military_history: 'declined',
|
||||
demographics_learner_education_level: 'declined',
|
||||
demographics_parent_education_level: 'declined',
|
||||
demographics_work_status: 'declined',
|
||||
demographics_work_status_description: '',
|
||||
demographics_current_work_sector: 'declined',
|
||||
demographics_future_work_sector: 'declined',
|
||||
demographics_user: 1,
|
||||
demographicsOptions: {
|
||||
actions: {
|
||||
POST: {
|
||||
gender: {
|
||||
choices: [
|
||||
{
|
||||
value: 'woman',
|
||||
display_name: 'Woman',
|
||||
},
|
||||
{
|
||||
value: 'man',
|
||||
display_name: 'Man',
|
||||
},
|
||||
{
|
||||
value: 'non-binary',
|
||||
display_name: 'Non-binary',
|
||||
},
|
||||
{
|
||||
value: 'self-describe',
|
||||
display_name: 'Prefer to self describe',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
income: {
|
||||
choices: [
|
||||
{
|
||||
value: 'less-than-10k',
|
||||
display_name: 'Less than US $10,000',
|
||||
},
|
||||
{
|
||||
value: '10k-25k',
|
||||
display_name: 'US $10,000 - $25,000',
|
||||
},
|
||||
{
|
||||
value: '25k-50k',
|
||||
display_name: 'US $25,000 - $50,000',
|
||||
},
|
||||
{
|
||||
value: '50k-75k',
|
||||
display_name: 'US $50,000 - $75,000',
|
||||
},
|
||||
{
|
||||
value: '75k-100k',
|
||||
display_name: 'US $75,000 - $100,000',
|
||||
},
|
||||
{
|
||||
value: 'over-100k',
|
||||
display_name: 'Over US $100,000',
|
||||
},
|
||||
{
|
||||
value: 'unsure',
|
||||
display_name: "I don't know",
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
learner_education_level: {
|
||||
choices: [
|
||||
{
|
||||
value: 'no-high-school',
|
||||
display_name: 'No High School',
|
||||
},
|
||||
{
|
||||
value: 'some-high-school',
|
||||
display_name: 'Some High School',
|
||||
},
|
||||
{
|
||||
value: 'high-school-ged-equivalent',
|
||||
display_name: 'High School diploma, GED, or equivalent',
|
||||
},
|
||||
{
|
||||
value: 'some-college',
|
||||
display_name: 'Some college, but no degree',
|
||||
},
|
||||
{
|
||||
value: 'associates',
|
||||
display_name: 'Associates degree',
|
||||
},
|
||||
{
|
||||
value: 'bachelors',
|
||||
display_name: 'Bachelors degree',
|
||||
},
|
||||
{
|
||||
value: 'masters',
|
||||
display_name: 'Masters degree',
|
||||
},
|
||||
{
|
||||
value: 'professional',
|
||||
display_name: 'Professional degree',
|
||||
},
|
||||
{
|
||||
value: 'doctorate',
|
||||
display_name: 'Doctorate degree',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
parent_education_level: {
|
||||
choices: [
|
||||
{
|
||||
value: 'no-high-school',
|
||||
display_name: 'No High School',
|
||||
},
|
||||
{
|
||||
value: 'some-high-school',
|
||||
display_name: 'Some High School',
|
||||
},
|
||||
{
|
||||
value: 'high-school-ged-equivalent',
|
||||
display_name: 'High School diploma, GED, or equivalent',
|
||||
},
|
||||
{
|
||||
value: 'some-college',
|
||||
display_name: 'Some college, but no degree',
|
||||
},
|
||||
{
|
||||
value: 'associates',
|
||||
display_name: 'Associates degree',
|
||||
},
|
||||
{
|
||||
value: 'bachelors',
|
||||
display_name: 'Bachelors degree',
|
||||
},
|
||||
{
|
||||
value: 'masters',
|
||||
display_name: 'Masters degree',
|
||||
},
|
||||
{
|
||||
value: 'professional',
|
||||
display_name: 'Professional degree',
|
||||
},
|
||||
{
|
||||
value: 'doctorate',
|
||||
display_name: 'Doctorate degree',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
military_history: {
|
||||
choices: [
|
||||
{
|
||||
value: 'never-served',
|
||||
display_name: 'Never served in the military',
|
||||
},
|
||||
{
|
||||
value: 'training',
|
||||
display_name: 'Only on active duty for training',
|
||||
},
|
||||
{
|
||||
value: 'active',
|
||||
display_name: 'Now on active duty',
|
||||
},
|
||||
{
|
||||
value: 'previously-active',
|
||||
display_name: 'On active duty in the past, but not now',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
work_status: {
|
||||
choices: [
|
||||
{
|
||||
value: 'full-time',
|
||||
display_name: 'Employed, working full-time',
|
||||
},
|
||||
{
|
||||
value: 'part-time',
|
||||
display_name: 'Employed, working part-time',
|
||||
},
|
||||
{
|
||||
value: 'self-employed',
|
||||
display_name: 'Self-Employed',
|
||||
},
|
||||
{
|
||||
value: 'not-employed-looking',
|
||||
display_name: 'Not employed, looking for work',
|
||||
},
|
||||
{
|
||||
value: 'not-employed-not-looking',
|
||||
display_name: 'Not employed, not looking for work',
|
||||
},
|
||||
{
|
||||
value: 'unable',
|
||||
display_name: 'Unable to work',
|
||||
},
|
||||
{
|
||||
value: 'retired',
|
||||
display_name: 'Retired',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
current_work_sector: {
|
||||
choices: [
|
||||
{
|
||||
value: 'accommodation-food',
|
||||
display_name: 'Accommodation and Food Services',
|
||||
},
|
||||
{
|
||||
value: 'administrative-support-waste-remediation',
|
||||
display_name: 'Administrative and Support and Waste Management and Remediation Services',
|
||||
},
|
||||
{
|
||||
value: 'agriculture-forestry-fishing-hunting',
|
||||
display_name: 'Agriculture, Forestry, Fishing and Hunting',
|
||||
},
|
||||
{
|
||||
value: 'arts-entertainment-recreation',
|
||||
display_name: 'Arts, Entertainment, and Recreation',
|
||||
},
|
||||
{
|
||||
value: 'construction',
|
||||
display_name: 'Construction',
|
||||
},
|
||||
{
|
||||
value: 'educational',
|
||||
display_name: 'Education Services',
|
||||
},
|
||||
{
|
||||
value: 'finance-insurance',
|
||||
display_name: 'Finance and Insurance',
|
||||
},
|
||||
{
|
||||
value: 'healthcare-social',
|
||||
display_name: 'Health Care and Social Assistance',
|
||||
},
|
||||
{
|
||||
value: 'information',
|
||||
display_name: 'Information',
|
||||
},
|
||||
{
|
||||
value: 'management',
|
||||
display_name: 'Management of Companies and Enterprises',
|
||||
},
|
||||
{
|
||||
value: 'manufacturing',
|
||||
display_name: 'Manufacturing',
|
||||
},
|
||||
{
|
||||
value: 'mining-quarry-oil-gas',
|
||||
display_name: 'Mining, Quarrying, and Oil and Gas Extraction',
|
||||
},
|
||||
{
|
||||
value: 'professional-scientific-technical',
|
||||
display_name: 'Professional, Scientific, and Technical Services',
|
||||
},
|
||||
{
|
||||
value: 'public-admin',
|
||||
display_name: 'Public Administration',
|
||||
},
|
||||
{
|
||||
value: 'real-estate',
|
||||
display_name: 'Real Estate and Rental and Leasing',
|
||||
},
|
||||
{
|
||||
value: 'retail',
|
||||
display_name: 'Retail Trade',
|
||||
},
|
||||
{
|
||||
value: 'transport-warehousing',
|
||||
display_name: 'Transportation and Warehousing',
|
||||
},
|
||||
{
|
||||
value: 'utilities',
|
||||
display_name: 'Utilities',
|
||||
},
|
||||
{
|
||||
value: 'trade',
|
||||
display_name: 'Wholesale Trade',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
future_work_sector: {
|
||||
choices: [
|
||||
{
|
||||
value: 'accommodation-food',
|
||||
display_name: 'Accommodation and Food Services',
|
||||
},
|
||||
{
|
||||
value: 'administrative-support-waste-remediation',
|
||||
display_name: 'Administrative and Support and Waste Management and Remediation Services',
|
||||
},
|
||||
{
|
||||
value: 'agriculture-forestry-fishing-hunting',
|
||||
display_name: 'Agriculture, Forestry, Fishing and Hunting',
|
||||
},
|
||||
{
|
||||
value: 'arts-entertainment-recreation',
|
||||
display_name: 'Arts, Entertainment, and Recreation',
|
||||
},
|
||||
{
|
||||
value: 'construction',
|
||||
display_name: 'Construction',
|
||||
},
|
||||
{
|
||||
value: 'educational',
|
||||
display_name: 'Education Services',
|
||||
},
|
||||
{
|
||||
value: 'finance-insurance',
|
||||
display_name: 'Finance and Insurance',
|
||||
},
|
||||
{
|
||||
value: 'healthcare-social',
|
||||
display_name: 'Health Care and Social Assistance',
|
||||
},
|
||||
{
|
||||
value: 'information',
|
||||
display_name: 'Information',
|
||||
},
|
||||
{
|
||||
value: 'management',
|
||||
display_name: 'Management of Companies and Enterprises',
|
||||
},
|
||||
{
|
||||
value: 'manufacturing',
|
||||
display_name: 'Manufacturing',
|
||||
},
|
||||
{
|
||||
value: 'mining-quarry-oil-gas',
|
||||
display_name: 'Mining, Quarrying, and Oil and Gas Extraction',
|
||||
},
|
||||
{
|
||||
value: 'professional-scientific-technical',
|
||||
display_name: 'Professional, Scientific, and Technical Services',
|
||||
},
|
||||
{
|
||||
value: 'public-admin',
|
||||
display_name: 'Public Administration',
|
||||
},
|
||||
{
|
||||
value: 'real-estate',
|
||||
display_name: 'Real Estate and Rental and Leasing',
|
||||
},
|
||||
{
|
||||
value: 'retail',
|
||||
display_name: 'Retail Trade',
|
||||
},
|
||||
{
|
||||
value: 'transport-warehousing',
|
||||
display_name: 'Transportation and Warehousing',
|
||||
},
|
||||
{
|
||||
value: 'utilities',
|
||||
display_name: 'Utilities',
|
||||
},
|
||||
{
|
||||
value: 'trade',
|
||||
display_name: 'Wholesale Trade',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Other',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
user_ethnicity: {
|
||||
child: {
|
||||
children: {
|
||||
ethnicity: {
|
||||
choices: [
|
||||
{
|
||||
value: 'american-indian-or-alaska-native',
|
||||
display_name: 'American Indian or Alaska Native',
|
||||
},
|
||||
{
|
||||
value: 'asian',
|
||||
display_name: 'Asian',
|
||||
},
|
||||
{
|
||||
value: 'black-or-african-american',
|
||||
display_name: 'Black or African American',
|
||||
},
|
||||
{
|
||||
value: 'hispanic-latin-spanish',
|
||||
display_name: 'Hispanic, Latin, or Spanish origin',
|
||||
},
|
||||
{
|
||||
value: 'middle-eastern-or-north-african',
|
||||
display_name: 'Middle Eastern or North African',
|
||||
},
|
||||
{
|
||||
value: 'native-hawaiian-or-pacific-islander',
|
||||
display_name: 'Native Hawaiian or Other Pacific Islander',
|
||||
},
|
||||
{
|
||||
value: 'white',
|
||||
display_name: 'White',
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
display_name: 'Some other race, ethnicity, or origin',
|
||||
},
|
||||
{
|
||||
value: 'declined',
|
||||
display_name: 'Prefer not to respond',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
formErrors: {},
|
||||
intl: {},
|
||||
forwardRef: () => {},
|
||||
drafts: {},
|
||||
};
|
||||
auth.getAuthenticatedHttpClient = jest.fn(() => ({
|
||||
patch: async () => ({
|
||||
data: { status: 200 },
|
||||
catch: () => {},
|
||||
}),
|
||||
}));
|
||||
auth.getAuthenticatedUser = jest.fn(() => ({ userId: 1 }));
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an Alert if an error occurs', () => {
|
||||
props = {
|
||||
...props,
|
||||
formErrors: {
|
||||
demographicsError: 'api-error',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set user input correctly when user provides gender self-description', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_gender: 'self-describe',
|
||||
demographics_gender_description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should set user input correctly when user provides answers to work_status question', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_work_status: 'other',
|
||||
demographics_work_status_description: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render ethnicity text correctly', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_user_ethnicity: ['asian'],
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render ethnicity correctly when multiple options are selected', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
...props.formValues,
|
||||
demographics_user_ethnicity: ['hispanic-latin-spanish', 'white'],
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render an Alert when demographicsOptions props are empty', () => {
|
||||
props = {
|
||||
...props,
|
||||
formValues: {
|
||||
demographicsOptions: null,
|
||||
},
|
||||
};
|
||||
|
||||
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,3707 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DemographicsSection should render 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render an Alert if an error occurs 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert alert-danger"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<span>
|
||||
An error occurred attempting to retrieve or save your account information. Please try again later.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render an Alert when demographicsOptions props are empty 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
tabIndex="-1"
|
||||
>
|
||||
<div
|
||||
className="alert d-flex align-items-start alert alert-danger"
|
||||
>
|
||||
<div />
|
||||
<div>
|
||||
<span>
|
||||
An error occurred attempting to retrieve or save your account information. Please try again later.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render ethnicity correctly when multiple options are selected 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Hispanic, Latin, or Spanish origin, White
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should render ethnicity text correctly 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Asian
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should set user input correctly when user provides answers to work_status question 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Other: test
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`DemographicsSection should set user input correctly when user provides gender self-description 1`] = `
|
||||
<div
|
||||
className="account-section"
|
||||
id="demographics-information"
|
||||
>
|
||||
<h2
|
||||
className="section-heading"
|
||||
>
|
||||
Optional Information
|
||||
</h2>
|
||||
<p>
|
||||
<a
|
||||
href="http://localhost:5335/demographics"
|
||||
onClick={[Function]}
|
||||
rel="noopener noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Why does localhost collect this information?
|
||||
<span>
|
||||
|
||||
<span
|
||||
aria-hidden={false}
|
||||
aria-label="Opens in a new window"
|
||||
className="fa fa-external-link"
|
||||
title="Opens in a new window"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</p>
|
||||
<div
|
||||
id="demographics-fields"
|
||||
>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Gender identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer to self describe: test
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Race/Ethnicity identity
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
<button
|
||||
className="p-0 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Add race/ethnicity identity
|
||||
</button>
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Family income
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
U.S. Military status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Your education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Parents/Guardians education level
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Employment status
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Current work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pgn-transition-replace-group position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": null,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"padding": ".1px 0",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="form-group"
|
||||
>
|
||||
<div
|
||||
className="d-flex align-items-start"
|
||||
>
|
||||
<h6
|
||||
aria-level="3"
|
||||
>
|
||||
Future work industry
|
||||
</h6>
|
||||
<button
|
||||
className="ml-3 btn btn-link"
|
||||
disabled={false}
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
className="svg-inline--fa fa-pencil-alt fa-w-16 mr-1"
|
||||
data-icon="pencil-alt"
|
||||
data-prefix="fas"
|
||||
focusable="false"
|
||||
role="img"
|
||||
style={Object {}}
|
||||
viewBox="0 0 512 512"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M497.9 142.1l-46.1 46.1c-4.7 4.7-12.3 4.7-17 0l-111-111c-4.7-4.7-4.7-12.3 0-17l46.1-46.1c18.7-18.7 49.1-18.7 67.9 0l60.1 60.1c18.8 18.7 18.8 49.1 0 67.9zM284.2 99.8L21.6 362.4.4 483.9c-2.9 16.4 11.4 30.6 27.8 27.8l121.5-21.3 262.6-262.6c4.7-4.7 4.7-12.3 0-17l-111-111c-4.8-4.7-12.4-4.7-17.1 0zM124.1 339.9c-5.5-5.5-5.5-14.3 0-19.8l154-154c5.5-5.5 14.3-5.5 19.8 0s5.5 14.3 0 19.8l-154 154c-5.5 5.5-14.3 5.5-19.8 0zM88 424h48v36.3l-64.5 11.3-31.1-31.1L51.7 376H88v48z"
|
||||
fill="currentColor"
|
||||
style={Object {}}
|
||||
/>
|
||||
</svg>
|
||||
Edit
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
data-hj-suppress={true}
|
||||
>
|
||||
Prefer not to respond
|
||||
</p>
|
||||
<p
|
||||
className="small text-muted mt-n2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,21 +0,0 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
import Alert from '../Alert';
|
||||
|
||||
const RequestInProgressAlert = () => (
|
||||
<Alert
|
||||
className="alert-warning mt-n2"
|
||||
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="account.settings.editable.field.password.reset.button.forbidden"
|
||||
defaultMessage="Your previous request is in progress, please try again in few moments."
|
||||
description="A message displayed when a previous password reset request is still in progress."
|
||||
/>
|
||||
</Alert>
|
||||
);
|
||||
|
||||
export default RequestInProgressAlert;
|
||||
@@ -7,7 +7,6 @@ import { StatefulButton } from '@edx/paragon';
|
||||
import { resetPassword } from './data/actions';
|
||||
import messages from './messages';
|
||||
import ConfirmationAlert from './ConfirmationAlert';
|
||||
import RequestInProgressAlert from './RequestInProgressAlert';
|
||||
|
||||
const ResetPassword = (props) => {
|
||||
const { email, intl, status } = props;
|
||||
@@ -22,7 +21,7 @@ const ResetPassword = (props) => {
|
||||
</h6>
|
||||
<p>
|
||||
<StatefulButton
|
||||
variant="link"
|
||||
className="btn-link"
|
||||
state={status}
|
||||
onClick={(e) => {
|
||||
// Swallow clicks if the state is pending.
|
||||
@@ -44,7 +43,6 @@ const ResetPassword = (props) => {
|
||||
/>
|
||||
</p>
|
||||
{status === 'complete' ? <ConfirmationAlert email={email} /> : null}
|
||||
{status === 'forbidden' ? <RequestInProgressAlert /> : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,7 +18,3 @@ export const resetPasswordSuccess = () => ({
|
||||
export const resetPasswordReset = () => ({
|
||||
type: RESET_PASSWORD.RESET,
|
||||
});
|
||||
|
||||
export const resetPasswordForbidden = () => ({
|
||||
type: RESET_PASSWORD.FORBIDDEN,
|
||||
});
|
||||
|
||||
@@ -17,11 +17,6 @@ const reducer = (state = defaultState, action = null) => {
|
||||
...state,
|
||||
status: 'complete',
|
||||
};
|
||||
case RESET_PASSWORD.FORBIDDEN:
|
||||
return {
|
||||
...state,
|
||||
status: 'forbidden',
|
||||
};
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
import { put, call, takeEvery } from 'redux-saga/effects';
|
||||
|
||||
import {
|
||||
resetPasswordBegin, resetPasswordForbidden, resetPasswordSuccess, RESET_PASSWORD,
|
||||
} from './actions';
|
||||
import { resetPasswordBegin, resetPasswordSuccess, RESET_PASSWORD } from './actions';
|
||||
import { postResetPassword } from './service';
|
||||
|
||||
function* handleResetPassword(action) {
|
||||
yield put(resetPasswordBegin());
|
||||
try {
|
||||
const response = yield call(postResetPassword, action.payload.email);
|
||||
yield put(resetPasswordSuccess(response));
|
||||
} catch (error) {
|
||||
if (error.response && error.response.status === 403) {
|
||||
yield put(resetPasswordForbidden(error));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const response = yield call(postResetPassword, action.payload.email);
|
||||
yield put(resetPasswordSuccess(response));
|
||||
}
|
||||
|
||||
export default function* saga() {
|
||||
|
||||
@@ -12,8 +12,9 @@ export const siteLanguageListSelector = createSelector(
|
||||
|
||||
export const siteLanguageOptionsSelector = createSelector(
|
||||
siteLanguageSelector,
|
||||
siteLanguage => siteLanguage.siteLanguageList.map(({ code, name }) => ({
|
||||
value: code,
|
||||
label: name,
|
||||
})),
|
||||
siteLanguage =>
|
||||
siteLanguage.siteLanguageList.map(({ code, name }) => ({
|
||||
value: code,
|
||||
label: name,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { convertKeyNames, snakeCaseObject } from '@edx/frontend-platform/utils';
|
||||
import siteLanguageList from './constants';
|
||||
import { snakeCaseObject, convertKeyNames } from '../data/utils';
|
||||
|
||||
export async function getSiteLanguageList() {
|
||||
return siteLanguageList;
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import React from 'react';
|
||||
import renderer from 'react-test-renderer';
|
||||
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { mergeConfig, setConfig } from '@edx/frontend-platform';
|
||||
import JumpNav from '../JumpNav';
|
||||
|
||||
const IntlJumpNav = injectIntl(JumpNav);
|
||||
|
||||
describe('JumpNav', () => {
|
||||
mergeConfig({
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION: false,
|
||||
});
|
||||
|
||||
let props = {};
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
intl: {},
|
||||
displayDemographicsLink: false,
|
||||
};
|
||||
});
|
||||
|
||||
it('should not render Optional Information link', () => {
|
||||
const tree = renderer.create((
|
||||
// Had to wrap the following in a router or I will receive an error stating:
|
||||
// "Invariant failed: You should not use <NavLink> outside a <Router>"
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IntlJumpNav {...props} />
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
))
|
||||
.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render Optional Information link', () => {
|
||||
setConfig({
|
||||
ENABLE_DEMOGRAPHICS_COLLECTION: true,
|
||||
});
|
||||
|
||||
props = {
|
||||
...props,
|
||||
displayDemographicsLink: true,
|
||||
};
|
||||
|
||||
const tree = renderer.create((
|
||||
// Same as previous test
|
||||
<Router>
|
||||
<IntlProvider locale="en">
|
||||
<IntlJumpNav {...props} />
|
||||
</IntlProvider>
|
||||
</Router>
|
||||
))
|
||||
.toJSON();
|
||||
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,194 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`JumpNav should not render Optional Information link 1`] = `
|
||||
<div
|
||||
className="jump-nav"
|
||||
>
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
style={Object {}}
|
||||
>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#basic-information"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Account Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#profile-information"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Profile Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#social-media"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Social Media Links
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#site-preferences"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Site Preferences
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#linked-accounts"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Linked Accounts
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#delete-account"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Delete My Account
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`JumpNav should render Optional Information link 1`] = `
|
||||
<div
|
||||
className="jump-nav"
|
||||
>
|
||||
<ul
|
||||
className="list-unstyled"
|
||||
style={Object {}}
|
||||
>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#basic-information"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Account Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#profile-information"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Profile Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#demographics-information"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Optional Information
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#social-media"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Social Media Links
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#site-preferences"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Site Preferences
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#linked-accounts"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Linked Accounts
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className=""
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
className="active"
|
||||
href="/#delete-account"
|
||||
onClick={[Function]}
|
||||
style={Object {}}
|
||||
>
|
||||
Delete My Account
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
@@ -11,16 +11,14 @@ class ThirdPartyAuth extends Component {
|
||||
onClickDisconnect = (e) => {
|
||||
e.preventDefault();
|
||||
const providerId = e.currentTarget.getAttribute('data-provider-id');
|
||||
if (this.props.disconnectionStatuses[providerId] === 'pending') {
|
||||
return;
|
||||
}
|
||||
if (this.props.disconnectionStatuses[providerId] === 'pending') return;
|
||||
const disconnectUrl = e.currentTarget.getAttribute('data-disconnect-url');
|
||||
this.props.disconnectAuth(disconnectUrl, providerId);
|
||||
}
|
||||
|
||||
renderUnconnectedProvider(url, name) {
|
||||
return (
|
||||
<>
|
||||
<React.Fragment>
|
||||
<h6 aria-level="3">{name}</h6>
|
||||
<Hyperlink destination={url} className="btn btn-outline-primary">
|
||||
<FormattedMessage
|
||||
@@ -30,7 +28,7 @@ class ThirdPartyAuth extends Component {
|
||||
values={{ name }}
|
||||
/>
|
||||
</Hyperlink>
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -38,7 +36,7 @@ class ThirdPartyAuth extends Component {
|
||||
const hasError = this.props.errors[id];
|
||||
|
||||
return (
|
||||
<>
|
||||
<React.Fragment>
|
||||
<h6 aria-level="3">
|
||||
{name}
|
||||
<span className="small font-weight-normal text-muted ml-2">
|
||||
@@ -60,7 +58,7 @@ class ThirdPartyAuth extends Component {
|
||||
) : null}
|
||||
|
||||
<StatefulButton
|
||||
variant="link"
|
||||
className="btn-link"
|
||||
state={this.props.disconnectionStatuses[id]}
|
||||
labels={{
|
||||
default: (
|
||||
@@ -77,7 +75,7 @@ class ThirdPartyAuth extends Component {
|
||||
data-disconnect-url={url}
|
||||
data-provider-id={id}
|
||||
/>
|
||||
</>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -87,9 +85,9 @@ class ThirdPartyAuth extends Component {
|
||||
return (
|
||||
<div className="form-group" key={id}>
|
||||
{
|
||||
connected
|
||||
? this.renderConnectedProvider(disconnectUrl, name, id)
|
||||
: this.renderUnconnectedProvider(connectUrl, name)
|
||||
connected ?
|
||||
this.renderConnectedProvider(disconnectUrl, name, id) :
|
||||
this.renderUnconnectedProvider(connectUrl, name)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
@@ -100,15 +98,13 @@ class ThirdPartyAuth extends Component {
|
||||
<FormattedMessage
|
||||
id="account.settings.sso.no.providers"
|
||||
defaultMessage="No accounts can be linked at this time."
|
||||
description="Displayed when no third-party accounts are available for the user to link to their account on the platform."
|
||||
description="Displayed when no third party accounts are available to link an edX account to"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.providers === undefined) {
|
||||
return null;
|
||||
}
|
||||
if (this.props.providers === undefined) return null;
|
||||
|
||||
if (this.props.providers.length === 0) {
|
||||
return this.renderNoProviders();
|
||||
@@ -118,6 +114,7 @@ class ThirdPartyAuth extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ThirdPartyAuth.propTypes = {
|
||||
providers: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
|
||||
@@ -1,318 +1,129 @@
|
||||
{
|
||||
"account.settings.message.duplicate.tpa.provider": "إنّ حساب {provider} الذي اخترتَه مرتبط مسبقًا بحساب آخر في edX. ",
|
||||
"account.settings.message.managed.settings": "تتم إدارة إعدادات الملف الشخصي بواسطة {ManagerTitle}. اتصل بالمسؤول أو {support} للحصول على المساعدة.",
|
||||
"account.settings.message.managed.settings.support": "الدعم",
|
||||
"account.settings.page.heading": "إعدادات الحساب",
|
||||
"account.settings.loading.message": "جاري التحميل...",
|
||||
"account.settings.loading.error": "خطأ: {error}",
|
||||
"account.settings.banner.beta.language": "لقد قمت بتعيين لغتك الى {beta_language}، والتي لم تتم ترجمتها بالكامل حاليًا. يمكنك مساعدتنا في ترجمة هذه اللغة بالكامل من خلال الانضمام إلى مجتمع Transifex وإضافة ترجمات من الإنجليزية للمتعلمين الذين يتحدثون {beta_language}.",
|
||||
"account.settings.banner.beta.language.action.switch.back": "العودة لـِ {previous_language}",
|
||||
"account.settings.banner.beta.language.action.help.translate": "المساعدة في الترجمة إلى {beta_language}",
|
||||
"account.settings.section.account.information": "معلومات الحساب",
|
||||
"account.settings.section.account.information.description": "تتضمن هذه الإعدادات معلومات أساسية عن حسابك.",
|
||||
"account.settings.section.profile.information": "معلومات الملف الشخصي",
|
||||
"account.settings.section.demographics.information": "معلومات اختيارية",
|
||||
"account.settings.section.site.preferences": "تفضيلات الموقع",
|
||||
"account.settings.section.linked.accounts": "الحسابات المرتبطة",
|
||||
"account.settings.section.linked.accounts.description": "يمكنك ربط حساباتك الشخصية لتسهيل عملية تسجيل دخولك إلى edX.",
|
||||
"account.settings.field.username": "اسم المستخدم",
|
||||
"account.settings.field.username.help.text": "اسم المستخدم الخاص بك الذي يميزك في edX. لا يمكنك تغيير اسم المستخدم الخاص بك لاحقاً.",
|
||||
"account.settings.field.full.name": "الاسم الكامل",
|
||||
"account.settings.field.full.name.empty": "إضافة اسم",
|
||||
"account.settings.field.full.name.help.text": "الاسم المستخدم للتحقق من هويتك والذي سوف يظهر على الشهادات الخاصة بك.",
|
||||
"account.settings.field.email": "البريد الالكتروني (الدخول)",
|
||||
"account.settings.field.email.empty": "إضافة عنوان البريد الإلكتروني",
|
||||
"account.settings.field.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر فوق الرابط في الرسالة لتحديث عنوان بريدك الإلكتروني.",
|
||||
"account.settings.field.email.help.text": "أنت تتلقى رسائل من edX وفرق المساق على هذا العنوان.",
|
||||
"account.settings.field.secondary.email": "عنوان البريد الإلكتروني للاسترداد",
|
||||
"account.settings.field.secondary.email.empty": "إضافة عنوان البريد الإلكتروني للاسترداد",
|
||||
"account.settings.field.secondary.email.confirmation": "لقد أرسلنا رسالة تأكيد إلى {value}. انقر فوق الرابط في الرسالة لتحديث عنوان بريد الاسترداد الإلكتروني.",
|
||||
"account.settings.email.field.confirmation.header": "تعليق عملية التأكيد",
|
||||
"account.settings.field.dob": "سنة الميلاد",
|
||||
"account.settings.field.dob.empty": "إضافة سنة الميلاد",
|
||||
"account.settings.field.year_of_birth.options.empty": "اختر سنة ميلاد",
|
||||
"account.settings.field.country": "الدولة",
|
||||
"account.settings.field.country.empty": "إضافة البلد ",
|
||||
"account.settings.field.country.options.empty": "اختر دولة",
|
||||
"account.settings.field.state": "الحالة",
|
||||
"account.settings.field.state.empty": "إضافة منطقة",
|
||||
"account.settings.field.state.options.empty": "اختر منطقة",
|
||||
"account.settings.field.site.language": "لغة الموقع",
|
||||
"account.settings.field.site.language.help.text": "اللغة المستخدمة في كافة أقسام هذا الموقع. يتوفّر هذا الموقع حاليًا بعدد محدود من اللغات.",
|
||||
"account.settings.field.education": "المستوى التعليمي",
|
||||
"account.settings.field.education.empty": "إضافة المستوى التعليمي",
|
||||
"account.settings.field.education.levels.empty": "اختر المستوى التعليمي",
|
||||
"account.settings.field.education.levels.p": "دكتوراه",
|
||||
"account.settings.field.education.levels.m": "ماجستير أو شهادة مهنيّة",
|
||||
"account.settings.field.education.levels.b": "بكالوريوس",
|
||||
"account.settings.field.education.levels.a": "زمالة",
|
||||
"account.settings.field.education.levels.hs": "شهادة الثانوية العامة",
|
||||
"account.settings.field.education.levels.jhs": "شهادة الثانوية الصغرى/الإعدادية/المرحلة المتوسّطة",
|
||||
"account.settings.field.education.levels.el": "شهادة المدرسة الابتدائية",
|
||||
"account.settings.field.education.levels.none": "لا يوجد تعليم رسمي",
|
||||
"account.settings.field.education.levels.o": "نوع آخر من التعليم",
|
||||
"account.settings.field.gender": "الجنس",
|
||||
"account.settings.field.gender.empty": "إضافة الجنس",
|
||||
"account.settings.field.gender.options.empty": "اختر جنسًا",
|
||||
"account.settings.field.gender.options.f": "أنثى",
|
||||
"account.settings.field.gender.options.m": "ذكر",
|
||||
"account.settings.field.gender.options.o": "آخر",
|
||||
"account.settings.field.language.proficiencies": "لغة التحدّث",
|
||||
"account.settings.field.language.proficiencies.empty": "إضافة لغة التحدث",
|
||||
"account.settings.field.language_proficiencies.options.empty": "اختر لغة",
|
||||
"account.settings.field.time.zone": "المنطقة الزمنية",
|
||||
"account.settings.field.time.zone.empty": "ضبط المنطقة الزمنية",
|
||||
"account.settings.field.time.zone.description": "حدد المنطقة الزمنية لعرض تواريخ المساق. إذا لم تحدد منطقة زمنية، فسيتم عرض تواريخ المساق، بما في ذلك المواعيد النهائية للواجب في المنطقة الزمنية المحلية في المتصفح.",
|
||||
"account.settings.field.time.zone.default": "الافتراضي (منطقة التوقيت المحلي)",
|
||||
"account.settings.field.time.zone.all": "جميع المناطق الزمنية",
|
||||
"account.settings.field.time.zone.country": "المنطقة الزمنية للدولة",
|
||||
"account.settings.section.social.media": "روابط منصات التواصل الإجتماعي",
|
||||
"account.settings.section.social.media.description": "اختياريا، قم بربط حساباتك الشخصية بأيقونات منصات التواصل الاجتماعي في ملف التعريف الخاص بك.",
|
||||
"account.settings.field.social.platform.name.linkedin": "لينكد إن",
|
||||
"account.settings.field.social.platform.name.linkedin.empty": "إضافة عنوان ملف لينكد إن ",
|
||||
"account.settings.jump.nav.delete.account": "احذف حسابي",
|
||||
"account.settings.field.social.platform.name.twitter": "تويتر",
|
||||
"account.settings.field.social.platform.name.twitter.empty": "إضافة عنوان صفحة تويتر",
|
||||
"account.settings.field.social.platform.name.facebook": "فيسبوك",
|
||||
"account.settings.field.social.platform.name.facebook.empty": "إضافة عنوان صفحة فيسبوك",
|
||||
"account.settings.editable.field.action.save": "حفظ",
|
||||
"account.settings.editable.field.action.cancel": "إلغاء",
|
||||
"account.settings.editable.field.action.edit": "تحرير",
|
||||
"account.settings.static.field.empty": "لم يتم تحديد قيمة، فضلًا اتصل بمدير {enterprise} لتعيين بعض التغييرات.",
|
||||
"account.settings.static.field.empty.no.admin": "لم يتم تحديد قيمة",
|
||||
"account.settings.coaching.consent.welcome.header": "لنبدأ",
|
||||
"account.settings.coaching.consent.welcome.subheader": "نحن هنا لأجلك من البداية حتى النهاية",
|
||||
"account.settings.coaching.consent.description": "تتضمن برامج البكالوريوس التدريب الذي يركز على مهنتك وتعليمك وكيفية تحقيق نتائج مبهرة من خلال التواصل الشخصي مع خبراء متمرسين. إذا كنت مهتمًا، فقدّم المعلومات أدناه وانقر فوق \"إرسال\"، وسيتصل بك شريكنا في التدريب عبر البريد الإلكتروني و/أو الرسائل النصية لمساعدتك على المضي قدمًا. تنطبق الشروط والأحكام.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* يتم تضمين خدمات التدريب بدون أي تكلفة إضافية للمتعلمين الذين لديهم أرقام هواتف أمريكية. يتضمن التدريب رسائل نصية متكررة. قد تنطبق أسعار الرسائل والبيانات. إيقاف النص لإلغاء الاشتراك.",
|
||||
"account.settings.coaching.consent.accept-coaching": "سجّل للاستفادة من خدمات التدريب",
|
||||
"account.settings.coaching.consent.decline-coaching": "أفضّل عدم الاتصال بخدمات التدريب المجانية",
|
||||
"account.settings.coaching.consent.label.name": "يرجى تأكيد الاسم",
|
||||
"account.settings.coaching.consent.label.phone-number": "فضلًا أدخل رقم الهاتف ",
|
||||
"account.settings.coaching.consent.success.header": "تمت العملية بنجاح",
|
||||
"account.settings.coaching.consent.success.message": "لقد اشتركت في التدريب. يمكنك توقع استلام رسالة عبر البريد الإلكتروني أو الرسائل القصيرة في الأيام المقبلة.",
|
||||
"account.settings.coaching.consent.success.continue": "البدء في مساقي",
|
||||
"account.settings.coaching.managed.support": "الدعم",
|
||||
"account.settings.coaching.managed.alert": "تتم إدارة اسمك بواسطة {ManagerTitle}. اتصل بالمسؤول للحصول على المساعدة.",
|
||||
"account.settings.field.phone_number": "رقم الهاتف",
|
||||
"account.settings.field.phone_number.empty": "إضافة رقم الهاتف",
|
||||
"account.settings.field.coaching_consent": "اتفاقية التدريب",
|
||||
"account.settings.field.coaching_consent.tooltip": "تتضمن برامج البكالوريوس التدريب القائم على الرسائل النصية الذي يساعدك على إقران التجارب التعليمية مع أهدافك المهنية من خلال النصائح الشخصية. يتم تضمين خدمات التدريب بدون أي تكلفة إضافية، وهي متوفرة للمتعلمين الذين لديهم أرقام هواتف نقالة أمريكية. تنطبق أسعار المراسلة القياسية. أرسل \"قف\" في أي وقت لإلغاء الرسائل.",
|
||||
"account.settings.field.coaching_consent.error": "مطلوب رقم هاتف أمريكي صالح للدخول في التدريب",
|
||||
"account.settings.delete.account.before.proceeding": "قبل المتابعة، يرجى {actionLink}.",
|
||||
"account.settings.delete.account.header": "احذف حسابي",
|
||||
"account.settings.delete.account.subheader": "نأسف لذهابك!",
|
||||
"account.settings.delete.account.text.1": "يرجى ملاحظة: إن حذف حسابك والبيانات الشخصية قرار نهائي ولا يمكن التراجع عنه. لن تتمكن edX من استعادة حسابك أو البيانات التي تم حذفها.",
|
||||
"account.settings.delete.account.text.2": "بمجرد حذف حسابك ، لا يمكنك استخدامه لأخذ دورات تدريبية على تطبيق edX أو edx.org أو أي موقع آخر تستضيفه edX. وهذا يشمل الوصول إلى edx.org من نظام صاحب العمل أو الجامعة والوصول إلى المواقع الخاصة التي تقدمها MIT Open Learning و Wharton Executive Education و Harvard Medical School.",
|
||||
"account.settings.delete.account.text.3.link": "اتبع الإرشادات التالية لطباعة أو تحميل شهادة.",
|
||||
"account.settings.delete.account.text.warning": "تحذير: حذف الحساب قرار نهائي. يرجى قراءة ما سبق بعناية قبل المتابعة. هذا إجراء لا رجعة فيه، ولن تتمكن بعد الآن من استخدام البريد الإلكتروني نفسه على edX.",
|
||||
"account.settings.delete.account.text.change.instead": "هل تريد تغيير البريد الإلكتروني أو الاسم أو كلمة المرور بدلاً من ذلك؟",
|
||||
"account.settings.delete.account.button": "احذف حسابي",
|
||||
"account.settings.delete.account.please.activate": "تنشيط حسابك",
|
||||
"account.settings.delete.account.please.unlink": "إلغاء ربط جميع حسابات التواصل الاجتماعي",
|
||||
"account.settings.delete.account.modal.header": "هل أنت متأكد؟",
|
||||
"account.settings.delete.account.modal.text.1": "لقد حددت \"حذف حسابي\". إن حذف حسابك وبياناتك الشخصية قرار نهائي لا يمكن التراجع عنه. لن تتمكن edX من استعادة حسابك أو البيانات التي تم حذفها.",
|
||||
"account.settings.delete.account.modal.text.2": "عند المتابعة، فلن تتمكن من استخدام هذا الحساب للحصول على مساقات على تطبيق edX أو edx.org أو أي موقع آخر يستضيفه edX. وهذا يشمل الوصول إلى edx.org من نظام صاحب العمل أو الجامعة والوصول إلى المواقع الخاصة التي يقدمها معهد ماساتشوستس للتكنولوجيا التعليم المفتوح، وارتون التعليم التنفيذي، وكلية هارفارد الطبية.",
|
||||
"account.settings.delete.account.modal.enter.password": "إذا كنت لا تزال ترغب في المتابعة وحذف حسابك ، فيرجى إدخال كلمة مرور حسابك:",
|
||||
"account.settings.delete.account.modal.confirm.delete": "تعم، أحذف",
|
||||
"account.settings.delete.account.modal.confirm.cancel": "إلغاء",
|
||||
"account.settings.delete.account.error.unable.to.delete": "تعذر حذف الحساب",
|
||||
"account.settings.delete.account.error.no.password": "كلمة المرور مطلوبة",
|
||||
"account.settings.delete.account.error.invalid.password": "كلمة المرور المدخلة غير صحيحة",
|
||||
"account.settings.delete.account.error.unable.to.delete.details": "عذراً ، حدث خطأ أثناء محاولة معالجة طلبك. الرجاء معاودة المحاولة في وقت لاحق.",
|
||||
"account.settings.delete.account.modal.after.header": "نأسف لذهابك! سيتم حذف حسابك قريبا.",
|
||||
"account.settings.delete.account.modal.after.text": "قد يستغرق حذف الحساب ، بما في ذلك الإزالة من قوائم البريد الإلكتروني ، بضعة أسابيع حتى تتم معالجته بالكامل من خلال نظامنا. إذا كنت ترغب في إلغاء الاشتراك في رسائل البريد الإلكتروني قبل ذلك الحين ، يرجى إلغاء الاشتراك من تذييل أي بريد إلكتروني.",
|
||||
"account.settings.delete.account.modal.after.button": "إغلاق ",
|
||||
"account.settings.delete.account.text.3": "قد تفقد أيضًا إمكانية الوصول إلى الشهادات التي تم التحقق منها وبيانات اعتماد البرامج الأخرى مثل شهادات MicroMasters. إذا أردت عمل نسخة من هذه السجلات قبل متابعة الحذف، {actionLink}.",
|
||||
"account.settings.message.demographics.service.issue": "حدث خطأ أثناء محاولة استرداد معلومات حسابك أو حفظها. يرجى المحاولة مرة أخرى لاحقًا.",
|
||||
"account.settings.field.demographics.gender": "هوية الجنس",
|
||||
"account.settings.field.demographics.gender.empty": "إضافة هوية الجنس",
|
||||
"account.settings.field.demographics.gender.options.empty": "حدد هوية الجنس",
|
||||
"account.settings.field.demographics.gender_description": "وصف هوية الجنس",
|
||||
"account.settings.field.demographics.gender_description.empty": "أدخل وصفًا",
|
||||
"account.settings.field.demographics.ethnicity": "هوية العرق/ الأصل",
|
||||
"account.settings.field.demographics.ethnicity.empty": "إضافة هوية العرق/الأصل",
|
||||
"account.settings.field.demographics.ethnicity.options.empty": "فضلًا اختر جميع ما ينطبق",
|
||||
"account.settings.field.demographics.income": "الدخل المادي الأسري",
|
||||
"account.settings.field.demographics.income.empty": "إضافة الدخل المادي الأسري",
|
||||
"account.settings.field.demographics.income.options.empty": "حدد نطاق الدخل المادي للأسرة",
|
||||
"account.settings.field.demographics.military_history": "حالة الخدمة العسكرية في الولايات المتحدة الأمريكية",
|
||||
"account.settings.field.demographics.military_history.empty": "إضافة حالة الخدمة العسكرية",
|
||||
"account.settings.field.demographics.military_history.options.empty": "اختر حالة الخدمة العسكرية",
|
||||
"account.settings.field.demographics.learner_education_level": "مؤهلك التعليمي",
|
||||
"account.settings.field.demographics.learner_education_level.empty": "إضافة مؤهلك التعليمي",
|
||||
"account.settings.field.demographics.parent_education_level": "مؤهل الوالدين/الآوصياء التعليمي",
|
||||
"account.settings.field.demographics.parent_education_level.empty": "إضافة المؤهل التعليمي",
|
||||
"account.settings.field.demographics.education_level.options.empty": "حدد موهلاً تعليميًا",
|
||||
"account.settings.field.demographics.work_status": "الحالة الوظيفية",
|
||||
"account.settings.field.demographics.work_status.empty": "إضافة الحالة الوظيفية",
|
||||
"account.settings.field.demographics.work_status.options.empty": "فضلًا حدد حالتك الوظيفية",
|
||||
"account.settings.field.demographics.work_status_description": "وصف الحالة الوظيفية",
|
||||
"account.settings.field.demographics.work_status_description.empty": "أدخل وصفًا",
|
||||
"account.settings.field.demographics.current_work_sector": "مجال العمل الحالي",
|
||||
"account.settings.field.demographics.current_work_sector.empty": "إضافة مجال العمل",
|
||||
"account.settings.field.demographics.future_work_sector": "مجال العمل المستقبلي",
|
||||
"account.settings.field.demographics.future_work_sector.empty": "إضافة مجال العمل",
|
||||
"account.settings.field.demographics.work_sector.options.empty": "حدد مجال العمل",
|
||||
"account.settings.section.demographics.why": "لماذا تجمع edX هذه المعلومات؟",
|
||||
"error.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في نص الرابط. الرجاء التحقق من الرابط والمحاولة مجددا.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "الدعم الفني",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "لقد أرسلنا رسالة إلى {email}. انقر فوق الرابط في الرسالة لإعادة تعيين كلمة المرور. إذا لم يتم استلام الرسالة؟ اتصل بـ {technicalSupportLink}.",
|
||||
"account.settings.editable.field.password.reset.button": "تغيير كلمة المرور",
|
||||
"account.settings.editable.field.password.reset.button.forbidden": "طلبك السابق قيد التقدم، يرجى إعادة المحاولة بعد لحظات قليلة.",
|
||||
"account.settings.editable.field.password.reset.label": "كلمة المرور",
|
||||
"account.settings.sso.link.account": "تسجيل الدخول كـ {name}",
|
||||
"account.settings.sso.account.connected": "مربوط",
|
||||
"account.settings.sso.account.disconnect.error": "حدثت مشكلة أثناء قطع اتصال هذا الحساب، اتصل بالدعم عند استمرار المشكلة.",
|
||||
"account.settings.sso.unlink.account": "إلغاء ربط حساب {name} ",
|
||||
"account.settings.sso.no.providers": "لا يمكن ربط أية حسابات حاليًا",
|
||||
"id.verification.access.blocked.denied": "لا يمكنك التحقق من هويتك في الوقت الحالي. إذا لم تقم بعد بتنشيط حسابك، فيرجى التحقق من مجلد البريد المهمل للحصول على رسالة التفعيل من {email}.",
|
||||
"id.verification.next": "التالي",
|
||||
"id.verification.support": "support",
|
||||
"id.verification.example.card.alt": "مثال على بطاقة هوية صحيحة بالاسم الكامل وصورة.",
|
||||
"id.verification.requirements.title": "متطلبات التحقق من الصورة",
|
||||
"id.verification.requirements.description": "يجب عليك اتباع الآتي لإكمال عملية التحقق الإلكتروني من هويتك:",
|
||||
"id.verification.requirements.card.device.title": "جهاز مزود بكاميرا",
|
||||
"id.verification.requirements.card.device.allow": "موافق",
|
||||
"id.verification.requirements.card.id.title": "صورة التحقق من الشخصية.",
|
||||
"id.verification.requirements.card.id.text": "تحتاج إلى بطاقة هوية صحيحة للتحقق تحوي اسمك الكامل وصورتك.",
|
||||
"id.verification.privacy.title": "بيانات الخصوصية.",
|
||||
"id.verification.privacy.need.photo.question": "لماذا تحتاج edX إلى صورتي؟ ",
|
||||
"id.verification.privacy.need.photo.answer": "نستخدم صور التحقق الخاصة بك لتأكيد هويتك والتأكد من صحة شهادتك.",
|
||||
"id.verification.privacy.do.with.photo.question": "ما الذي تفعله edX بهذه الصورة؟",
|
||||
"id.verification.privacy.do.with.photo.answer": "سنقوم بتشفير صورتك بأمان وإرسالها لخدمة التحقق للمراجعة . لن يتم حفظ صورتك ومعلوماتك أو عرضها في أي مكان على edX بعد اكتمال عملية التحقق.",
|
||||
"id.verification.access.blocked.title": "التحقق من الهوية",
|
||||
"id.verification.access.blocked.enrollment": "أنت الآن ملتحق بمساق يتطلب التحقق من الهوية.",
|
||||
"id.verification.access.blocked.pending": "لقد قمت بالفعل بإرسال معلومات التحقق الخاصة بك. ستصلك رسالة على لوحة المعلومات عند اكتمال عملية التحقق (عادةً خلال 5 أيام).",
|
||||
"id.verification.photo.take": "التقاط صورة ",
|
||||
"id.verification.photo.retake": "Retake Photo?",
|
||||
"id.verification.photo.enable.detection": "تمكين خاصية التعرف على الوجه",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "عند تحديد هذا الخيار، فسيظهر مربع حول وجهك. يمكن رؤية وجهك بوضوح إذا كان المربع المحيط به أزرق. إذا لم يكن وجهك في وضع جيد أو إذا لم يكن قابلاً للاكتشاف، فسيكون المربع باللون الأحمر.",
|
||||
"id.verification.photo.enable.detection.id.help.text": "عند تحديد هذا الخيار، فسيظهر مربع حول صورة وجهك في بطاقة الهوية. يمكن رؤية وجهك بوضوح إذا كان المربع المحيط به أزرق. إذا لم يكن وجهك في وضع جيد أو إذا لم يكن قابلاً للاكتشاف، فسيكون المربع باللون الأحمر.",
|
||||
"id.verification.photo.feedback.correct": "وضع الوجه جيد.",
|
||||
"id.verification.photo.feedback.two.faces": "تم تحديد أكثر من وجه.",
|
||||
"id.verification.photo.feedback.no.faces": "لم يتم تحديد أي وجه.",
|
||||
"id.verification.photo.feedback.top.left": "وضع خاطئ. أعلى اليسار.",
|
||||
"id.verification.photo.feedback.top.center": "وضع خاطئ. أعلى الوسط.",
|
||||
"id.verification.photo.feedback.top.right": "وضع خاطئ. أعلى اليمين.",
|
||||
"id.verification.photo.feedback.center.left": "وضع خاطئ. وسط اليسار.",
|
||||
"id.verification.photo.feedback.center.center": "وضع خاطئ. قريب جدًا من الكاميرا.",
|
||||
"id.verification.photo.feedback.center.right": "وضع خاطئ. وسط اليمين.",
|
||||
"id.verification.photo.feedback.bottom.left": "وضع خاطئ. أسفل اليسار.",
|
||||
"id.verification.photo.feedback.bottom.center": "وضع خاطئ. أسفل الوسط.",
|
||||
"id.verification.photo.feedback.bottom.right": "وضع خاطئ. أسفل اليمين.",
|
||||
"id.verification.camera.access.title": "صلاحيات الكاميرا",
|
||||
"id.verification.camera.access.title.success": "تمكين الوصول للكاميرا",
|
||||
"id.verification.camera.access.title.failed": "تعذّر الوصول للكاميرا.",
|
||||
"id.verification.camera.access.click.allow": "فضلًا تأكد من اختيار الأمر \"السماح\"",
|
||||
"id.verification.camera.access.enable": "تمكين الكاميرا",
|
||||
"id.verification.camera.access.problems": "هل تواجه أية مشكلة؟",
|
||||
"id.verification.camera.access.skip": "قم بتخطي ملفات الصور وتحميلها بدلاً من ذلك",
|
||||
"id.verification.camera.access.success": "يبدو أن الكاميرا تعمل وجاهزة.",
|
||||
"id.verification.camera.access.failure": "يبدو أننا غير قادرين على الوصول إلى الكاميرا. ستحتاج إلى تحميل ملفات الصور الخاصة بك و معرّف الصور الخاص بك.",
|
||||
"id.verification.camera.access.failure.temporary": "يبدو أننا غير قادرين على الوصول إلى الكاميرا. يرجى التحقق من أن كاميرا الويب متصلة ومن أنك سمحت للمتصفح بالوصول إليها.",
|
||||
"id.verification.camera.access.failure.temporary.chrome": "تمكين الوصول للكاميرا في متصفّح كروم: ",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step1": "فتح متصفّح كروم.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2": "توجه إلى المزيد > الإعدادات.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "لنظام ويندوز: Alt+F أو Alt+E أو F10 متبوعاً بـ SPACEBAR",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "لنظام ماك: Command+,",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step3": "ضمن علامة التبويب \"الخصوصية والأمان\"، حدد \"إعدادات الموقع\" ثم \"الكاميرا\".",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step4": "ضمن \"قائمة الحظر\"، ابحث عن \"edx.org\" وحدده.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step5": "في القسم \"الصلاحيات\"، قم بتحديث صلاحيات الكاميرا إلى \"السماح\".",
|
||||
"id.verification.camera.access.failure.temporary.ie11": "تمكين الوصول للكاميرا في متصفح انترنت إكسبلورر:",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step1": "افتح إدارة إعدادات Flash Player عن طريق الانتقال إلى إعدادات Windows > لوحة التحكم > Flash Player.",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step2": "حدد علامة التبويب \"Camera and Mic\" (الكاميرا والميكروفون)، ثم حدد الزر \"Camera and Microphone Settings by Site\" (إعدادات الكاميرا والميكروفون حسب الموقع).",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step3": "اختر \"edx.org\" من قائمة مواقع ويب وقم بتغيير الصلاحيات من خلال تحديد \"السماح\" في القائمة المنسدلة.",
|
||||
"id.verification.camera.access.failure.temporary.firefox": "لتمكين الوصول للكاميرا في متصفّح فايرفوكس: ",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step1": "فتح متصفّح فايرفوكس.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step2": "أدخل \"about:preferences\" في شريط عنوان الرابط.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step3": "حدد علامة التبويب \"الخصوصية والأمان\"، وانتقل إلى القسم \"الصلاحيات\".",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step4": "بجوار \"Camera\" (الكاميرا)، حدد \"Settings…\" (الإعدادات…) الزر.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step5": "في شريط البحث أدخل \"edx.org.\"",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step6": "في عمود الحالة لـ \"edx.org,\" حدد \"السماح\" من القائمة المنسدلة.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step7": "اختر \"حفظ التغييرات.\"",
|
||||
"id.verification.camera.access.failure.temporary.safari": "تمكين الوصول للكاميرا في متصفّح سفاري: ",
|
||||
"id.verification.camera.access.failure.temporary.safari.step1": "فتح متصفّح سفاري.",
|
||||
"id.verification.camera.access.failure.temporary.safari.step2": "انقر فوق قائمة تطبيق سفاري، ثم حدد \"Preferences\" (التفضيلات). يمكنك أيضاً استخدام الأمرCommand+ كاختصار للوحة المفاتيح",
|
||||
"id.verification.camera.access.failure.temporary.safari.step3": "حدد علامة التبويب \"مواقع ويب\" ثم حدد \"كاميرا\".",
|
||||
"id.verification.camera.access.failure.temporary.safari.step4": "حدد \"edx.org\" وقم بتغيير صلاحيات الكاميرا إلى \"السماح\".",
|
||||
"id.verification.camera.access.failure.unsupported": "It looks like your browser does not support camera access.",
|
||||
"id.verification.camera.access.failure.unsupported.chrome.explanation": "The Chrome browser currently does not support camera access on iOS devices, such as iPhones and iPads.",
|
||||
"id.verification.camera.access.failure.unsupported.instructions": "Please use another browser to complete Identity Verification.",
|
||||
"id.verification.photo.tips.title": "تلميحات مفيدة للصورة",
|
||||
"id.verification.photo.tips.description": "بعد ذلك، سنحتاج منك التقاط صورة لوجهك. يرجى مراجعة التلميحات المفيدة أدناه.",
|
||||
"id.verification.photo.tips.list.title": "تلميحات الصورة",
|
||||
"id.verification.photo.tips.list.description": "لالتقاط صورة ناجحة، يُرجى التأكّد ممّا يلي:",
|
||||
"id.verification.photo.tips.list.well.lit": "أنّ الإضاءة جيّدة على وجهك.",
|
||||
"id.verification.photo.tips.list.inside.frame": "أنّ وجهك داخل إطار الصورة بالكامل.",
|
||||
"id.verification.portrait.photo.title.camera": "التقط صورة لنفسك",
|
||||
"id.verification.portrait.photo.title.upload": "ارفع صورتك",
|
||||
"id.verification.portrait.photo.preview.alt": "معاينة صورة وجه المستخدم.",
|
||||
"id.verification.portrait.photo.instructions.camera": "عندما يكون وجهك في موضعه، استخدم زر التقاط صورة أدناه لالتقاط الصورة.",
|
||||
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ",
|
||||
"id.verification.camera.help.sight.question": "ماذا لو لم أتمكن من رؤية صورة الكاميرا ؟ أو إذا لم أتمكن من رؤية صورتي لتحديد أي جانب مرئي؟",
|
||||
"id.verification.camera.help.sight.answer.portrait": "قد تتمكن من إكمال إجراء التقاط الصور من دون مساعدة، ولكن قد يتطلب الأمر بضع محاولات ضبط وضع الكاميرا بشكل صحيح. يختلف وضع الكاميرا المثالي باختلاف الكمبيوتر، ولكن بشكل عام، يكون أفضل موضع للتصوير في الرأس هو 12 إلى 18 بوصة (30-45 سم) تقريبًا من الكاميرا، مع وضع الرأس في المنتصف بالنسبة إلى شاشة الكمبيوتر. إذا تم رفض الصور التي ترسلها، فحاول تحريك اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة.",
|
||||
"id.verification.camera.help.sight.answer.id": "قد تتمكن من إكمال إجراء التقاط الصور من دون مساعدة، ولكن قد يتطلب الأمر بضع محاولات لضبط وضع الكاميرا بشكل صحيح. يختلف الوضع الأمثل للكاميرا باختلاف جهاز الكمبيوتر، ولكن بشكل عام، يكون أفضل وضع لصورة بطاقة تعريف من 8 إلى 12 بوصة (من 20 إلى 30 سم) عن الكاميرا، مع وضع بطاقة الهوية في الوسط بالنسبة للكاميرا. إذا تم رفض الصور التي ترسلها، فحاول تحريك اتجاه الكمبيوتر أو الكاميرا لتغيير زاوية الإضاءة. إن السبب الأكثر شيوعاً للرفض هو عدم القدرة على قراءة النص الموجود على بطاقة الهوية.",
|
||||
"id.verification.camera.help.difficulty.question.portrait": "ماذا لو واجهت صعوبة في تثبيت رأسي في الموضع المناسب للكاميرا؟",
|
||||
"id.verification.camera.help.difficulty.question.id": "ماذا لو واجهت صعوبة في تثبيت بطاقة هويتي في الموضع المناسب للكاميرا؟",
|
||||
"id.verification.camera.help.difficulty.answer": "إذا كنت بحاجة إلى المساعدة في التقاط صورة للتقديم، فاتصل بدعم edX للحصول على اقتراحات إضافية.",
|
||||
"id.verification.camera.help.upload.question": "What if I want to upload a photo instead?",
|
||||
"id.verification.camera.help.upload.answer": "On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.",
|
||||
"id.verification.id.photo.unclear.question": "هل صورة بطاقة الهوية غير واضحة أو ضبابية؟",
|
||||
"id.verification.id.tips.title": "تلميحات مفيدة للهوية ",
|
||||
"id.verification.id.tips.description": "بعد ذلك، سنكون بحاجة إلى التقاط صورة لبطاقة هوية صالحة تتضمن اسمك الكامل وصورتك. يرجى تجهيز بطاقة هويتك.",
|
||||
"id.verification.id.tips.list.well.lit": "إضاءة بطاقة هويتك جيدة.",
|
||||
"id.verification.id.tips.list.clear": "تأكد من قدرتك على رؤية صورتك وقراءة اسمك بوضوح.",
|
||||
"id.verification.id.photo.title.camera": "التقط صورة لبطاقتك الشخصية",
|
||||
"id.verification.id.photo.title.upload": "حمّل صورة لهويتك",
|
||||
"id.verification.id.photo.preview.alt": "معاينة صورة الهوية.",
|
||||
"id.verification.id.photo.instructions.camera": "عندما تكون بطاقة هويتك في موضعها، استخدم زر التقاط صورة أدناه لالتقاط الصورة.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
|
||||
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
|
||||
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
|
||||
"id.verification.account.name.title": "التحقق من اسم الحساب",
|
||||
"id.verification.account.name.instructions": "يجب أن يكون الاسم الموجود على حسابك والاسم الموجود على المعرّف الخاص بك متطابقًا تمامًا. إذا لم يكن الأمر كذلك، فيرجى النقر فوق \"لا\" لتحديث اسم حسابك.",
|
||||
"id.verification.account.name.radio.label": "هل يتطابق الاسم الموجود على هويتك مع اسم الحساب أدناه؟",
|
||||
"id.verification.account.name.radio.yes": "نعم",
|
||||
"id.verification.account.name.radio.no": "لا",
|
||||
"id.verification.account.name.error": "يرجى تحديث اسم الحساب لمطابقة الاسم على بطاقة الهوية.",
|
||||
"id.verification.account.name.warning.prefix": "يُرجى الملاحظة:",
|
||||
"id.verification.account.name.settings": "إعدادات الحساب",
|
||||
"id.verification.account.name.label": "اسم الحساب",
|
||||
"id.verification.account.name.photo.alt": "صورة من هويتك للتقديم.",
|
||||
"id.verification.account.name.save": "حفظ ثم التالي",
|
||||
"id.verification.review.title": "مراجعة صورك",
|
||||
"id.verification.review.description": "يُرجى التأكّد من أنّ الصور والمعلومات التي قدّمتها تمكّننا من التحقّق من هويّتك. ",
|
||||
"id.verification.review.portrait.label": "صورتك الشخصية",
|
||||
"id.verification.review.portrait.alt": "صورة شخصية للتقديم.",
|
||||
"id.verification.review.portrait.retake": "إعادة التقاط الصورة شخصية",
|
||||
"id.verification.review.id.label": "معرّف صورتك",
|
||||
"id.verification.review.id.alt": "صورة من هويتك للتقديم.",
|
||||
"id.verification.review.id.retake": "إعادة التقاط صورة الهوية",
|
||||
"id.verification.review.confirm": "إرسال",
|
||||
"id.verification.submission.alert.error.face": "مطلوب صورة لوجهك. يرجى إعادة التقاط الصورة الشخصية.",
|
||||
"id.verification.submission.alert.error.id": "مطلوب صورة لبطاقة هويتك. يرجى إعادة التقاط صورة بطاقة الهوية.",
|
||||
"id.verification.submission.alert.error.name": "مطلوب اسم حساب صالح. يرجى تحديث اسم حسابك لمطابقة الاسم على هويتك.",
|
||||
"id.verification.submission.alert.error.unsupported": "One or more of the files you have uploaded is in an unsupported format. Please choose from the following: ",
|
||||
"id.verification.review.error": "صفحة فريق دعم edX ",
|
||||
"id.verification.submitted.title": "جارِ التحقق من الهوية",
|
||||
"id.verification.submitted.text": "لقد تلقينا معلوماتك وجاري الآن العمل على التحقق من هويتك. ستصلك رسالة على لوحة المعلومات عند اكتمال عملية التحقق (عادةً خلال 5 أيام). في غضون ذلك، لا يزال بإمكانك الوصول إلى كل محتوى المساق المتوفر.",
|
||||
"id.verification.return.dashboard": "العودة إلى لوحة المعلومات",
|
||||
"id.verification.return.course": "العودة للمساق",
|
||||
"id.verification.photo.upload.help.title": "Upload a Photo Instead",
|
||||
"id.verification.photo.camera.help.title": "Use Your Camera Instead",
|
||||
"id.verification.photo.upload.help.text": "If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.",
|
||||
"id.verification.photo.camera.help.text": "If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.",
|
||||
"id.verification.upload.help.button": "Switch to Upload Mode",
|
||||
"id.verification.camera.help.button": "Switch to Camera Mode",
|
||||
"id.verification.choose.mode.title": "Photo Requirements Options",
|
||||
"id.verification.choose.mode.hep.text": "To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.",
|
||||
"id.verification.choose.mode.radio.upload": "Upload photos from my device",
|
||||
"id.verification.choose.mode.radio.camera": "Take pictures using my camera",
|
||||
"id.verification.account.name.managed.alert": "Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help.",
|
||||
"id.verification.request.camera.access.instructions": "لالتقاط صورة باستخدام كاميرا الويب، قد تتلقى طلب المتصفح للوصول إلى الكاميرا. {clickAllow}",
|
||||
"id.verification.requirements.account.managed.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help before completing the Photo Verification process.",
|
||||
"id.verification.requirements.card.device.text": "أنت بحاجة إلى جهاز مزود بكاميرا. إذا تلقيت طلب المتصفح للوصول إلى الكاميرا، فيرجى التأكد من النقر فوق {السماح}.",
|
||||
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
|
||||
"idv.submission.alert.error": "\nواجهنا خطأ فني أثناء محاولة رفع طلب التحقق من الهوية\nقد تكون هذه مشكلة مؤقتة، لذا يرجى المحاولة مرة أخرى بعد بضع دقائق\nعند استمرار المشكلة يرجى الانتقال إلى {support_link} للحصول على المساعدة",
|
||||
"id.verification.account.name.edit": "Edit {sr}"
|
||||
"account.settings.message.duplicate.tpa.provider": "The {provider} account you selected is already linked to another edX account.",
|
||||
"account.settings.message.managed.settings": "Your profile settings are managed by {managerTitle}. Contact your administrator or {support} for help.",
|
||||
"account.settings.message.managed.settings.support": "support",
|
||||
"account.settings.page.heading": "Account Settings",
|
||||
"account.settings.loading.message": "Loading...",
|
||||
"account.settings.loading.error": "Error: {error}",
|
||||
"account.settings.banner.beta.language": "You have set your language to {beta_language}, which is currently not fully translated. You can help us translate this language fully by joining the Transifex community and adding translations from English for learners that speak {beta_language}.",
|
||||
"account.settings.banner.beta.language.action.switch.back": "Switch Back to {previous_language}",
|
||||
"account.settings.banner.beta.language.action.help.translate": "Help Translate into {beta_language}",
|
||||
"account.settings.section.account.information": "Account Information",
|
||||
"account.settings.section.account.information.description": "These settings include basic information about your account.",
|
||||
"account.settings.section.profile.information": "Profile Information",
|
||||
"account.settings.section.site.preferences": "Site Preferences",
|
||||
"account.settings.section.linked.accounts": "Linked Accounts",
|
||||
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to edX.",
|
||||
"account.settings.field.username": "Username",
|
||||
"account.settings.field.username.help.text": "The name that identifies you on edX. You cannot change your username.",
|
||||
"account.settings.field.full.name": "Full name",
|
||||
"account.settings.field.full.name.empty": "Add name",
|
||||
"account.settings.field.full.name.help.text": "The name that is used for ID verification and that appears on your certificates.",
|
||||
"account.settings.field.email": "Email address (Sign in)",
|
||||
"account.settings.field.email.empty": "Add email address",
|
||||
"account.settings.field.email.confirmation": "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 edX and course teams at this address.",
|
||||
"account.settings.field.secondary.email": "Recovery email address",
|
||||
"account.settings.field.secondary.email.empty": "Add a recovery email address",
|
||||
"account.settings.field.secondary.email.confirmation": "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.site.language": "Site language",
|
||||
"account.settings.field.site.language.help.text": "The language used throughout this site. This site is currently available in a limited number of languages.",
|
||||
"account.settings.field.education": "Education",
|
||||
"account.settings.field.education.empty": "Add level of education",
|
||||
"account.settings.field.education.levels.empty": "Select a level of education",
|
||||
"account.settings.field.education.levels.p": "Doctorate",
|
||||
"account.settings.field.education.levels.m": "Master's or professional degree",
|
||||
"account.settings.field.education.levels.b": "Bachelor's Degree",
|
||||
"account.settings.field.education.levels.a": "Associate's degree",
|
||||
"account.settings.field.education.levels.hs": "Secondary/high school",
|
||||
"account.settings.field.education.levels.jhs": "Junior secondary/junior high/middle school",
|
||||
"account.settings.field.education.levels.el": "Elementary/primary school",
|
||||
"account.settings.field.education.levels.none": "No formal education",
|
||||
"account.settings.field.education.levels.o": "Other education",
|
||||
"account.settings.field.gender": "Gender",
|
||||
"account.settings.field.gender.empty": "Add gender",
|
||||
"account.settings.field.gender.options.empty": "Select a gender",
|
||||
"account.settings.field.gender.options.f": "Female",
|
||||
"account.settings.field.gender.options.m": "Male",
|
||||
"account.settings.field.gender.options.o": "Other",
|
||||
"account.settings.field.language.proficiencies": "Spoken languages",
|
||||
"account.settings.field.language.proficiencies.empty": "Add a spoken language",
|
||||
"account.settings.field.language_proficiencies.options.empty": "Select a Language",
|
||||
"account.settings.field.time.zone": "Time zone",
|
||||
"account.settings.field.time.zone.empty": "Set time zone",
|
||||
"account.settings.field.time.zone.description": "Select the time zone for displaying course dates. If you do not specify a time zone, course dates, including assignment deadlines, will be displayed in your 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 edX profile.",
|
||||
"account.settings.field.social.platform.name.linkedin": "LinkedIn",
|
||||
"account.settings.field.social.platform.name.linkedin.empty": "Add LinkedIn profile",
|
||||
"account.settings.jump.nav.delete.account": "Delete My Account",
|
||||
"account.settings.field.social.platform.name.twitter": "Twitter",
|
||||
"account.settings.field.social.platform.name.twitter.empty": "Add Twitter profile",
|
||||
"account.settings.field.social.platform.name.facebook": "Facebook",
|
||||
"account.settings.field.social.platform.name.facebook.empty": "Add Facebook profile",
|
||||
"account.settings.editable.field.action.save": "Save",
|
||||
"account.settings.editable.field.action.cancel": "Cancel",
|
||||
"account.settings.editable.field.action.edit": "Edit",
|
||||
"account.settings.static.field.empty": "No value set. Contact your {enterprise} administrator to make changes.",
|
||||
"account.settings.static.field.empty.no.admin": "No value set.",
|
||||
"account.settings.coaching.consent.welcome.header": "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 will receive a text message confirmation.",
|
||||
"account.settings.coaching.consent.success.continue": "Start my course",
|
||||
"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 in English and Spanish languages. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.",
|
||||
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
|
||||
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
|
||||
"account.settings.delete.account.header": "Delete My Account",
|
||||
"account.settings.delete.account.subheader": "We're sorry to see you go!",
|
||||
"account.settings.delete.account.text.1": "Please note: Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.",
|
||||
"account.settings.delete.account.text.2": "Once your account is deleted, you cannot use it to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your 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 the instructions for printing or downloading a certificate",
|
||||
"account.settings.delete.account.text.warning": "Warning: Account deletion is permanent. Please read the above carefully before proceeding. This is an irreversible action, and you will no longer be able to use the same email on edX.",
|
||||
"account.settings.delete.account.text.change.instead": "Want to change your email, name, or password instead?",
|
||||
"account.settings.delete.account.button": "Delete My Account",
|
||||
"account.settings.delete.account.please.activate": "activate your account",
|
||||
"account.settings.delete.account.please.unlink": "unlink all social media accounts",
|
||||
"account.settings.delete.account.modal.header": "Are you sure?",
|
||||
"account.settings.delete.account.modal.text.1": "You have selected \"Delete My Account\". Deletion of your account and personal data is permanent and cannot be undone. edX will not be able to recover your account or the data that is deleted.",
|
||||
"account.settings.delete.account.modal.text.2": "If you proceed, you will be unable to use this account to take courses on the edX app, edx.org, or any other site hosted by edX. This includes access to edx.org from your employer's or university's system and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School.",
|
||||
"account.settings.delete.account.modal.enter.password": "If you still wish to continue and delete your account, please enter your account password:",
|
||||
"account.settings.delete.account.modal.confirm.delete": "Yes, Delete",
|
||||
"account.settings.delete.account.modal.confirm.cancel": "Cancel",
|
||||
"account.settings.delete.account.error.unable.to.delete": "Unable to delete account",
|
||||
"account.settings.delete.account.error.no.password": "A password is required",
|
||||
"account.settings.delete.account.error.unable.to.delete.details": "Sorry, there was an error trying to process your request. Please try again later.",
|
||||
"account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.",
|
||||
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
|
||||
"account.settings.delete.account.modal.after.button": "Close",
|
||||
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
|
||||
"account.settings.editable.field.password.reset.button": "Reset Password",
|
||||
"account.settings.editable.field.password.reset.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."
|
||||
}
|
||||
@@ -11,7 +11,6 @@
|
||||
"account.settings.section.account.information": "Información de la cuenta",
|
||||
"account.settings.section.account.information.description": "Estas configuraciones incluyen información básica sobre tu cuenta.",
|
||||
"account.settings.section.profile.information": "Información del perfil",
|
||||
"account.settings.section.demographics.information": "Información opcional",
|
||||
"account.settings.section.site.preferences": "Preferencias del sitio",
|
||||
"account.settings.section.linked.accounts": "Cuentas vinculadas",
|
||||
"account.settings.section.linked.accounts.description": "Puedes vincular tus cuentas de redes sociales para simplificar el proceso de iniciar sesión en edX.",
|
||||
@@ -34,9 +33,6 @@
|
||||
"account.settings.field.country": "País",
|
||||
"account.settings.field.country.empty": "Agregar país",
|
||||
"account.settings.field.country.options.empty": "Seleccionar un país",
|
||||
"account.settings.field.state": "Estado",
|
||||
"account.settings.field.state.empty": "Añada un estado",
|
||||
"account.settings.field.state.options.empty": "Seleccionar un estado",
|
||||
"account.settings.field.site.language": "Idioma del sitio",
|
||||
"account.settings.field.site.language.help.text": "El idioma que se usará para el sitio. Actualmente solo hay disponibilidad de usar un número limitado de idiomas.",
|
||||
"account.settings.field.education": "Educación",
|
||||
@@ -57,7 +53,7 @@
|
||||
"account.settings.field.gender.options.f": "Femenino",
|
||||
"account.settings.field.gender.options.m": "Masculino",
|
||||
"account.settings.field.gender.options.o": "Otro",
|
||||
"account.settings.field.language.proficiencies": "Idioma hablado",
|
||||
"account.settings.field.language.proficiencies": "Idiomas hablados",
|
||||
"account.settings.field.language.proficiencies.empty": "Agregar un idioma hablado",
|
||||
"account.settings.field.language_proficiencies.options.empty": "Selecciona lenguaje",
|
||||
"account.settings.field.time.zone": "Zona horaria",
|
||||
@@ -78,26 +74,24 @@
|
||||
"account.settings.editable.field.action.save": "Guardar",
|
||||
"account.settings.editable.field.action.cancel": "Cancelar",
|
||||
"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.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.*",
|
||||
"account.settings.coaching.consent.text-messaging.disclaimer": "* Los servicios de entrenamiento se incluyen sin costo adicional para los alumnos con números de teléfono de EE. UU. El entrenamiento incluye mensajes de texto recurrentes. Se pueden aplicar tarifas por mensajes y datos. Envía STOP para cancelar la suscripción.",
|
||||
"account.settings.coaching.consent.accept-coaching": "Registrarse para coaching",
|
||||
"account.settings.coaching.consent.decline-coaching": "Prefiero no ser contactado con servicios de coaching gratuitos.",
|
||||
"account.settings.coaching.consent.label.name": "Por favor confirme su nombre",
|
||||
"account.settings.coaching.consent.label.phone-number": "Ingrese su número de teléfono móvil",
|
||||
"account.settings.coaching.consent.success.header": "¡Éxito!",
|
||||
"account.settings.coaching.consent.success.message": "Estás inscrito para coaching. Puedes esperar un mensaje por correo electrónico o SMS en los próximos días.",
|
||||
"account.settings.coaching.consent.success.continue": "Iniciar mi curso",
|
||||
"account.settings.coaching.managed.support": "soporte",
|
||||
"account.settings.coaching.managed.alert": "{ManagerTitle} administra su Nombre. Póngase en contacto con su administrador para obtener ayuda.",
|
||||
"account.settings.field.phone_number": "Teléfono",
|
||||
"account.settings.field.phone_number.empty": "Añadir un número de teléfono",
|
||||
"account.settings.field.coaching_consent": "Consentimiento de coaching",
|
||||
"account.settings.field.coaching_consent.tooltip": "Los programas de MicroBachelors incluyen entrenamiento basado en mensajes de texto que lo ayuda a emparejar experiencias educativas con sus objetivos profesionales a través de asesoramiento personalizado. Los servicios de entrenamiento se incluyen sin costo adicional y están disponibles para estudiantes con números de teléfono móvil de EE. UU. Se aplican tarifas de mensajería estándar. Envíe \"STOP\" en cualquier momento para cancelar la suscripción a los mensajes.",
|
||||
"account.settings.field.coaching_consent.error": "Se requiere un número de teléfono válido de EE. UU. Para optar por el coaching",
|
||||
"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 will receive a text message confirmation.",
|
||||
"account.settings.coaching.consent.success.continue": "Start my course",
|
||||
"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 in English and Spanish languages. 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": "Antes de continuar, por favor {actionLink}.",
|
||||
"account.settings.delete.account.header": "Eliminar mi cuenta",
|
||||
"account.settings.delete.account.subheader": "¡Sentimos que te vayas!",
|
||||
@@ -108,7 +102,7 @@
|
||||
"account.settings.delete.account.text.change.instead": "En lugar de eso, ¿quieres cambiar tu correo electrónico, nombre o contraseña?",
|
||||
"account.settings.delete.account.button": "Eliminar mi cuenta",
|
||||
"account.settings.delete.account.please.activate": "activar su cuenta",
|
||||
"account.settings.delete.account.please.unlink": "Desvincular todas las cuentas de redes sociales.",
|
||||
"account.settings.delete.account.please.unlink": "unlink all social media accounts",
|
||||
"account.settings.delete.account.modal.header": "¿Está seguro?",
|
||||
"account.settings.delete.account.modal.text.1": "Has seleccionado “Eliminar mi cuenta”. La eliminación de tu cuenta y datos personales es permanente e irreversible. edX no será capaz de recuperar tu cuenta o los datos que se hayan borrado.",
|
||||
"account.settings.delete.account.modal.text.2": "Si procedes, no será posible usar esta cuenta para tomar cursos ni en la aplicación móvil de edX, ni en edx.org, ni en cualquier otro sitio hospedado por edX. Esto incluye el acceso a edx.org desde el sistema de tu empleador o universidad, y el acceso a sitios privados ofrecidos por MIT Open Learning, Wharton Executive Education, y Harvard Medical School.",
|
||||
@@ -117,202 +111,19 @@
|
||||
"account.settings.delete.account.modal.confirm.cancel": "Cancelar",
|
||||
"account.settings.delete.account.error.unable.to.delete": "No es posible eliminar esta cuenta",
|
||||
"account.settings.delete.account.error.no.password": "Se requiere una contraseña",
|
||||
"account.settings.delete.account.error.invalid.password": "Contraseña incorrecta",
|
||||
"account.settings.delete.account.error.unable.to.delete.details": "Ocurrió un error al procesar tu solicitud. Por favor, intente nuevamente más tarde.",
|
||||
"account.settings.delete.account.modal.after.header": "¡Sentimos que te vayas! Tu cuenta será eliminada en breve.",
|
||||
"account.settings.delete.account.modal.after.text": "La eliminación de cuenta, incluyendo la eliminación de las listas de correo electrónico, puede tardar unas semanas en procesarse totalmente en nuestro sistema. Si quieres renunciar a recibir correos antes de que la eliminación se haya completado, por favor date de baja mediante el enlace que aparece al final de los correos.",
|
||||
"account.settings.delete.account.modal.after.button": "Cerrar",
|
||||
"account.settings.delete.account.text.3": "Puede que también pierdas el acceso a los certificados verificados y otros certificados de programas como los de los MicroMasters. Si quieres hacer una copia de dichos certificados para tus archivos antes de proceder a la eliminación, {actionLink}.",
|
||||
"account.settings.message.demographics.service.issue": "Ocurrió un error al intentar recuperar o guardar la información de tu cuenta. Por favor inténtalo más tarde.",
|
||||
"account.settings.field.demographics.gender": "Identidad de género",
|
||||
"account.settings.field.demographics.gender.empty": "Añade identidad de género",
|
||||
"account.settings.field.demographics.gender.options.empty": "Selecciona una identidad de género",
|
||||
"account.settings.field.demographics.gender_description": "Descripción de identidad de género",
|
||||
"account.settings.field.demographics.gender_description.empty": "Ingresa descripción",
|
||||
"account.settings.field.demographics.ethnicity": "Identidad étnica/raza",
|
||||
"account.settings.field.demographics.ethnicity.empty": "Añade identidad étnica/raza",
|
||||
"account.settings.field.demographics.ethnicity.options.empty": "Selecciona todas las que apliquen",
|
||||
"account.settings.field.demographics.income": "Ingreso familiar",
|
||||
"account.settings.field.demographics.income.empty": "Añade ingreso familiar",
|
||||
"account.settings.field.demographics.income.options.empty": "Selecciona un rango de ingreso familiar",
|
||||
"account.settings.field.demographics.military_history": "Estatus militar en EE.UU.",
|
||||
"account.settings.field.demographics.military_history.empty": "Añade estatus militar",
|
||||
"account.settings.field.demographics.military_history.options.empty": "Selecciona estatus militar",
|
||||
"account.settings.field.demographics.learner_education_level": "Tu nivel educacional",
|
||||
"account.settings.field.demographics.learner_education_level.empty": "Añade nivel educacional",
|
||||
"account.settings.field.demographics.parent_education_level": "Nivel educacional de padres/tutores",
|
||||
"account.settings.field.demographics.parent_education_level.empty": "Añade nivel educacional",
|
||||
"account.settings.field.demographics.education_level.options.empty": "Selecciona nivel educacional",
|
||||
"account.settings.field.demographics.work_status": "Estatus laboral",
|
||||
"account.settings.field.demographics.work_status.empty": "Añade estatus laboral",
|
||||
"account.settings.field.demographics.work_status.options.empty": "Selecciona estatus laboral",
|
||||
"account.settings.field.demographics.work_status_description": "Descripción estatus laboral",
|
||||
"account.settings.field.demographics.work_status_description.empty": "Ingresa descripción",
|
||||
"account.settings.field.demographics.current_work_sector": "Área profesional actual",
|
||||
"account.settings.field.demographics.current_work_sector.empty": "Añade área profesional",
|
||||
"account.settings.field.demographics.future_work_sector": "Área profesional futura",
|
||||
"account.settings.field.demographics.future_work_sector.empty": "Añade área profesional",
|
||||
"account.settings.field.demographics.work_sector.options.empty": "Selecciona área profesional",
|
||||
"account.settings.section.demographics.why": "¿Por qué edX obtiene esta información?",
|
||||
"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}.",
|
||||
"account.settings.editable.field.password.reset.button": "Restablecer contraseña",
|
||||
"account.settings.editable.field.password.reset.button.forbidden": "Su solicitud anterior está en progreso, intente nuevamente en unos momentos.",
|
||||
"account.settings.editable.field.password.reset.label": "Contraseña",
|
||||
"account.settings.sso.link.account": "Iniciar sesión con {name}",
|
||||
"account.settings.sso.account.connected": "Vinculado",
|
||||
"account.settings.sso.account.disconnect.error": "Hubo un problema al desconectar esta Cuenta. Si el problema persiste, contacte soporte.",
|
||||
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
|
||||
"account.settings.sso.unlink.account": "Desvincular la cuenta de {accountName} ",
|
||||
"account.settings.sso.no.providers": "No se pueden vincular cuentas en este momento.",
|
||||
"id.verification.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.example.card.alt": "Ejemplo de un documento de identidad válido con foto y nombre completo.",
|
||||
"id.verification.requirements.title": "Requerimientos de verificación por foto",
|
||||
"id.verification.requirements.description": "Para completar la verificación por foto en línea, necesitarás lo siguiente:",
|
||||
"id.verification.requirements.card.device.title": "Dispositivo con cámara",
|
||||
"id.verification.requirements.card.device.allow": "Permitir",
|
||||
"id.verification.requirements.card.id.title": "Identificación por foto",
|
||||
"id.verification.requirements.card.id.text": "Necesitas un documento de identidad válido que contenga tu foto y nombre completo.",
|
||||
"id.verification.privacy.title": "Información de privacidad",
|
||||
"id.verification.privacy.need.photo.question": "¿Por qué edX necesita mi foto?",
|
||||
"id.verification.privacy.need.photo.answer": "Utilizamos tus fotos de verificación para confirmar tu identidad y garantizar la validez de tu certificado.",
|
||||
"id.verification.privacy.do.with.photo.question": "¿Qué hace edX con esta foto?",
|
||||
"id.verification.privacy.do.with.photo.answer": "Encriptamos de forma segura tu foto y la enviamos a nuestro servicio de autorización para su revisión. Tu foto e información no se guardan ni se ven en ninguna parte de edX después de que se completa el proceso de verificación.",
|
||||
"id.verification.access.blocked.title": "Verificación de identidad",
|
||||
"id.verification.access.blocked.enrollment": "Actualmente, no estás inscrito en un curso que requiera verificación de identidad.",
|
||||
"id.verification.access.blocked.pending": "Ya has enviado tu información de verificación de identidad. Recibirás un mensaje en tu panel principal cuando el proceso de verificación esté completado (usualmente dentro de los 5 días).",
|
||||
"id.verification.photo.take": "Tomar la foto",
|
||||
"id.verification.photo.retake": "Retake Photo?",
|
||||
"id.verification.photo.enable.detection": "Habilitar la detección de rostro",
|
||||
"id.verification.photo.enable.detection.portrait.help.text": "Si está marcada, aparecerá un cuadro alrededor de tu cara. Tu rostro se puede ver claramente si el cuadro que lo rodea es azul. Si Tu cara no está en una buena posición o es indetectable, el cuadro será rojo.",
|
||||
"id.verification.photo.enable.detection.id.help.text": "Si está marcada, aparecerá una casilla alrededor de la cara de tu documento de identificación. La cara se puede ver claramente si la caja que la rodea es azul. Si la cara no está en una buena posición o es indetectable, el cuadro será rojo.",
|
||||
"id.verification.photo.feedback.correct": "La cara está en una buena posición.",
|
||||
"id.verification.photo.feedback.two.faces": "Más de un rostro detectado.",
|
||||
"id.verification.photo.feedback.no.faces": "No se detectó el rostro.",
|
||||
"id.verification.photo.feedback.top.left": "Posición incorrecta. Arriba a la izquierda.",
|
||||
"id.verification.photo.feedback.top.center": "Posición incorrecta. Centro Superior.",
|
||||
"id.verification.photo.feedback.top.right": "Posición incorrecta. Parte superior derecha.",
|
||||
"id.verification.photo.feedback.center.left": "Posición incorrecta. Centro izquierda.",
|
||||
"id.verification.photo.feedback.center.center": "Posición incorrecta. Demasiado cerca de la cámara.",
|
||||
"id.verification.photo.feedback.center.right": "Posición incorrecta. Centro derecha.",
|
||||
"id.verification.photo.feedback.bottom.left": "Posición incorrecta. Abajo a la izquierda.",
|
||||
"id.verification.photo.feedback.bottom.center": "Posición incorrecta. Parte inferior central.",
|
||||
"id.verification.photo.feedback.bottom.right": "Posición incorrecta. Abajo a la derecha.",
|
||||
"id.verification.camera.access.title": "Permisos de la cámara",
|
||||
"id.verification.camera.access.title.success": "Acceso a la cámara habilitado",
|
||||
"id.verification.camera.access.title.failed": "Acceso a la cámara no habilitado",
|
||||
"id.verification.camera.access.click.allow": "Por favor asegúrate de hacer clic en \"Permitir\"",
|
||||
"id.verification.camera.access.enable": "Habilitar cámara",
|
||||
"id.verification.camera.access.problems": "¿Tienes problemas?",
|
||||
"id.verification.camera.access.skip": "Omitir y cargar un archivo de imagen",
|
||||
"id.verification.camera.access.success": "Parece que tu cámara está funcionando y está lista.",
|
||||
"id.verification.camera.access.failure": "Parece que no podemos acceder a tu cámara. Deberás cargar archivos de imagen de tu rostro y tu identificación con foto.",
|
||||
"id.verification.camera.access.failure.temporary": "Parece que no podemos acceder a tu cámara. Verifica que tu cámara web esté conectada y que permitiste que tu navegador acceda a ella.",
|
||||
"id.verification.camera.access.failure.temporary.chrome": "Para habilitar el acceso a la cámara en Chrome:",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step1": "Abrir Chrome.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2": "Navega a Más > Configuración.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.windows": "Para Windows: Alt + F, Alt + E o F10 seguido de la barra espaciadora",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step2.mac": "Para Mac: Comando +,",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step3": "En la pestaña \"Privacidad y seguridad\", selecciona \"Configuración del sitio\" y luego \"Cámara.\"",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step4": "En \"Bloqueado\", busca \"edx.org\" y selecciónalo.",
|
||||
"id.verification.camera.access.failure.temporary.chrome.step5": "En la sección \"Permisos\", actualiza los permisos de la cámara a \"Permitir\".",
|
||||
"id.verification.camera.access.failure.temporary.ie11": "Para habilitar el acceso a la cámara en Internet Explorer:",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step1": "Abre el Administrador de configuración de Flash Player navegando a Configuración de Windows > Panel de control > Flash Player.",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step2": "Selecciona la pestaña \"Cámara y micrófono,\" y luego selecciona el botón \"Configuración de cámara y micrófono por sitio.\"",
|
||||
"id.verification.camera.access.failure.temporary.ie11.step3": "Elige \"edx.org\" de la lista de sitios web y cambia los permisos seleccionando \"Permitir\" en el menú desplegable.",
|
||||
"id.verification.camera.access.failure.temporary.firefox": "Para habilitar el acceso a la cámara en Firefox:",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step1": "Abrir Firefox.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step2": "Coloca \"about:preferences\" en la barra del URL.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step3": "Selecciona la pestaña \"Privacidad y seguridad\" y navega hasta la sección \"Permisos.\"",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step4": "Junto a \"Cámara,\" selecciona el botón \"Configuración...\"",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step5": "En la barra de búsqueda, ingresa \"edx.org\".",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step6": "En la columna de estado de \"edx.org\", selecciona \"Permitir\" en el menú desplegable.",
|
||||
"id.verification.camera.access.failure.temporary.firefox.step7": "Selecciona \"Guardar cambios.\"",
|
||||
"id.verification.camera.access.failure.temporary.safari": "Para habilitar el acceso a la cámara en Safari:",
|
||||
"id.verification.camera.access.failure.temporary.safari.step1": "Abrir Safari.",
|
||||
"id.verification.camera.access.failure.temporary.safari.step2": "Haz clic en el menú de la aplicación Safari y luego selecciona \"Preferencias\". También puedes utilizar Command +, como método abreviado de teclado.",
|
||||
"id.verification.camera.access.failure.temporary.safari.step3": "Selecciona la pestaña \"Sitios web\" y luego selecciona \"Cámara\".",
|
||||
"id.verification.camera.access.failure.temporary.safari.step4": "Selecciona \"edx.org\" y cambia los permisos de la cámara a \"Permitir.\"",
|
||||
"id.verification.camera.access.failure.unsupported": "It looks like your browser does not support camera access.",
|
||||
"id.verification.camera.access.failure.unsupported.chrome.explanation": "The Chrome browser currently does not support camera access on iOS devices, such as iPhones and iPads.",
|
||||
"id.verification.camera.access.failure.unsupported.instructions": "Please use another browser to complete Identity Verification.",
|
||||
"id.verification.photo.tips.title": "Consejos útiles de fotos",
|
||||
"id.verification.photo.tips.description": "A continuación, necesitaremos que tomes una foto de tu rostro. Por favor, revisa los siguientes consejos útiles.",
|
||||
"id.verification.photo.tips.list.title": "Consejos para fotos",
|
||||
"id.verification.photo.tips.list.description": "Para tomar la foto correctamente, asegúrate de: ",
|
||||
"id.verification.photo.tips.list.well.lit": "El rostro esté bien iluminado",
|
||||
"id.verification.photo.tips.list.inside.frame": "Tu cara está completamente dentro del marco de la foto.",
|
||||
"id.verification.portrait.photo.title.camera": "Toma una foto de ti mismo",
|
||||
"id.verification.portrait.photo.title.upload": "Sube una foto tuya",
|
||||
"id.verification.portrait.photo.preview.alt": "Previsualización de la foto con el rostro del usuario",
|
||||
"id.verification.portrait.photo.instructions.camera": "Cuando tu rostro esté en posición, usa el botón Tomar foto a continuación para tomar tu foto.",
|
||||
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ",
|
||||
"id.verification.camera.help.sight.question": "¿Qué pasa si no puedo ver la imagen de la cámara o si no puedo ver mi foto para determinar qué lado es visible?",
|
||||
"id.verification.camera.help.sight.answer.portrait": "Es posible que puedas completar el procedimiento de captura de imágenes sin ayuda, pero es posible que necesites un par de intentos de envío para que la cámara se coloque correctamente. La posición óptima de la cámara varía con cada computadora, pero generalmente la mejor distancia para una foto de rostro es aproximadamente a 12 a 18 pulgadas (30 a 45 centímetros) de la cámara, con la cabeza centrada en relación con la pantalla de la computadora. Si las fotos que envías son rechazadas, intenta mover la computadora o la orientación de la cámara para cambiar el ángulo de iluminación.",
|
||||
"id.verification.camera.help.sight.answer.id": "Es posible que puedas completar el procedimiento de captura de imágenes sin ayuda, pero es posible que necesites un par de intentos de envío para que la cámara se coloque correctamente. El posicionamiento óptimo de la cámara varía con cada computadora pero, generalmente, la mejor distancia para una foto de un documento de identificación es a 8 a 12 pulgadas (20 a 30 centímetros) de la cámara, con el documento de identificación centrado en relación con la cámara. Si las fotos que envías son rechazadas, intenta mover la computadora o la orientación de la cámara para cambiar el ángulo de iluminación. La razón más común de rechazo es la imposibilidad de leer el texto del documento de identidad.",
|
||||
"id.verification.camera.help.difficulty.question.portrait": "¿Qué sucede si tengo dificultades para mantener la cabeza en posición con respecto a la cámara?",
|
||||
"id.verification.camera.help.difficulty.question.id": "¿Qué sucede si tengo dificultades para mantener mi identificación en posición con respecto a la cámara?",
|
||||
"id.verification.camera.help.difficulty.answer": "Si necesitas ayuda para tomar una foto para enviarla, comunícate con soporte de edX para obtener sugerencias adicionales.",
|
||||
"id.verification.camera.help.upload.question": "What if I want to upload a photo instead?",
|
||||
"id.verification.camera.help.upload.answer": "On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.",
|
||||
"id.verification.id.photo.unclear.question": "¿La imagen de tu identificación no es clara o está demasiado borrosa?",
|
||||
"id.verification.id.tips.title": "Consejos útiles para la verificación",
|
||||
"id.verification.id.tips.description": "A continuación, necesitaremos que tomes una foto de un documento de identidad válido que incluya tu foto y nombre completo. Ten tu ID a mano.",
|
||||
"id.verification.id.tips.list.well.lit": "Tu identificación está bien iluminada.",
|
||||
"id.verification.id.tips.list.clear": "Asegúrate de que puedes ver tu foto y leer claramente tu nombre.",
|
||||
"id.verification.id.photo.title.camera": "Toma una Foto de tu Identificación",
|
||||
"id.verification.id.photo.title.upload": "Carga una foto de tu identificación",
|
||||
"id.verification.id.photo.preview.alt": "Previsualización de Foto ID",
|
||||
"id.verification.id.photo.instructions.camera": "Cuando tu identificación esté en su lugar, usa el botón Tomar foto a continuación para tomar tu foto.",
|
||||
"id.verification.id.photo.instructions.upload": "Please upload an ID photo. Ensure the entire ID fits inside the frame and is well-lit. The file size must be under 10 MB. Supported formats: ",
|
||||
"id.verification.id.photo.instructions.upload.error.invalidFileType": "The file you have selected is not a supported image type. Please choose from the following formats: ",
|
||||
"id.verification.id.photo.instructions.upload.error.fileTooLarge": "The file you have selected is too large. Please try again with a file less than 10MB.",
|
||||
"id.verification.account.name.title": "Verificación de nombre de cuenta",
|
||||
"id.verification.account.name.instructions": "El nombre de tu cuenta y el nombre de tu identificación deben coincidir exactamente. De lo contrario, haz clic en \"No\" para actualizar el nombre de tu cuenta.",
|
||||
"id.verification.account.name.radio.label": "¿El nombre de tu identificación coincide con el nombre de la cuenta a continuación?",
|
||||
"id.verification.account.name.radio.yes": "Si",
|
||||
"id.verification.account.name.radio.no": "No",
|
||||
"id.verification.account.name.error": "Actualiza el nombre de la cuenta para que coincida con el nombre de tu identificación.",
|
||||
"id.verification.account.name.warning.prefix": "Ten en cuenta:",
|
||||
"id.verification.account.name.settings": "Configuración de cuenta",
|
||||
"id.verification.account.name.label": "Nombre de la cuenta",
|
||||
"id.verification.account.name.photo.alt": "Foto de tu identificación a enviar.",
|
||||
"id.verification.account.name.save": "Guardar y siguiente",
|
||||
"id.verification.review.title": "Revisar tus fotos",
|
||||
"id.verification.review.description": "Asegúrate de que podamos verificar tu identidad con las imágenes y la información suministradas.",
|
||||
"id.verification.review.portrait.label": "Tu Retrato",
|
||||
"id.verification.review.portrait.alt": "Foto de tu rostro a enviar.",
|
||||
"id.verification.review.portrait.retake": "Retomar Foto de Retrato",
|
||||
"id.verification.review.id.label": "Tu Foto de identificación",
|
||||
"id.verification.review.id.alt": "Foto de tu identificación a enviar.",
|
||||
"id.verification.review.id.retake": "Retomar Foto de identificación",
|
||||
"id.verification.review.confirm": "Enviar",
|
||||
"id.verification.submission.alert.error.face": "Se requiere una foto de tu rostro. Vuelve a tomar tu foto de retrato.",
|
||||
"id.verification.submission.alert.error.id": "Se requiere una foto de tu documento de ID. Vuelve a tomar tu foto de ID.",
|
||||
"id.verification.submission.alert.error.name": "Se requiere un nombre de cuenta válido. Actualiza el nombre de tu cuenta para que coincida con el nombre que figura en tu ID.",
|
||||
"id.verification.submission.alert.error.unsupported": "One or more of the files you have uploaded is in an unsupported format. Please choose from the following: ",
|
||||
"id.verification.review.error": "Página de soporte de edX",
|
||||
"id.verification.submitted.title": "Verificación de identidad en progreso.",
|
||||
"id.verification.submitted.text": "Hemos recibido tu información y estamos verificando tu identidad. Verás un mensaje en tu tablero cuando se complete el proceso de verificación (generalmente en un periodo de 5 días). Mientras tanto, aún puedes acceder a todo el contenido del curso disponible.",
|
||||
"id.verification.return.dashboard": "Volver al panel principal",
|
||||
"id.verification.return.course": "Regresar al curso",
|
||||
"id.verification.photo.upload.help.title": "Upload a Photo Instead",
|
||||
"id.verification.photo.camera.help.title": "Use Your Camera Instead",
|
||||
"id.verification.photo.upload.help.text": "If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.",
|
||||
"id.verification.photo.camera.help.text": "If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.",
|
||||
"id.verification.upload.help.button": "Switch to Upload Mode",
|
||||
"id.verification.camera.help.button": "Switch to Camera Mode",
|
||||
"id.verification.choose.mode.title": "Photo Requirements Options",
|
||||
"id.verification.choose.mode.hep.text": "To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.",
|
||||
"id.verification.choose.mode.radio.upload": "Upload photos from my device",
|
||||
"id.verification.choose.mode.radio.camera": "Take pictures using my camera",
|
||||
"id.verification.account.name.managed.alert": "Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help.",
|
||||
"id.verification.request.camera.access.instructions": "Para tomar una foto con tu cámara web, es posible que recibas un aviso del navegador para acceder a tu cámara. {clickAllow}",
|
||||
"id.verification.requirements.account.managed.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help before completing the Photo Verification process.",
|
||||
"id.verification.requirements.card.device.text": "Necesitas un dispositivo que tenga una cámara. Si has recibido un aviso del navegador para habilitar acceso a tu cámara, por favor asegúrate de seleccionar [allow].",
|
||||
"id.verification.account.name.summary.alert": "Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help.",
|
||||
"idv.submission.alert.error": "\n Se produjo un error técnico al intentar enviar la verificación de ID.\n Es posible que sea una cuestión temporal, así que inténtalo de nuevo en unos minutos.\n Si el problema continúa, dirígete a {support_link} para obtener ayuda.\n ",
|
||||
"id.verification.account.name.edit": "Edit {sr}"
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time."
|
||||
}
|
||||
@@ -11,7 +11,6 @@
|
||||
"account.settings.section.account.information": "Account Information",
|
||||
"account.settings.section.account.information.description": "These settings include basic information about your account.",
|
||||
"account.settings.section.profile.information": "Profile Information",
|
||||
"account.settings.section.demographics.information": "Optional Information",
|
||||
"account.settings.section.site.preferences": "Site Preferences",
|
||||
"account.settings.section.linked.accounts": "Linked Accounts",
|
||||
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to edX.",
|
||||
@@ -34,9 +33,6 @@
|
||||
"account.settings.field.country": "Country",
|
||||
"account.settings.field.country.empty": "Add country",
|
||||
"account.settings.field.country.options.empty": "Select a Country",
|
||||
"account.settings.field.state": "State",
|
||||
"account.settings.field.state.empty": "Add state",
|
||||
"account.settings.field.state.options.empty": "Select a State",
|
||||
"account.settings.field.site.language": "Site language",
|
||||
"account.settings.field.site.language.help.text": "The language used throughout this site. This site is currently available in a limited number of languages.",
|
||||
"account.settings.field.education": "Education",
|
||||
@@ -57,7 +53,7 @@
|
||||
"account.settings.field.gender.options.f": "Female",
|
||||
"account.settings.field.gender.options.m": "Male",
|
||||
"account.settings.field.gender.options.o": "Other",
|
||||
"account.settings.field.language.proficiencies": "Spoken language",
|
||||
"account.settings.field.language.proficiencies": "Spoken languages",
|
||||
"account.settings.field.language.proficiencies.empty": "Add a spoken language",
|
||||
"account.settings.field.language_proficiencies.options.empty": "Select a Language",
|
||||
"account.settings.field.time.zone": "Time zone",
|
||||
@@ -89,14 +85,12 @@
|
||||
"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.message": "You're signed up for coaching. You will receive a text message confirmation.",
|
||||
"account.settings.coaching.consent.success.continue": "Start my course",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
|
||||
"account.settings.field.phone_number": "Phone Number",
|
||||
"account.settings.field.phone_number.empty": "Add a phone number",
|
||||
"account.settings.field.coaching_consent": "Coaching consent",
|
||||
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.",
|
||||
"account.settings.field.coaching_consent.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 in English and Spanish languages. 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",
|
||||
@@ -117,202 +111,19 @@
|
||||
"account.settings.delete.account.modal.confirm.cancel": "Cancel",
|
||||
"account.settings.delete.account.error.unable.to.delete": "Unable to delete account",
|
||||
"account.settings.delete.account.error.no.password": "A password is required",
|
||||
"account.settings.delete.account.error.invalid.password": "Password is incorrect",
|
||||
"account.settings.delete.account.error.unable.to.delete.details": "Sorry, there was an error trying to process your request. Please try again later.",
|
||||
"account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.",
|
||||
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
|
||||
"account.settings.delete.account.modal.after.button": "Close",
|
||||
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
|
||||
"account.settings.message.demographics.service.issue": "An error occurred attempting to retrieve or save your account information. Please try again later.",
|
||||
"account.settings.field.demographics.gender": "Gender identity",
|
||||
"account.settings.field.demographics.gender.empty": "Add gender identity",
|
||||
"account.settings.field.demographics.gender.options.empty": "Select a gender identity",
|
||||
"account.settings.field.demographics.gender_description": "Gender identity description",
|
||||
"account.settings.field.demographics.gender_description.empty": "Enter description",
|
||||
"account.settings.field.demographics.ethnicity": "Race/Ethnicity identity",
|
||||
"account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity",
|
||||
"account.settings.field.demographics.ethnicity.options.empty": "Select all that apply",
|
||||
"account.settings.field.demographics.income": "Family income",
|
||||
"account.settings.field.demographics.income.empty": "Add family income",
|
||||
"account.settings.field.demographics.income.options.empty": "Select a family income range",
|
||||
"account.settings.field.demographics.military_history": "U.S. Military status",
|
||||
"account.settings.field.demographics.military_history.empty": "Add military status",
|
||||
"account.settings.field.demographics.military_history.options.empty": "Select military status",
|
||||
"account.settings.field.demographics.learner_education_level": "Your education level",
|
||||
"account.settings.field.demographics.learner_education_level.empty": "Add education level",
|
||||
"account.settings.field.demographics.parent_education_level": "Parents/Guardians education level",
|
||||
"account.settings.field.demographics.parent_education_level.empty": "Add education level",
|
||||
"account.settings.field.demographics.education_level.options.empty": "Select education level",
|
||||
"account.settings.field.demographics.work_status": "Employment status",
|
||||
"account.settings.field.demographics.work_status.empty": "Add employment status",
|
||||
"account.settings.field.demographics.work_status.options.empty": "Select employment status",
|
||||
"account.settings.field.demographics.work_status_description": "Employment status description",
|
||||
"account.settings.field.demographics.work_status_description.empty": "Enter description",
|
||||
"account.settings.field.demographics.current_work_sector": "Current work industry",
|
||||
"account.settings.field.demographics.current_work_sector.empty": "Add work industry",
|
||||
"account.settings.field.demographics.future_work_sector": "Future work industry",
|
||||
"account.settings.field.demographics.future_work_sector.empty": "Add work industry",
|
||||
"account.settings.field.demographics.work_sector.options.empty": "Select work industry",
|
||||
"account.settings.section.demographics.why": "Why does edX collect this information?",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
|
||||
"account.settings.editable.field.password.reset.button": "Reset Password",
|
||||
"account.settings.editable.field.password.reset.button.forbidden": "Your previous request is in progress, please try again in few moments.",
|
||||
"account.settings.editable.field.password.reset.label": "Password",
|
||||
"account.settings.sso.link.account": "Sign in with {name}",
|
||||
"account.settings.sso.account.connected": "Linked",
|
||||
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
|
||||
"account.settings.sso.unlink.account": "Unlink {name} account",
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
|
||||
"id.verification.access.blocked.denied": "You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}.",
|
||||
"id.verification.next": "Next",
|
||||
"id.verification.support": "support",
|
||||
"id.verification.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 edX need my photo?",
|
||||
"id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
|
||||
"id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
|
||||
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
|
||||
"id.verification.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 edX 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": "edX Support Page",
|
||||
"id.verification.submitted.title": "Identity Verification in Progress",
|
||||
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.",
|
||||
"id.verification.return.dashboard": "Return to Your Dashboard",
|
||||
"id.verification.return.course": "Return to Course",
|
||||
"id.verification.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}"
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time."
|
||||
}
|
||||
@@ -11,7 +11,6 @@
|
||||
"account.settings.section.account.information": "Account Information",
|
||||
"account.settings.section.account.information.description": "These settings include basic information about your account.",
|
||||
"account.settings.section.profile.information": "Profile Information",
|
||||
"account.settings.section.demographics.information": "Optional Information",
|
||||
"account.settings.section.site.preferences": "Site Preferences",
|
||||
"account.settings.section.linked.accounts": "Linked Accounts",
|
||||
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to edX.",
|
||||
@@ -34,9 +33,6 @@
|
||||
"account.settings.field.country": "Country",
|
||||
"account.settings.field.country.empty": "Add country",
|
||||
"account.settings.field.country.options.empty": "Select a Country",
|
||||
"account.settings.field.state": "State",
|
||||
"account.settings.field.state.empty": "Add state",
|
||||
"account.settings.field.state.options.empty": "Select a State",
|
||||
"account.settings.field.site.language": "Site language",
|
||||
"account.settings.field.site.language.help.text": "The language used throughout this site. This site is currently available in a limited number of languages.",
|
||||
"account.settings.field.education": "Education",
|
||||
@@ -57,7 +53,7 @@
|
||||
"account.settings.field.gender.options.f": "Female",
|
||||
"account.settings.field.gender.options.m": "Male",
|
||||
"account.settings.field.gender.options.o": "Other",
|
||||
"account.settings.field.language.proficiencies": "Spoken language",
|
||||
"account.settings.field.language.proficiencies": "Spoken languages",
|
||||
"account.settings.field.language.proficiencies.empty": "Add a spoken language",
|
||||
"account.settings.field.language_proficiencies.options.empty": "Select a Language",
|
||||
"account.settings.field.time.zone": "Time zone",
|
||||
@@ -89,14 +85,12 @@
|
||||
"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.message": "You're signed up for coaching. You will receive a text message confirmation.",
|
||||
"account.settings.coaching.consent.success.continue": "Start my course",
|
||||
"account.settings.coaching.managed.support": "support",
|
||||
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
|
||||
"account.settings.field.phone_number": "Phone Number",
|
||||
"account.settings.field.phone_number.empty": "Add a phone number",
|
||||
"account.settings.field.coaching_consent": "Coaching consent",
|
||||
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text ‘STOP’ at anytime to opt-out of messages.",
|
||||
"account.settings.field.coaching_consent.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 in English and Spanish languages. 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",
|
||||
@@ -117,202 +111,19 @@
|
||||
"account.settings.delete.account.modal.confirm.cancel": "Cancel",
|
||||
"account.settings.delete.account.error.unable.to.delete": "Unable to delete account",
|
||||
"account.settings.delete.account.error.no.password": "A password is required",
|
||||
"account.settings.delete.account.error.invalid.password": "Password is incorrect",
|
||||
"account.settings.delete.account.error.unable.to.delete.details": "Sorry, there was an error trying to process your request. Please try again later.",
|
||||
"account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.",
|
||||
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
|
||||
"account.settings.delete.account.modal.after.button": "Close",
|
||||
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
|
||||
"account.settings.message.demographics.service.issue": "An error occurred attempting to retrieve or save your account information. Please try again later.",
|
||||
"account.settings.field.demographics.gender": "Gender identity",
|
||||
"account.settings.field.demographics.gender.empty": "Add gender identity",
|
||||
"account.settings.field.demographics.gender.options.empty": "Select a gender identity",
|
||||
"account.settings.field.demographics.gender_description": "Gender identity description",
|
||||
"account.settings.field.demographics.gender_description.empty": "Enter description",
|
||||
"account.settings.field.demographics.ethnicity": "Race/Ethnicity identity",
|
||||
"account.settings.field.demographics.ethnicity.empty": "Add race/ethnicity identity",
|
||||
"account.settings.field.demographics.ethnicity.options.empty": "Select all that apply",
|
||||
"account.settings.field.demographics.income": "Family income",
|
||||
"account.settings.field.demographics.income.empty": "Add family income",
|
||||
"account.settings.field.demographics.income.options.empty": "Select a family income range",
|
||||
"account.settings.field.demographics.military_history": "U.S. Military status",
|
||||
"account.settings.field.demographics.military_history.empty": "Add military status",
|
||||
"account.settings.field.demographics.military_history.options.empty": "Select military status",
|
||||
"account.settings.field.demographics.learner_education_level": "Your education level",
|
||||
"account.settings.field.demographics.learner_education_level.empty": "Add education level",
|
||||
"account.settings.field.demographics.parent_education_level": "Parents/Guardians education level",
|
||||
"account.settings.field.demographics.parent_education_level.empty": "Add education level",
|
||||
"account.settings.field.demographics.education_level.options.empty": "Select education level",
|
||||
"account.settings.field.demographics.work_status": "Employment status",
|
||||
"account.settings.field.demographics.work_status.empty": "Add employment status",
|
||||
"account.settings.field.demographics.work_status.options.empty": "Select employment status",
|
||||
"account.settings.field.demographics.work_status_description": "Employment status description",
|
||||
"account.settings.field.demographics.work_status_description.empty": "Enter description",
|
||||
"account.settings.field.demographics.current_work_sector": "Current work industry",
|
||||
"account.settings.field.demographics.current_work_sector.empty": "Add work industry",
|
||||
"account.settings.field.demographics.future_work_sector": "Future work industry",
|
||||
"account.settings.field.demographics.future_work_sector.empty": "Add work industry",
|
||||
"account.settings.field.demographics.work_sector.options.empty": "Select work industry",
|
||||
"account.settings.section.demographics.why": "Why does edX collect this information?",
|
||||
"error.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
|
||||
"account.settings.editable.field.password.reset.button.confirmation.support.link": "technical support",
|
||||
"account.settings.editable.field.password.reset.button.confirmation": "We've sent a message to {email}. Click the link in the message to reset your password. Didn't receive the message? Contact {technicalSupportLink}.",
|
||||
"account.settings.editable.field.password.reset.button": "Reset Password",
|
||||
"account.settings.editable.field.password.reset.button.forbidden": "Your previous request is in progress, please try again in few moments.",
|
||||
"account.settings.editable.field.password.reset.label": "Password",
|
||||
"account.settings.sso.link.account": "Sign in with {name}",
|
||||
"account.settings.sso.account.connected": "Linked",
|
||||
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
|
||||
"account.settings.sso.unlink.account": "Unlink {name} account",
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
|
||||
"id.verification.access.blocked.denied": "You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}.",
|
||||
"id.verification.next": "Next",
|
||||
"id.verification.support": "support",
|
||||
"id.verification.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 edX need my photo?",
|
||||
"id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
|
||||
"id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
|
||||
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
|
||||
"id.verification.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 edX 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": "edX Support Page",
|
||||
"id.verification.submitted.title": "Identity Verification in Progress",
|
||||
"id.verification.submitted.text": "We have received your information and are verifying your identity. You will see a message on your dashboard when the verification process is complete (usually within 5 days). In the meantime, you can still access all available course content.",
|
||||
"id.verification.return.dashboard": "Return to Your Dashboard",
|
||||
"id.verification.return.course": "Return to Course",
|
||||
"id.verification.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}"
|
||||
"account.settings.sso.no.providers": "No accounts can be linked at this time."
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './IdVerification.messages';
|
||||
import { ERROR_REASONS } from './IdVerificationContext';
|
||||
|
||||
function AccessBlocked({ error, intl }) {
|
||||
const handleMessage = () => {
|
||||
if (error === ERROR_REASONS.COURSE_ENROLLMENT) {
|
||||
return <p>{intl.formatMessage(messages['id.verification.access.blocked.enrollment'])}</p>;
|
||||
}
|
||||
if (error === ERROR_REASONS.EXISTING_REQUEST) {
|
||||
return <p>{intl.formatMessage(messages['id.verification.access.blocked.pending'])}</p>;
|
||||
}
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="id.verification.access.blocked.denied"
|
||||
defaultMessage="You cannot verify your identity at this time. If you have yet to activate your account, please check your spam folder for the activation email from {email}."
|
||||
description="Text that displays when user is denied from making a request, and to check their email for an activation email."
|
||||
values={{
|
||||
email: <strong>no-reply@registration.edx.org</strong>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 aria-level="1" tabIndex="-1">
|
||||
{intl.formatMessage(messages['id.verification.access.blocked.title'])}
|
||||
</h3>
|
||||
{handleMessage()}
|
||||
<div className="action-row">
|
||||
<a className="btn btn-primary mt-3" href={`${getConfig().LMS_BASE_URL}/dashboard`}>
|
||||
{intl.formatMessage(messages['id.verification.return.dashboard'])}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AccessBlocked.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
error: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(AccessBlocked);
|
||||
@@ -1,365 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/media-has-caption */
|
||||
/* eslint-disable jsx-a11y/no-access-key */
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import * as blazeface from '@tensorflow-models/blazeface';
|
||||
import CameraPhoto, { FACING_MODES } from 'jslib-html5-camera-photo';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form, Spinner } from '@edx/paragon';
|
||||
|
||||
import shutter from './data/camera-shutter.base64.json';
|
||||
import messages from './IdVerification.messages';
|
||||
|
||||
class Camera extends React.Component {
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.cameraPhoto = null;
|
||||
this.videoRef = React.createRef();
|
||||
this.canvasRef = React.createRef();
|
||||
this.setDetection = this.setDetection.bind(this);
|
||||
this.state = {
|
||||
dataUri: '',
|
||||
videoHasLoaded: false,
|
||||
shouldDetect: false,
|
||||
isFinishedLoadingDetection: true,
|
||||
shouldGiveFeedback: true,
|
||||
feedback: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.cameraPhoto = new CameraPhoto(this.videoRef.current);
|
||||
this.cameraPhoto.startCamera(
|
||||
this.props.isPortrait ? FACING_MODES.USER : FACING_MODES.ENVIRONMENT,
|
||||
{ width: 640, height: 480 },
|
||||
);
|
||||
}
|
||||
|
||||
async componentWillUnmount() {
|
||||
this.cameraPhoto.stopCamera();
|
||||
}
|
||||
|
||||
setDetection() {
|
||||
this.setState(
|
||||
(state) => ({ shouldDetect: !state.shouldDetect }),
|
||||
() => {
|
||||
if (this.state.shouldDetect) {
|
||||
this.setState({ isFinishedLoadingDetection: false });
|
||||
this.startDetection();
|
||||
}
|
||||
this.sendEvent();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setVideoHasLoaded() {
|
||||
this.setState({ videoHasLoaded: 'true' });
|
||||
}
|
||||
|
||||
getGridPosition(coordinates) {
|
||||
// Used to determine where a face is (i.e. top-left, center-right, bottom-center, etc.)
|
||||
|
||||
const x = coordinates[0];
|
||||
const y = coordinates[1];
|
||||
|
||||
let messageBase = 'id.verification.photo.feedback';
|
||||
|
||||
const heightUpperLimit = 320;
|
||||
const heightMiddleLimit = 160;
|
||||
|
||||
if (y < heightMiddleLimit) {
|
||||
messageBase += '.top';
|
||||
} else if (y < heightUpperLimit && y >= heightMiddleLimit) {
|
||||
messageBase += '.center';
|
||||
} else {
|
||||
messageBase += '.bottom';
|
||||
}
|
||||
|
||||
const widthRightLimit = 213;
|
||||
const widthMiddleLimit = 427;
|
||||
|
||||
if (x < widthRightLimit) {
|
||||
messageBase += '.right';
|
||||
} else if (x >= widthRightLimit && x < widthMiddleLimit) {
|
||||
messageBase += '.center';
|
||||
} else {
|
||||
messageBase += '.left';
|
||||
}
|
||||
|
||||
return messageBase;
|
||||
}
|
||||
|
||||
getSizeFactor() {
|
||||
let sizeFactor = 1;
|
||||
const settings = this.cameraPhoto.getCameraSettings();
|
||||
if (settings) {
|
||||
const videoWidth = settings.width;
|
||||
const videoHeight = settings.height;
|
||||
// need to multiply by 3 because each pixel contains 3 bytes
|
||||
const currentSize = videoWidth * videoHeight * 3;
|
||||
// chose a limit of 9,999,999 (bytes) so that result will
|
||||
// always be less than 10MB
|
||||
const ratio = 9999999 / currentSize;
|
||||
|
||||
if (ratio < 1) {
|
||||
// if the current resolution creates an image larger than 10 MB, adjust sizeFactor (resolution)
|
||||
// to ensure that image will have a file size of less than 10 MB.
|
||||
sizeFactor = ratio;
|
||||
} else if (videoWidth === 640 && videoHeight === 480) {
|
||||
// otherwise increase the resolution to try and prevent blurry images.
|
||||
sizeFactor = 2;
|
||||
}
|
||||
}
|
||||
return sizeFactor;
|
||||
}
|
||||
|
||||
detectFromVideoFrame = (model, video) => {
|
||||
model.estimateFaces(video).then((predictions) => {
|
||||
if (this.state.shouldDetect && !this.state.dataUri) {
|
||||
this.showDetections(predictions);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.detectFromVideoFrame(model, video);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
showDetections = (predictions) => {
|
||||
let canvasContext;
|
||||
if (predictions.length > 0) {
|
||||
canvasContext = this.canvasRef.current.getContext('2d');
|
||||
canvasContext.clearRect(0, 0, canvasContext.canvas.width, canvasContext.canvas.height);
|
||||
}
|
||||
// predictions is an array of objects describing each detected face
|
||||
predictions.forEach((prediction) => {
|
||||
const start = [prediction.topLeft[0], prediction.topLeft[1]];
|
||||
const end = [prediction.bottomRight[0], prediction.bottomRight[1]];
|
||||
const size = [end[0] - start[0], end[1] - start[1]];
|
||||
|
||||
// landmarks is an array of points representing each facial landmark (i.e. right eye, left eye, nose, etc.)
|
||||
const features = prediction.landmarks;
|
||||
let isInPosition = true;
|
||||
|
||||
// for each of the landmarks, determine if it is in position
|
||||
for (let j = 0; j < features.length; j++) {
|
||||
const x = features[j][0];
|
||||
const y = features[j][1];
|
||||
|
||||
let isInRange;
|
||||
if (this.props.isPortrait) {
|
||||
isInRange = this.isInRangeForPortrait(x, y);
|
||||
} else {
|
||||
isInRange = this.isInRangeForID(x, y);
|
||||
}
|
||||
// if it is not in range, give feedback depending on which feature is out of range
|
||||
isInPosition = isInPosition && isInRange;
|
||||
}
|
||||
|
||||
// draw a box depending on if all landmarks are in position
|
||||
if (isInPosition) {
|
||||
canvasContext.strokeStyle = '#00ffff';
|
||||
canvasContext.lineWidth = 6;
|
||||
canvasContext.strokeRect(start[0], start[1], size[0], size[1]);
|
||||
// give positive feedback here if user is in correct position
|
||||
this.giveFeedback(predictions.length, [], true);
|
||||
} else {
|
||||
canvasContext.fillStyle = 'rgba(255, 51, 0, 0.75)';
|
||||
canvasContext.fillRect(start[0], start[1], size[0], size[1]);
|
||||
this.giveFeedback(predictions.length, features[0], false);
|
||||
}
|
||||
});
|
||||
|
||||
if (predictions.length === 0) {
|
||||
this.giveFeedback(predictions.length, [], false);
|
||||
}
|
||||
}
|
||||
|
||||
startDetection() {
|
||||
setTimeout(() => {
|
||||
if (this.state.videoHasLoaded) {
|
||||
const loadModelPromise = blazeface.load();
|
||||
Promise.all([loadModelPromise])
|
||||
.then((values) => {
|
||||
this.setState({ isFinishedLoadingDetection: true });
|
||||
this.detectFromVideoFrame(values[0], this.videoRef.current);
|
||||
});
|
||||
} else {
|
||||
this.setState({ isFinishedLoadingDetection: true });
|
||||
this.setState({ shouldDetect: false });
|
||||
// TODO: add error message
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
sendEvent() {
|
||||
let eventName = 'edx.id_verification';
|
||||
if (this.props.isPortrait) {
|
||||
eventName += '.user_photo';
|
||||
} else {
|
||||
eventName += '.id_photo';
|
||||
}
|
||||
|
||||
if (this.state.shouldDetect) {
|
||||
eventName += '.face_detection_enabled';
|
||||
} else {
|
||||
eventName += '.face_detection_disabled';
|
||||
}
|
||||
sendTrackEvent(eventName);
|
||||
}
|
||||
|
||||
giveFeedback(numFaces, rightEye, isCorrect) {
|
||||
if (this.state.shouldGiveFeedback) {
|
||||
const currentFeedback = this.state.feedback;
|
||||
let newFeedback = '';
|
||||
if (numFaces === 1) {
|
||||
// only give feedback if one face is detected otherwise
|
||||
// it would be difficult to tell a user which face to move
|
||||
if (isCorrect) {
|
||||
newFeedback = this.props.intl.formatMessage(messages['id.verification.photo.feedback.correct']);
|
||||
} else {
|
||||
// give feedback based on where user is
|
||||
newFeedback = this.props.intl.formatMessage(messages[this.getGridPosition(rightEye)]);
|
||||
}
|
||||
} else if (numFaces > 1) {
|
||||
newFeedback = this.props.intl.formatMessage(messages['id.verification.photo.feedback.two.faces']);
|
||||
} else {
|
||||
newFeedback = this.props.intl.formatMessage(messages['id.verification.photo.feedback.no.faces']);
|
||||
}
|
||||
if (currentFeedback !== newFeedback) {
|
||||
// only update status if it is different, so we don't overload the user with status updates
|
||||
this.setState({ feedback: newFeedback });
|
||||
}
|
||||
// turn off feedback for one to ensure that instructions aren't disruptive/interrupting
|
||||
this.setState({ shouldGiveFeedback: false });
|
||||
setTimeout(() => {
|
||||
this.setState({ shouldGiveFeedback: true });
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
isInRangeForPortrait(x, y) {
|
||||
return x > 47 && x < 570 && y > 100 && y < 410;
|
||||
}
|
||||
|
||||
isInRangeForID(x, y) {
|
||||
return x > 120 && x < 470 && y > 120 && y < 350;
|
||||
}
|
||||
|
||||
takePhoto() {
|
||||
if (this.state.dataUri) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
sizeFactor: this.getSizeFactor(),
|
||||
};
|
||||
|
||||
this.playShutterClick();
|
||||
const dataUri = this.cameraPhoto.getDataUri(config);
|
||||
this.setState({ dataUri });
|
||||
this.props.onImageCapture(dataUri);
|
||||
}
|
||||
|
||||
playShutterClick() {
|
||||
const audio = new Audio(`data:audio/mp3;base64,${shutter.base64}`);
|
||||
audio.play();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.setState({ dataUri: '' });
|
||||
if (this.state.shouldDetect) {
|
||||
this.startDetection();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const cameraFlashClass = this.state.dataUri
|
||||
? 'do-transition camera-flash'
|
||||
: 'camera-flash';
|
||||
return (
|
||||
<div className="camera-outer-wrapper shadow">
|
||||
<Form.Group style={{ textAlign: 'left', padding: '0.5rem', marginBottom: '0.5rem' }}>
|
||||
<Form.Check
|
||||
id="videoDetection"
|
||||
name="videoDetection"
|
||||
label={this.props.intl.formatMessage(messages['id.verification.photo.enable.detection'])}
|
||||
aria-describedby="videoDetectionHelpText"
|
||||
checked={this.state.shouldDetect}
|
||||
onChange={this.setDetection}
|
||||
style={{ padding: '0rem', marginLeft: '1.25rem', float: this.state.isFinishedLoadingDetection ? 'none' : 'left' }}
|
||||
/>
|
||||
{!this.state.isFinishedLoadingDetection && <Spinner animation="border" variant="primary" style={{ marginLeft: '0.5rem' }} data-testid="spinner" />}
|
||||
<Form.Text id="videoDetectionHelpText" data-testid="videoDetectionHelpText">
|
||||
{this.props.isPortrait
|
||||
? this.props.intl.formatMessage(messages['id.verification.photo.enable.detection.portrait.help.text'])
|
||||
: this.props.intl.formatMessage(messages['id.verification.photo.enable.detection.id.help.text'])}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<div className="camera-wrapper">
|
||||
<div className={cameraFlashClass} />
|
||||
<video
|
||||
ref={this.videoRef}
|
||||
data-testid="video"
|
||||
autoPlay
|
||||
className="camera-video"
|
||||
onLoadedData={() => { this.setVideoHasLoaded(); }}
|
||||
style={{
|
||||
display: this.state.dataUri ? 'none' : 'block',
|
||||
WebkitTransform: 'scaleX(-1)',
|
||||
transform: 'scaleX(-1)',
|
||||
}}
|
||||
playsInline
|
||||
/>
|
||||
<canvas
|
||||
ref={this.canvasRef}
|
||||
data-testid="detection-canvas"
|
||||
className="canvas-video"
|
||||
style={{
|
||||
display: !this.state.shouldDetect || this.state.dataUri ? 'none' : 'block',
|
||||
WebkitTransform: 'scaleX(-1)',
|
||||
transform: 'scaleX(-1)',
|
||||
}}
|
||||
width="640"
|
||||
height="480"
|
||||
/>
|
||||
<img
|
||||
data-hj-suppress
|
||||
alt="imgCamera"
|
||||
src={this.state.dataUri}
|
||||
className="camera-video"
|
||||
style={{ display: this.state.dataUri ? 'block' : 'none' }}
|
||||
/>
|
||||
<div role="status" className="sr-only">{this.state.feedback}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn camera-btn ${
|
||||
this.state.dataUri
|
||||
? 'btn-outline-primary'
|
||||
: 'btn-primary'
|
||||
}`}
|
||||
accessKey="c"
|
||||
onClick={() => {
|
||||
this.takePhoto();
|
||||
}}
|
||||
>
|
||||
{this.state.dataUri
|
||||
? this.props.intl.formatMessage(messages['id.verification.photo.retake'])
|
||||
: this.props.intl.formatMessage(messages['id.verification.photo.take'])}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Camera.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onImageCapture: PropTypes.func.isRequired,
|
||||
isPortrait: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(Camera);
|
||||
@@ -1,66 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Collapsible } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import messages from './IdVerification.messages';
|
||||
import IdVerificationContext from './IdVerificationContext';
|
||||
|
||||
function CameraHelp(props) {
|
||||
const { optimizelyExperimentName } = useContext(IdVerificationContext);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ optimizelyExperimentName
|
||||
&& (
|
||||
<Collapsible
|
||||
styling="card"
|
||||
title={props.intl.formatMessage(messages['id.verification.camera.help.upload.question'])}
|
||||
className="mb-4 shadow"
|
||||
defaultOpen={props.isOpen}
|
||||
>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.camera.help.upload.answer'])}
|
||||
</p>
|
||||
</Collapsible>
|
||||
)}
|
||||
<Collapsible
|
||||
styling="card"
|
||||
title={props.intl.formatMessage(messages['id.verification.camera.help.sight.question'])}
|
||||
className="mb-4 shadow"
|
||||
defaultOpen={props.isOpen}
|
||||
>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages[`id.verification.camera.help.sight.answer.${props.isPortrait ? 'portrait' : 'id'}`])}
|
||||
</p>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
styling="card"
|
||||
title={props.intl.formatMessage(messages[`id.verification.camera.help.difficulty.question.${props.isPortrait ? 'portrait' : 'id'}`])}
|
||||
className="mb-4 shadow"
|
||||
defaultOpen={props.isOpen}
|
||||
>
|
||||
<p>
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.camera.help.difficulty.answer'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CameraHelp.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
isOpen: PropTypes.bool,
|
||||
isPortrait: PropTypes.bool,
|
||||
};
|
||||
|
||||
CameraHelp.defaultProps = {
|
||||
isOpen: false,
|
||||
isPortrait: false,
|
||||
};
|
||||
|
||||
export default injectIntl(CameraHelp);
|
||||
@@ -1,55 +0,0 @@
|
||||
import React, { useState, useContext } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Collapsible } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
|
||||
import messages from './IdVerification.messages';
|
||||
import ImageFileUpload from './ImageFileUpload';
|
||||
import IdVerificationContext from './IdVerificationContext';
|
||||
import ImagePreview from './ImagePreview';
|
||||
import SupportedMediaTypes from './SupportedMediaTypes';
|
||||
|
||||
function CameraHelpWithUpload(props) {
|
||||
const { setIdPhotoFile, idPhotoFile, userId } = useContext(IdVerificationContext);
|
||||
const [hasUploadedImage, setHasUploadedImage] = useState(false);
|
||||
|
||||
function setAndTrackIdPhotoFile(image) {
|
||||
sendTrackEvent('edx.id_verification.upload_id', {
|
||||
category: 'id_verification',
|
||||
user_id: userId,
|
||||
});
|
||||
setHasUploadedImage(true);
|
||||
setIdPhotoFile(image);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Collapsible
|
||||
styling="card"
|
||||
title={props.intl.formatMessage(messages['id.verification.id.photo.unclear.question'])}
|
||||
data-testid="collapsible"
|
||||
className="mb-4 shadow"
|
||||
defaultOpen={props.isOpen}
|
||||
>
|
||||
{idPhotoFile && hasUploadedImage && <ImagePreview src={idPhotoFile} alt={props.intl.formatMessage(messages['id.verification.id.photo.preview.alt'])} />}
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
|
||||
<SupportedMediaTypes />
|
||||
</p>
|
||||
<ImageFileUpload onFileChange={setAndTrackIdPhotoFile} intl={props.intl} />
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CameraHelpWithUpload.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
isOpen: PropTypes.bool,
|
||||
};
|
||||
|
||||
CameraHelpWithUpload.defaultProps = {
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
export default injectIntl(CameraHelpWithUpload);
|
||||
@@ -1,66 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Button, Collapsible } from '@edx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import IdVerificationContext, { MEDIA_ACCESS } from './IdVerificationContext';
|
||||
import messages from './IdVerification.messages';
|
||||
|
||||
function CollapsibleImageHelp(props) {
|
||||
const {
|
||||
shouldUseCamera, setShouldUseCamera, optimizelyExperimentName, mediaAccess,
|
||||
} = useContext(IdVerificationContext);
|
||||
|
||||
function handleClick() {
|
||||
setShouldUseCamera(!shouldUseCamera);
|
||||
}
|
||||
|
||||
if (optimizelyExperimentName && mediaAccess !== MEDIA_ACCESS.DENIED && mediaAccess !== MEDIA_ACCESS.UNSUPPORTED) {
|
||||
return (
|
||||
<Collapsible
|
||||
styling="card"
|
||||
title={shouldUseCamera ? props.intl.formatMessage(messages['id.verification.photo.upload.help.title']) : props.intl.formatMessage(messages['id.verification.photo.camera.help.title'])}
|
||||
className="mb-4 shadow"
|
||||
defaultOpen
|
||||
>
|
||||
<p data-testid="help-text">
|
||||
{shouldUseCamera
|
||||
? props.intl.formatMessage(messages['id.verification.photo.upload.help.text'])
|
||||
: props.intl.formatMessage(messages['id.verification.photo.camera.help.text'])}
|
||||
</p>
|
||||
{ (mediaAccess === MEDIA_ACCESS.PENDING && !shouldUseCamera)
|
||||
? (
|
||||
// if a user has not enabled camera access yet, and they are trying to switch
|
||||
// to camera mode, direct them to panel that requests camera access
|
||||
<Link
|
||||
to={{ pathname: 'request-camera-access', state: { fromPortraitCapture: props.isPortrait, fromIdCapture: !props.isPortrait } }}
|
||||
className="btn btn-primary"
|
||||
data-testid="access-link"
|
||||
>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.camera.help.button'])}
|
||||
</Link>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
title={shouldUseCamera ? 'Upload Portrait Photo' : 'Take Portrait Photo'}
|
||||
data-testid="toggle-button"
|
||||
onClick={handleClick}
|
||||
style={{ marginTop: '0.5rem' }}
|
||||
>
|
||||
{shouldUseCamera ? props.intl.formatMessage(messages['id.verification.photo.upload.help.button']) : props.intl.formatMessage(messages['id.verification.photo.camera.help.button'])}
|
||||
</Button>
|
||||
)}
|
||||
</Collapsible>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
CollapsibleImageHelp.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
isPortrait: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(CollapsibleImageHelp);
|
||||
@@ -1,721 +0,0 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
'id.verification.next': {
|
||||
id: 'id.verification.next',
|
||||
defaultMessage: 'Next',
|
||||
description: 'Next button.',
|
||||
},
|
||||
'id.verification.support': {
|
||||
id: 'id.verification.support',
|
||||
defaultMessage: 'support',
|
||||
description: 'Website support.',
|
||||
},
|
||||
'id.verification.continue.upload': {
|
||||
id: 'id.verification.continue.upload',
|
||||
defaultMessage: 'Continue with Upload',
|
||||
description: 'Button to continue with upload.',
|
||||
},
|
||||
'id.verification.example.card.alt': {
|
||||
id: 'id.verification.example.card.alt',
|
||||
defaultMessage: 'Example of a valid identification card with a full name and photo.',
|
||||
description: 'Alt text for an example identification card.',
|
||||
},
|
||||
'id.verification.requirements.title': {
|
||||
id: 'id.verification.requirements.title',
|
||||
defaultMessage: 'Photo Verification Requirements',
|
||||
description: 'Title for the Photo Verification Requirements page.',
|
||||
},
|
||||
'id.verification.requirements.description': {
|
||||
id: 'id.verification.requirements.description',
|
||||
defaultMessage: 'In order to complete Photo Verification online, you will need the following:',
|
||||
description: 'Description for the Photo Verification Requirements page.',
|
||||
},
|
||||
'id.verification.requirements.card.device.title': {
|
||||
id: 'id.verification.requirements.card.device.title',
|
||||
defaultMessage: 'Device with Camera',
|
||||
description: 'Title for the Device with Camera card.',
|
||||
},
|
||||
'id.verification.requirements.card.device.allow': {
|
||||
id: 'id.verification.requirements.card.device.allow',
|
||||
defaultMessage: 'Allow',
|
||||
description: 'Bold text emphasizing that the user needs to click "allow" in order to enable the camera.',
|
||||
},
|
||||
'id.verification.requirements.card.id.title': {
|
||||
id: 'id.verification.requirements.card.id.title',
|
||||
defaultMessage: 'Photo Identification',
|
||||
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.',
|
||||
description: 'Text that explains that the user needs a photo ID.',
|
||||
},
|
||||
'id.verification.privacy.title': {
|
||||
id: 'id.verification.privacy.title',
|
||||
defaultMessage: 'Privacy Information',
|
||||
description: 'Title for Privacy Information.',
|
||||
},
|
||||
'id.verification.privacy.need.photo.question': {
|
||||
id: 'id.verification.privacy.need.photo.question',
|
||||
defaultMessage: 'Why does {siteName} need my photo?',
|
||||
description: 'Question about why the platform needs a verification photo.',
|
||||
},
|
||||
'id.verification.privacy.need.photo.answer': {
|
||||
id: 'id.verification.privacy.need.photo.answer',
|
||||
defaultMessage: 'We use your verification photos to confirm your identity and ensure the validity of your certificate.',
|
||||
description: 'Answering why the platform needs a verification photo.',
|
||||
},
|
||||
'id.verification.privacy.do.with.photo.question': {
|
||||
id: 'id.verification.privacy.do.with.photo.question',
|
||||
defaultMessage: 'What does {siteName} do with this photo?',
|
||||
description: 'Question about what the platform does with the verification photo.',
|
||||
},
|
||||
'id.verification.privacy.do.with.photo.answer': {
|
||||
id: 'id.verification.privacy.do.with.photo.answer',
|
||||
defaultMessage: 'We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on {siteName} after the verification process is complete.',
|
||||
description: 'Answering what the platform does with the verification photo.',
|
||||
},
|
||||
'id.verification.access.blocked.title': {
|
||||
id: 'id.verification.access.blocked.title',
|
||||
defaultMessage: 'Identity Verification',
|
||||
description: 'Title for text that displays when a user is blocked from ID verification.',
|
||||
},
|
||||
'id.verification.access.blocked.enrollment': {
|
||||
id: 'id.verification.access.blocked.enrollment',
|
||||
defaultMessage: 'You are not currently enrolled in a course that requires identity verification.',
|
||||
description: 'Text that displays when user is trying to verify while not enrolled in a course that requires ID verification.',
|
||||
},
|
||||
'id.verification.access.blocked.pending': {
|
||||
id: 'id.verification.access.blocked.pending',
|
||||
defaultMessage: 'You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 5 days).',
|
||||
description: 'Text that displays when user has a pending or approved request.',
|
||||
},
|
||||
'id.verification.photo.take': {
|
||||
id: 'id.verification.photo.take',
|
||||
defaultMessage: 'Take Photo',
|
||||
description: 'Button to take photo.',
|
||||
},
|
||||
'id.verification.photo.retake': {
|
||||
id: 'id.verification.photo.retake',
|
||||
defaultMessage: 'Retake Photo?',
|
||||
description: 'Button to retake photo.',
|
||||
},
|
||||
'id.verification.photo.enable.detection': {
|
||||
id: 'id.verification.photo.enable.detection',
|
||||
defaultMessage: 'Enable Face Detection',
|
||||
description: 'Text label for the checkbox to enable face detection.',
|
||||
},
|
||||
'id.verification.photo.enable.detection.portrait.help.text': {
|
||||
id: 'id.verification.photo.enable.detection.portrait.help.text',
|
||||
defaultMessage: '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.',
|
||||
description: 'Help text that appears for enabling face detection on the portrait photo panel.',
|
||||
},
|
||||
'id.verification.photo.enable.detection.id.help.text': {
|
||||
id: 'id.verification.photo.enable.detection.id.help.text',
|
||||
defaultMessage: '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.',
|
||||
description: 'Help text that appears for enabling face detection on the portrait photo panel.',
|
||||
},
|
||||
'id.verification.photo.feedback.correct': {
|
||||
id: 'id.verification.photo.feedback.correct',
|
||||
defaultMessage: 'Face is in a good position.',
|
||||
description: 'Text for screen reader when user\'s face is in a good position.',
|
||||
},
|
||||
'id.verification.photo.feedback.two.faces': {
|
||||
id: 'id.verification.photo.feedback.two.faces',
|
||||
defaultMessage: 'More than one face detected.',
|
||||
description: 'Text for screen reader when more than one face detected.',
|
||||
},
|
||||
'id.verification.photo.feedback.no.faces': {
|
||||
id: 'id.verification.photo.feedback.no.faces',
|
||||
defaultMessage: 'No face detected.',
|
||||
description: 'Text for screen reader when no face detected.',
|
||||
},
|
||||
'id.verification.photo.feedback.top.left': {
|
||||
id: 'id.verification.photo.feedback.top.left',
|
||||
defaultMessage: 'Incorrect position. Top left.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.top.center': {
|
||||
id: 'id.verification.photo.feedback.top.center',
|
||||
defaultMessage: 'Incorrect position. Top center.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.top.right': {
|
||||
id: 'id.verification.photo.feedback.top.right',
|
||||
defaultMessage: 'Incorrect position. Top right.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.center.left': {
|
||||
id: 'id.verification.photo.feedback.center.left',
|
||||
defaultMessage: 'Incorrect position. Center left.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.center.center': {
|
||||
id: 'id.verification.photo.feedback.center.center',
|
||||
defaultMessage: 'Incorrect position. Too close to camera.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.center.right': {
|
||||
id: 'id.verification.photo.feedback.center.right',
|
||||
defaultMessage: 'Incorrect position. Center right.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.bottom.left': {
|
||||
id: 'id.verification.photo.feedback.bottom.left',
|
||||
defaultMessage: 'Incorrect position. Bottom left.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.bottom.center': {
|
||||
id: 'id.verification.photo.feedback.bottom.center',
|
||||
defaultMessage: 'Incorrect position. Bottom center.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.photo.feedback.bottom.right': {
|
||||
id: 'id.verification.photo.feedback.bottom.right',
|
||||
defaultMessage: 'Incorrect position. Bottom right.',
|
||||
description: 'Text for screen reader when face is in a bad position.',
|
||||
},
|
||||
'id.verification.camera.access.title': {
|
||||
id: 'id.verification.camera.access.title',
|
||||
defaultMessage: 'Camera Permissions',
|
||||
description: 'Title for the Camera Access page.',
|
||||
},
|
||||
'id.verification.camera.access.title.success': {
|
||||
id: 'id.verification.camera.access.title.success',
|
||||
defaultMessage: 'Camera Access Enabled',
|
||||
description: 'Title for the Camera Access page when camera is enabled.',
|
||||
},
|
||||
'id.verification.camera.access.title.failed': {
|
||||
id: 'id.verification.camera.access.title.failed',
|
||||
defaultMessage: 'Camera Access Failed',
|
||||
description: 'Title for the Camera Access page when camera access is denied or unavailable.',
|
||||
},
|
||||
'id.verification.camera.access.click.allow': {
|
||||
id: 'id.verification.camera.access.click.allow',
|
||||
defaultMessage: 'Please make sure to click "Allow"',
|
||||
description: 'Instruction to allow camera access.',
|
||||
},
|
||||
'id.verification.camera.access.enable': {
|
||||
id: 'id.verification.camera.access.enable',
|
||||
defaultMessage: 'Enable Camera',
|
||||
description: 'Text to enable camera.',
|
||||
},
|
||||
'id.verification.camera.access.problems': {
|
||||
id: 'id.verification.camera.access.problems',
|
||||
defaultMessage: 'Having problems?',
|
||||
description: 'Text for when the user is having problems enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.skip': {
|
||||
id: 'id.verification.camera.access.skip',
|
||||
defaultMessage: 'Skip and upload image files instead',
|
||||
description: 'Text to skip camera access and enable image uploading.',
|
||||
},
|
||||
'id.verification.camera.access.success': {
|
||||
id: 'id.verification.camera.access.success',
|
||||
defaultMessage: 'Looks like your camera is working and ready.',
|
||||
description: 'Text to confirm that camera is working.',
|
||||
},
|
||||
'id.verification.camera.access.failure': {
|
||||
id: 'id.verification.camera.access.failure',
|
||||
defaultMessage: 'It looks like we\'re unable to access your camera. You will need to upload image files of you and your photo id.',
|
||||
description: 'Text indicating that the camera could not be accessed and image upload will be enabled.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary': {
|
||||
id: 'id.verification.camera.access.failure.temporary',
|
||||
defaultMessage: '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.',
|
||||
description: 'Text indicating that the camera could not be accessed.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.chrome': {
|
||||
id: 'id.verification.camera.access.failure.temporary.chrome',
|
||||
defaultMessage: 'To enable camera access in Chrome:',
|
||||
description: 'Description for the directions on enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.chrome.step1': {
|
||||
id: 'id.verification.camera.access.failure.temporary.chrome.step1',
|
||||
defaultMessage: 'Open Chrome.',
|
||||
description: 'Text for step one of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.chrome.step2': {
|
||||
id: 'id.verification.camera.access.failure.temporary.chrome.step2',
|
||||
defaultMessage: 'Navigate to More > Settings.',
|
||||
description: 'Text for step two of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.chrome.step2.windows': {
|
||||
id: 'id.verification.camera.access.failure.temporary.chrome.step2.windows',
|
||||
defaultMessage: 'For Windows: Alt+F, Alt+E, or F10 followed by the spacebar',
|
||||
description: 'Text for Windows keyboard shortcut in chrome.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.chrome.step2.mac': {
|
||||
id: 'id.verification.camera.access.failure.temporary.chrome.step2.mac',
|
||||
defaultMessage: 'For Mac: Command+,',
|
||||
description: 'Text for Mac keyboard shortcut in chrome.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.chrome.step3': {
|
||||
id: 'id.verification.camera.access.failure.temporary.chrome.step3',
|
||||
defaultMessage: 'Under the "Privacy and security" tab, select "Site Settings" and then "Camera."',
|
||||
description: 'Text for step three of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.chrome.step4': {
|
||||
id: 'id.verification.camera.access.failure.temporary.chrome.step4',
|
||||
defaultMessage: 'Under "Blocked," find "edx.org" and select it.',
|
||||
description: 'Text for step four of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.chrome.step5': {
|
||||
id: 'id.verification.camera.access.failure.temporary.chrome.step5',
|
||||
defaultMessage: 'In the "Permissions" section, update the camera permissions to "Allow."',
|
||||
description: 'Text for step five of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.ie11': {
|
||||
id: 'id.verification.camera.access.failure.temporary.ie11',
|
||||
defaultMessage: 'To enable camera access in Internet Explorer:',
|
||||
description: 'Description for the directions on enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.ie11.step1': {
|
||||
id: 'id.verification.camera.access.failure.temporary.ie11.step1',
|
||||
defaultMessage: 'Open the Flash Player Settings Manager by navigating to Windows Settings > Control Panel > Flash Player.',
|
||||
description: 'Text for step one of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.ie11.step2': {
|
||||
id: 'id.verification.camera.access.failure.temporary.ie11.step2',
|
||||
defaultMessage: 'Select the "Camera and Mic" tab, and then select the "Camera and Microphone Settings by Site" button.',
|
||||
description: 'Text for step two of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.ie11.step3': {
|
||||
id: 'id.verification.camera.access.failure.temporary.ie11.step3',
|
||||
defaultMessage: 'Choose "edx.org" from the list of websites and change the permissions by selecting "Allow" in the dropdown menu.',
|
||||
description: 'Text for step three of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.firefox': {
|
||||
id: 'id.verification.camera.access.failure.temporary.firefox',
|
||||
defaultMessage: 'To enable camera access in Firefox:',
|
||||
description: 'Description for the directions on enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.firefox.step1': {
|
||||
id: 'id.verification.camera.access.failure.temporary.firefox.step1',
|
||||
defaultMessage: 'Open Firefox.',
|
||||
description: 'Text for step one of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.firefox.step2': {
|
||||
id: 'id.verification.camera.access.failure.temporary.firefox.step2',
|
||||
defaultMessage: 'Enter "about:preferences" in the URL bar.',
|
||||
description: 'Text for step two of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.firefox.step3': {
|
||||
id: 'id.verification.camera.access.failure.temporary.firefox.step3',
|
||||
defaultMessage: 'Select the "Privacy & Security" tab, and navigate to the "Permissions" section.',
|
||||
description: 'Text for step three of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.firefox.step4': {
|
||||
id: 'id.verification.camera.access.failure.temporary.firefox.step4',
|
||||
defaultMessage: 'Next to "Camera," select the "Settings…" button.',
|
||||
description: 'Text for step four of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.firefox.step5': {
|
||||
id: 'id.verification.camera.access.failure.temporary.firefox.step5',
|
||||
defaultMessage: 'In the search bar, enter "edx.org."',
|
||||
description: 'Text for step five of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.firefox.step6': {
|
||||
id: 'id.verification.camera.access.failure.temporary.firefox.step6',
|
||||
defaultMessage: 'In the status column for "edx.org," select "Allow" from the drop down.',
|
||||
description: 'Text for step six of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.firefox.step7': {
|
||||
id: 'id.verification.camera.access.failure.temporary.firefox.step7',
|
||||
defaultMessage: 'Select "Save Changes."',
|
||||
description: 'Text for step seven of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.safari': {
|
||||
id: 'id.verification.camera.access.failure.temporary.safari',
|
||||
defaultMessage: 'To enable camera access in Safari:',
|
||||
description: 'Description for the directions on enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.safari.step1': {
|
||||
id: 'id.verification.camera.access.failure.temporary.safari.step1',
|
||||
defaultMessage: 'Open Safari.',
|
||||
description: 'Text for step one of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.safari.step2': {
|
||||
id: 'id.verification.camera.access.failure.temporary.safari.step2',
|
||||
defaultMessage: 'Click on the Safari app menu, then select "Preferences." You can also use Command+, as a keyboard shortcut.',
|
||||
description: 'Text for step two of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.safari.step3': {
|
||||
id: 'id.verification.camera.access.failure.temporary.safari.step3',
|
||||
defaultMessage: 'Select the "Websites" tab and then select "Camera."',
|
||||
description: 'Text for step three of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.temporary.safari.step4': {
|
||||
id: 'id.verification.camera.access.failure.temporary.safari.step4',
|
||||
defaultMessage: 'Select "edx.org" and change the camera permissions to "Allow."',
|
||||
description: 'Text for step four of enabling camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.unsupported': {
|
||||
id: 'id.verification.camera.access.failure.unsupported',
|
||||
defaultMessage: 'It looks like your browser does not support camera access.',
|
||||
description: 'Text indicating that the user\'s browser does not support camera access.',
|
||||
},
|
||||
'id.verification.camera.access.failure.unsupported.chrome.explanation': {
|
||||
id: 'id.verification.camera.access.failure.unsupported.chrome.explanation',
|
||||
defaultMessage: 'The Chrome browser currently does not support camera access on iOS devices, such as iPhones and iPads.',
|
||||
description: 'Explanation for why certain web browsers, like Chrome, do not support accessing the user\'s camera.',
|
||||
},
|
||||
'id.verification.camera.access.failure.unsupported.instructions': {
|
||||
id: 'id.verification.camera.access.failure.unsupported.instructions',
|
||||
defaultMessage: 'Please use another browser to complete Identity Verification.',
|
||||
description: 'Instructions for the user to user another web browser to complete the process.',
|
||||
},
|
||||
'id.verification.photo.tips.title': {
|
||||
id: 'id.verification.photo.tips.title',
|
||||
defaultMessage: 'Helpful Photo Tips',
|
||||
description: 'Title for the Photo Tips page.',
|
||||
},
|
||||
'id.verification.photo.tips.description': {
|
||||
id: 'id.verification.photo.tips.description',
|
||||
defaultMessage: 'Next, we\'ll need you to take a photo of your face. Please review the helpful tips below.',
|
||||
description: 'Description for the photo tips page.',
|
||||
},
|
||||
'id.verification.photo.tips.list.title': {
|
||||
id: 'id.verification.photo.tips.list.title',
|
||||
defaultMessage: 'Photo Tips',
|
||||
description: 'Title for the list of photo tips.',
|
||||
},
|
||||
'id.verification.photo.tips.list.description': {
|
||||
id: 'id.verification.photo.tips.list.description',
|
||||
defaultMessage: 'To take a successful photo, make sure that:',
|
||||
description: 'Description for the list of photo tips.',
|
||||
},
|
||||
'id.verification.photo.tips.list.well.lit': {
|
||||
id: 'id.verification.photo.tips.list.well.lit',
|
||||
defaultMessage: 'Your face is well-lit.',
|
||||
description: 'Tip to make sure the user\'s face is well lit.',
|
||||
},
|
||||
'id.verification.photo.tips.list.inside.frame': {
|
||||
id: 'id.verification.photo.tips.list.inside.frame',
|
||||
defaultMessage: 'Your entire face fits inside the frame.',
|
||||
description: 'Tip to make sure the user\'s face fits inside the frame.',
|
||||
},
|
||||
'id.verification.portrait.photo.title.camera': {
|
||||
id: 'id.verification.portrait.photo.title.camera',
|
||||
defaultMessage: 'Take a Photo of Yourself',
|
||||
description: 'Title for the Portrait Photo page if camera access is enabled.',
|
||||
},
|
||||
'id.verification.portrait.photo.title.upload': {
|
||||
id: 'id.verification.portrait.photo.title.upload',
|
||||
defaultMessage: 'Upload a Photo of Yourself',
|
||||
description: 'Title for the Portrait Photo page if camera access is disabled.',
|
||||
},
|
||||
'id.verification.portrait.photo.preview.alt': {
|
||||
id: 'id.verification.portrait.photo.preview.alt',
|
||||
defaultMessage: 'Preview of photo of user\'s face.',
|
||||
description: 'Alt text for the portrait photo preview.',
|
||||
},
|
||||
'id.verification.portrait.photo.instructions.camera': {
|
||||
id: 'id.verification.portrait.photo.instructions.camera',
|
||||
defaultMessage: 'When your face is in position, use the Take Photo button below to take your photo.',
|
||||
description: 'Instructions to use the camera to take a portrait photo..',
|
||||
},
|
||||
'id.verification.portrait.photo.instructions.upload': {
|
||||
id: 'id.verification.portrait.photo.instructions.upload',
|
||||
defaultMessage: 'Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. Supported formats: ',
|
||||
description: 'Instructions for portrait photo upload.',
|
||||
},
|
||||
'id.verification.camera.help.sight.question': {
|
||||
id: 'id.verification.camera.help.sight.question',
|
||||
defaultMessage: 'What if I can\'t see the camera image or if I can\'t see my photo to determine which side is visible?',
|
||||
description: 'Question on what to do if the user cannot see the camera image or photo during verification.',
|
||||
},
|
||||
'id.verification.camera.help.sight.answer.portrait': {
|
||||
id: 'id.verification.camera.help.sight.answer.portrait',
|
||||
defaultMessage: '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.',
|
||||
description: 'Confirming what to do if the camera image of the portrait cannot be seen during verification.',
|
||||
},
|
||||
'id.verification.camera.help.sight.answer.id': {
|
||||
id: 'id.verification.camera.help.sight.answer.id',
|
||||
defaultMessage: '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.',
|
||||
description: 'Confirming what to do if the camera image of the ID cannot be seen during verification.',
|
||||
},
|
||||
'id.verification.camera.help.difficulty.question.portrait': {
|
||||
id: 'id.verification.camera.help.difficulty.question.portrait',
|
||||
defaultMessage: 'What if I have difficulty holding my head in position relative to the camera?',
|
||||
description: 'Question on what to do if the user has difficulty holding their head relative to the camera.',
|
||||
},
|
||||
'id.verification.camera.help.difficulty.question.id': {
|
||||
id: 'id.verification.camera.help.difficulty.question.id',
|
||||
defaultMessage: 'What if I have difficulty holding my ID in position relative to the camera?',
|
||||
description: 'Question on what to do if the user has difficulty holding their ID relative to the camera.',
|
||||
},
|
||||
'id.verification.camera.help.difficulty.answer': {
|
||||
id: 'id.verification.camera.help.difficulty.answer',
|
||||
defaultMessage: 'If you require assistance with taking a photo for submission, contact {siteName} support for additional suggestions.',
|
||||
description: 'Confirming what to do if the user has difficult holding their head relative to the camera.',
|
||||
},
|
||||
'id.verification.camera.help.upload.question': {
|
||||
id: 'id.verification.camera.help.upload.question',
|
||||
defaultMessage: 'What if I want to upload a photo instead?',
|
||||
description: 'Question on what to do if the user would like to upload a photo instead.',
|
||||
},
|
||||
'id.verification.camera.help.upload.answer': {
|
||||
id: 'id.verification.camera.help.upload.answer',
|
||||
defaultMessage: 'On the next page you will have the option to switch to upload mode. By selecting that option, you will be able to upload a photo instead.',
|
||||
description: 'Confirming what to do if the user would like to upload a photo.',
|
||||
},
|
||||
'id.verification.id.photo.unclear.question': {
|
||||
id: 'id.verification.id.photo.unclear.question',
|
||||
defaultMessage: 'Is your ID image not clear or too blurry?',
|
||||
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',
|
||||
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.',
|
||||
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.',
|
||||
description: 'Tip to ensure ID is well lit.',
|
||||
},
|
||||
'id.verification.id.tips.list.clear': {
|
||||
id: 'id.verification.id.tips.list.clear',
|
||||
defaultMessage: 'Ensure that you can see your photo and clearly read your name.',
|
||||
description: 'Tip to ensure ID and name can be seen clearly.',
|
||||
},
|
||||
'id.verification.id.photo.title.camera': {
|
||||
id: 'id.verification.id.photo.title.camera',
|
||||
defaultMessage: 'Take a Photo of Your ID',
|
||||
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',
|
||||
description: 'Title for the ID Photo page if camera access is disabled.',
|
||||
},
|
||||
'id.verification.id.photo.preview.alt': {
|
||||
id: 'id.verification.id.photo.preview.alt',
|
||||
defaultMessage: 'Preview of photo ID.',
|
||||
description: 'Alt text for the ID photo preview.',
|
||||
},
|
||||
'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.',
|
||||
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: ',
|
||||
description: 'Instructions for ID photo upload.',
|
||||
},
|
||||
'id.verification.id.photo.instructions.upload.error.invalidFileType': {
|
||||
id: 'id.verification.id.photo.instructions.upload.error.invalidFileType',
|
||||
defaultMessage: 'The file you have selected is not a supported image type. Please choose from the following formats: ',
|
||||
description: 'Error message for file upload that is not a supported image type.',
|
||||
},
|
||||
'id.verification.id.photo.instructions.upload.error.fileTooLarge': {
|
||||
id: 'id.verification.id.photo.instructions.upload.error.fileTooLarge',
|
||||
defaultMessage: 'The file you have selected is too large. Please try again with a file less than 10MB.',
|
||||
description: 'Error message for file upload that is larger than 10MB.',
|
||||
},
|
||||
'id.verification.account.name.title': {
|
||||
id: 'id.verification.account.name.title',
|
||||
defaultMessage: 'Account Name Check',
|
||||
description: 'Title for the Account Name Check page.',
|
||||
},
|
||||
'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.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.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.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.account.name.error': {
|
||||
id: 'id.verification.account.name.error',
|
||||
defaultMessage: 'Please update account name to match the name on your ID.',
|
||||
description: 'Error that shows when the user needs to update their account name to match the name on their ID.',
|
||||
},
|
||||
'id.verification.account.name.warning.prefix': {
|
||||
id: 'id.verification.account.name.warning.prefix',
|
||||
defaultMessage: 'Please Note:',
|
||||
description: 'Prefix to the warning that any change to the account name will be saved to the account.',
|
||||
},
|
||||
'id.verification.account.name.settings': {
|
||||
id: 'id.verification.account.name.settings',
|
||||
defaultMessage: 'Account Settings',
|
||||
description: 'Link to Account Settings.',
|
||||
},
|
||||
'id.verification.account.name.label': {
|
||||
id: 'id.verification.account.name.label',
|
||||
defaultMessage: 'Account Name',
|
||||
description: 'Label for account name input.',
|
||||
},
|
||||
'id.verification.account.name.photo.alt': {
|
||||
id: 'id.verification.account.name.photo.alt',
|
||||
defaultMessage: 'Photo of your ID to be submitted.',
|
||||
description: 'Alt text for the photo of the user\'s ID.',
|
||||
},
|
||||
'id.verification.account.name.save': {
|
||||
id: 'id.verification.account.name.save',
|
||||
defaultMessage: 'Save and Next',
|
||||
description: 'Button to save the account name.',
|
||||
},
|
||||
'id.verification.review.title': {
|
||||
id: 'id.verification.review.title',
|
||||
defaultMessage: 'Review Your Photos',
|
||||
description: 'Title for the review your photos page.',
|
||||
},
|
||||
'id.verification.review.description': {
|
||||
id: 'id.verification.review.description',
|
||||
defaultMessage: 'Make sure we can verify your identity with the photos and information you have provided.',
|
||||
description: 'Description for the review your photos page.',
|
||||
},
|
||||
'id.verification.review.portrait.label': {
|
||||
id: 'id.verification.review.portrait.label',
|
||||
defaultMessage: 'Your Portrait',
|
||||
description: 'Label for the portrait card.',
|
||||
},
|
||||
'id.verification.review.portrait.alt': {
|
||||
id: 'id.verification.review.portrait.alt',
|
||||
defaultMessage: 'Photo of your face to be submitted.',
|
||||
description: 'Alt text for the portrait photo.',
|
||||
},
|
||||
'id.verification.review.portrait.retake': {
|
||||
id: 'id.verification.review.portrait.retake',
|
||||
defaultMessage: 'Retake Portrait Photo',
|
||||
description: 'Button to retake the portrait photo.',
|
||||
},
|
||||
'id.verification.review.id.label': {
|
||||
id: 'id.verification.review.id.label',
|
||||
defaultMessage: 'Your Photo ID',
|
||||
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.',
|
||||
description: 'Alt text for the ID photo.',
|
||||
},
|
||||
'id.verification.review.id.retake': {
|
||||
id: 'id.verification.review.id.retake',
|
||||
defaultMessage: 'Retake ID Photo',
|
||||
description: 'Button to retake the ID photo.',
|
||||
},
|
||||
'id.verification.review.confirm': {
|
||||
id: 'id.verification.review.confirm',
|
||||
defaultMessage: 'Submit',
|
||||
description: 'Button to confirm all information is correct and submit.',
|
||||
},
|
||||
'id.verification.submission.alert.error.face': {
|
||||
id: 'id.verification.submission.alert.error.face',
|
||||
defaultMessage: 'A photo of your face is required. Please retake your portrait photo.',
|
||||
description: 'Error message displayed when the user\'s portrait photo is missing.',
|
||||
},
|
||||
'id.verification.submission.alert.error.id': {
|
||||
id: 'id.verification.submission.alert.error.id',
|
||||
defaultMessage: 'A photo of your ID card is required. Please retake your ID photo.',
|
||||
description: 'Error message displayed when the user\'s ID photo is missing.',
|
||||
},
|
||||
'id.verification.submission.alert.error.name': {
|
||||
id: 'id.verification.submission.alert.error.name',
|
||||
defaultMessage: 'A valid account name is required. Please update your account name to match the name on your ID.',
|
||||
description: 'Error message displayed when the user\'s account name is missing.',
|
||||
},
|
||||
'id.verification.submission.alert.error.unsupported': {
|
||||
id: 'id.verification.submission.alert.error.unsupported',
|
||||
defaultMessage: 'One or more of the files you have uploaded is in an unsupported format. Please choose from the following: ',
|
||||
description: 'Error message displayed when the user uploads an unsupported file type.',
|
||||
},
|
||||
'id.verification.review.error': {
|
||||
id: 'id.verification.review.error',
|
||||
defaultMessage: '{siteName} Support Page',
|
||||
description: 'Text linking to the platform support page.',
|
||||
},
|
||||
'id.verification.submitted.title': {
|
||||
id: 'id.verification.submitted.title',
|
||||
defaultMessage: 'Identity Verification in Progress',
|
||||
description: 'Title for the submitted page.',
|
||||
},
|
||||
'id.verification.submitted.text': {
|
||||
id: 'id.verification.submitted.text',
|
||||
defaultMessage: '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.',
|
||||
description: 'Text confirming that ID verification request has been received.',
|
||||
},
|
||||
'id.verification.return.dashboard': {
|
||||
id: 'id.verification.return.dashboard',
|
||||
defaultMessage: 'Return to Your Dashboard',
|
||||
description: 'Button to return to the dashboard.',
|
||||
},
|
||||
'id.verification.return.course': {
|
||||
id: 'id.verification.return.course',
|
||||
defaultMessage: 'Return to Course',
|
||||
description: 'Return to the course which ID verification was accessed from.',
|
||||
},
|
||||
'id.verification.photo.upload.help.title': {
|
||||
id: 'id.verification.photo.upload.help.title',
|
||||
defaultMessage: 'Upload a Photo Instead',
|
||||
description: 'Title for section that allows switching to photo upload mode.',
|
||||
},
|
||||
'id.verification.photo.camera.help.title': {
|
||||
id: 'id.verification.photo.camera.help.title',
|
||||
defaultMessage: 'Use Your Camera Instead',
|
||||
description: 'Title for section that allows switching to camera mode.',
|
||||
},
|
||||
'id.verification.photo.upload.help.text': {
|
||||
id: 'id.verification.photo.upload.help.text',
|
||||
defaultMessage: 'If you are having trouble using the photo capture above, you may want to upload a photo instead. To upload a photo, click the button below.',
|
||||
description: 'Help text for switching to upload mode.',
|
||||
},
|
||||
'id.verification.photo.camera.help.text': {
|
||||
id: 'id.verification.photo.camera.help.text',
|
||||
defaultMessage: 'If you are having trouble uploading a photo above, you may want to use your camera instead. To use your camera, click the button below.',
|
||||
description: 'Help text for switching to camera mode.',
|
||||
},
|
||||
'id.verification.photo.upload.help.button': {
|
||||
id: 'id.verification.upload.help.button',
|
||||
defaultMessage: 'Switch to Upload Mode',
|
||||
description: 'Button used to switch to upload mode.',
|
||||
},
|
||||
'id.verification.photo.camera.help.button': {
|
||||
id: 'id.verification.camera.help.button',
|
||||
defaultMessage: 'Switch to Camera Mode',
|
||||
description: 'Button used to switch to camera mode.',
|
||||
},
|
||||
'id.verification.choose.mode.title': {
|
||||
id: 'id.verification.choose.mode.title',
|
||||
defaultMessage: 'Photo Requirements Options',
|
||||
description: 'Title for section that allows user to choose photo mode.',
|
||||
},
|
||||
'id.verification.choose.mode.help.text': {
|
||||
id: 'id.verification.choose.mode.hep.text',
|
||||
defaultMessage: 'To complete verification, please select one of the following options to submit photos. You will be able to switch between these options throughout the process if needed.',
|
||||
description: 'Help text for section that allows user to choose photo mode.',
|
||||
},
|
||||
'id.verification.choose.mode.radio.upload': {
|
||||
id: 'id.verification.choose.mode.radio.upload',
|
||||
defaultMessage: 'Upload photos from my device',
|
||||
description: 'Radio button to choose to upload photos.',
|
||||
},
|
||||
'id.verification.choose.mode.radio.camera': {
|
||||
id: 'id.verification.choose.mode.radio.camera',
|
||||
defaultMessage: 'Take pictures using my camera',
|
||||
description: 'Radio button to choose to use camera for photos.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,25 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
const IdVerificationContext = React.createContext({});
|
||||
|
||||
const MEDIA_ACCESS = {
|
||||
PENDING: 'pending',
|
||||
UNSUPPORTED: 'unsupported',
|
||||
DENIED: 'denied',
|
||||
GRANTED: 'granted',
|
||||
};
|
||||
|
||||
const ERROR_REASONS = {
|
||||
COURSE_ENROLLMENT: 'course_enrollment',
|
||||
EXISTING_REQUEST: 'existing_request',
|
||||
CANNOT_VERIFY: 'cannot_verify',
|
||||
};
|
||||
|
||||
const VERIFIED_MODES = ['verified', 'professional', 'masters', 'executive_education'];
|
||||
|
||||
export default IdVerificationContext;
|
||||
export {
|
||||
MEDIA_ACCESS,
|
||||
ERROR_REASONS,
|
||||
VERIFIED_MODES,
|
||||
};
|
||||
@@ -1,147 +0,0 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { AppContext } from '@edx/frontend-platform/react';
|
||||
|
||||
import { getProfileDataManager } from '../account-settings/data/service';
|
||||
import PageLoading from '../account-settings/PageLoading';
|
||||
|
||||
import { getExistingIdVerification, getEnrollments } from './data/service';
|
||||
import AccessBlocked from './AccessBlocked';
|
||||
import { hasGetUserMediaSupport } from './getUserMediaShim';
|
||||
import IdVerificationContext, { MEDIA_ACCESS, ERROR_REASONS, VERIFIED_MODES } from './IdVerificationContext';
|
||||
|
||||
export default function IdVerificationContextProvider({ children }) {
|
||||
const { authenticatedUser } = useContext(AppContext);
|
||||
|
||||
const [existingIdVerification, setExistingIdVerification] = useState(null);
|
||||
useEffect(() => {
|
||||
// Call verification status endpoint to check whether we can verify.
|
||||
(async () => {
|
||||
const existingIdV = await getExistingIdVerification();
|
||||
setExistingIdVerification(existingIdV);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const [facePhotoFile, setFacePhotoFile] = useState(null);
|
||||
const [idPhotoFile, setIdPhotoFile] = useState(null);
|
||||
const [idPhotoName, setIdPhotoName] = useState(null);
|
||||
const [mediaStream, setMediaStream] = useState(null);
|
||||
const [mediaAccess, setMediaAccess] = useState(
|
||||
hasGetUserMediaSupport ? MEDIA_ACCESS.PENDING : MEDIA_ACCESS.UNSUPPORTED,
|
||||
);
|
||||
|
||||
const [canVerify, setCanVerify] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
useEffect(() => {
|
||||
// Check for an existing verification attempt
|
||||
if (existingIdVerification && !existingIdVerification.canVerify) {
|
||||
const { status } = existingIdVerification;
|
||||
setCanVerify(false);
|
||||
if (status === 'pending' || status === 'approved') {
|
||||
setError(ERROR_REASONS.EXISTING_REQUEST);
|
||||
} else {
|
||||
setError(ERROR_REASONS.CANNOT_VERIFY);
|
||||
}
|
||||
}
|
||||
}, [existingIdVerification]);
|
||||
useEffect(() => {
|
||||
// Check whether the learner is enrolled in a verified course mode.
|
||||
(async () => {
|
||||
/* eslint-disable arrow-body-style */
|
||||
const enrollments = await getEnrollments();
|
||||
const verifiedEnrollments = enrollments.filter((enrollment) => {
|
||||
return VERIFIED_MODES.includes(enrollment.mode);
|
||||
});
|
||||
if (verifiedEnrollments.length === 0) {
|
||||
setCanVerify(false);
|
||||
setError(ERROR_REASONS.COURSE_ENROLLMENT);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const [profileDataManager, setProfileDataManager] = useState(null);
|
||||
useEffect(() => {
|
||||
// Determine if the user's profile data is managed by a third-party identity provider.
|
||||
// If so, they cannot update their account name manually.
|
||||
if (authenticatedUser.roles.length > 0) {
|
||||
(async () => {
|
||||
const thirdPartyManager = await getProfileDataManager(
|
||||
authenticatedUser.username,
|
||||
authenticatedUser.roles,
|
||||
);
|
||||
if (thirdPartyManager) {
|
||||
setProfileDataManager(thirdPartyManager);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [authenticatedUser]);
|
||||
|
||||
const [optimizelyExperimentName, setOptimizelyExperimentName] = useState('');
|
||||
const [shouldUseCamera, setShouldUseCamera] = useState(false);
|
||||
|
||||
// If the user reaches the end of the flow and goes back to retake their photos,
|
||||
// this flag ensures that they are directed straight back to the summary panel
|
||||
const [reachedSummary, setReachedSummary] = useState(false);
|
||||
|
||||
const contextValue = {
|
||||
existingIdVerification,
|
||||
facePhotoFile,
|
||||
idPhotoFile,
|
||||
idPhotoName,
|
||||
mediaStream,
|
||||
mediaAccess,
|
||||
userId: authenticatedUser.userId,
|
||||
nameOnAccount: authenticatedUser.name,
|
||||
profileDataManager,
|
||||
optimizelyExperimentName,
|
||||
shouldUseCamera,
|
||||
reachedSummary,
|
||||
setExistingIdVerification,
|
||||
setFacePhotoFile,
|
||||
setIdPhotoFile,
|
||||
setIdPhotoName,
|
||||
setOptimizelyExperimentName,
|
||||
setShouldUseCamera,
|
||||
setReachedSummary,
|
||||
tryGetUserMedia: async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
|
||||
setMediaAccess(MEDIA_ACCESS.GRANTED);
|
||||
setMediaStream(stream);
|
||||
setShouldUseCamera(true);
|
||||
// stop the stream, as we are not using it yet
|
||||
const tracks = stream.getTracks();
|
||||
tracks.forEach(track => track.stop());
|
||||
} catch (err) {
|
||||
setMediaAccess(MEDIA_ACCESS.DENIED);
|
||||
setShouldUseCamera(false);
|
||||
}
|
||||
},
|
||||
stopUserMedia: () => {
|
||||
if (mediaStream) {
|
||||
const tracks = mediaStream.getTracks();
|
||||
tracks.forEach(track => track.stop());
|
||||
setMediaStream(null);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// If we are waiting for verification status endpoint, show spinner.
|
||||
if (!existingIdVerification) {
|
||||
return <PageLoading srMessage="Loading verification status" />;
|
||||
}
|
||||
|
||||
if (!canVerify) {
|
||||
return <AccessBlocked error={error} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<IdVerificationContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</IdVerificationContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
IdVerificationContextProvider.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import {
|
||||
Route, Switch, Redirect, useRouteMatch, useLocation,
|
||||
} from 'react-router-dom';
|
||||
import qs from 'qs';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Modal, Button } from '@edx/paragon';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { idVerificationSelector } from './data/selectors';
|
||||
import './getUserMediaShim';
|
||||
|
||||
import IdVerificationContextProvider from './IdVerificationContextProvider';
|
||||
import ReviewRequirementsPanel from './panels/ReviewRequirementsPanel';
|
||||
import ChooseModePanel from './panels/ChooseModePanel';
|
||||
import RequestCameraAccessPanel from './panels/RequestCameraAccessPanel';
|
||||
import PortraitPhotoContextPanel from './panels/PortraitPhotoContextPanel';
|
||||
import TakePortraitPhotoPanel from './panels/TakePortraitPhotoPanel';
|
||||
import IdContextPanel from './panels/IdContextPanel';
|
||||
import GetNameIdPanel from './panels/GetNameIdPanel';
|
||||
import TakeIdPhotoPanel from './panels/TakeIdPhotoPanel';
|
||||
import SummaryPanel from './panels/SummaryPanel';
|
||||
import SubmittedPanel from './panels/SubmittedPanel';
|
||||
|
||||
import messages from './IdVerification.messages';
|
||||
|
||||
// eslint-disable-next-line react/prefer-stateless-function
|
||||
function IdVerificationPage(props) {
|
||||
const { path } = useRouteMatch();
|
||||
const { search } = useLocation();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
// Course run key is passed as a query string
|
||||
useEffect(() => {
|
||||
if (search) {
|
||||
const parsed = qs.parse(search, {
|
||||
ignoreQueryPrefix: true,
|
||||
interpretNumericEntities: true,
|
||||
});
|
||||
if (Object.prototype.hasOwnProperty.call(parsed, 'course_id') && parsed.course_id) {
|
||||
sessionStorage.setItem('courseRunKey', parsed.course_id);
|
||||
}
|
||||
}
|
||||
}, [search]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* If user reloads, redirect to the beginning of the process */}
|
||||
<Redirect to={`${path}/review-requirements`} />
|
||||
<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>
|
||||
</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)}>
|
||||
Privacy Information
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
open={isModalOpen}
|
||||
title={props.intl.formatMessage(messages['id.verification.privacy.title'])}
|
||||
body={(
|
||||
<div>
|
||||
<h6>
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.privacy.need.photo.question'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</h6>
|
||||
<p>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}</p>
|
||||
<h6>
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.privacy.do.with.photo.question'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.privacy.do.with.photo.answer'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
IdVerificationPage.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default connect(idVerificationSelector, {})(injectIntl(IdVerificationPage));
|
||||
@@ -1,60 +0,0 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { intlShape } from '@edx/frontend-platform/i18n';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import messages from './IdVerification.messages';
|
||||
import SupportedMediaTypes from './SupportedMediaTypes';
|
||||
|
||||
export default function ImageFileUpload({ onFileChange, intl }) {
|
||||
const [error, setError] = useState(null);
|
||||
const errorTypes = {
|
||||
invalidFileType: 'invalidFileType',
|
||||
fileTooLarge: 'fileTooLarge',
|
||||
};
|
||||
const maxFileSize = 10000000;
|
||||
|
||||
const handleChange = useCallback((e) => {
|
||||
if (e.target.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileObject = e.target.files[0];
|
||||
if (!fileObject.type.startsWith('image')) {
|
||||
setError(errorTypes.invalidFileType);
|
||||
} else if (fileObject.size >= maxFileSize) {
|
||||
setError(errorTypes.fileTooLarge);
|
||||
} else {
|
||||
setError(null);
|
||||
const fileReader = new FileReader();
|
||||
fileReader.addEventListener('load', () => onFileChange(fileReader.result));
|
||||
fileReader.readAsDataURL(fileObject);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
data-testid="fileUpload"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
{error && (
|
||||
<Alert
|
||||
id="fileError"
|
||||
variant="danger"
|
||||
tabIndex="-1"
|
||||
style={{ marginTop: '1rem' }}
|
||||
>
|
||||
{intl.formatMessage(messages[`id.verification.id.photo.instructions.upload.error.${error}`])}
|
||||
<SupportedMediaTypes />
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
ImageFileUpload.propTypes = {
|
||||
onFileChange: PropTypes.func.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
@@ -1,22 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function ImagePreview({ src, alt, id }) {
|
||||
return (
|
||||
<div id={id} className="image-preview">
|
||||
|
||||
<img data-hj-suppress style={{ objectFit: 'contain' }} src={src} alt={alt} />
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ImagePreview.propTypes = {
|
||||
src: PropTypes.string.isRequired,
|
||||
alt: PropTypes.string.isRequired,
|
||||
id: PropTypes.string,
|
||||
};
|
||||
|
||||
ImagePreview.defaultProps = {
|
||||
id: undefined,
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function SupportedMediaTypes() {
|
||||
const SUPPORTED_TYPES = ['.png', '.jpeg', '.jpg', '.bmp', '.webp', '.tiff'];
|
||||
|
||||
const getSupportedTypes = () => SUPPORTED_TYPES.map((type, index) => {
|
||||
if (index === SUPPORTED_TYPES.length - 1) {
|
||||
return type;
|
||||
}
|
||||
return `${type}, `;
|
||||
});
|
||||
|
||||
return <span>{getSupportedTypes()}</span>;
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
.page__id-verification {
|
||||
.verification-panel {
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
.card.accent {
|
||||
border-top: solid 4px theme-color('warning');
|
||||
}
|
||||
.image-preview {
|
||||
margin-bottom: 1rem;
|
||||
max-width: 20rem;
|
||||
img {
|
||||
display: block;
|
||||
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;
|
||||
flex-direction: row-reverse;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
.btn-primary {
|
||||
min-width: 8rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
.btn-link {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
text-decoration: underline;
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
.camera-outer-wrapper {
|
||||
margin-bottom: 15px;
|
||||
text-align: center;
|
||||
border-radius: 0.3rem;
|
||||
}
|
||||
.camera-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.camera-video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.canvas-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.camera-btn {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.camera-flash {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
opacity :1;
|
||||
transition:opacity .9s ease-out;
|
||||
}
|
||||
|
||||
.camera-flash.do-transition {
|
||||
opacity: 0;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 119 KiB |
File diff suppressed because one or more lines are too long
@@ -1,3 +0,0 @@
|
||||
export const storeName = 'idVerifiction';
|
||||
|
||||
export const idVerificationSelector = state => ({ ...state[storeName] });
|
||||
@@ -1,94 +0,0 @@
|
||||
import qs from 'qs';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
|
||||
/**
|
||||
* Get ID verification status from LMS.
|
||||
*
|
||||
* Returns {
|
||||
* status: String,
|
||||
* expires: String|null,
|
||||
* canVerify: Boolean,
|
||||
* }
|
||||
*/
|
||||
export async function getExistingIdVerification() {
|
||||
const url = `${getConfig().LMS_BASE_URL}/verify_student/status/`;
|
||||
const requestConfig = {
|
||||
headers: { Accept: 'application/json' },
|
||||
};
|
||||
try {
|
||||
const response = await getAuthenticatedHttpClient().get(url, requestConfig);
|
||||
return {
|
||||
status: response.data.status || null,
|
||||
expires: response.data.expires || null,
|
||||
canVerify: response.data.can_verify || false,
|
||||
};
|
||||
} catch (e) {
|
||||
return { status: null, expires: null, canVerify: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the learner's enrollments. Used to check whether the learner is enrolled
|
||||
* in a verified course mode.
|
||||
*
|
||||
* Returns an array: [{...data, mode: String}]
|
||||
*/
|
||||
export async function getEnrollments() {
|
||||
const url = `${getConfig().LMS_BASE_URL}/api/enrollment/v1/enrollment`;
|
||||
const requestConfig = {
|
||||
headers: { Accept: 'application/json' },
|
||||
};
|
||||
try {
|
||||
const { data } = await getAuthenticatedHttpClient().get(url, requestConfig);
|
||||
return data;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit ID verifiction to LMS.
|
||||
*
|
||||
* verificationData should take the shape of:
|
||||
* - facePhotoFile (String): Base64-encoded image.
|
||||
* - idPhotoFile (String|null): Optional Base64-encoded image
|
||||
* - idPhotoName (String|null): Optional string to change the user's name to.
|
||||
* - courseRunKey (String|null): Optional course run to redirect to.
|
||||
*
|
||||
* Returns { success: Boolean, message: String|null }
|
||||
*/
|
||||
export async function submitIdVerification(verificationData) {
|
||||
const keyMap = {
|
||||
facePhotoFile: 'face_image',
|
||||
idPhotoFile: 'photo_id_image',
|
||||
idPhotoName: 'full_name',
|
||||
optimizelyExperimentName: 'experiment_name',
|
||||
};
|
||||
const postData = {};
|
||||
// Don't include blank/null/undefined values.
|
||||
// Note that this will also drop the value `false`.
|
||||
Object.keys(keyMap).forEach((jsKey) => {
|
||||
const apiKey = keyMap[jsKey];
|
||||
if (verificationData[jsKey]) {
|
||||
postData[apiKey] = verificationData[jsKey];
|
||||
}
|
||||
});
|
||||
|
||||
const url = `${getConfig().LMS_BASE_URL}/verify_student/submit-photos/`;
|
||||
const urlEncodedPostData = qs.stringify(postData);
|
||||
const requestConfig = {
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
};
|
||||
try {
|
||||
await getAuthenticatedHttpClient().post(url, urlEncodedPostData, requestConfig);
|
||||
return { success: true, message: null };
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
status: e.customAttributes.httpErrorStatus,
|
||||
message: String(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
/**
|
||||
* This polyfill is from MDN:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
|
||||
*
|
||||
* Their description:
|
||||
* "Here's an example of using navigator.mediaDevices.getUserMedia(), with a
|
||||
* polyfill to cope with older browsers. Note that this polyfill does not
|
||||
* correct for legacy differences in constraints syntax, which means constraints
|
||||
* won't work well across browsers. It is recommended to use the adapter.js
|
||||
* polyfill instead, which does handle constraints."
|
||||
*
|
||||
* Despite the lack of support for differences in constraints we'll use this
|
||||
* since it's small and simple and we don't have a need for constraints at the
|
||||
* moment. I've added an export hasGetUserMediaSupport before the polyfill to
|
||||
* help us understand support before making calls to getUserMedia.
|
||||
*/
|
||||
|
||||
// IIFE to check getUserMedia support. Must be run before the polyfill.
|
||||
const hasGetUserMediaSupport = (() => {
|
||||
// Modern API
|
||||
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
||||
return true;
|
||||
}
|
||||
// Deprecated APIs
|
||||
if (navigator.webkitGetUserMedia || navigator.mozGetUserMedia) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
// Older browsers might not implement mediaDevices at all, so we set an empty object first
|
||||
if (navigator.mediaDevices === undefined) {
|
||||
navigator.mediaDevices = {};
|
||||
}
|
||||
|
||||
// Some browsers partially implement mediaDevices. We can't just assign an object
|
||||
// with getUserMedia as it would overwrite existing properties.
|
||||
// Here, we will just add the getUserMedia property if it's missing.
|
||||
if (navigator.mediaDevices.getUserMedia === undefined) {
|
||||
// eslint-disable-next-line func-names
|
||||
navigator.mediaDevices.getUserMedia = function (constraints) {
|
||||
// First get ahold of the legacy getUserMedia, if present
|
||||
const getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
||||
|
||||
// Some browsers just don't implement it - return a rejected promise with an error
|
||||
// to keep a consistent interface
|
||||
if (!getUserMedia) {
|
||||
return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
|
||||
}
|
||||
|
||||
// Otherwise, wrap the call to the old navigator.getUserMedia with a Promise
|
||||
return new Promise(((resolve, reject) => {
|
||||
getUserMedia.call(navigator, constraints, resolve, reject);
|
||||
}));
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/prefer-default-export
|
||||
export { hasGetUserMediaSupport };
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './IdVerificationPage';
|
||||
@@ -1,43 +0,0 @@
|
||||
import React, { useRef, useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Redirect } from 'react-router';
|
||||
import { useVerificationRedirectSlug } from '../routing-utilities';
|
||||
|
||||
export default function BasePanel({
|
||||
children,
|
||||
focusOnMount,
|
||||
name,
|
||||
title,
|
||||
}) {
|
||||
const headingRef = useRef();
|
||||
|
||||
// focus heading element on mount
|
||||
useEffect(() => {
|
||||
if (focusOnMount && headingRef.current) {
|
||||
headingRef.current.focus();
|
||||
}
|
||||
}, [headingRef.current]);
|
||||
|
||||
const redirectSlug = useVerificationRedirectSlug(name);
|
||||
if (redirectSlug) {
|
||||
return <Redirect to={redirectSlug} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`verification-panel ${name}-panel`}>
|
||||
<h3 aria-level="1" ref={headingRef} tabIndex="-1">{title}</h3>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
BasePanel.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
focusOnMount: PropTypes.bool,
|
||||
name: PropTypes.string.isRequired,
|
||||
title: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
BasePanel.defaultProps = {
|
||||
focusOnMount: true,
|
||||
};
|
||||
@@ -1,60 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import IdVerificationContext from '../IdVerificationContext';
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function ChooseModePanel(props) {
|
||||
const panelSlug = 'choose-mode';
|
||||
const { shouldUseCamera, setShouldUseCamera } = useContext(IdVerificationContext);
|
||||
|
||||
function onPhotoModeChange(value) {
|
||||
setShouldUseCamera(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={props.intl.formatMessage(messages['id.verification.choose.mode.title'])}
|
||||
>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.choose.mode.help.text'])}
|
||||
</p>
|
||||
<fieldset>
|
||||
<Form.Group controlId="formChoosePhotoOption" style={{ marginLeft: '1.25rem' }}>
|
||||
<Form.Check
|
||||
type="radio"
|
||||
id="useUploadMode"
|
||||
label={props.intl.formatMessage(messages['id.verification.choose.mode.radio.upload'])}
|
||||
name="photoMode"
|
||||
checked={!shouldUseCamera}
|
||||
onChange={() => onPhotoModeChange(false)}
|
||||
/>
|
||||
<Form.Check
|
||||
type="radio"
|
||||
id="useCameraMode"
|
||||
label={props.intl.formatMessage(messages['id.verification.choose.mode.radio.camera'])}
|
||||
name="photoMode"
|
||||
checked={shouldUseCamera}
|
||||
onChange={() => onPhotoModeChange(true)}
|
||||
/>
|
||||
</Form.Group>
|
||||
</fieldset>
|
||||
<div className="action-row">
|
||||
<Link to={useNextPanelSlug(panelSlug)} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
ChooseModePanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ChooseModePanel);
|
||||
@@ -1,74 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
export function EnableCameraDirectionsPanel(props) {
|
||||
if (props.browserName === 'Internet Explorer') {
|
||||
return (
|
||||
<>
|
||||
<h6>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11'])}</h6>
|
||||
<ol>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11.step1'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11.step2'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.ie11.step3'])}</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (props.browserName === 'Chrome') {
|
||||
return (
|
||||
<>
|
||||
<h6>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome'])}</h6>
|
||||
<ol>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step1'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step2'])}</li>
|
||||
<ul>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step2.windows'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step2.mac'])}</li>
|
||||
</ul>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step3'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step4'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.chrome.step5'])}</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (props.browserName === 'Firefox') {
|
||||
return (
|
||||
<>
|
||||
<h6>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox'])}</h6>
|
||||
<ol>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step1'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step2'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step3'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step4'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step5'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step6'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.firefox.step7'])}</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (props.browserName === 'Safari') {
|
||||
return (
|
||||
<>
|
||||
<h6>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari'])}</h6>
|
||||
<ol>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step1'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step2'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step3'])}</li>
|
||||
<li>{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary.safari.step4'])}</li>
|
||||
</ol>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <></>;
|
||||
}
|
||||
|
||||
EnableCameraDirectionsPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
browserName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(EnableCameraDirectionsPanel);
|
||||
@@ -1,177 +0,0 @@
|
||||
import React, {
|
||||
useContext, useState, useEffect, useRef,
|
||||
} from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { Hyperlink, Form } from '@edx/paragon';
|
||||
import { Link, useHistory } from 'react-router-dom';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import IdVerificationContext from '../IdVerificationContext';
|
||||
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function GetNameIdPanel(props) {
|
||||
const { push } = useHistory();
|
||||
const panelSlug = 'get-name-id';
|
||||
const [nameMatches, setNameMatches] = useState(true);
|
||||
const nameInputRef = useRef();
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
|
||||
const {
|
||||
nameOnAccount, userId, profileDataManager, idPhotoName, setIdPhotoName,
|
||||
} = useContext(IdVerificationContext);
|
||||
const nameOnAccountValue = nameOnAccount || '';
|
||||
const invalidName = !nameMatches && (!idPhotoName || idPhotoName === nameOnAccount);
|
||||
const blankName = !nameOnAccount && !idPhotoName;
|
||||
|
||||
useEffect(() => {
|
||||
setIdPhotoName(null);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!nameMatches && nameInputRef.current) {
|
||||
nameInputRef.current.focus();
|
||||
}
|
||||
if (!nameMatches) {
|
||||
sendTrackEvent('edx.id_verification.name_change', {
|
||||
category: 'id_verification',
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
if (blankName) {
|
||||
setNameMatches(false);
|
||||
}
|
||||
}, [nameMatches, blankName]);
|
||||
|
||||
function getNameValue() {
|
||||
if (!nameMatches) {
|
||||
// Explicitly check for null, as an empty string should still be used here
|
||||
if (idPhotoName === null) {
|
||||
return nameOnAccountValue;
|
||||
}
|
||||
return idPhotoName;
|
||||
}
|
||||
return nameOnAccountValue;
|
||||
}
|
||||
|
||||
function getErrorMessage() {
|
||||
if (profileDataManager) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="id.verification.account.name.managed.alert"
|
||||
defaultMessage="Your profile settings are managed by {managerTitle}, so you are not allowed to update your name. Please contact your {profileDataManager} administrator or {support} for help."
|
||||
description="Alert message informing the user their account name is managed by a third party."
|
||||
values={{
|
||||
managerTitle: <strong>{profileDataManager}</strong>,
|
||||
profileDataManager,
|
||||
support: (
|
||||
<Hyperlink destination={getConfig().SUPPORT_URL} target="_blank">
|
||||
{props.intl.formatMessage(messages['id.verification.support'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return props.intl.formatMessage(messages['id.verification.account.name.error']);
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
// If the input is empty, or if no changes have been made to the
|
||||
// mismatching name, the user should not be able to proceed.
|
||||
if (!invalidName && !blankName) {
|
||||
push(nextPanelSlug);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={props.intl.formatMessage(messages['id.verification.account.name.title'])}
|
||||
>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.account.name.instructions'])}
|
||||
</p>
|
||||
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form.Group>
|
||||
<Form.Label htmlFor="nameMatchesYes">
|
||||
{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.Group>
|
||||
<Form.Group>
|
||||
<Form.Label htmlFor="photo-id-name">
|
||||
{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}
|
||||
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>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
|
||||
<div className="action-row">
|
||||
<Link
|
||||
to={nextPanelSlug}
|
||||
className={`btn btn-primary ${(invalidName || blankName) && 'disabled'}`}
|
||||
data-testid="next-button"
|
||||
aria-disabled={invalidName || blankName}
|
||||
>
|
||||
{
|
||||
!nameMatches
|
||||
? props.intl.formatMessage(messages['id.verification.account.name.save'])
|
||||
: props.intl.formatMessage(messages['id.verification.next'])
|
||||
}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
GetNameIdPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(GetNameIdPanel);
|
||||
@@ -1,56 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import CameraHelp from '../CameraHelp';
|
||||
import messages from '../IdVerification.messages';
|
||||
import exampleCard from '../assets/example-card.png';
|
||||
|
||||
function IdContextPanel(props) {
|
||||
const panelSlug = 'id-context';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={props.intl.formatMessage(messages['id.verification.id.tips.title'])}
|
||||
>
|
||||
<p>{props.intl.formatMessage(messages['id.verification.id.tips.description'])}</p>
|
||||
<div className="card mb-4 shadow accent">
|
||||
<div className="card-body">
|
||||
<h6>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.list.title'])}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.list.description'])}
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
{props.intl.formatMessage(messages['id.verification.id.tips.list.well.lit'])}
|
||||
</li>
|
||||
<li>
|
||||
{props.intl.formatMessage(messages['id.verification.id.tips.list.clear'])}
|
||||
</li>
|
||||
</ul>
|
||||
<img
|
||||
src={exampleCard}
|
||||
alt={props.intl.formatMessage(messages['id.verification.example.card.alt'])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CameraHelp isOpen />
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
IdContextPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(IdContextPanel);
|
||||
@@ -1,53 +0,0 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import CameraHelp from '../CameraHelp';
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function PortraitPhotoContextPanel(props) {
|
||||
const panelSlug = 'portrait-photo-context';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={props.intl.formatMessage(messages['id.verification.photo.tips.title'])}
|
||||
>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.description'])}
|
||||
</p>
|
||||
<div className="card mb-4 shadow accent">
|
||||
<div className="card-body">
|
||||
<h6>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.list.title'])}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.list.description'])}
|
||||
</p>
|
||||
<ul className="mb-0">
|
||||
<li>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.list.well.lit'])}
|
||||
</li>
|
||||
<li>
|
||||
{props.intl.formatMessage(messages['id.verification.photo.tips.list.inside.frame'])}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<CameraHelp isOpen isPortrait />
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
PortraitPhotoContextPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(PortraitPhotoContextPanel);
|
||||
@@ -1,142 +0,0 @@
|
||||
import React, { useState, useEffect, useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Bowser from 'bowser';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import IdVerificationContext, { MEDIA_ACCESS } from '../IdVerificationContext';
|
||||
import { EnableCameraDirectionsPanel } from './EnableCameraDirectionsPanel';
|
||||
import { UnsupportedCameraDirectionsPanel } from './UnsupportedCameraDirectionsPanel';
|
||||
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function RequestCameraAccessPanel(props) {
|
||||
const [returnUrl, setReturnUrl] = useState('dashboard');
|
||||
const [returnText, setReturnText] = useState('id.verification.return.dashboard');
|
||||
const panelSlug = 'request-camera-access';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
const {
|
||||
tryGetUserMedia, mediaAccess, userId, optimizelyExperimentName,
|
||||
} = useContext(IdVerificationContext);
|
||||
const browserName = Bowser.parse(window.navigator.userAgent).browser.name;
|
||||
|
||||
useEffect(() => {
|
||||
if (mediaAccess === MEDIA_ACCESS.UNSUPPORTED) {
|
||||
sendTrackEvent('edx.id_verification.camera.unsupported', {
|
||||
category: 'id_verification',
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
if (mediaAccess === MEDIA_ACCESS.DENIED) {
|
||||
sendTrackEvent('edx.id_verification.camera.denied', {
|
||||
category: 'id_verification',
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
}, [mediaAccess, userId]);
|
||||
|
||||
// If the user accessed IDV through a course,
|
||||
// link back to that course rather than the dashboard
|
||||
useEffect(() => {
|
||||
if (sessionStorage.getItem('courseRunKey')) {
|
||||
setReturnUrl(`courses/${sessionStorage.getItem('courseRunKey')}`);
|
||||
setReturnText('id.verification.return.course');
|
||||
}
|
||||
}, []);
|
||||
|
||||
const getTitle = () => {
|
||||
if (mediaAccess === MEDIA_ACCESS.GRANTED) {
|
||||
return props.intl.formatMessage(messages['id.verification.camera.access.title.success']);
|
||||
}
|
||||
if ([MEDIA_ACCESS.UNSUPPORTED, MEDIA_ACCESS.DENIED].includes(mediaAccess)) {
|
||||
return props.intl.formatMessage(messages['id.verification.camera.access.title.failed']);
|
||||
}
|
||||
return props.intl.formatMessage(messages['id.verification.camera.access.title']);
|
||||
};
|
||||
|
||||
const returnToDashboardLink = (
|
||||
<a className="btn btn-primary" href={`${getConfig().LMS_BASE_URL}/${returnUrl}`}>
|
||||
{props.intl.formatMessage(messages[returnText])}
|
||||
</a>
|
||||
);
|
||||
|
||||
const nextButtonLink = (
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.continue.upload'])}
|
||||
</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={getTitle()}
|
||||
>
|
||||
{mediaAccess === MEDIA_ACCESS.PENDING && (
|
||||
<div>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="id.verification.request.camera.access.instructions"
|
||||
defaultMessage="In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}"
|
||||
description="Instructions to enable camera access."
|
||||
values={{
|
||||
clickAllow: <strong>{props.intl.formatMessage(messages['id.verification.camera.access.click.allow'])}</strong>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<div className="action-row">
|
||||
<button type="button" className="btn btn-primary" onClick={tryGetUserMedia}>
|
||||
{props.intl.formatMessage(messages['id.verification.camera.access.enable'])}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mediaAccess === MEDIA_ACCESS.GRANTED && (
|
||||
<div>
|
||||
<p data-testid="camera-access-success">
|
||||
{props.intl.formatMessage(messages['id.verification.camera.access.success'])}
|
||||
</p>
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mediaAccess === MEDIA_ACCESS.DENIED && (
|
||||
<div data-testid="camera-failure-instructions">
|
||||
<p data-testid="camera-access-failure">
|
||||
{props.intl.formatMessage(messages['id.verification.camera.access.failure.temporary'])}
|
||||
</p>
|
||||
<EnableCameraDirectionsPanel browserName={browserName} intl={props.intl} />
|
||||
<div className="action-row">
|
||||
{optimizelyExperimentName ? nextButtonLink : returnToDashboardLink}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{mediaAccess === MEDIA_ACCESS.UNSUPPORTED && (
|
||||
<div data-testid="camera-unsupported-instructions">
|
||||
<p data-testid="camera-unsupported-failure">
|
||||
{props.intl.formatMessage(messages['id.verification.camera.access.failure.unsupported'])}
|
||||
</p>
|
||||
<UnsupportedCameraDirectionsPanel browserName={browserName} intl={props.intl} />
|
||||
<div className="action-row">
|
||||
{optimizelyExperimentName ? nextButtonLink : returnToDashboardLink}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
RequestCameraAccessPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(RequestCameraAccessPanel);
|
||||
@@ -1,149 +0,0 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Hyperlink } from '@edx/paragon';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
|
||||
import IdVerificationContext from '../IdVerificationContext';
|
||||
import messages from '../IdVerification.messages';
|
||||
import exampleCard from '../assets/example-card.png';
|
||||
|
||||
function ReviewRequirementsPanel(props) {
|
||||
const {
|
||||
userId, profileDataManager, setOptimizelyExperimentName,
|
||||
} = useContext(IdVerificationContext);
|
||||
const panelSlug = 'review-requirements';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
|
||||
const getExperiments = () => {
|
||||
const {
|
||||
experimentVariables: {
|
||||
experimentName = '',
|
||||
} = {},
|
||||
} = window;
|
||||
|
||||
if (experimentName) {
|
||||
setOptimizelyExperimentName(experimentName);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
sendTrackEvent('edx.id_verification.started', {
|
||||
category: 'id_verification',
|
||||
user_id: userId,
|
||||
});
|
||||
|
||||
getExperiments();
|
||||
}, [userId]);
|
||||
|
||||
function renderManagedProfileMessage() {
|
||||
if (!profileDataManager) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Alert className="alert alert-primary" role="alert">
|
||||
<FormattedMessage
|
||||
id="id.verification.requirements.account.managed.alert"
|
||||
defaultMessage="Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help before completing the Photo Verification process."
|
||||
description="Alert message informing the user their account data is managed by a third party."
|
||||
values={{
|
||||
managerTitle: <strong>{profileDataManager}</strong>,
|
||||
profileDataManager,
|
||||
support: (
|
||||
<Hyperlink destination={getConfig().SUPPORT_URL} target="_blank">
|
||||
{props.intl.formatMessage(messages['id.verification.support'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={props.intl.formatMessage(messages['id.verification.requirements.title'])}
|
||||
focusOnMount={false}
|
||||
>
|
||||
{renderManagedProfileMessage()}
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.requirements.description'])}
|
||||
</p>
|
||||
<div className="card mb-4 shadow accent">
|
||||
<div className="card-body">
|
||||
<h6 aria-level="3">
|
||||
{props.intl.formatMessage(messages['id.verification.requirements.card.device.title'])}
|
||||
</h6>
|
||||
<p className="mb-0">
|
||||
<FormattedMessage
|
||||
id="id.verification.requirements.card.device.text"
|
||||
defaultMessage="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}."
|
||||
description="Text explaining that the user needs access to a camera."
|
||||
values={{
|
||||
allow: <strong>{props.intl.formatMessage(messages['id.verification.requirements.card.device.allow'])}</strong>,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card mb-4 shadow accent">
|
||||
<div className="card-body">
|
||||
<h6 aria-level="3">
|
||||
{props.intl.formatMessage(messages['id.verification.requirements.card.id.title'])}
|
||||
</h6>
|
||||
<p className="mb-0">
|
||||
{props.intl.formatMessage(messages['id.verification.requirements.card.id.text'])}
|
||||
<img
|
||||
src={exampleCard}
|
||||
alt={props.intl.formatMessage(messages['id.verification.example.card.alt'])}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<h4 aria-level="2" className="mb-3">
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.title'])}
|
||||
</h4>
|
||||
<h6 aria-level="3">
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.privacy.need.photo.question'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}
|
||||
</p>
|
||||
<h6 aria-level="3">
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.privacy.do.with.photo.question'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</h6>
|
||||
<p>
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.privacy.do.with.photo.answer'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="action-row">
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
ReviewRequirementsPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(ReviewRequirementsPanel);
|
||||
@@ -1,45 +0,0 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import BasePanel from './BasePanel';
|
||||
|
||||
import IdVerificationContext from '../IdVerificationContext';
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
function SubmittedPanel(props) {
|
||||
const { userId } = useContext(IdVerificationContext);
|
||||
const panelSlug = 'submitted';
|
||||
|
||||
useEffect(() => {
|
||||
sendTrackEvent('edx.id_verification.submitted', {
|
||||
category: 'id_verification',
|
||||
user_id: userId,
|
||||
});
|
||||
}, [userId]);
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={props.intl.formatMessage(messages['id.verification.submitted.title'])}
|
||||
>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.submitted.text'])}
|
||||
</p>
|
||||
<a
|
||||
className="btn btn-primary"
|
||||
href={`${getConfig().LMS_BASE_URL}/dashboard`}
|
||||
data-testid="return-button"
|
||||
>
|
||||
{props.intl.formatMessage(messages['id.verification.return.dashboard'])}
|
||||
</a>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
SubmittedPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SubmittedPanel);
|
||||
@@ -1,246 +0,0 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import { getConfig, history } from '@edx/frontend-platform';
|
||||
import {
|
||||
Alert, Hyperlink, Input, Button, Spinner,
|
||||
} from '@edx/paragon';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { submitIdVerification } from '../data/service';
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import IdVerificationContext from '../IdVerificationContext';
|
||||
import ImagePreview from '../ImagePreview';
|
||||
|
||||
import messages from '../IdVerification.messages';
|
||||
import CameraHelpWithUpload from '../CameraHelpWithUpload';
|
||||
import SupportedMediaTypes from '../SupportedMediaTypes';
|
||||
|
||||
function SummaryPanel(props) {
|
||||
const panelSlug = 'summary';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
const {
|
||||
facePhotoFile,
|
||||
idPhotoFile,
|
||||
profileDataManager,
|
||||
nameOnAccount,
|
||||
idPhotoName,
|
||||
stopUserMedia,
|
||||
optimizelyExperimentName,
|
||||
setReachedSummary,
|
||||
} = useContext(IdVerificationContext);
|
||||
const nameToBeUsed = idPhotoName || nameOnAccount || '';
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submissionError, setSubmissionError] = useState(null);
|
||||
|
||||
useEffect(() => setReachedSummary(true), []);
|
||||
|
||||
function renderManagedProfileMessage() {
|
||||
if (!profileDataManager) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<p id="profile-manager-warning">
|
||||
<FormattedMessage
|
||||
id="id.verification.account.name.summary.alert"
|
||||
defaultMessage="Your account settings are managed by {managerTitle}. If the name on your photo ID does not match the name on your account, please contact your {profileDataManager} administrator or {support} for help."
|
||||
description="Alert message informing the user their account data is managed by a third party."
|
||||
values={{
|
||||
managerTitle: <strong>{profileDataManager}</strong>,
|
||||
profileDataManager,
|
||||
support: (
|
||||
<Hyperlink destination={getConfig().SUPPORT_URL} target="_blank">
|
||||
{props.intl.formatMessage(messages['id.verification.support'])}
|
||||
</Hyperlink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function SubmitButton() {
|
||||
async function handleClick() {
|
||||
setIsSubmitting(true);
|
||||
const verificationData = {
|
||||
facePhotoFile,
|
||||
idPhotoFile,
|
||||
courseRunKey: sessionStorage.getItem('courseRunKey'),
|
||||
};
|
||||
if (idPhotoName) {
|
||||
verificationData.idPhotoName = idPhotoName;
|
||||
}
|
||||
if (optimizelyExperimentName) {
|
||||
verificationData.optimizelyExperimentName = optimizelyExperimentName;
|
||||
}
|
||||
const result = await submitIdVerification(verificationData);
|
||||
if (result.success) {
|
||||
stopUserMedia();
|
||||
history.push(nextPanelSlug);
|
||||
} else {
|
||||
stopUserMedia();
|
||||
setIsSubmitting(false);
|
||||
setSubmissionError(result);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
title="Confirmation"
|
||||
disabled={isSubmitting}
|
||||
onClick={handleClick}
|
||||
data-testid="submit-button"
|
||||
>
|
||||
{props.intl.formatMessage(messages['id.verification.review.confirm'])}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function getError() {
|
||||
if (submissionError.status === 400) {
|
||||
if (submissionError.message.includes('face_image')) {
|
||||
return props.intl.formatMessage(messages['id.verification.submission.alert.error.face']);
|
||||
}
|
||||
if (submissionError.message.includes('Photo ID image')) {
|
||||
return props.intl.formatMessage(messages['id.verification.submission.alert.error.id']);
|
||||
}
|
||||
if (submissionError.message.includes('Name')) {
|
||||
return props.intl.formatMessage(messages['id.verification.submission.alert.error.name']);
|
||||
}
|
||||
if (submissionError.message.includes('unsupported format')) {
|
||||
return (
|
||||
<>
|
||||
{props.intl.formatMessage(messages['id.verification.submission.alert.error.unsupported'])}
|
||||
<SupportedMediaTypes />
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="idv.submission.alert.error"
|
||||
defaultMessage={`
|
||||
We encountered a technical error while trying to submit ID verification.
|
||||
This might be a temporary issue, so please try again in a few minutes.
|
||||
If the problem persists, please go to {support_link} for help.
|
||||
`}
|
||||
values={{
|
||||
support_link: (
|
||||
<Alert.Link href="https://support.edx.org/hc/en-us">
|
||||
{props.intl.formatMessage(
|
||||
messages['id.verification.review.error'],
|
||||
{ siteName: getConfig().SITE_NAME },
|
||||
)}
|
||||
</Alert.Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={props.intl.formatMessage(messages['id.verification.review.title'])}
|
||||
>
|
||||
{submissionError && (
|
||||
<Alert
|
||||
variant="danger"
|
||||
data-testid="submission-error"
|
||||
dismissible
|
||||
onClose={() => setSubmissionError(null)}
|
||||
>
|
||||
{getError()}
|
||||
</Alert>
|
||||
)}
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.review.description'])}
|
||||
</p>
|
||||
<div className="row mb-4">
|
||||
<div className="col-6">
|
||||
<label htmlFor="photo-of-face" className="font-weight-bold">
|
||||
{props.intl.formatMessage(messages['id.verification.review.portrait.label'])}
|
||||
</label>
|
||||
<ImagePreview
|
||||
id="photo-of-face"
|
||||
src={facePhotoFile}
|
||||
alt={props.intl.formatMessage(messages['id.verification.review.portrait.alt'])}
|
||||
/>
|
||||
<Link
|
||||
className="btn btn-outline-primary"
|
||||
to={{
|
||||
pathname: 'take-portrait-photo',
|
||||
state: { fromSummary: true },
|
||||
}}
|
||||
data-testid="portrait-retake"
|
||||
>
|
||||
{props.intl.formatMessage(messages['id.verification.review.portrait.retake'])}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<label htmlFor="photo-of-id/edit" className="font-weight-bold">
|
||||
{props.intl.formatMessage(messages['id.verification.review.id.label'])}
|
||||
</label>
|
||||
<ImagePreview
|
||||
id="photo-of-id"
|
||||
src={idPhotoFile}
|
||||
alt={props.intl.formatMessage(messages['id.verification.review.id.alt'])}
|
||||
/>
|
||||
<Link
|
||||
className="btn btn-outline-primary"
|
||||
to={{
|
||||
pathname: 'take-id-photo',
|
||||
state: { fromSummary: true },
|
||||
}}
|
||||
data-testid="id-retake"
|
||||
>
|
||||
{props.intl.formatMessage(messages['id.verification.review.id.retake'])}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
{!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'])}
|
||||
</label>
|
||||
{renderManagedProfileMessage()}
|
||||
<div className="d-flex">
|
||||
<Input
|
||||
id="name-to-be-used"
|
||||
type="text"
|
||||
readOnly
|
||||
value={nameToBeUsed}
|
||||
onChange={() => {}}
|
||||
aria-describedby={profileDataManager ? 'profile-manager-warning' : null}
|
||||
/>
|
||||
{!profileDataManager && (
|
||||
<Link
|
||||
className="btn btn-link ml-3 px-0"
|
||||
to={{
|
||||
pathname: 'get-name-id',
|
||||
state: { fromSummary: true },
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="id.verification.account.name.edit"
|
||||
defaultMessage="Edit {sr}"
|
||||
description="Button to edit account name, with clarifying information for screen readers."
|
||||
values={{
|
||||
sr: <span className="sr-only">Account Name</span>,
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<SubmitButton />{' '}
|
||||
{isSubmitting && <Spinner animation="border" variant="primary" />}
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
SummaryPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SummaryPanel);
|
||||
@@ -1,64 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import Camera from '../Camera';
|
||||
import IdVerificationContext from '../IdVerificationContext';
|
||||
|
||||
import messages from '../IdVerification.messages';
|
||||
import CameraHelp from '../CameraHelp';
|
||||
import ImagePreview from '../ImagePreview';
|
||||
import ImageFileUpload from '../ImageFileUpload';
|
||||
import CollapsibleImageHelp from '../CollapsibleImageHelp';
|
||||
import SupportedMediaTypes from '../SupportedMediaTypes';
|
||||
|
||||
function TakeIdPhotoPanel(props) {
|
||||
const panelSlug = 'take-id-photo';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
const {
|
||||
setIdPhotoFile, idPhotoFile, optimizelyExperimentName, shouldUseCamera,
|
||||
} = useContext(IdVerificationContext);
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={shouldUseCamera ? props.intl.formatMessage(messages['id.verification.id.photo.title.camera']) : props.intl.formatMessage(messages['id.verification.id.photo.title.upload'])}
|
||||
>
|
||||
<div>
|
||||
{idPhotoFile && !shouldUseCamera && <ImagePreview src={idPhotoFile} alt={props.intl.formatMessage(messages['id.verification.id.photo.preview.alt'])} />}
|
||||
|
||||
{shouldUseCamera ? (
|
||||
<div>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.camera'])}
|
||||
</p>
|
||||
<Camera onImageCapture={setIdPhotoFile} isPortrait={false} />
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ marginBottom: '1.25rem' }}>
|
||||
<p data-testid="upload-text">
|
||||
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
|
||||
<SupportedMediaTypes />
|
||||
</p>
|
||||
<ImageFileUpload onFileChange={setIdPhotoFile} intl={props.intl} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{shouldUseCamera && !optimizelyExperimentName && <CameraHelp />}
|
||||
<CollapsibleImageHelp isPortrait={false} />
|
||||
<div className="action-row" style={{ visibility: idPhotoFile ? 'unset' : 'hidden' }}>
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
TakeIdPhotoPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(TakeIdPhotoPanel);
|
||||
@@ -1,64 +0,0 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { useNextPanelSlug } from '../routing-utilities';
|
||||
import BasePanel from './BasePanel';
|
||||
import ImageFileUpload from '../ImageFileUpload';
|
||||
import ImagePreview from '../ImagePreview';
|
||||
import Camera from '../Camera';
|
||||
import CameraHelp from '../CameraHelp';
|
||||
import IdVerificationContext from '../IdVerificationContext';
|
||||
|
||||
import messages from '../IdVerification.messages';
|
||||
import CollapsibleImageHelp from '../CollapsibleImageHelp';
|
||||
import SupportedMediaTypes from '../SupportedMediaTypes';
|
||||
|
||||
function TakePortraitPhotoPanel(props) {
|
||||
const panelSlug = 'take-portrait-photo';
|
||||
const nextPanelSlug = useNextPanelSlug(panelSlug);
|
||||
const {
|
||||
setFacePhotoFile, facePhotoFile, shouldUseCamera, optimizelyExperimentName,
|
||||
} = useContext(IdVerificationContext);
|
||||
|
||||
return (
|
||||
<BasePanel
|
||||
name={panelSlug}
|
||||
title={shouldUseCamera ? props.intl.formatMessage(messages['id.verification.portrait.photo.title.camera']) : props.intl.formatMessage(messages['id.verification.portrait.photo.title.upload'])}
|
||||
>
|
||||
<div>
|
||||
{facePhotoFile && !shouldUseCamera && <ImagePreview src={facePhotoFile} alt={props.intl.formatMessage(messages['id.verification.portrait.photo.preview.alt'])} />}
|
||||
|
||||
{shouldUseCamera ? (
|
||||
<div>
|
||||
<p>
|
||||
{props.intl.formatMessage(messages['id.verification.portrait.photo.instructions.camera'])}
|
||||
</p>
|
||||
<Camera onImageCapture={setFacePhotoFile} isPortrait />
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ marginBottom: '1.25rem' }}>
|
||||
<p data-testid="upload-text">
|
||||
{props.intl.formatMessage(messages['id.verification.portrait.photo.instructions.upload'])}
|
||||
<SupportedMediaTypes />
|
||||
</p>
|
||||
<ImageFileUpload onFileChange={setFacePhotoFile} intl={props.intl} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{shouldUseCamera && !optimizelyExperimentName && <CameraHelp isPortrait />}
|
||||
<CollapsibleImageHelp isPortrait />
|
||||
<div className="action-row" style={{ visibility: facePhotoFile ? 'unset' : 'hidden' }}>
|
||||
<Link to={nextPanelSlug} className="btn btn-primary" data-testid="next-button">
|
||||
{props.intl.formatMessage(messages['id.verification.next'])}
|
||||
</Link>
|
||||
</div>
|
||||
</BasePanel>
|
||||
);
|
||||
}
|
||||
|
||||
TakePortraitPhotoPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(TakePortraitPhotoPanel);
|
||||
@@ -1,21 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import messages from '../IdVerification.messages';
|
||||
|
||||
export function UnsupportedCameraDirectionsPanel(props) {
|
||||
return (
|
||||
<>
|
||||
{props.browserName === 'Chrome' && <span>{props.intl.formatMessage(messages['id.verification.camera.access.failure.unsupported.chrome.explanation'])}</span>}
|
||||
<span> </span>
|
||||
<span>{props.intl.formatMessage(messages['id.verification.camera.access.failure.unsupported.instructions'])}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
UnsupportedCameraDirectionsPanel.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
browserName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(UnsupportedCameraDirectionsPanel);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user