Compare commits

..

63 Commits

Author SHA1 Message Date
Olivia Ruiz-Knott
caddea6758 Consider this for alert reference? 2020-06-30 16:02:54 -04:00
Justin Hynes
bbe9fef27b ugh, quality 2020-06-30 15:50:30 -04:00
Justin Hynes
1b9ff1446b Revert earlier change to avoid double conversion of data that is not needed. 2020-06-30 15:44:19 -04:00
Justin Hynes
38ab6d99ab Fix linting/quality issues. 2020-06-30 11:59:37 -04:00
Justin Hynes
77644bafc5 Add a few more tests. 2020-06-30 10:14:30 -04:00
Justin Hynes
74d9c18568 Add supporting pieces to start writing unit tests for DemographicsSection 2020-06-30 09:50:57 -04:00
Justin Hynes
8baccba066 Small update to error handling in postDemographics call 2020-06-30 09:50:56 -04:00
Justin Hynes
157f31cbc5 Add a ref and supporting callback(s) so we can focus the user on the alert if an error occurred updating their demographics data. 2020-06-30 09:50:55 -04:00
Justin Hynes
73d23c96b9 use lodash/get for page crash fix as recommended by TJ 2020-06-30 09:50:53 -04:00
Justin Hynes
09c0ef2b2b reverse unneeded changes in sagas.js 2020-06-30 09:50:51 -04:00
Justin Hynes
5ea94107af add an Alert to let the end user know there was an issue saving information on a PATCH call failure 2020-06-30 09:50:47 -04:00
Justin Hynes
f5d5eaa375 WIP, trying to update fetchSettings saga to pass back field errors to help determine if we need to show an alert for service issues. 2020-06-30 09:50:45 -04:00
Justin Hynes
a608c536b5 Work in progress on error message logic if Demographics is not available 2020-06-30 09:50:44 -04:00
Justin Hynes
0bfdcfabbb MICROBA-309 | Accounts MFE and Demographics continued
[MICROBA-309]
- Adjust error handling in getDemographics() call to do a POST to create the entity only when encountering a 404 on a GET
- Fix issue that can cause the Accounts page to error out if the Demographics service wasn't running. Add check for `demographics_user_ethnicity` prop existence before attempting to access the value
2020-06-30 09:50:41 -04:00
edX Transifex Bot
56569c717c fix(i18n): update translations 2020-06-28 17:06:01 -04:00
Albert (AJ) St. Aubin
45511860c2 Merge pull request #249 from edx/aj/MICROBA-368_coaching_text
[MICROBA-368] Updated text for the Coaching consent to align with
2020-06-26 15:00:14 -04:00
Albert (AJ) St. Aubin
11f7d56e75 [MICROBA-368] Updated text for the Coaching consent to align with
messaging timeline
2020-06-26 14:28:09 -04:00
Thomas Tracy
f19380ebac Hide demographics sidenav link when demographics is off (#248) 2020-06-25 12:56:34 -04:00
Thomas Tracy
5eb43871c7 MB-17: Refactor coaching_consent form (#244)
We were having issues with how the coaching consent form was sending
data. Previously, we were hitting 2 endpoints - one from the coaching
plugin, and one from the LMS. This changes fixes a few issues:

* CoachingConsent now hits one api that handles saving the phone_number,
full_name, and coaching_consent.
* This component does not need to share any of this data, so it is not
connected to Redux. Everything to do with patching is done in the
CoachingConsent component.
* Fetching data is still done through actions provided by redux.
* This change does not effect the fields in the root account settings
page

* Add tests for coaching consent form
2020-06-24 10:37:58 -04:00
Bianca Severino
a2388bffc2 Merge pull request #245 from edx/bseverino/idv-ui
IDV UI improvements
2020-06-22 16:46:59 -04:00
Bianca Severino
ebe6af0913 Implemented UI improvements
Fixed padding on 'next' button and collapsed camera help text

Added functionality to submit button

Styling edit to differentiate cards

Removed todo comments
2020-06-22 14:11:20 -04:00
edX Transifex Bot
fd6ba7847a fix(i18n): update translations 2020-06-21 17:05:29 -04:00
Renovate Bot
f051905da1 fix(deps): update dependency @fortawesome/fontawesome-svg-core to v1.2.29 2020-06-18 21:45:19 +00:00
Olivia Ruiz-Knott
5eddb35e0b Merge pull request #242 from edx/ork/MICROBA-309_collect-demographics-on-account-page
MICROBA-309 Collect demographics on account page
2020-06-18 17:17:12 -04:00
Olivia Ruiz-Knott
0196245c13 MICROBA-309 Collect demographics on account page
Add new fields to account page for demographics collection

MICROBA-309
2020-06-18 17:07:54 -04:00
Renovate Bot
0c53a29094 fix(deps): update dependency formdata-polyfill to v3.0.20 2020-06-17 21:13:19 +00:00
Renovate Bot
60643f6215 fix(deps): update dependency @fortawesome/react-fontawesome to v0.1.11 2020-06-16 23:14:18 +00:00
Renovate Bot
b0aa91fc98 fix(deps): update dependency qs to v6.9.4 2020-06-16 20:21:47 +00:00
Bianca Severino
09813fa689 Merge pull request #210 from edx/hack/idv
Hackathon XXIV -- Add ID verification page
2020-06-16 15:57:16 -04:00
Bianca Severino
b05a40d0aa Code cleanup and i18n compatibility
Replaced hardcoded text with i18n components

Added title to confirmation button for a/b testing purposes

Redirect the user to the beginning of the process on reload to avoid losing camera access, also removed unused code

Update src/id-verification/IdVerificationContext.jsx, make children prop required

Co-authored-by: Kyle McCormick <kmccormick@edx.org>

Remove todo comment in ImageFileUpload.jsx

Added comment to service.js and removed unused code from index.js
2020-06-16 15:46:19 -04:00
Kyle McCormick
1a76468587 Add ID verification page
Fix 404 page from displaying when it shouldn't

Was displaying below the settings page itself

Add panels for each step of the verification flow

Add IdVerificationContext

Add BasePanel

Revert "Add BasePanel"

This reverts commit 2ddca14b4b8b35cd23f0525f49070753c09d361d.

Add BasePanel

Focus heading elements on mount

Add current account name to GetNameIdPanel

Fix nested button focus problem

Add a friendly camera permission request

Remove double ctas

lint

Add initial service.js file; not tested

Add image file uploads

fix temp heading of photo context panel

Add photo id name input

Add redirect logic for users who find themselves too far in the flow

add wip SubmittedPanel

Wire up Submit button

Update routing-utilities.js

Preview image on summarypenl; still buggy

Add some content and styles

update first panel content

Update headings

Use ImagePreview component on SummaryPanel

Fix service.js; it works now

More content and fix for name input

ImagePreview: Change 'name' to 'alt'

Add content to camera request

Fix ImagePreview alt text

Iterating on SummaryPage

Add privacy info modal

Update name entry

Make the footer sticky to the bottom

Update upload photo screens

Add photo to name panel

camera component

overlay camera button

implement camera component

remove camera tracking

Add retake photo buttons

Add service for verification status endpoint

delete old css

lint fix

Better name edit state

portrait photo page

Add edit name from summary

Add content to submitted screen

lint

Rename status service function

Add clarity to summary view text

First pass at gating based on existing idv

Clean up IDv-status-gating screens

finish up webcam/photo pages

remove unused tracking library
2020-06-10 15:02:27 -04:00
Renovate Bot
93ef8d2b04 fix(deps): update dependency @fortawesome/react-fontawesome to v0.1.10 2020-06-02 18:19:14 +00:00
edX Transifex Bot
98efe2649a fix(i18n): update translations 2020-05-31 17:04:50 -04:00
AsadAzam
80cc8f156f Merge pull request #234 from edx/asad/prod-1542
Added invalid password check on delete
2020-05-29 17:27:33 +05:00
Asad
1364ca5711 Added invalid password check on delete 2020-05-29 14:28:20 +05:00
edX Transifex Bot
c337e03a4d fix(i18n): update translations 2020-05-24 17:04:37 -04:00
Thomas Tracy
c6aedbea29 MB-366 don't save non eligible coaching number (#230)
* MB-366 don't save non eligible coaching number

This fixes a few little bugs in the system as well as the one listed in
the ticket.

* We no longer make an extra API call to user profile when toggling
coaching
* The user now must toggle coaching off to save a non-eligible phone
number
* If the user changes to another US based phone number, or is not
consented to coaching, the phone number will save as normal.

* Fix weird code
2020-05-19 10:35:21 -04:00
edX Transifex Bot
6e099457db fix(i18n): update translations 2020-05-17 17:04:24 -04:00
Olivia Ruiz-Knott
54fa5b970a Merge pull request #220 from edx/ork/MICROBA-311_add-state-to-account-settings
MICROBA-311 Add state field to account settings
2020-05-13 10:12:49 -04:00
Olivia Ruiz-Knott
38371d1a46 MICROBA-311 Add state field to account settings
Add state field dropdown to account settings page if country is US;
add states list; handle localization
2020-05-12 14:56:43 -04:00
edX Transifex Bot
f805480e21 fix(i18n): update translations 2020-05-11 02:01:25 -04:00
Matt Tuchfarber
cc3bf06a6f Merge pull request #227 from edx/tuchfarber/fix_coaching_form_for_ents
Only save coaching name if not managed profile
2020-05-08 14:33:42 -04:00
Matt Tuchfarber
0e8d7622c6 Only save coaching name if not managed profile
Managed profiles don't allow the user to change their name so we disable
the name input and skip name submission during coaching signup.
2020-05-08 14:28:21 -04:00
Waheed Ahmed
9faf28203b Merge pull request #226 from edx/waheed/PROD-1427-handle-403-password-reset-response
Handle 403 password reset response.
2020-05-07 23:35:10 +05:00
Waheed Ahmed
e94d05d9e2 Handle 403 password reset response.
Recently we have changed rate limiting configuration for password reset
endpoint to one request per email and IP. I have added the frontend
functionality to show proper error message to users.

PROD-1427
2020-05-07 22:39:36 +05:00
Thomas Tracy
8b4f7818fc Fix coaching toggle when phone number not saved (#225) 2020-05-06 14:57:07 -04:00
Thomas Tracy
f18c71d13a Prevent phone_number save on coaching save failure (#222)
Originally, we had it set up so that the coaching consent form saved 33
values in parallel. This add a new saga (saveMultipleSettings) to save
those 3 values in sync. This way, if one fails, the rest of the calls
fail.

Something to watch out for here: the order matters. The array that you
give the save function must be ordered in the way you need the data
saved.
2020-05-06 10:43:19 -04:00
Matt Tuchfarber
a1aeb7035e Merge pull request #224 from edx/tuchfarber/fix_coaching_api_call
Fix coaching data API call
2020-05-05 16:40:50 -04:00
Matt Tuchfarber
ef85448e27 Fix coaching data API call
Was mixing extraction of data key in call
2020-05-05 16:10:30 -04:00
Matt Tuchfarber
741cfb6aac Merge pull request #221 from edx/tuchfarber/catch_403_for_inactive_user
Catch 403 for inactive user on coaching API
2020-05-05 11:35:00 -04:00
Matt Tuchfarber
c808c8c0a1 Catch 403 for inactive user on coaching API 2020-05-05 11:14:05 -04:00
Matt Tuchfarber
4eb96e64c7 Merge pull request #218 from edx/tuchfarber/reset_coaching_submission_states
Reset coaching submission state on submit
2020-05-04 11:00:19 -04:00
Albert (AJ) St. Aubin
f1ec989054 Merge pull request #219 from edx/aj/coaching_text_change
Update to the text used by the Coarching account settings
2020-05-04 08:44:18 -04:00
Albert (AJ) St. Aubin
54032e6ec5 Update to the text used by the Coarching account settings 2020-05-01 15:45:44 -04:00
Matt Tuchfarber
ef5c303fbc Reset coaching submission state on submit 2020-05-01 14:51:51 -04:00
Thomas Tracy
caa06a08b0 MICROBA-hotfix: coaching sign-up save phone number (#217)
Before this fix, we were having an issues where upon the first time a
user signs up with the coaching form, the phone number would not save.
This was because of the way we patch the user in the form. The phone
number was saving properly, then getting overwritten with `null`.

This fixes that issue, and also cleans up an error message.
2020-05-01 14:14:50 -04:00
Mike OConnell
5e4278ea5a Merge pull request #216 from edx/sameen/fix-account-settings-bug
Handled no enterprise returned case
2020-05-01 11:43:23 -04:00
sameenfatima78
96dc3f7e3f fixed-account-setting-bug 2020-05-01 20:19:46 +05:00
Matt Tuchfarber
5726be2805 Merge pull request #213 from edx/tuchfarber/default_consent_false
Default eligble for coaching to false
2020-04-27 17:07:58 -04:00
Matt Tuchfarber
73c66d5d18 Default eligble for coaching to false
A user without coaching data should no longer see the toggle
2020-04-27 16:43:58 -04:00
Sameen Fatima
19ef66cf42 Merge pull request #211 from edx/sameen/ENT-2741
ENT-2741: Ent name on account settings should be of current ent
2020-04-24 12:57:24 +05:00
sameenfatima78
c83d76e1a9 ENT-2741-Fixed-Enterprise-Name-Banner-scenario
incorporated-feedback

fixed-lint-issue

incorporated-additional-feedback
2020-04-23 18:05:36 +05:00
David Joy
2f2abd54ff Fixing logout URLs for dev and test. (#212) 2020-04-21 12:56:16 -04:00
71 changed files with 10944 additions and 6372 deletions

1
.env
View File

@@ -5,6 +5,7 @@ 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

View File

@@ -1,5 +1,5 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:19000/account/'
BASE_URL='localhost:1997'
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
@@ -8,13 +8,15 @@ LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
MARKETING_SITE_BASE_URL='http://localhost:18000'
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
NODE_ENV='development'
ORDER_HISTORY_URL='localhost:19000/ecommerce/orders'
PORT=1997 # For standalone dev server only.
ORDER_HISTORY_URL='localhost:1996/orders'
PORT=1997
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
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=''
COACHING_ENABLED=true
ENABLE_DEMOGRAPHICS_COLLECTION=true

View File

@@ -6,8 +6,9 @@ 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/login'
LOGOUT_URL='http://localhost:18000/logout'
MARKETING_SITE_BASE_URL='http://localhost:18000'
DEMOGRAPHICS_BASE_URL='http://localhost:18360'
NODE_ENV=null
ORDER_HISTORY_URL='localhost:1996/orders'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
@@ -16,3 +17,4 @@ SITE_NAME='edX'
SUPPORT_URL='http://localhost:18000/support'
USER_INFO_COOKIE_NAME='edx-user-info'
COACHING_ENABLED=''
ENABLE_DEMOGRAPHICS_COLLECTION=''

8995
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,6 @@
},
"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",
@@ -34,17 +33,18 @@
"@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/fontawesome-svg-core": "1.2.29",
"@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.9",
"@fortawesome/react-fontawesome": "0.1.11",
"babel-polyfill": "6.26.0",
"classnames": "2.2.6",
"font-awesome": "4.7.0",
"form-urlencoded": "4.0.1",
"formdata-polyfill": "3.0.19",
"formdata-polyfill": "3.0.20",
"history": "4.10.1",
"jslib-html5-camera-photo": "^3.1.3",
"lodash.camelcase": "4.3.0",
"lodash.debounce": "4.0.8",
"lodash.findindex": "4.6.0",
@@ -53,10 +53,12 @@
"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.4",
"react": "16.10.2",
"react-dom": "16.10.2",
"react-redux": "7.1.3",
@@ -74,7 +76,7 @@
"universal-cookie": "4.0.3"
},
"devDependencies": {
"@edx/frontend-build": "github:edx/frontend-build#kdmccormick/devstack-frontends",
"@edx/frontend-build": "2.0.6",
"codecov": "3.6.5",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.2",

View File

@@ -31,9 +31,12 @@ 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) {
@@ -82,11 +85,15 @@ class AccountSettingsPage extends React.Component {
return concatTimeZoneOptions;
});
getLocalizedOptions = memoize(locale => ({
getLocalizedOptions = memoize((locale, country) => ({
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']),
@@ -209,11 +216,15 @@ class AccountSettingsPage extends React.Component {
// Memoized options lists
const {
countryOptions,
stateOptions,
languageProficiencyOptions,
yearOfBirthOptions,
educationLevelOptions,
genderOptions,
} = this.getLocalizedOptions(this.context.locale);
} = this.getLocalizedOptions(this.context.locale, this.props.formValues.country);
// Show State field only if the country is US (could include Canada later)
const showState = this.props.formValues.country == COUNTRY_WITH_STATES;
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
this.props.timeZoneOptions,
@@ -294,6 +305,22 @@ class AccountSettingsPage extends React.Component {
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">
@@ -337,7 +364,9 @@ class AccountSettingsPage extends React.Component {
/>
}
</div>
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION &&
<DemographicsSection />
}
<div className="account-section" id="social-media">
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.social.media'])}
@@ -488,11 +517,11 @@ AccountSettingsPage.propTypes = {
social_link_facebook: PropTypes.string,
social_link_twitter: PropTypes.string,
time_zone: PropTypes.string,
coaching: PropTypes.objectOf(PropTypes.shape({
coaching_consent: PropTypes.string.isRequired,
coaching: PropTypes.shape({
coaching_consent: PropTypes.bool.isRequired,
user: PropTypes.number.isRequired,
eligible_for_coaching: PropTypes.bool.isRequired,
})),
}),
}).isRequired,
siteLanguage: PropTypes.shape({
previousValue: PropTypes.string,

View File

@@ -46,6 +46,11 @@ 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',
@@ -156,6 +161,21 @@ 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',
@@ -272,6 +292,7 @@ const messages = defineMessages({
defaultMessage: 'Select a Language',
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',

View File

@@ -23,6 +23,7 @@ function EditableField(props) {
emptyLabel,
type,
value,
userSuppliedValue,
options,
saveState,
error,
@@ -66,15 +67,22 @@ function EditableField(props) {
const renderValue = (rawValue) => {
if (!rawValue) return renderEmptyLabel();
let value = rawValue;
if (options) {
// Use == instead of === to prevent issues when HTML casts numbers as strings
// eslint-disable-next-line eqeqeq
const selectedOption = options.find(option => option.value == rawValue);
if (selectedOption) return selectedOption.label;
if (selectedOption) {
value = selectedOption.label;
};
}
return rawValue;
if (userSuppliedValue) {
value += `: ${userSuppliedValue}`;
}
return value;
};
const renderConfirmationMessage = () => {
@@ -106,6 +114,7 @@ function EditableField(props) {
options={options}
{...others}
/>
<>{others.children}</>
</ValidationFormGroup>
<p>
<StatefulButton
@@ -162,6 +171,7 @@ EditableField.propTypes = {
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]),

View File

@@ -4,6 +4,7 @@ import { NavHashLink } from 'react-router-hash-link';
import Scrollspy from 'react-scrollspy';
import messages from './AccountSettingsPage.messages';
import { getConfig } from '@edx/frontend-platform';
function JumpNav({ intl }) {
@@ -13,6 +14,7 @@ function JumpNav({ intl }) {
items={[
'basic-information',
'profile-information',
'demographics-information',
'social-media',
'site-preferences',
'linked-accounts',
@@ -31,6 +33,13 @@ function JumpNav({ intl }) {
{intl.formatMessage(messages['account.settings.section.profile.information'])}
</NavHashLink>
</li>
{getConfig().ENABLE_DEMOGRAPHICS_COLLECTION &&
<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'])}

View File

@@ -7,12 +7,14 @@ 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, saveSettings } from '../data/actions';
import { fetchSettings } from '../data/actions';
import { coachingConsentPageSelector } from '../data/selectors';
const Logo = ({ src, alt, ...attributes }) => (
@@ -57,104 +59,75 @@ class CoachingConsent extends React.Component {
formErrors: {},
formSubmitted: false,
declineSubmitted: false,
allSubmissionsComplete: false,
submissionSuccess: 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 handleSubmit(e) {
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) {
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,
});
// 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,
});
declineSubmitted: false,
}, () => this.patchUsingCoachingConsentForm(body));
}
async declineCoaching(e) {
declineCoaching(e) {
e.preventDefault();
this.setState({
formErrors: {},
declineSubmitted: true,
});
// 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,
const body = {
coaching_consent: false,
consent_form_seen: true,
});
};
this.setState({
formErrors: {},
formSubmitted: false,
declineSubmitted: true,
}, () => this.patchUsingCoachingConsentForm(body));
}
renderView(currentView) {
@@ -168,6 +141,7 @@ class CoachingConsent extends React.Component {
formErrors={this.state.formErrors}
formValues={this.props.formValues}
redirectUrl={this.state.redirectUrl}
profileDataManager={this.props.profileDataManager}
/>);
case VIEWS.SUCCESS_PENDING:
return <PageLoading srMessage="Submitting..." />;
@@ -191,18 +165,17 @@ 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.allSubmissionsComplete) {
if (this.state.submissionSuccess) {
currentView = VIEWS.SUCCESS;
} else {
currentView = VIEWS.SUCCESS_PENDING;
}
} else if (this.state.declineSubmitted && !formHasErrors) {
if (this.state.allSubmissionsComplete) {
if (this.state.submissionSuccess) {
currentView = VIEWS.DECLINED;
} else {
currentView = VIEWS.DECLINE_PENDING;
@@ -260,6 +233,8 @@ AutoRedirect.propTypes = {
CoachingConsent.defaultProps = {
loaded: false,
saveState: undefined,
profileDataManager: null,
};
CoachingConsent.propTypes = {
@@ -284,10 +259,10 @@ CoachingConsent.propTypes = {
phone_number: PropTypes.object,
}).isRequired,
fetchSettings: PropTypes.func.isRequired,
saveSettings: PropTypes.func.isRequired,
saveState: PropTypes.string,
profileDataManager: PropTypes.string,
};
export default connect(coachingConsentPageSelector, {
fetchSettings,
saveSettings,
})(injectIntl(CoachingConsent));

View File

@@ -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 will receive a text message confirmation.",
defaultMessage: "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
description: 'Text announcing that you have signed up and will receive texts',
},
'account.settings.coaching.consent.success.continue': {
@@ -56,6 +56,11 @@ 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;

View File

@@ -1,15 +1,29 @@
import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape, FormattedMessage } 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">
@@ -19,12 +33,17 @@ const CoachingForm = props => (
<div>
<form onSubmit={props.onSubmit}>
<div className="py-3">
<ErrorMessage message={props.formErrors.name} />
{
!!props.profileDataManager &&
<ManagedProfileAlert profileDataManager={props.profileDataManager} />
}
<ErrorMessage message={props.formErrors.full_name} />
<label className="h6" htmlFor="fullName">{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}</label>
<Input
type="text"
name="full-name"
id="fullName"
disabled={!!props.profileDataManager}
defaultValue={props.formValues.name}
/>
</div>
@@ -33,7 +52,7 @@ const CoachingForm = props => (
<label className="h6" htmlFor="phoneNumber">{props.intl.formatMessage(messages['account.settings.coaching.consent.label.phone-number'])}</label>
<Input
type="text"
name="full-name"
name="phone_number"
id="phoneNumber"
defaultValue={props.formValues.phone_number}
/>
@@ -87,10 +106,11 @@ CoachingForm.propTypes = {
}).isRequired,
formErrors: PropTypes.shape({
coaching: PropTypes.string,
name: PropTypes.string,
full_name: PropTypes.string,
phone_number: PropTypes.string,
}),
redirectUrl: PropTypes.string.isRequired,
profileDataManager: PropTypes.string.isRequired,
};
ErrorMessage.defaultProps = {
@@ -101,4 +121,9 @@ ErrorMessage.propTypes = {
message: PropTypes.string,
};
ManagedProfileAlert.propTypes = {
profileDataManager: PropTypes.string.isRequired,
intl: intlShape.isRequired,
};
export default injectIntl(CoachingForm);

View File

@@ -5,7 +5,7 @@ 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 } from '../data/actions';
import { saveSettings, updateDraft, saveMultipleSettings } from '../data/actions';
import EditableField from '../EditableField';
@@ -18,7 +18,25 @@ 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={props.saveSettings}
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,
},
]);
}
return props.saveSettings('phone_number', props.phone_number);
}}
/>
<ValidationFormGroup
for="coachingConsent"
@@ -37,9 +55,11 @@ 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 = {
...props.coaching,
phone_number: props.phone_number,
user,
eligible_for_coaching,
coaching_consent: e.target.checked,
};
props.saveSettings(name, value);
@@ -53,18 +73,20 @@ const CoachingToggle = props => (
CoachingToggle.defaultProps = {
phone_number: '',
error: '',
saveState: undefined,
};
CoachingToggle.propTypes = {
name: PropTypes.string.isRequired,
error: PropTypes.string,
coaching: PropTypes.objectOf(PropTypes.shape({
coaching_consent: PropTypes.string.isRequired,
coaching: PropTypes.shape({
coaching_consent: PropTypes.bool.isRequired,
user: PropTypes.number.isRequired,
eligible_for_coaching: PropTypes.bool.isRequired,
})).isRequired,
saveState: PropTypes.func.isRequired,
}).isRequired,
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
saveSettings: PropTypes.func.isRequired,
saveMultipleSettings: PropTypes.func.isRequired,
updateDraft: PropTypes.func.isRequired,
intl: intlShape.isRequired,
phone_number: PropTypes.string,
@@ -73,4 +95,5 @@ CoachingToggle.propTypes = {
export default connect(editableFieldSelector, {
saveSettings,
updateDraft,
saveMultipleSettings,
})(injectIntl(CoachingToggle));

View File

@@ -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 in English and Spanish languages. 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 to learners with U.S. mobile phone numbers. 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': {

View File

@@ -1,5 +1,6 @@
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
@@ -7,21 +8,20 @@ import { getConfig } from '@edx/frontend-platform';
* @param {Number} userId users are identified in the api by LMS id
*/
export async function getCoachingPreferences(userId) {
let data = null;
let data = {};
try {
({ data } = await getAuthenticatedHttpClient()
.get(`${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`));
} catch (error) {
// 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.
// If a user isn't active the API call will fail with a lack of credentials.
data = {
coaching_consent: false,
user: userId,
eligible_for_coaching: true,
eligible_for_coaching: false,
consent_form_seen: false,
};
}
return data;
}
@@ -40,9 +40,11 @@ export async function patchCoachingPreferences(userId, commitValues) {
.catch((error) => {
const apiError = Object.create(error);
apiError.fieldErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
// eslint-disable-next-line prefer-destructuring
apiError.fieldErrors.coaching = apiError.fieldErrors.phone_number[0];
delete apiError.fieldErrors.phone_number;
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;
}
throw apiError;
});
return commitValues;

View File

@@ -0,0 +1,104 @@
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', () => {
return 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();
});
});

View File

@@ -0,0 +1,282 @@
// 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"
>
Lets 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 youre interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*
</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="btn w-100 btn-outline-primary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
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"
>
Lets 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 youre interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*
</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="btn w-100 btn-outline-primary"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
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>
`;

View File

@@ -2,6 +2,7 @@ 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';
@@ -99,6 +100,25 @@ 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 => ({

View File

@@ -31,4 +31,173 @@ 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 SELF_DESCRIBE = 'self-describe';
export const DEMOGRAPHICS_GENDER_OPTIONS = [
'',
'woman',
'man',
'non-binary',
SELF_DESCRIBE,
];
export const OTHER = 'other';
export const DEMOGRAPHICS_ETHNICITY_OPTIONS = [
'american-indian-or-alaska-native',
'asian',
'black-or-african-american',
'hispanic-latin-spanish',
'middle-eastern-or-north-african',
'native-hawaiian-or-pacific-islander',
'white',
OTHER,
];
export const DEMOGRAPHICS_INCOME_OPTIONS = [
'',
'less-than-10k',
'10k-25k',
'25k-50k',
'50k-75k',
'over-100k',
'unsure',
];
export const DEMOGRAPHICS_MILITARY_HISTORY_OPTIONS = [
'',
'never-served',
'training',
'active',
'previously-active',
];
export const DEMOGRAPHICS_EDUCATION_LEVEL_OPTIONS = [
'',
'no-high-school',
'some-high-school',
'high-school-ged-equivalent',
'some-college',
'associates',
'bachelors',
'masters',
'professional',
'doctorate',
];
export const DEMOGRAPHICS_WORK_STATUS_OPTIONS = [
'',
'full-time',
'part-time',
'not-employed-looking',
'not-employed-not-looking',
'unable',
'retired',
'other',
];
export const DEMOGRAPHICS_WORK_SECTOR_OPTIONS = [
'',
'accommodation-food',
'administrative-support-waste-remediation',
'agriculture-forestry-fishing-hunting',
'arts-entertainment-recreation',
'construction',
'educational',
'finance-insurance',
'healthcare-social',
'information',
'management',
'manufacturing',
'mining-quarry-oil-gas',
'professional-scientific-technical',
'public-admin',
'real-estate',
'retail',
'transport-warehousing',
'utilities',
'trade',
'other',
];
export const DECLINED = 'declined';

View File

@@ -7,6 +7,7 @@ import {
SAVE_PREVIOUS_SITE_LANGUAGE,
UPDATE_DRAFT,
RESET_DRAFTS,
SAVE_MULTIPLE_SETTINGS,
} from './actions';
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from '../delete-account';
@@ -144,6 +145,24 @@ 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: Object.assign({}, state.errors, action.payload.errors),
};
case FETCH_TIME_ZONES.SUCCESS:
return {
@@ -177,6 +196,7 @@ const reducer = (state = defaultState, action) => {
case RESET_PASSWORD.BEGIN:
case RESET_PASSWORD.SUCCESS:
case RESET_PASSWORD.FORBIDDEN:
return {
...state,
resetPassword: resetPasswordReducer(state.resetPassword, action),

View File

@@ -12,6 +12,7 @@ import {
fetchSettingsFailure,
closeForm,
SAVE_SETTINGS,
SAVE_MULTIPLE_SETTINGS,
saveSettingsBegin,
saveSettingsSuccess,
saveSettingsFailure,
@@ -19,6 +20,9 @@ import {
FETCH_TIME_ZONES,
fetchTimeZones,
fetchTimeZonesSuccess,
saveMultipleSettingsBegin,
saveMultipleSettingsSuccess,
saveMultipleSettingsFailure,
} from './actions';
// Sub-modules
@@ -100,6 +104,35 @@ 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));
@@ -109,6 +142,7 @@ export function* handleFetchTimeZones(action) {
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(),

View File

@@ -1,6 +1,5 @@
import { createSelector, createStructuredSelector } from 'reselect';
import { siteLanguageOptionsSelector, siteLanguageListSelector } from '../site-language';
import { siteLanguageListSelector, siteLanguageOptionsSelector } from '../site-language';
export const storeName = 'accountSettings';
@@ -167,6 +166,7 @@ export const coachingConsentPageSelector = createSelector(
accountSettingsSelector,
formValuesSelector,
activeAccountSelector,
profileDataManagerSelector,
saveStateSelector,
confirmationValuesSelector,
errorSelector,
@@ -174,6 +174,7 @@ export const coachingConsentPageSelector = createSelector(
accountSettings,
formValues,
activeAccount,
profileDataManager,
saveState,
confirmationValues,
errors,
@@ -182,9 +183,25 @@ 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,
}),
);

View File

@@ -1,12 +1,14 @@
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, patchDemographics } from '../demographics/data/service';
const SOCIAL_PLATFORMS = [
{ id: 'twitter', key: 'social_link_twitter' },
@@ -137,12 +139,11 @@ 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 ('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;
}
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;
}
}
}
@@ -162,6 +163,7 @@ export async function getSettings(username, userRoles, userId) {
getProfileDataManager(username, userRoles),
getTimeZones(),
getConfig().COACHING_ENABLED && getCoachingPreferences(userId),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId),
]);
return {
@@ -171,6 +173,7 @@ export async function getSettings(username, userRoles, userId) {
profileDataManager: results[3],
timeZones: results[4],
coaching: results[5],
...results[6], // demographics
};
}
@@ -184,9 +187,11 @@ 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 accountCommitValues = omit(commitValues, preferenceKeys);
const isDemographicsKey = (value, key) => key.includes('demographics');
const accountCommitValues = omit(commitValues, preferenceKeys, coachingKeys);
const preferenceCommitValues = pick(commitValues, preferenceKeys);
const coachingCommitValues = pick(commitValues, coachingKeys);
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
const patchRequests = [];
if (!isEmpty(accountCommitValues)) {
@@ -198,6 +203,9 @@ 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

View File

@@ -27,6 +27,10 @@ export class AsyncActionType {
get RESET() {
return `${this.topic}__${this.name}__RESET`;
}
get FORBIDDEN() {
return `${this.topic}__${this.name}__FORBIDDEN`;
}
}
/**

View File

@@ -12,6 +12,7 @@ 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');
});
});

View File

@@ -19,6 +19,8 @@ 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';
}

View File

@@ -15,7 +15,9 @@ export function* handleDeleteAccount(action) {
const response = yield call(postDeleteAccount, action.payload.password);
yield put(deleteAccountSuccess(response));
} catch (e) {
if (typeof e.response.data === 'string') {
if (e.response.status === 403) {
yield put(deleteAccountFailure('invalid-password'));
} else if (typeof e.response.data === 'string') {
yield put(deleteAccountFailure());
} else {
throw e;

View File

@@ -91,6 +91,11 @@ 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.',

View File

@@ -0,0 +1,78 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { CheckBox } from '@edx/paragon';
import { DECLINED } from '../data/constants';
export 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 = () => {
return options.map((option, index) => {
const isFirst = index == 0;
const isChecked = selected.includes(option.value);
return (
<div key={index} className="checkboxOption">
<CheckBox
type="checkbox"
id={option.value}
name={option.value}
value={option.value}
checked={isChecked}
autoFocus={isFirst}
label={option.label}
onChange={(value) => handleToggle(value, option.value)}
/>
</div>
)
})
}
return (
<div role="group">
{renderCheckboxes()}
</div>
)
}
Checkboxes.propTypes = {
id: PropTypes.string,
options: PropTypes.array,
values: PropTypes.array,
onChange: PropTypes.func,
};
Checkboxes.defaultProps = {
options: [],
values: [],
}
export default Checkboxes;

View File

@@ -0,0 +1,308 @@
import {
DECLINED,
DEMOGRAPHICS_EDUCATION_LEVEL_OPTIONS,
DEMOGRAPHICS_ETHNICITY_OPTIONS,
DEMOGRAPHICS_GENDER_OPTIONS,
DEMOGRAPHICS_INCOME_OPTIONS,
DEMOGRAPHICS_MILITARY_HISTORY_OPTIONS,
DEMOGRAPHICS_WORK_SECTOR_OPTIONS,
DEMOGRAPHICS_WORK_STATUS_OPTIONS,
OTHER,
SELF_DESCRIBE,
} from '../data/constants';
import {
FormattedMessage,
injectIntl,
intlShape,
} from '@edx/frontend-platform/i18n';
import { saveMultipleSettings, updateDraft } from '../data/actions';
import Alert from '../Alert';
import Checkboxes from './Checkboxes';
import EditableField from '../EditableField';
import { Input } from '@edx/paragon';
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { demographicsSectionSelector } from '../data/selectors';
import get from 'lodash.get';
import isEmpty from 'lodash.isempty';
import memoize from 'memoize-one';
import messages from './DemographicsSection.messages';
class DemographicsSection extends React.Component {
constructor(props, context) {
super(props, context);
this.alertRef = React.createRef();
}
componentDidUpdate() {
if(!isEmpty(this.props.formErrors)) {
this.alertRef.current.focus();
}
}
getLocalizedOptions = memoize((locale) => ({
demographicsGenderOptions: DEMOGRAPHICS_GENDER_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.gender.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
demographicsEthnicityOptions: DEMOGRAPHICS_ETHNICITY_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.ethnicity.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
demographicsIncomeOptions: DEMOGRAPHICS_INCOME_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.income.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
demographicsMilitaryHistoryOptions: DEMOGRAPHICS_MILITARY_HISTORY_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.military_history.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
demographicsEducationLevelOptions: DEMOGRAPHICS_EDUCATION_LEVEL_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.education_level.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
demographicsWorkStatusOptions: DEMOGRAPHICS_WORK_STATUS_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.work_status.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
demographicsWorkSectorOptions: DEMOGRAPHICS_WORK_SECTOR_OPTIONS.map(key => ({
value: key,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.work_sector.options.${key || 'empty'}`]),
})).concat(this.getDeclinedOption()),
}));
getDeclinedOption() {
return [{
value: DECLINED,
label: this.props.intl.formatMessage(messages[`account.settings.field.demographics.options.declined`])
}]
}
ethnicityFieldDisplay = () => {
if (get(this, 'props.formValues.demographics_user_ethnicity')) {
const ethnicities = this.props.formValues.demographics_user_ethnicity;
return ethnicities.map((e) => {
if (e == DECLINED) {
return this.props.intl.formatMessage(messages[`account.settings.field.demographics.options.declined`]);
}
return this.props.intl.formatMessage(messages[`account.settings.field.demographics.ethnicity.options.${e}`]);
}).join(", ")
}
}
handleEditableFieldChange = (name, value) => {
this.props.updateDraft(name, value);
};
handleSubmit = (formId, values) => {
// We have some custom fields in this section. Instead of relying on the
// submitted values, submit the values stored in 'drafts'.
const drafts = this.props.drafts;
const settingsArray = []
for (let field in drafts) {
settingsArray.push({
formId: field,
commitValues: drafts[field]
})
}
this.props.saveMultipleSettings(settingsArray, formId);
};
/**
* If an error is encountered when trying to communicate with the Demographics IDA then we will
* display an Alert letting the user know that their info will not be retrieved or displayed
* and temporarily cannot be updated.
*/
renderDemographicsServiceIssueWarning() {
if (!isEmpty(this.props.formErrors)) {
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>
);
} else {
return null;
}
}
render() {
const editableFieldProps = {
onChange: this.handleEditableFieldChange,
onSubmit: this.handleSubmit,
};
const {
demographicsGenderOptions,
demographicsEthnicityOptions,
demographicsIncomeOptions,
demographicsMilitaryHistoryOptions,
demographicsEducationLevelOptions,
demographicsWorkStatusOptions,
demographicsWorkSectorOptions,
} = this.getLocalizedOptions(this.context.locale);
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">
<h2 className="section-heading">
{this.props.intl.formatMessage(messages['account.settings.section.demographics.information'])}
</h2>
{this.renderDemographicsServiceIssueWarning()}
<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()}
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>
)
}
};
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,
}).isRequired,
formErrors: PropTypes.shape({
demographicsError: PropTypes.string,
}).isRequired,
updateDraft: PropTypes.func.isRequired
};
export default connect(demographicsSectionSelector, {
saveMultipleSettings,
updateDraft,
})(injectIntl(DemographicsSection));

View File

@@ -0,0 +1,456 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'account.settings.section.demographics.information': {
id: 'account.settings.section.demographics.information',
defaultMessage: 'Optional Information',
description: 'The optional information section heading.',
},
'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.options.woman': {
id: 'account.settings.field.demographics.gender.options.woman',
defaultMessage: 'Woman',
description: 'The label for the woman gender identity option.',
},
'account.settings.field.demographics.gender.options.man': {
id: 'account.settings.field.demographics.gender.options.man',
defaultMessage: 'Man',
description: 'The label for the man gender identity option.',
},
'account.settings.field.demographics.gender.options.non-binary': {
id: 'account.settings.field.demographics.gender.options.non-binary',
defaultMessage: 'Non-binary',
description: 'The label for the non-binary gender identity option.',
},
'account.settings.field.demographics.gender.options.self-describe': {
id: 'account.settings.field.demographics.gender.options.self-describe',
defaultMessage: 'Prefer to self-describe',
description: 'The label for self-describe gender identity option.',
},
'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.',
},
'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.',
},
'account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native': {
id: 'account.settings.field.demographics.ethnicity.options.american-indian-or-alaska-native',
defaultMessage: 'American Indian or Alaska Native',
description: 'The label for the American Indian or Alaska Native ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.asian': {
id: 'account.settings.field.demographics.ethnicity.options.asian',
defaultMessage: 'Asian',
description: 'The label for the Asian ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.black-or-african-american': {
id: 'account.settings.field.demographics.ethnicity.options.black-or-african-american',
defaultMessage: 'Black or African American',
description: 'The label for the Black or African American ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish': {
id: 'account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish',
defaultMessage: 'Hispanic, Latin, or Spanish origin',
description: 'The label for the Hispanic, Latin, or Spanish origin ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african': {
id: 'account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african',
defaultMessage: 'Middle Eastern or North African',
description: 'The label for the Middle Eastern or North African ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander': {
id: 'account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander',
defaultMessage: 'Native Hawaiian or Other Pacific Islander',
description: 'The label for the Native Hawaiian or Other Pacific Islander ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.white': {
id: 'account.settings.field.demographics.ethnicity.options.white',
defaultMessage: 'White',
description: 'The label for the White ethnicity option.',
},
'account.settings.field.demographics.ethnicity.options.other': {
id: 'account.settings.field.demographics.ethnicity.options.other',
defaultMessage: 'Some other race, ethnicity, or origin',
description: 'The label for the Some other race, ethnicity, or origin ethnicity option.',
},
'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.',
},
'account.settings.field.demographics.income.options.less-than-10k': {
id: 'account.settings.field.demographics.income.options.less-than-10k',
defaultMessage: 'Less than US $10,000',
description: 'The label for the less than US $10,000 income option.',
},
'account.settings.field.demographics.income.options.10k-25k': {
id: 'account.settings.field.demographics.income.options.10k-25k',
defaultMessage: 'US $10,000 - $25,000',
description: 'The label for the US $10,000 - $25,000 income option.',
},
'account.settings.field.demographics.income.options.25k-50k': {
id: 'account.settings.field.demographics.income.options.25k-50k',
defaultMessage: 'US $25,000 - $50,000',
description: 'The label for the US $25,000 - $50,000 income option.',
},
'account.settings.field.demographics.income.options.50k-75k': {
id: 'account.settings.field.demographics.income.options.50k-75k',
defaultMessage: 'US $50,000 - $75,000',
description: 'The label for the US $50,000 - $75,000 income option.',
},
'account.settings.field.demographics.income.options.over-100k': {
id: 'account.settings.field.demographics.income.options.over-100k',
defaultMessage: 'Over US $100,000',
description: 'The label for the over US $100,000 income option.',
},
'account.settings.field.demographics.income.options.unsure': {
id: 'account.settings.field.demographics.income.options.unsure',
defaultMessage: 'I don\'t know',
description: 'The label for the I don\'t know income option.',
},
'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.',
},
'account.settings.field.demographics.military_history.options.never-served': {
id: 'account.settings.field.demographics.income.options.never-served',
defaultMessage: 'Never served in the military',
description: 'The label for the never served in the military military history option.',
},
'account.settings.field.demographics.military_history.options.training': {
id: 'account.settings.field.demographics.income.options.training',
defaultMessage: 'Only on active duty for training',
description: 'The label for the only on active duty for training military history option.',
},
'account.settings.field.demographics.military_history.options.active': {
id: 'account.settings.field.demographics.income.options.active',
defaultMessage: 'Now on active duty',
description: 'The label for the now on active duty military history option.',
},
'account.settings.field.demographics.military_history.options.previously-active': {
id: 'account.settings.field.demographics.income.options.previously-active',
defaultMessage: 'On active duty in the past, but not now',
description: 'The label for the on active duty in the past, but not now military history option.',
},
'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.',
},
'account.settings.field.demographics.education_level.options.no-high-school': {
id: 'account.settings.field.demographics.education_level.options.no-high-school',
defaultMessage: 'No High School',
description: 'The label for the no high school education level option.',
},
'account.settings.field.demographics.education_level.options.some-high-school': {
id: 'account.settings.field.demographics.education_level.options.some-high-school',
defaultMessage: 'Some High School',
description: 'The label for the some high school education level option.',
},
'account.settings.field.demographics.education_level.options.high-school-ged-equivalent': {
id: 'account.settings.field.demographics.education_level.options.high-school-ged-equivalent',
defaultMessage: 'High School diploma, GED, or equivalent',
description: 'The label for the high school diploma, GED, or equivalent education level option.',
},
'account.settings.field.demographics.education_level.options.some-college': {
id: 'account.settings.field.demographics.education_level.options.some-college',
defaultMessage: 'Some college, but no degree',
description: 'The label for the some college, but no degree education level option.',
},
'account.settings.field.demographics.education_level.options.some-college': {
id: 'account.settings.field.demographics.education_level.options.some-college',
defaultMessage: 'Some college, but no degree',
description: 'The label for the some college, but no degree education level option.',
},
'account.settings.field.demographics.education_level.options.associates': {
id: 'account.settings.field.demographics.education_level.options.associates',
defaultMessage: 'Associates degree',
description: 'The label for the Associates degree education level option.',
},
'account.settings.field.demographics.education_level.options.bachelors': {
id: 'account.settings.field.demographics.education_level.options.bachelors',
defaultMessage: 'Bachelors degree',
description: 'The label for the Bachelors degree education level option.',
},
'account.settings.field.demographics.education_level.options.masters': {
id: 'account.settings.field.demographics.education_level.options.masters',
defaultMessage: 'Masters degree',
description: 'The label for the Masters degree education level option.',
},
'account.settings.field.demographics.education_level.options.professional': {
id: 'account.settings.field.demographics.education_level.options.professional',
defaultMessage: 'Professional degree',
description: 'The label for the Professional degree education level option.',
},
'account.settings.field.demographics.education_level.options.doctorate': {
id: 'account.settings.field.demographics.education_level.options.doctorate',
defaultMessage: 'Doctorate degree',
description: 'The label for the Doctorate degree education level option.',
},
'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.options.full-time': {
id: 'account.settings.field.demographics.work_status.options.full-time',
defaultMessage: 'Employed, working full-time',
description: 'The label for the employed, working full-time work status option.',
},
'account.settings.field.demographics.work_status.options.part-time': {
id: 'account.settings.field.demographics.work_status.options.part-time',
defaultMessage: 'Employed, working part-time',
description: 'The label for the employed, working part-time work status option.',
},
'account.settings.field.demographics.work_status.options.not-employed-looking': {
id: 'account.settings.field.demographics.work_status.options.not-employed-looking',
defaultMessage: 'Not employed, looking for work',
description: 'The label for the not employed, looking for work work status option.',
},
'account.settings.field.demographics.work_status.options.not-employed-not-looking': {
id: 'account.settings.field.demographics.work_status.options.not-employed-not-looking',
defaultMessage: 'Not employed, not looking for work',
description: 'The label for the not employed, not looking for work work status option.',
},
'account.settings.field.demographics.work_status.options.unable': {
id: 'account.settings.field.demographics.work_status.options.unable',
defaultMessage: 'Unable to work',
description: 'The label for the unable to work work status option.',
},
'account.settings.field.demographics.work_status.options.retired': {
id: 'account.settings.field.demographics.work_status.options.retired',
defaultMessage: 'Retired',
description: 'The label for the retired work status option.',
},
'account.settings.field.demographics.work_status.options.other': {
id: 'account.settings.field.demographics.work_status.options.other',
defaultMessage: 'Other',
description: 'The label for the other work status option.',
},
'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.',
},
'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.',
},
'account.settings.field.demographics.work_sector.options.accommodation-food': {
id: 'account.settings.field.demographics.work_sector.options.accommodation-food',
defaultMessage: 'Accommodation and Food Services',
description: 'The label for the Accommodation and Food Services work sector option.',
},
'account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation': {
id: 'account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation',
defaultMessage: 'Administrative and Support and Waste Management and Remediation Services',
description: 'The label for the Administrative and Support and Waste Management and Remediation Services work sector option.',
},
'account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting': {
id: 'account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting',
defaultMessage: 'Agriculture, Forestry, Fishing and Hunting',
description: 'The label for the Agriculture, Forestry, Fishing and Hunting work sector option.',
},
'account.settings.field.demographics.work_sector.options.arts-entertainment-recreation': {
id: 'account.settings.field.demographics.work_sector.options.arts-entertainment-recreation',
defaultMessage: 'Arts, Entertainment, and Recreation',
description: 'The label for the Arts, Entertainment, and Recreation work sector option.',
},
'account.settings.field.demographics.work_sector.options.construction': {
id: 'account.settings.field.demographics.work_sector.options.construction',
defaultMessage: 'Construction',
description: 'The label for the Construction work sector option.',
},
'account.settings.field.demographics.work_sector.options.educational': {
id: 'account.settings.field.demographics.work_sector.options.educational',
defaultMessage: 'Education Services',
description: 'The label for the Education Services work sector option.',
},
'account.settings.field.demographics.work_sector.options.finance-insurance': {
id: 'account.settings.field.demographics.work_sector.options.finance-insurance',
defaultMessage: 'Finance and Insurance',
description: 'The label for the Finance and Insurance work sector option.',
},
'account.settings.field.demographics.work_sector.options.healthcare-social': {
id: 'account.settings.field.demographics.work_sector.options.healthcare-social',
defaultMessage: 'Health Care and Social Assistance',
description: 'The label for the Health Care and Social Assistance work sector option.',
},
'account.settings.field.demographics.work_sector.options.information': {
id: 'account.settings.field.demographics.work_sector.options.information',
defaultMessage: 'Information',
description: 'The label for the Information work sector option.',
},
'account.settings.field.demographics.work_sector.options.management': {
id: 'account.settings.field.demographics.work_sector.options.management',
defaultMessage: 'Management of Companies and Enterprises',
description: 'The label for the Management of Companies and Enterprises work sector option.',
},
'account.settings.field.demographics.work_sector.options.manufacturing': {
id: 'account.settings.field.demographics.work_sector.options.manufacturing',
defaultMessage: 'Manufacturing',
description: 'The label for the Manufacturing work sector option.',
},
'account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas': {
id: 'account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas',
defaultMessage: 'Mining, Quarrying, and Oil and Gas Extraction',
description: 'The label for the Mining, Quarrying, and Oil and Gas Extraction work sector option.',
},
'account.settings.field.demographics.work_sector.options.professional-scientific-technical': {
id: 'account.settings.field.demographics.work_sector.options.professional-scientific-technical',
defaultMessage: 'Professional, Scientific, and Technical Services',
description: 'The label for the Professional, Scientific, and Technical Services work sector option.',
},
'account.settings.field.demographics.work_sector.options.public-admin': {
id: 'account.settings.field.demographics.work_sector.options.public-admin',
defaultMessage: 'Public Administration',
description: 'The label for the Public Administration work sector option.',
},
'account.settings.field.demographics.work_sector.options.real-estate': {
id: 'account.settings.field.demographics.work_sector.options.real-estate',
defaultMessage: 'Real Estate and Rental and Leasing',
description: 'The label for the Real Estate and Rental and Leasing work sector option.',
},
'account.settings.field.demographics.work_sector.options.retail': {
id: 'account.settings.field.demographics.work_sector.options.retail',
defaultMessage: 'Retail Trade',
description: 'The label for the Retail Trade work sector option.',
},
'account.settings.field.demographics.work_sector.options.transport-warehousing': {
id: 'account.settings.field.demographics.work_sector.options.transport-warehousing',
defaultMessage: 'Transportation and Warehousing',
description: 'The label for the Transportation and Warehousing work sector option.',
},
'account.settings.field.demographics.work_sector.options.utilities': {
id: 'account.settings.field.demographics.work_sector.options.utilities',
defaultMessage: 'Utilities',
description: 'The label for the Utilities work sector option.',
},
'account.settings.field.demographics.work_sector.options.trade': {
id: 'account.settings.field.demographics.work_sector.options.trade',
defaultMessage: 'Wholesale Trade',
description: 'The label for the Wholesale Trade work sector option.',
},
'account.settings.field.demographics.work_sector.options.other': {
id: 'account.settings.field.demographics.work_sector.options.other',
defaultMessage: 'Other',
description: 'The label for the Other work sector option.',
},
'account.settings.field.demographics.options.declined': {
id: 'account.settings.field.demographics.options.declined',
defaultMessage: 'Prefer not to respond',
description: 'The label for the declined option.',
},
});
export default messages;

View File

@@ -0,0 +1,102 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import { convertData, TO, FROM } from './utils';
/**
* If there was an error making the PATCH call then we create an apiError object containing a
* 'demographicsError' fieldError. The content of the error itself isn't particularly important.
* This will trigger the `renderDemographicsServiceIssueWarningMessage()` (in
* DemographicsSection.jsx) to display an Alert to let the end-user know that there may be an
* issue communicating with the Demographics service.
*
* @param {Error} error
*/
export function createDemographicsError(error) {
const apiError = Object.create(error);
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 requestUrl = `${getConfig().DEMOGRAPHICS_BASE_URL}/demographics/api/v1/demographics/`;
const commitValues = { user: userId };
let data = {};
({ data } = await getAuthenticatedHttpClient()
.post(requestUrl, commitValues)
.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);
}

View File

@@ -0,0 +1,50 @@
export const TO = 'to';
export const FROM = 'from';
// 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;
}

View File

@@ -0,0 +1,128 @@
import * as auth from '@edx/frontend-platform/auth';
import * as selectors from '../../data/selectors';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import DemographicsSection from '../DemographicsSection';
import { Provider } from 'react-redux';
import React from 'react';
import configureStore from 'redux-mock-store';
import renderer from 'react-test-renderer';
jest.mock('@edx/frontend-platform/auth');
const IntlDemographicsSection = injectIntl(DemographicsSection);
jest.mock('../../data/selectors', () => {
return 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,
},
formErrors: {},
intl: {},
};
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: {
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: {
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: {
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: {
demographics_user_ethnicity: ['hispanic-latin-spanish', 'white']
}
}
const wrapper = renderer.create(reduxWrapper(<IntlDemographicsSection {...props} />)).toJSON();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,3713 @@
// 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>
<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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</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>
<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
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
Prefer not to respond
</p>
<p
className="small text-muted mt-n2"
/>
</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>
<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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add gender 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"
>
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add family income
</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"
>
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add military status
</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"
>
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</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"
>
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</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"
>
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add employment status
</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"
>
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</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"
>
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</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>
<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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add gender 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"
>
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add family income
</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"
>
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add military status
</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"
>
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</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"
>
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</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"
>
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add employment status
</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"
>
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</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"
>
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</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>
<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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add gender 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"
>
Race/Ethnicity identity
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add family income
</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"
>
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add military status
</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"
>
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</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"
>
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</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"
>
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</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"
>
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</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>
<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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add family income
</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"
>
U.S. Military status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add military status
</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"
>
Your education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</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"
>
Parents/Guardians education level
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add education level
</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"
>
Employment status
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add employment status
</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"
>
Current work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</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"
>
Future work industry
</h6>
<button
className="btn ml-3 btn-link"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[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>
<button
className="btn btn-link p-0"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
type="button"
>
Add work industry
</button>
</p>
<p
className="small text-muted mt-n2"
/>
</div>
</div>
</div>
</div>
`;

View File

@@ -0,0 +1,24 @@
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 = (props) => {
return (
<Alert
className="alert-warning mt-n2"
icon={<FontAwesomeIcon className="mr-2" icon={faExclamationTriangle} />}
>
<FormattedMessage
id="account.settings.editable.field.password.reset.button.forbidden"
defaultMessage="Your previous request is in progress, please try again in few moments."
description="A message displayed when a previous password reset request is still in progress."
/>
</Alert>
);
};
export default RequestInProgressAlert;

View File

@@ -7,6 +7,7 @@ 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;
@@ -43,6 +44,7 @@ const ResetPassword = (props) => {
/>
</p>
{status === 'complete' ? <ConfirmationAlert email={email} /> : null}
{status === 'forbidden' ? <RequestInProgressAlert /> : null}
</div>
);
};

View File

@@ -18,3 +18,7 @@ export const resetPasswordSuccess = () => ({
export const resetPasswordReset = () => ({
type: RESET_PASSWORD.RESET,
});
export const resetPasswordForbidden = () => ({
type: RESET_PASSWORD.FORBIDDEN,
});

View File

@@ -17,6 +17,11 @@ const reducer = (state = defaultState, action = null) => {
...state,
status: 'complete',
};
case RESET_PASSWORD.FORBIDDEN:
return {
...state,
status: 'forbidden',
};
default:
}

View File

@@ -1,12 +1,20 @@
import { put, call, takeEvery } from 'redux-saga/effects';
import { resetPasswordBegin, resetPasswordSuccess, RESET_PASSWORD } from './actions';
import { resetPasswordBegin, resetPasswordForbidden, resetPasswordSuccess, RESET_PASSWORD } from './actions';
import { postResetPassword } from './service';
function* handleResetPassword(action) {
yield put(resetPasswordBegin());
const response = yield call(postResetPassword, action.payload.email);
yield put(resetPasswordSuccess(response));
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;
}
}
}
export default function* saga() {

View File

@@ -11,6 +11,7 @@
"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.",
@@ -33,6 +34,9 @@
"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",
@@ -85,12 +89,14 @@
"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.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
"account.settings.coaching.consent.success.continue": "Start my course",
"account.settings.coaching.managed.support": "support",
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
"account.settings.field.phone_number": "Phone Number",
"account.settings.field.phone_number.empty": "Add a phone number",
"account.settings.field.coaching_consent": "Coaching consent",
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available in English and Spanish languages. 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 to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.",
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
"account.settings.delete.account.header": "Delete My Account",
@@ -111,19 +117,180 @@
"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.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.options.woman": "Woman",
"account.settings.field.demographics.gender.options.man": "Man",
"account.settings.field.demographics.gender.options.non-binary": "Non-binary",
"account.settings.field.demographics.gender.options.self-describe": "Prefer to self-describe",
"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.ethnicity.options.american-indian-or-alaska-native": "American Indian or Alaska Native",
"account.settings.field.demographics.ethnicity.options.asian": "Asian",
"account.settings.field.demographics.ethnicity.options.black-or-african-american": "Black or African American",
"account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish": "Hispanic, Latin, or Spanish origin",
"account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african": "Middle Eastern or North African",
"account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander": "Native Hawaiian or Other Pacific Islander",
"account.settings.field.demographics.ethnicity.options.white": "White",
"account.settings.field.demographics.ethnicity.options.other": "Some other race, ethnicity, or origin",
"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.income.options.less-than-10k": "Less than US $10,000",
"account.settings.field.demographics.income.options.10k-25k": "US $10,000 - $25,000",
"account.settings.field.demographics.income.options.25k-50k": "US $25,000 - $50,000",
"account.settings.field.demographics.income.options.50k-75k": "US $50,000 - $75,000",
"account.settings.field.demographics.income.options.over-100k": "Over US $100,000",
"account.settings.field.demographics.income.options.unsure": "I don't know",
"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.income.options.never-served": "Never served in the military",
"account.settings.field.demographics.income.options.training": "Only on active duty for training",
"account.settings.field.demographics.income.options.active": "Now on active duty",
"account.settings.field.demographics.income.options.previously-active": "On active duty in the past, but not now",
"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.education_level.options.no-high-school": "No High School",
"account.settings.field.demographics.education_level.options.some-high-school": "Some High School",
"account.settings.field.demographics.education_level.options.high-school-ged-equivalent": "High School diploma, GED, or equivalent",
"account.settings.field.demographics.education_level.options.some-college": "Some college, but no degree",
"account.settings.field.demographics.education_level.options.associates": "Associates degree",
"account.settings.field.demographics.education_level.options.bachelors": "Bachelors degree",
"account.settings.field.demographics.education_level.options.masters": "Masters degree",
"account.settings.field.demographics.education_level.options.professional": "Professional degree",
"account.settings.field.demographics.education_level.options.doctorate": "Doctorate degree",
"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.options.full-time": "Employed, working full-time",
"account.settings.field.demographics.work_status.options.part-time": "Employed, working part-time",
"account.settings.field.demographics.work_status.options.not-employed-looking": "Not employed, looking for work",
"account.settings.field.demographics.work_status.options.not-employed-not-looking": "Not employed, not looking for work",
"account.settings.field.demographics.work_status.options.unable": "Unable to work",
"account.settings.field.demographics.work_status.options.retired": "Retired",
"account.settings.field.demographics.work_status.options.other": "Other",
"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.field.demographics.work_sector.options.accommodation-food": "Accommodation and Food Services",
"account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation": "Administrative and Support and Waste Management and Remediation Services",
"account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting": "Agriculture, Forestry, Fishing and Hunting",
"account.settings.field.demographics.work_sector.options.arts-entertainment-recreation": "Arts, Entertainment, and Recreation",
"account.settings.field.demographics.work_sector.options.construction": "Construction",
"account.settings.field.demographics.work_sector.options.educational": "Education Services",
"account.settings.field.demographics.work_sector.options.finance-insurance": "Finance and Insurance",
"account.settings.field.demographics.work_sector.options.healthcare-social": "Health Care and Social Assistance",
"account.settings.field.demographics.work_sector.options.information": "Information",
"account.settings.field.demographics.work_sector.options.management": "Management of Companies and Enterprises",
"account.settings.field.demographics.work_sector.options.manufacturing": "Manufacturing",
"account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas": "Mining, Quarrying, and Oil and Gas Extraction",
"account.settings.field.demographics.work_sector.options.professional-scientific-technical": "Professional, Scientific, and Technical Services",
"account.settings.field.demographics.work_sector.options.public-admin": "Public Administration",
"account.settings.field.demographics.work_sector.options.real-estate": "Real Estate and Rental and Leasing",
"account.settings.field.demographics.work_sector.options.retail": "Retail Trade",
"account.settings.field.demographics.work_sector.options.transport-warehousing": "Transportation and Warehousing",
"account.settings.field.demographics.work_sector.options.utilities": "Utilities",
"account.settings.field.demographics.work_sector.options.trade": "Wholesale Trade",
"account.settings.field.demographics.work_sector.options.other": "Other",
"account.settings.field.demographics.options.declined": "Prefer not to respond",
"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."
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
"id.verification.next": "Next",
"id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
"id.verification.requirements.card.device.title": "Device with Camera",
"id.verification.requirements.card.device.allow": "Allow",
"id.verification.requirements.card.id.title": "Photo Identification",
"id.verification.requirements.card.id.text": "You need a valid ID that contains your full name and photo.",
"id.verification.privacy.title": "Privacy Information",
"id.verification.privacy.need.photo.question": "Why does edX need my photo?",
"id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
"id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.existing.request.title": "Identity Verification",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time.",
"id.verification.photo.take": "Take Photo",
"id.verification.photo.retake": "Retake Photo",
"id.verification.camera.access.title": "Camera Permissions",
"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.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 Your Photo",
"id.verification.portrait.photo.title.upload": "Upload Your Portrait Photo",
"id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.help.sight.answer": "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. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.head.question": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.head.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.id.tips.description": "Next you'll need an eligible ID photo, make sure that:",
"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 ID Photo",
"id.verification.id.photo.title.upload": "Upload Your ID Photo",
"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. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.account.name.title": "Account Name Check",
"id.verification.account.name.instructions": "Please check the Account Name below to ensure it matches the name on your ID. If not, click \"Edit\".",
"id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.account.name.settings": "Account Settings",
"id.verification.account.name.label": "Name on ID",
"id.verification.account.name.edit": "Edit",
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.account.name.save": "Save",
"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": "Confirm",
"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 1-2 days). In the meantime, you can still access all available course content.",
"id.verification.submitted.return": "Return to Your Dashboard",
"id.verification.account.name.warning": "{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}.",
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}."
}

View File

@@ -11,6 +11,7 @@
"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": "Optional Information",
"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.",
@@ -33,6 +34,9 @@
"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",
@@ -74,24 +78,26 @@
"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 value set. Contact your {enterprise} administrator to make changes.",
"account.settings.static.field.empty.no.admin": "No value set.",
"account.settings.coaching.consent.welcome.header": "Lets get started.",
"account.settings.coaching.consent.welcome.subheader": "We're here for you from start to finish",
"account.settings.coaching.consent.description": "MicroBachelors programs include coaching that focuses on your career, education, and how you'll achieve results through one-on-one communication with an experienced professional. If youre interested, provide the information below and click “Submit,” and our coaching partner will connect with you via email and/or text message to help you move forward. Terms and conditions apply.*",
"account.settings.coaching.consent.text-messaging.disclaimer": "* Coaching services are included at no additional cost to learners with US phone numbers. Coaching includes recurring text messages. Message and data rates may apply. Text STOP to opt-out.",
"account.settings.coaching.consent.accept-coaching": "Sign up for coaching",
"account.settings.coaching.consent.decline-coaching": "I prefer not to be contacted with free coaching services",
"account.settings.coaching.consent.label.name": "Please confirm your name",
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
"account.settings.coaching.consent.success.header": "Success!",
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You 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.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": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
"account.settings.coaching.consent.success.continue": "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.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!",
@@ -102,7 +108,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": "unlink all social media accounts",
"account.settings.delete.account.please.unlink": "Desvincular todas las cuentas de redes sociales.",
"account.settings.delete.account.modal.header": "¿Está seguro?",
"account.settings.delete.account.modal.text.1": "Has seleccionado “Eliminar mi cuenta”. La eliminación de tu cuenta y datos personales es permanente e irreversible. edX no será capaz de recuperar tu cuenta o los datos que se hayan borrado.",
"account.settings.delete.account.modal.text.2": "Si procedes, no será posible usar esta cuenta para tomar cursos ni en la aplicación móvil de edX, ni en edx.org, ni en cualquier otro sitio hospedado por edX. Esto incluye el acceso a edx.org desde el sistema de tu empleador o universidad, y el acceso a sitios privados ofrecidos por MIT Open Learning, Wharton Executive Education, y Harvard Medical School.",
@@ -111,19 +117,180 @@
"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": "Password is incorrect",
"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.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.options.woman": "Woman",
"account.settings.field.demographics.gender.options.man": "Man",
"account.settings.field.demographics.gender.options.non-binary": "Non-binary",
"account.settings.field.demographics.gender.options.self-describe": "Prefer to self-describe",
"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.ethnicity.options.american-indian-or-alaska-native": "American Indian or Alaska Native",
"account.settings.field.demographics.ethnicity.options.asian": "Asian",
"account.settings.field.demographics.ethnicity.options.black-or-african-american": "Black or African American",
"account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish": "Hispanic, Latin, or Spanish origin",
"account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african": "Middle Eastern or North African",
"account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander": "Native Hawaiian or Other Pacific Islander",
"account.settings.field.demographics.ethnicity.options.white": "White",
"account.settings.field.demographics.ethnicity.options.other": "Some other race, ethnicity, or origin",
"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.income.options.less-than-10k": "Less than US $10,000",
"account.settings.field.demographics.income.options.10k-25k": "US $10,000 - $25,000",
"account.settings.field.demographics.income.options.25k-50k": "US $25,000 - $50,000",
"account.settings.field.demographics.income.options.50k-75k": "US $50,000 - $75,000",
"account.settings.field.demographics.income.options.over-100k": "Over US $100,000",
"account.settings.field.demographics.income.options.unsure": "I don't know",
"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.income.options.never-served": "Never served in the military",
"account.settings.field.demographics.income.options.training": "Only on active duty for training",
"account.settings.field.demographics.income.options.active": "Now on active duty",
"account.settings.field.demographics.income.options.previously-active": "On active duty in the past, but not now",
"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.education_level.options.no-high-school": "No High School",
"account.settings.field.demographics.education_level.options.some-high-school": "Some High School",
"account.settings.field.demographics.education_level.options.high-school-ged-equivalent": "High School diploma, GED, or equivalent",
"account.settings.field.demographics.education_level.options.some-college": "Some college, but no degree",
"account.settings.field.demographics.education_level.options.associates": "Associates degree",
"account.settings.field.demographics.education_level.options.bachelors": "Bachelors degree",
"account.settings.field.demographics.education_level.options.masters": "Masters degree",
"account.settings.field.demographics.education_level.options.professional": "Professional degree",
"account.settings.field.demographics.education_level.options.doctorate": "Doctorate degree",
"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.options.full-time": "Employed, working full-time",
"account.settings.field.demographics.work_status.options.part-time": "Employed, working part-time",
"account.settings.field.demographics.work_status.options.not-employed-looking": "Not employed, looking for work",
"account.settings.field.demographics.work_status.options.not-employed-not-looking": "Not employed, not looking for work",
"account.settings.field.demographics.work_status.options.unable": "Unable to work",
"account.settings.field.demographics.work_status.options.retired": "Retired",
"account.settings.field.demographics.work_status.options.other": "Other",
"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.field.demographics.work_sector.options.accommodation-food": "Accommodation and Food Services",
"account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation": "Administrative and Support and Waste Management and Remediation Services",
"account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting": "Agriculture, Forestry, Fishing and Hunting",
"account.settings.field.demographics.work_sector.options.arts-entertainment-recreation": "Arts, Entertainment, and Recreation",
"account.settings.field.demographics.work_sector.options.construction": "Construction",
"account.settings.field.demographics.work_sector.options.educational": "Education Services",
"account.settings.field.demographics.work_sector.options.finance-insurance": "Finance and Insurance",
"account.settings.field.demographics.work_sector.options.healthcare-social": "Health Care and Social Assistance",
"account.settings.field.demographics.work_sector.options.information": "Information",
"account.settings.field.demographics.work_sector.options.management": "Management of Companies and Enterprises",
"account.settings.field.demographics.work_sector.options.manufacturing": "Manufacturing",
"account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas": "Mining, Quarrying, and Oil and Gas Extraction",
"account.settings.field.demographics.work_sector.options.professional-scientific-technical": "Professional, Scientific, and Technical Services",
"account.settings.field.demographics.work_sector.options.public-admin": "Public Administration",
"account.settings.field.demographics.work_sector.options.real-estate": "Real Estate and Rental and Leasing",
"account.settings.field.demographics.work_sector.options.retail": "Retail Trade",
"account.settings.field.demographics.work_sector.options.transport-warehousing": "Transportation and Warehousing",
"account.settings.field.demographics.work_sector.options.utilities": "Utilities",
"account.settings.field.demographics.work_sector.options.trade": "Wholesale Trade",
"account.settings.field.demographics.work_sector.options.other": "Other",
"account.settings.field.demographics.options.declined": "Prefer not to respond",
"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": "There was a problem disconnecting this account. Contact support if the problem persists.",
"account.settings.sso.account.disconnect.error": "Hubo un problema al desconectar esta Cuenta. Si el problema persiste, contacte soporte.",
"account.settings.sso.unlink.account": "Desvincular la cuenta de {accountName} ",
"account.settings.sso.no.providers": "No accounts can be linked at this time."
"account.settings.sso.no.providers": "No se pueden vincular cuentas en este momento.",
"id.verification.next": "Next",
"id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
"id.verification.requirements.card.device.title": "Device with Camera",
"id.verification.requirements.card.device.allow": "Allow",
"id.verification.requirements.card.id.title": "Photo Identification",
"id.verification.requirements.card.id.text": "You need a valid ID that contains your full name and photo.",
"id.verification.privacy.title": "Privacy Information",
"id.verification.privacy.need.photo.question": "Why does edX need my photo?",
"id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
"id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.existing.request.title": "Identity Verification",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time.",
"id.verification.photo.take": "Take Photo",
"id.verification.photo.retake": "Retake Photo",
"id.verification.camera.access.title": "Camera Permissions",
"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.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 Your Photo",
"id.verification.portrait.photo.title.upload": "Upload Your Portrait Photo",
"id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.help.sight.answer": "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. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.head.question": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.head.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.id.tips.description": "Next you'll need an eligible ID photo, make sure that:",
"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 ID Photo",
"id.verification.id.photo.title.upload": "Upload Your ID Photo",
"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. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.account.name.title": "Account Name Check",
"id.verification.account.name.instructions": "Please check the Account Name below to ensure it matches the name on your ID. If not, click \"Edit\".",
"id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.account.name.settings": "Account Settings",
"id.verification.account.name.label": "Name on ID",
"id.verification.account.name.edit": "Edit",
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.account.name.save": "Save",
"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": "Confirm",
"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 1-2 days). In the meantime, you can still access all available course content.",
"id.verification.submitted.return": "Return to Your Dashboard",
"id.verification.account.name.warning": "{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}.",
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}."
}

View File

@@ -11,6 +11,7 @@
"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.",
@@ -33,6 +34,9 @@
"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",
@@ -85,12 +89,14 @@
"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.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
"account.settings.coaching.consent.success.continue": "Start my course",
"account.settings.coaching.managed.support": "support",
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
"account.settings.field.phone_number": "Phone Number",
"account.settings.field.phone_number.empty": "Add a phone number",
"account.settings.field.coaching_consent": "Coaching consent",
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available in English and Spanish languages. 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 to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.",
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
"account.settings.delete.account.header": "Delete My Account",
@@ -111,19 +117,180 @@
"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.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.options.woman": "Woman",
"account.settings.field.demographics.gender.options.man": "Man",
"account.settings.field.demographics.gender.options.non-binary": "Non-binary",
"account.settings.field.demographics.gender.options.self-describe": "Prefer to self-describe",
"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.ethnicity.options.american-indian-or-alaska-native": "American Indian or Alaska Native",
"account.settings.field.demographics.ethnicity.options.asian": "Asian",
"account.settings.field.demographics.ethnicity.options.black-or-african-american": "Black or African American",
"account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish": "Hispanic, Latin, or Spanish origin",
"account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african": "Middle Eastern or North African",
"account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander": "Native Hawaiian or Other Pacific Islander",
"account.settings.field.demographics.ethnicity.options.white": "White",
"account.settings.field.demographics.ethnicity.options.other": "Some other race, ethnicity, or origin",
"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.income.options.less-than-10k": "Less than US $10,000",
"account.settings.field.demographics.income.options.10k-25k": "US $10,000 - $25,000",
"account.settings.field.demographics.income.options.25k-50k": "US $25,000 - $50,000",
"account.settings.field.demographics.income.options.50k-75k": "US $50,000 - $75,000",
"account.settings.field.demographics.income.options.over-100k": "Over US $100,000",
"account.settings.field.demographics.income.options.unsure": "I don't know",
"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.income.options.never-served": "Never served in the military",
"account.settings.field.demographics.income.options.training": "Only on active duty for training",
"account.settings.field.demographics.income.options.active": "Now on active duty",
"account.settings.field.demographics.income.options.previously-active": "On active duty in the past, but not now",
"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.education_level.options.no-high-school": "No High School",
"account.settings.field.demographics.education_level.options.some-high-school": "Some High School",
"account.settings.field.demographics.education_level.options.high-school-ged-equivalent": "High School diploma, GED, or equivalent",
"account.settings.field.demographics.education_level.options.some-college": "Some college, but no degree",
"account.settings.field.demographics.education_level.options.associates": "Associates degree",
"account.settings.field.demographics.education_level.options.bachelors": "Bachelors degree",
"account.settings.field.demographics.education_level.options.masters": "Masters degree",
"account.settings.field.demographics.education_level.options.professional": "Professional degree",
"account.settings.field.demographics.education_level.options.doctorate": "Doctorate degree",
"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.options.full-time": "Employed, working full-time",
"account.settings.field.demographics.work_status.options.part-time": "Employed, working part-time",
"account.settings.field.demographics.work_status.options.not-employed-looking": "Not employed, looking for work",
"account.settings.field.demographics.work_status.options.not-employed-not-looking": "Not employed, not looking for work",
"account.settings.field.demographics.work_status.options.unable": "Unable to work",
"account.settings.field.demographics.work_status.options.retired": "Retired",
"account.settings.field.demographics.work_status.options.other": "Other",
"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.field.demographics.work_sector.options.accommodation-food": "Accommodation and Food Services",
"account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation": "Administrative and Support and Waste Management and Remediation Services",
"account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting": "Agriculture, Forestry, Fishing and Hunting",
"account.settings.field.demographics.work_sector.options.arts-entertainment-recreation": "Arts, Entertainment, and Recreation",
"account.settings.field.demographics.work_sector.options.construction": "Construction",
"account.settings.field.demographics.work_sector.options.educational": "Education Services",
"account.settings.field.demographics.work_sector.options.finance-insurance": "Finance and Insurance",
"account.settings.field.demographics.work_sector.options.healthcare-social": "Health Care and Social Assistance",
"account.settings.field.demographics.work_sector.options.information": "Information",
"account.settings.field.demographics.work_sector.options.management": "Management of Companies and Enterprises",
"account.settings.field.demographics.work_sector.options.manufacturing": "Manufacturing",
"account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas": "Mining, Quarrying, and Oil and Gas Extraction",
"account.settings.field.demographics.work_sector.options.professional-scientific-technical": "Professional, Scientific, and Technical Services",
"account.settings.field.demographics.work_sector.options.public-admin": "Public Administration",
"account.settings.field.demographics.work_sector.options.real-estate": "Real Estate and Rental and Leasing",
"account.settings.field.demographics.work_sector.options.retail": "Retail Trade",
"account.settings.field.demographics.work_sector.options.transport-warehousing": "Transportation and Warehousing",
"account.settings.field.demographics.work_sector.options.utilities": "Utilities",
"account.settings.field.demographics.work_sector.options.trade": "Wholesale Trade",
"account.settings.field.demographics.work_sector.options.other": "Other",
"account.settings.field.demographics.options.declined": "Prefer not to respond",
"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."
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
"id.verification.next": "Next",
"id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
"id.verification.requirements.card.device.title": "Device with Camera",
"id.verification.requirements.card.device.allow": "Allow",
"id.verification.requirements.card.id.title": "Photo Identification",
"id.verification.requirements.card.id.text": "You need a valid ID that contains your full name and photo.",
"id.verification.privacy.title": "Privacy Information",
"id.verification.privacy.need.photo.question": "Why does edX need my photo?",
"id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
"id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.existing.request.title": "Identity Verification",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time.",
"id.verification.photo.take": "Take Photo",
"id.verification.photo.retake": "Retake Photo",
"id.verification.camera.access.title": "Camera Permissions",
"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.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 Your Photo",
"id.verification.portrait.photo.title.upload": "Upload Your Portrait Photo",
"id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.help.sight.answer": "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. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.head.question": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.head.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.id.tips.description": "Next you'll need an eligible ID photo, make sure that:",
"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 ID Photo",
"id.verification.id.photo.title.upload": "Upload Your ID Photo",
"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. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.account.name.title": "Account Name Check",
"id.verification.account.name.instructions": "Please check the Account Name below to ensure it matches the name on your ID. If not, click \"Edit\".",
"id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.account.name.settings": "Account Settings",
"id.verification.account.name.label": "Name on ID",
"id.verification.account.name.edit": "Edit",
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.account.name.save": "Save",
"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": "Confirm",
"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 1-2 days). In the meantime, you can still access all available course content.",
"id.verification.submitted.return": "Return to Your Dashboard",
"id.verification.account.name.warning": "{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}.",
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}."
}

View File

@@ -11,6 +11,7 @@
"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.",
@@ -33,6 +34,9 @@
"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",
@@ -85,12 +89,14 @@
"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.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
"account.settings.coaching.consent.success.continue": "Start my course",
"account.settings.coaching.managed.support": "support",
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
"account.settings.field.phone_number": "Phone Number",
"account.settings.field.phone_number.empty": "Add a phone number",
"account.settings.field.coaching_consent": "Coaching consent",
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available in English and Spanish languages. 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 to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.",
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
"account.settings.delete.account.header": "Delete My Account",
@@ -111,19 +117,180 @@
"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.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.options.woman": "Woman",
"account.settings.field.demographics.gender.options.man": "Man",
"account.settings.field.demographics.gender.options.non-binary": "Non-binary",
"account.settings.field.demographics.gender.options.self-describe": "Prefer to self-describe",
"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.ethnicity.options.american-indian-or-alaska-native": "American Indian or Alaska Native",
"account.settings.field.demographics.ethnicity.options.asian": "Asian",
"account.settings.field.demographics.ethnicity.options.black-or-african-american": "Black or African American",
"account.settings.field.demographics.ethnicity.options.hispanic-latin-spanish": "Hispanic, Latin, or Spanish origin",
"account.settings.field.demographics.ethnicity.options.middle-eastern-or-north-african": "Middle Eastern or North African",
"account.settings.field.demographics.ethnicity.options.native-hawaiian-or-pacific-islander": "Native Hawaiian or Other Pacific Islander",
"account.settings.field.demographics.ethnicity.options.white": "White",
"account.settings.field.demographics.ethnicity.options.other": "Some other race, ethnicity, or origin",
"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.income.options.less-than-10k": "Less than US $10,000",
"account.settings.field.demographics.income.options.10k-25k": "US $10,000 - $25,000",
"account.settings.field.demographics.income.options.25k-50k": "US $25,000 - $50,000",
"account.settings.field.demographics.income.options.50k-75k": "US $50,000 - $75,000",
"account.settings.field.demographics.income.options.over-100k": "Over US $100,000",
"account.settings.field.demographics.income.options.unsure": "I don't know",
"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.income.options.never-served": "Never served in the military",
"account.settings.field.demographics.income.options.training": "Only on active duty for training",
"account.settings.field.demographics.income.options.active": "Now on active duty",
"account.settings.field.demographics.income.options.previously-active": "On active duty in the past, but not now",
"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.education_level.options.no-high-school": "No High School",
"account.settings.field.demographics.education_level.options.some-high-school": "Some High School",
"account.settings.field.demographics.education_level.options.high-school-ged-equivalent": "High School diploma, GED, or equivalent",
"account.settings.field.demographics.education_level.options.some-college": "Some college, but no degree",
"account.settings.field.demographics.education_level.options.associates": "Associates degree",
"account.settings.field.demographics.education_level.options.bachelors": "Bachelors degree",
"account.settings.field.demographics.education_level.options.masters": "Masters degree",
"account.settings.field.demographics.education_level.options.professional": "Professional degree",
"account.settings.field.demographics.education_level.options.doctorate": "Doctorate degree",
"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.options.full-time": "Employed, working full-time",
"account.settings.field.demographics.work_status.options.part-time": "Employed, working part-time",
"account.settings.field.demographics.work_status.options.not-employed-looking": "Not employed, looking for work",
"account.settings.field.demographics.work_status.options.not-employed-not-looking": "Not employed, not looking for work",
"account.settings.field.demographics.work_status.options.unable": "Unable to work",
"account.settings.field.demographics.work_status.options.retired": "Retired",
"account.settings.field.demographics.work_status.options.other": "Other",
"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.field.demographics.work_sector.options.accommodation-food": "Accommodation and Food Services",
"account.settings.field.demographics.work_sector.options.administrative-support-waste-remediation": "Administrative and Support and Waste Management and Remediation Services",
"account.settings.field.demographics.work_sector.options.agriculture-forestry-fishing-hunting": "Agriculture, Forestry, Fishing and Hunting",
"account.settings.field.demographics.work_sector.options.arts-entertainment-recreation": "Arts, Entertainment, and Recreation",
"account.settings.field.demographics.work_sector.options.construction": "Construction",
"account.settings.field.demographics.work_sector.options.educational": "Education Services",
"account.settings.field.demographics.work_sector.options.finance-insurance": "Finance and Insurance",
"account.settings.field.demographics.work_sector.options.healthcare-social": "Health Care and Social Assistance",
"account.settings.field.demographics.work_sector.options.information": "Information",
"account.settings.field.demographics.work_sector.options.management": "Management of Companies and Enterprises",
"account.settings.field.demographics.work_sector.options.manufacturing": "Manufacturing",
"account.settings.field.demographics.work_sector.options.mining-quarry-oil-gas": "Mining, Quarrying, and Oil and Gas Extraction",
"account.settings.field.demographics.work_sector.options.professional-scientific-technical": "Professional, Scientific, and Technical Services",
"account.settings.field.demographics.work_sector.options.public-admin": "Public Administration",
"account.settings.field.demographics.work_sector.options.real-estate": "Real Estate and Rental and Leasing",
"account.settings.field.demographics.work_sector.options.retail": "Retail Trade",
"account.settings.field.demographics.work_sector.options.transport-warehousing": "Transportation and Warehousing",
"account.settings.field.demographics.work_sector.options.utilities": "Utilities",
"account.settings.field.demographics.work_sector.options.trade": "Wholesale Trade",
"account.settings.field.demographics.work_sector.options.other": "Other",
"account.settings.field.demographics.options.declined": "Prefer not to respond",
"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."
"account.settings.sso.no.providers": "No accounts can be linked at this time.",
"id.verification.next": "Next",
"id.verification.requirements.title": "Photo Verification Requirements",
"id.verification.requirements.description": "In order to complete Photo Verification online, you will need the following:",
"id.verification.requirements.card.device.title": "Device with Camera",
"id.verification.requirements.card.device.allow": "Allow",
"id.verification.requirements.card.id.title": "Photo Identification",
"id.verification.requirements.card.id.text": "You need a valid ID that contains your full name and photo.",
"id.verification.privacy.title": "Privacy Information",
"id.verification.privacy.need.photo.question": "Why does edX need my photo?",
"id.verification.privacy.need.photo.answer": "We use your verification photos to confirm your identity and ensure the validity of your certificate.",
"id.verification.privacy.do.with.photo.question": "What does edX do with this photo?",
"id.verification.privacy.do.with.photo.answer": "We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.",
"id.verification.existing.request.title": "Identity Verification",
"id.verification.existing.request.pending.text": "You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).",
"id.verification.existing.request.denied.text": "You cannot verify your identity at this time.",
"id.verification.photo.take": "Take Photo",
"id.verification.photo.retake": "Retake Photo",
"id.verification.camera.access.title": "Camera Permissions",
"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.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 Your Photo",
"id.verification.portrait.photo.title.upload": "Upload Your Portrait Photo",
"id.verification.portrait.photo.preview.alt": "Preview of photo of user's face.",
"id.verification.portrait.photo.instructions.camera": "When your face is in position, use the Take Photo button below to take your photo.",
"id.verification.portrait.photo.instructions.upload": "Please upload a portrait photo. Ensure your entire face fits inside the frame and is well-lit. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.camera.help.sight.question": "What if I can't see the camera image or if I can't see my photo to determine which side is visible?",
"id.verification.camera.help.sight.answer": "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. The most common reason for rejection is inability to read the text on the ID card.",
"id.verification.camera.help.head.question": "What if I have difficulty holding my head in position relative to the camera?",
"id.verification.camera.help.head.answer": "If you require assistance with taking a photo for submission, contact edX support for additional suggestions.",
"id.verification.id.tips.title": "Helpful ID Tips",
"id.verification.id.tips.description": "Next you'll need an eligible ID photo, make sure that:",
"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 ID Photo",
"id.verification.id.photo.title.upload": "Upload Your ID Photo",
"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. (Supported formats: .jpg, .jpeg, .png)",
"id.verification.account.name.title": "Account Name Check",
"id.verification.account.name.instructions": "Please check the Account Name below to ensure it matches the name on your ID. If not, click \"Edit\".",
"id.verification.account.name.warning.prefix": "Please Note:",
"id.verification.account.name.settings": "Account Settings",
"id.verification.account.name.label": "Name on ID",
"id.verification.account.name.edit": "Edit",
"id.verification.account.name.photo.alt": "Photo of your ID to be submitted.",
"id.verification.account.name.save": "Save",
"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": "Confirm",
"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 1-2 days). In the meantime, you can still access all available course content.",
"id.verification.submitted.return": "Return to Your Dashboard",
"id.verification.account.name.warning": "{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}.",
"id.verification.request.camera.access.instructions": "In order to take a photo using your webcam, you may receive a browser prompt for access to your camera. {clickAllow}",
"id.verification.requirements.card.device.text": "You need a device that has a camera. If you receive a browser prompt for access to your camera, please make sure to click {allow}."
}

View File

@@ -0,0 +1,90 @@
import React from 'react';
import PropTypes from 'prop-types';
import CameraPhoto, { FACING_MODES } from 'jslib-html5-camera-photo';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
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.state = {
trackedObject: null,
dataUri: '',
};
}
componentDidMount() {
this.cameraPhoto = new CameraPhoto(this.videoRef.current);
this.cameraPhoto.startCameraMaxResolution(FACING_MODES.USER);
}
takePhoto() {
if (this.state.dataUri) {
return this.reset();
}
const config = {
sizeFactor: 1,
};
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: '' });
}
render() {
const cameraFlashClass = this.state.dataUri
? 'do-transition camera-flash'
: 'camera-flash';
return (
<div className='camera-outer-wrapper shadow'>
<div className='camera-wrapper'>
<div className={cameraFlashClass} />
<video
ref={this.videoRef}
autoPlay={true}
className='camera-video'
style={{ display: this.state.dataUri ? 'none' : 'block' }}
/>
<img
alt='imgCamera'
src={this.state.dataUri}
className='camera-video'
style={{ display: this.state.dataUri ? 'block' : 'none' }}
/>
</div>
<button
className='btn btn-primary camera-btn'
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,
};
export default injectIntl(Camera);

View File

@@ -0,0 +1,37 @@
import React from 'react';
import { Collapsible } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './IdVerification.messages';
function CameraHelp(props) {
return (
<div>
<Collapsible
styling="card"
title={props.intl.formatMessage(messages['id.verification.camera.help.sight.question'])}
className="mb-4 shadow"
>
<p>
{props.intl.formatMessage(messages['id.verification.camera.help.sight.answer'])}
</p>
</Collapsible>
<Collapsible
styling="card"
title={props.intl.formatMessage(messages['id.verification.camera.help.head.question'])}
className="mb-4 shadow"
>
<p>
{props.intl.formatMessage(messages['id.verification.camera.help.head.answer'])}
</p>
</Collapsible>
</div>
);
}
CameraHelp.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(CameraHelp);

View File

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

View File

@@ -0,0 +1,346 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'id.verification.next': {
id: 'id.verification.next',
defaultMessage: 'Next',
description: 'Next button.',
},
'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 ID 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 edX need my photo?',
description: 'Question about why edX needs a verification photo.',
},
'id.verification.privacy.need.photo.answer': {
id: 'id.verification.privacy.need.photo.answer',
defaultMessage: 'We use your verification photos to confirm your identity and ensure the validity of your certificate.',
description: 'Answering why edX needs a verification photo.',
},
'id.verification.privacy.do.with.photo.question': {
id: 'id.verification.privacy.do.with.photo.question',
defaultMessage: 'What does edX do with this photo?',
description: 'Question about what edX does with the verification photo.',
},
'id.verification.privacy.do.with.photo.answer': {
id: 'id.verification.privacy.do.with.photo.answer',
defaultMessage: 'We securely encrypt your photo and send it our authorization service for review. Your photo and information are not saved or visible anywhere on edX after the verification process is complete.',
description: 'Answering what edX does with the verification photo.',
},
'id.verification.existing.request.title': {
id: 'id.verification.existing.request.title',
defaultMessage: 'Identity Verification',
description: 'Title for text that displays when user has already made a request.',
},
'id.verification.existing.request.pending.text': {
id: 'id.verification.existing.request.pending.text',
defaultMessage: 'You have already submitted your verification information. You will see a message on your dashboard when the verification process is complete (usually within 1-2 days).',
description: 'Text that displays when user has a pending or approved request.',
},
'id.verification.existing.request.denied.text': {
id: 'id.verification.existing.request.denied.text',
defaultMessage: 'You cannot verify your identity at this time.',
description: 'Text that displays when user is denied from making a 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.camera.access.title': {
id: 'id.verification.camera.access.title',
defaultMessage: 'Camera Permissions',
description: 'Title for the Camera Access page.',
},
'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.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 Your Photo',
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 Your Portrait Photo',
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: .jpg, .jpeg, .png)',
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': {
id: 'id.verification.camera.help.sight.answer',
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. 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 or photo cannot be seen during verification.',
},
'id.verification.camera.help.head.question': {
id: 'id.verification.camera.help.head.question',
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.head.answer': {
id: 'id.verification.camera.help.head.answer',
defaultMessage: 'If you require assistance with taking a photo for submission, contact edX support for additional suggestions.',
description: 'Confirming what to do if the user has difficult holding their head relative to the camera.',
},
'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 you\'ll need an eligible ID photo, make sure that:',
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 ID Photo',
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 Your ID Photo',
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. (Supported formats: .jpg, .jpeg, .png)',
description: 'Instructions for ID photo upload.',
},
'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: 'Please check the Account Name below to ensure it matches the name on your ID. If not, click "Edit".',
description: 'Text to verify that the account name matches the name on the ID photo.',
},
'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: 'Name on ID',
description: 'Label for account name input.',
},
'id.verification.account.name.edit': {
id: 'id.verification.account.name.edit',
defaultMessage: 'Edit',
description: 'Button to edit account name.',
},
'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',
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: 'Confirm',
description: 'Button to confirm all information is correct.',
},
'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 1-2 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': {
id: 'id.verification.submitted.return',
defaultMessage: 'Return to Your Dashboard',
description: 'Button to return to the dashboard.',
},
});
export default messages;

View File

@@ -0,0 +1,90 @@
import React, { useState, useContext, useEffect } from 'react';
import PropTypes from 'prop-types';
import { AppContext } from '@edx/frontend-platform/react';
import { hasGetUserMediaSupport } from './getUserMediaShim';
import { getExistingIdVerification } from './data/service';
import PageLoading from '../account-settings/PageLoading';
import ExistingRequest from './ExistingRequest';
const IdVerificationContext = React.createContext({});
const MEDIA_ACCESS = {
PENDING: 'pending',
UNSUPPORTED: 'unsupported',
DENIED: 'denied',
GRANTED: 'granted',
};
function IdVerificationContextProvider({ children }) {
const [existingIdVerification, setExistingIdVerification] = useState(null);
const [facePhotoFile, setFacePhotoFile] = useState(null);
const [idPhotoFile, setIdPhotoFile] = useState(null);
const [idPhotoName, setIdPhotoName] = useState(null);
const [mediaStream, setMediaStream] = useState(null);
const [mediaAccess, setMediaAccess] = useState(hasGetUserMediaSupport ?
MEDIA_ACCESS.PENDING :
MEDIA_ACCESS.UNSUPPORTED);
const { authenticatedUser } = useContext(AppContext);
const contextValue = {
existingIdVerification,
facePhotoFile,
idPhotoFile,
idPhotoName,
mediaStream,
mediaAccess,
nameOnAccount: authenticatedUser.name,
setExistingIdVerification,
setFacePhotoFile,
setIdPhotoFile,
setIdPhotoName,
tryGetUserMedia: async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
setMediaAccess(MEDIA_ACCESS.GRANTED);
setMediaStream(stream);
// If we would like to stop the stream immediately. I guess we can leave it open
// const tracks = stream.getTracks();
// tracks.forEach(track => track.stop());
} catch (err) {
setMediaAccess(MEDIA_ACCESS.DENIED);
}
},
};
// Call verification status endpoint to check whether we can verify.
useEffect(() => {
(async () => {
const existingIdV = await getExistingIdVerification();
setExistingIdVerification(existingIdV);
})();
}, []);
// If we are waiting for verification status endpoint, show spinner.
if (!existingIdVerification) {
return <PageLoading srMessage="Loading verification status" />;
}
if (!existingIdVerification.canVerify) {
const { status } = existingIdVerification;
return (
<ExistingRequest status={status} />
);
}
return (
<IdVerificationContext.Provider value={contextValue}>
{children}
</IdVerificationContext.Provider>
);
}
IdVerificationContextProvider.propTypes = {
children: PropTypes.node.isRequired,
};
export {
IdVerificationContext,
IdVerificationContextProvider,
MEDIA_ACCESS,
};

View File

@@ -0,0 +1,84 @@
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { Route, Switch, Redirect, useRouteMatch } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Modal, Button } from '@edx/paragon';
import { idVerificationSelector } from './data/selectors';
import './getUserMediaShim';
import { IdVerificationContextProvider } from './IdVerificationContext';
import ReviewRequirementsPanel from './panels/ReviewRequirementsPanel';
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 [isModalOpen, setIsModalOpen] = useState(false);
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 exact path={path}>
<Redirect to={`${path}/review-requirements`} />
</Route>
<Route path={`${path}/review-requirements`} component={ReviewRequirementsPanel} />
<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 className="btn-link px-0" onClick={() => setIsModalOpen(true)}>
Privacy Information
</Button>
</div>
</div>
<Modal
open={isModalOpen}
title={props.intl.formatMessage(messages['id.verification.privacy.title'])}
body={(
<div>
<h6>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.question'])}</h6>
<p>{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}</p>
<h6>{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.question'])}</h6>
<p>{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.answer'])}</p>
</div>
)}
onClose={() => setIsModalOpen(false)}
/>
</div>
</>
);
}
IdVerificationPage.propTypes = {
intl: intlShape.isRequired,
};
export default connect(idVerificationSelector, {
})(injectIntl(IdVerificationPage));

View File

@@ -0,0 +1,27 @@
import React, { useCallback } from 'react';
import PropTypes from 'prop-types';
export default function ImageFileUpload({ onFileChange }) {
const handleChange = useCallback((e) => {
if (e.target.files.length === 0) {
return;
}
const fileObject = e.target.files[0];
const fileReader = new FileReader();
fileReader.addEventListener('load', () => onFileChange(fileReader.result));
fileReader.readAsDataURL(fileObject);
}, []);
return (
<input
type="file"
accept="image/*"
onChange={handleChange}
/>
);
}
ImageFileUpload.propTypes = {
onFileChange: PropTypes.func.isRequired,
};

View File

@@ -0,0 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
export default function ImagePreview({ src, alt, id }) {
return (
<div id={id} className="image-preview">
<img style={{ objectFit: 'contain' }} src={src} alt={alt} />
</div>
);
}
ImagePreview.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired,
id: PropTypes.string,
};

View File

@@ -0,0 +1,63 @@
.page__id-verification {
.verification-panel {
.card.requirements {
border-top: solid 4px theme-color('warning');
}
.image-preview {
margin-bottom: 1rem;
max-width: 20rem;
img {
display: block;
max-width: 100%;
max-height: 10rem;
}
}
}
.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%;
}
.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;
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
export const storeName = 'idVerifiction';
export const idVerificationSelector = state => ({ ...state[storeName] });

View File

@@ -0,0 +1,70 @@
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 };
}
}
/**
* 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',
};
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, message: String(e) }; // TODO: is String(e) right?
}
}

View File

@@ -0,0 +1,59 @@
/**
* 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 };

View File

@@ -0,0 +1 @@
export { default } from './IdVerificationPage';

View File

@@ -0,0 +1,43 @@
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();
}
}, []);
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,
};

View File

@@ -0,0 +1,91 @@
import React, { useContext, useState, useEffect, useRef } from 'react';
import { Input, Button } from '@edx/paragon';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import { IdVerificationContext } from '../IdVerificationContext';
import ImagePreview from '../ImagePreview';
import messages from '../IdVerification.messages';
function GetNameIdPanel(props) {
const panelSlug = 'get-name-id';
const [isEditing, setIsEditing] = useState(false);
const nameInputRef = useRef();
const nextPanelSlug = useNextPanelSlug(panelSlug);
useEffect(() => {
if (isEditing && nameInputRef.current) {
nameInputRef.current.focus();
}
}, [isEditing]);
const {
nameOnAccount, idPhotoName, setIdPhotoName, idPhotoFile,
} = useContext(IdVerificationContext);
const nameOnAccountValue = nameOnAccount || '';
return (
<BasePanel
name={panelSlug}
title="Account Name Check"
>
<p>
{props.intl.formatMessage(messages['id.verification.account.name.instructions'])}
</p>
<div className="alert alert-warning">
<FormattedMessage
id="id.verification.account.name.warning"
defaultMessage="{prefix} Any edit to your name will be saved to your account and can be reviewed on {accountSettings}."
description="Warning that any edit to the user's name will be saved to the account."
values={{
prefix: <strong>{props.intl.formatMessage(messages['id.verification.account.name.warning.prefix'])}</strong>,
accountSettings: <Link to="/">{props.intl.formatMessage(messages['id.verification.account.name.settings'])}</Link>,
}}
/>
</div>
<div className="form-group">
<label htmlFor="photo-id-name">
{props.intl.formatMessage(messages['id.verification.account.name.label'])}
</label>
<div className="d-flex">
<Input
id="photo-id-name"
type="text"
ref={nameInputRef}
disabled={!isEditing}
readOnly={!isEditing}
value={idPhotoName || nameOnAccountValue}
onChange={e => setIdPhotoName(e.target.value)}
/>
{!isEditing && (
<Button
className="btn-link px-0 ml-3"
onClick={() => setIsEditing(true)}
>
{props.intl.formatMessage(messages['id.verification.account.name.edit'])}
</Button>
)}
</div>
</div>
<ImagePreview
id="photo-of-id"
src={idPhotoFile}
alt={props.intl.formatMessage(messages['id.verification.account.name.photo.alt'])}
/>
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary">
{isEditing ? 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);

View File

@@ -0,0 +1,50 @@
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 messages from '../IdVerification.messages';
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">
<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.id.tips.list.well.lit'])}
</li>
<li>
{props.intl.formatMessage(messages['id.verification.id.tips.list.clear'])}
</li>
</ul>
</div>
</div>
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</BasePanel>
);
}
IdContextPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(IdContextPanel);

View File

@@ -0,0 +1,52 @@
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 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">
<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>
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</BasePanel>
);
}
PortraitPhotoContextPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(PortraitPhotoContextPanel);

View File

@@ -0,0 +1,88 @@
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import { Collapsible } from '@edx/paragon';
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 messages from '../IdVerification.messages';
function RequestCameraAccessPanel(props) {
const panelSlug = 'request-camera-access';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const { tryGetUserMedia, mediaAccess } = useContext(IdVerificationContext);
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.camera.access.title'])}
>
{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 className="btn btn-primary" onClick={tryGetUserMedia}>
{props.intl.formatMessage(messages['id.verification.camera.access.enable'])}
</button>
<Collapsible.Advanced className="mr-auto">
<Collapsible.Visible whenClosed>
<Collapsible.Trigger tag="button" className="btn btn-link px-0">
{props.intl.formatMessage(messages['id.verification.camera.access.problems'])}
</Collapsible.Trigger>
</Collapsible.Visible>
<Collapsible.Body>
<Link to={nextPanelSlug} className="btn btn-link">
{props.intl.formatMessage(messages['id.verification.camera.access.skip'])}
</Link>
</Collapsible.Body>
</Collapsible.Advanced>
</div>
</div>
)}
{mediaAccess === MEDIA_ACCESS.GRANTED && (
<div>
<p>
{props.intl.formatMessage(messages['id.verification.camera.access.success'])}
</p>
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</div>
)}
{[MEDIA_ACCESS.UNSUPPORTED, MEDIA_ACCESS.DENIED].includes(mediaAccess) && (
<div>
<p>
{props.intl.formatMessage(messages['id.verification.camera.access.failure'])}
</p>
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</div>
)}
</BasePanel>
);
}
RequestCameraAccessPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(RequestCameraAccessPanel);

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import messages from '../IdVerification.messages';
function ReviewRequirementsPanel(props) {
const panelSlug = 'review-requirements';
const nextPanelSlug = useNextPanelSlug(panelSlug);
return (
<BasePanel
name={panelSlug}
title="Photo Verification Requirements"
focusOnMount={false}
>
<p>
{props.intl.formatMessage(messages['id.verification.requirements.description'])}
</p>
<div className="card mb-4 shadow requirements">
<div className="card-body">
<h6>
{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 requirements">
<div className="card-body">
<h6>
{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'])}
</p>
</div>
</div>
<h4 className="mb-3">
{props.intl.formatMessage(messages['id.verification.privacy.title'])}
</h4>
<h6>
{props.intl.formatMessage(messages['id.verification.privacy.need.photo.question'])}
</h6>
<p>
{props.intl.formatMessage(messages['id.verification.privacy.need.photo.answer'])}
</p>
<h6>
{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.question'])}
</h6>
<p>
{props.intl.formatMessage(messages['id.verification.privacy.do.with.photo.answer'])}
</p>
<div className="action-row">
<Link to={nextPanelSlug} className="btn btn-primary">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
</BasePanel>
);
}
ReviewRequirementsPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(ReviewRequirementsPanel);

View File

@@ -0,0 +1,30 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import BasePanel from './BasePanel';
import messages from '../IdVerification.messages';
function SubmittedPanel(props) {
const panelSlug = 'submitted';
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`}>
{props.intl.formatMessage(messages['id.verification.return'])}
</a>
</BasePanel>
);
}
SubmittedPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(SubmittedPanel);

View File

@@ -0,0 +1,126 @@
import React, { useContext } from 'react';
import { history } from '@edx/frontend-platform';
import { Input, Button } from '@edx/paragon';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } 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';
function SummaryPanel(props) {
const panelSlug = 'summary';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const {
facePhotoFile,
idPhotoFile,
nameOnAccount,
idPhotoName,
} = useContext(IdVerificationContext);
const nameToBeUsed = idPhotoName || nameOnAccount || '';
function SubmitButton() {
async function handleClick() {
const verificationData = {
facePhotoFile,
idPhotoFile,
idPhotoName: nameToBeUsed,
};
const result = await submitIdVerification(verificationData);
if (result.success) {
history.push(nextPanelSlug);
}
}
return (
<Button className="btn btn-primary" title="Confirmation" onClick={handleClick}>
{props.intl.formatMessage(messages['id.verification.review.confirm'])}
</Button>
);
}
return (
<BasePanel
name={panelSlug}
title={props.intl.formatMessage(messages['id.verification.review.title'])}
>
<p>
{props.intl.formatMessage(messages['id.verification.review.description'])}
</p>
<div className="row mb-4">
<div className="col-6">
<label htmlFor="photo-of-face">
{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-inverse-primary shadow"
to={{
pathname: 'take-portrait-photo',
state: { fromSummary: true },
}}
>
{props.intl.formatMessage(messages['id.verification.review.portrait.retake'])}
</Link>
</div>
<div className="col-6">
<label htmlFor="photo-of-id/edit">
{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-inverse-primary shadow"
to={{
pathname: 'take-id-photo',
state: { fromSummary: true },
}}
>
{props.intl.formatMessage(messages['id.verification.review.id.retake'])}
</Link>
</div>
</div>
<div className="form-group">
<label htmlFor="name-to-be-used">
{props.intl.formatMessage(messages['id.verification.account.name.label'])}
</label>
<div className="d-flex">
<Input
id="name-to-be-used"
type="text"
readOnly
value={nameToBeUsed}
onChange={() => {}}
/>
<Link
className="btn btn-link ml-3 px-0"
to={{
pathname: 'get-name-id',
state: { fromSummary: true },
}}
>
{props.intl.formatMessage(messages['id.verification.account.name.edit'])}
</Link>
</div>
</div>
<SubmitButton />
</BasePanel>
);
}
SummaryPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(SummaryPanel);

View File

@@ -0,0 +1,58 @@
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, MEDIA_ACCESS } from '../IdVerificationContext';
import messages from '../IdVerification.messages';
function TakeIdPhotoPanel(props) {
const panelSlug = 'take-id-photo';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const { setIdPhotoFile, idPhotoFile, mediaAccess } = useContext(IdVerificationContext);
const shouldUseCamera = mediaAccess === MEDIA_ACCESS.GRANTED;
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} />
</div>
) : (
<div>
<p>
{props.intl.formatMessage(messages['id.verification.id.photo.instructions.upload'])}
</p>
<ImageFileUpload onFileChange={setIdPhotoFile} />
</div>
)}
</div>
<div className="action-row" style={{ visibility: idPhotoFile ? 'unset' : 'hidden' }}>
<Link to={nextPanelSlug} className="btn btn-primary">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
{shouldUseCamera && <CameraHelp />}
</BasePanel>
);
}
TakeIdPhotoPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(TakeIdPhotoPanel);

View File

@@ -0,0 +1,59 @@
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, MEDIA_ACCESS } from '../IdVerificationContext';
import messages from '../IdVerification.messages';
function TakePortraitPhotoPanel(props) {
const panelSlug = 'take-portrait-photo';
const nextPanelSlug = useNextPanelSlug(panelSlug);
const { setFacePhotoFile, facePhotoFile, mediaAccess } = useContext(IdVerificationContext);
const shouldUseCamera = mediaAccess === MEDIA_ACCESS.GRANTED;
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} />
</div>
) : (
<div>
<p>
{props.intl.formatMessage(messages['id.verification.portrait.photo.instructions.upload'])}
</p>
<ImageFileUpload onFileChange={setFacePhotoFile} />
</div>
)}
</div>
<div className="action-row" style={{ visibility: facePhotoFile ? 'unset' : 'hidden' }}>
<Link to={nextPanelSlug} className="btn btn-primary">
{props.intl.formatMessage(messages['id.verification.next'])}
</Link>
</div>
{shouldUseCamera && <CameraHelp />}
</BasePanel>
);
}
TakePortraitPhotoPanel.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(TakePortraitPhotoPanel);

View File

@@ -0,0 +1,48 @@
import { useContext } from 'react';
import { useLocation } from 'react-router-dom';
import { IdVerificationContext } from './IdVerificationContext';
const panelSteps = [
'review-requirements',
'request-camera-access',
'portrait-photo-context',
'take-portrait-photo',
'id-context',
'take-id-photo',
'get-name-id',
'summary',
'submitted',
];
// eslint-disable-next-line import/prefer-default-export
export const useNextPanelSlug = (originSlug) => {
// Go back to the summary view if that's where they came from
const location = useLocation();
const isFromSummary = location.state && location.state.fromSummary;
if (isFromSummary) {
return 'summary';
}
const nextIndex = panelSteps.indexOf(originSlug) + 1;
return nextIndex < panelSteps.length ? panelSteps[nextIndex] : null;
};
// check if the user is too far into the flow and if so, return the slug of the
// furthest panel they are allow to be.
export const useVerificationRedirectSlug = (slug) => {
const { facePhotoFile, idPhotoFile } = useContext(IdVerificationContext);
const indexOfCurrentPanel = panelSteps.indexOf(slug);
if (!facePhotoFile) {
if (indexOfCurrentPanel > panelSteps.indexOf('take-portrait-photo')) {
return 'portrait-photo-context';
}
} else if (!idPhotoFile) {
if (indexOfCurrentPanel > panelSteps.indexOf('take-id-photo')) {
return 'id-context';
}
}
// The user has satisfied requirements to view the panel they're on.
return null;
};

View File

@@ -11,6 +11,7 @@ import Footer, { messages as footerMessages } from '@edx/frontend-component-foot
import configureStore from './data/configureStore';
import AccountSettingsPage, { NotFoundPage } from './account-settings';
import IdVerificationPage from './id-verification';
import CoachingConsent from './account-settings/coaching/CoachingConsent';
import appMessages from './i18n';
@@ -18,11 +19,11 @@ import './index.scss';
import './assets/favicon.ico';
const HeaderFooterLayout = ({ children }) => (
<div>
<div className="d-flex flex-column" style={{ minHeight: '100vh' }}>
<Header />
<main>
{children}
</main>
<main className="flex-grow-1">
{children}
</main>
<Footer />
</div>
);
@@ -31,11 +32,14 @@ subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={configureStore()}>
<Switch>
<Route path="coaching_consent" component={CoachingConsent} />
<Route path="/coaching_consent" component={CoachingConsent} />
<HeaderFooterLayout>
<Route exact path="" component={AccountSettingsPage} />
<Route path="notfound" component={NotFoundPage} />
<Route path="*" component={NotFoundPage} />
<Switch>
<Route path="/id-verification" component={IdVerificationPage} />
<Route exact path="/" component={AccountSettingsPage} />
<Route path="/notfound" component={NotFoundPage} />
<Route path="*" component={NotFoundPage} />
</Switch>
</HeaderFooterLayout>
</Switch>
</AppProvider>,
@@ -60,6 +64,8 @@ initialize({
mergeConfig({
SUPPORT_URL: process.env.SUPPORT_URL,
COACHING_ENABLED: (process.env.COACHING_ENABLED || false),
ENABLE_DEMOGRAPHICS_COLLECTION: (process.env.ENABLE_DEMOGRAPHICS_COLLECTION || false),
DEMOGRAPHICS_BASE_URL: process.env.DEMOGRAPHICS_BASE_URL,
}, 'App loadConfig override handler');
},
},

View File

@@ -8,6 +8,7 @@ $fa-font-path: "~font-awesome/fonts";
@import "~@edx/frontend-component-footer/dist/footer";
@import "./account-settings/style";
@import "./id-verification/id-verification";
.word-break-all {
word-break: break-all !important;
@@ -50,3 +51,13 @@ $fa-font-path: "~font-awesome/fonts";
font-size: 0.75rem;
}
}
.checkboxOption {
input:focus {
outline: -webkit-focus-ring-color auto 5px;
}
label {
font-weight: normal;
}
}