Compare commits

..

4 Commits

Author SHA1 Message Date
Kyle McCormick
de2a751e36 Fix logout URL 2020-04-15 11:43:01 -04:00
Kyle McCormick
0375ebecda Use devstack frontend-build branch; add dev-build command 2020-04-15 11:43:00 -04:00
Kyle McCormick
f088e2783b Update .env.development for devstack 2020-04-15 11:43:00 -04:00
Kyle McCormick
abc29bbcee Make React routes relative 2020-04-15 11:43:00 -04:00
71 changed files with 6477 additions and 11049 deletions

1
.env
View File

@@ -5,7 +5,6 @@ CSRF_TOKEN_API_PATH=null
ECOMMERCE_BASE_URL=null
LANGUAGE_PREFERENCE_COOKIE_NAME=null
LMS_BASE_URL=null
DEMOGRAPHICS_BASE_URL=null
LOGIN_URL=null
LOGOUT_URL=null
MARKETING_SITE_BASE_URL=null

View File

@@ -1,5 +1,5 @@
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1997'
BASE_URL='localhost:19000/account/'
CREDENTIALS_BASE_URL='http://localhost:18150'
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
@@ -8,15 +8,13 @@ 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:1996/orders'
PORT=1997
ORDER_HISTORY_URL='localhost:19000/ecommerce/orders'
PORT=1997 # For standalone dev server only.
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
SUPPORT_URL='http://localhost:18000/support'
USER_INFO_COOKIE_NAME='edx-user-info'
# Temporary, Remove this once we are ready to release the feature.
COACHING_ENABLED=true
ENABLE_DEMOGRAPHICS_COLLECTION=true
COACHING_ENABLED=''

View File

@@ -6,9 +6,8 @@ ECOMMERCE_BASE_URL='http://localhost:18130'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
LOGOUT_URL='http://localhost:18000/login'
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'
@@ -17,4 +16,3 @@ SITE_NAME='edX'
SUPPORT_URL='http://localhost:18000/support'
USER_INFO_COOKIE_NAME='edx-user-info'
COACHING_ENABLED=''
ENABLE_DEMOGRAPHICS_COLLECTION=''

9207
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,7 @@
},
"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",
@@ -33,18 +34,17 @@
"@edx/frontend-component-header": "2.0.5",
"@edx/frontend-platform": "1.1.14",
"@edx/paragon": "7.1.5",
"@fortawesome/fontawesome-svg-core": "1.2.29",
"@fortawesome/fontawesome-svg-core": "1.2.28",
"@fortawesome/free-brands-svg-icons": "5.8.2",
"@fortawesome/free-regular-svg-icons": "5.7.2",
"@fortawesome/free-solid-svg-icons": "5.8.2",
"@fortawesome/react-fontawesome": "0.1.11",
"@fortawesome/react-fontawesome": "0.1.9",
"babel-polyfill": "6.26.0",
"classnames": "2.2.6",
"font-awesome": "4.7.0",
"form-urlencoded": "4.0.1",
"formdata-polyfill": "3.0.20",
"formdata-polyfill": "3.0.19",
"history": "4.10.1",
"jslib-html5-camera-photo": "^3.1.3",
"lodash.camelcase": "4.3.0",
"lodash.debounce": "4.0.8",
"lodash.findindex": "4.6.0",
@@ -53,12 +53,10 @@
"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",
@@ -76,7 +74,7 @@
"universal-cookie": "4.0.3"
},
"devDependencies": {
"@edx/frontend-build": "2.0.6",
"@edx/frontend-build": "github:edx/frontend-build#kdmccormick/devstack-frontends",
"codecov": "3.6.5",
"enzyme": "3.10.0",
"enzyme-adapter-react-16": "1.15.2",

View File

@@ -31,12 +31,9 @@ import {
YEAR_OF_BIRTH_OPTIONS,
EDUCATION_LEVELS,
GENDER_OPTIONS,
COUNTRY_WITH_STATES,
getStatesList,
} from './data/constants';
import { fetchSiteLanguages } from './site-language';
import CoachingToggle from './coaching/CoachingToggle';
import DemographicsSection from './demographics/DemographicsSection';
class AccountSettingsPage extends React.Component {
constructor(props, context) {
@@ -85,15 +82,11 @@ class AccountSettingsPage extends React.Component {
return concatTimeZoneOptions;
});
getLocalizedOptions = memoize((locale, country) => ({
getLocalizedOptions = memoize(locale => ({
countryOptions: [{
value: '',
label: this.props.intl.formatMessage(messages['account.settings.field.country.options.empty']),
}].concat(getCountryList(locale).map(({ code, name }) => ({ value: code, label: name }))),
stateOptions: [{
value: '',
label: this.props.intl.formatMessage(messages['account.settings.field.state.options.empty']),
}].concat(getStatesList(country)),
languageProficiencyOptions: [{
value: '',
label: this.props.intl.formatMessage(messages['account.settings.field.language_proficiencies.options.empty']),
@@ -216,15 +209,11 @@ class AccountSettingsPage extends React.Component {
// Memoized options lists
const {
countryOptions,
stateOptions,
languageProficiencyOptions,
yearOfBirthOptions,
educationLevelOptions,
genderOptions,
} = this.getLocalizedOptions(this.context.locale, this.props.formValues.country);
// Show State field only if the country is US (could include Canada later)
const showState = this.props.formValues.country == COUNTRY_WITH_STATES;
} = this.getLocalizedOptions(this.context.locale);
const timeZoneOptions = this.getLocalizedTimeZoneOptions(
this.props.timeZoneOptions,
@@ -305,22 +294,6 @@ 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">
@@ -364,9 +337,7 @@ 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'])}
@@ -517,11 +488,11 @@ AccountSettingsPage.propTypes = {
social_link_facebook: PropTypes.string,
social_link_twitter: PropTypes.string,
time_zone: PropTypes.string,
coaching: PropTypes.shape({
coaching_consent: PropTypes.bool.isRequired,
coaching: PropTypes.objectOf(PropTypes.shape({
coaching_consent: PropTypes.string.isRequired,
user: PropTypes.number.isRequired,
eligible_for_coaching: PropTypes.bool.isRequired,
}),
})),
}).isRequired,
siteLanguage: PropTypes.shape({
previousValue: PropTypes.string,

View File

@@ -46,11 +46,6 @@ const messages = defineMessages({
defaultMessage: 'Profile Information',
description: 'The profile information section heading.',
},
'account.settings.section.demographics.information': {
id: 'account.settings.section.demographics.information',
defaultMessage: 'Optional Information',
description: 'The optional information section heading.',
},
'account.settings.section.site.preferences': {
id: 'account.settings.section.site.preferences',
defaultMessage: 'Site Preferences',
@@ -161,21 +156,6 @@ const messages = defineMessages({
defaultMessage: 'Select a Country',
description: 'Option for empty value on account settings country field.',
},
'account.settings.field.state': {
id: 'account.settings.field.state',
defaultMessage: 'State',
description: 'Label for account settings state field.',
},
'account.settings.field.state.empty': {
id: 'account.settings.field.state.empty',
defaultMessage: 'Add state',
description: 'Placeholder for empty account settings state field.',
},
'account.settings.field.state.options.empty': {
id: 'account.settings.field.state.options.empty',
defaultMessage: 'Select a State',
description: 'Option for empty value on account settings state field.',
},
'account.settings.field.site.language': {
id: 'account.settings.field.site.language',
defaultMessage: 'Site language',
@@ -292,7 +272,6 @@ 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,7 +23,6 @@ function EditableField(props) {
emptyLabel,
type,
value,
userSuppliedValue,
options,
saveState,
error,
@@ -67,22 +66,15 @@ 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) {
value = selectedOption.label;
};
if (selectedOption) return selectedOption.label;
}
if (userSuppliedValue) {
value += `: ${userSuppliedValue}`;
}
return value;
return rawValue;
};
const renderConfirmationMessage = () => {
@@ -114,7 +106,6 @@ function EditableField(props) {
options={options}
{...others}
/>
<>{others.children}</>
</ValidationFormGroup>
<p>
<StatefulButton
@@ -171,7 +162,6 @@ 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,7 +4,6 @@ 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 }) {
@@ -14,7 +13,6 @@ function JumpNav({ intl }) {
items={[
'basic-information',
'profile-information',
'demographics-information',
'social-media',
'site-preferences',
'linked-accounts',
@@ -33,13 +31,6 @@ 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,14 +7,12 @@ import { faCheck } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import get from 'lodash.get';
import { getAuthenticatedHttpClient, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import PageLoading from '../PageLoading';
import CoachingConsentForm from './CoachingConsentForm';
import messages from './CoachingConsent.messages';
import LogoSVG from '../../logo.svg';
import { fetchSettings } from '../data/actions';
import { fetchSettings, saveSettings } from '../data/actions';
import { coachingConsentPageSelector } from '../data/selectors';
const Logo = ({ src, alt, ...attributes }) => (
@@ -59,75 +57,104 @@ class CoachingConsent extends React.Component {
formErrors: {},
formSubmitted: false,
declineSubmitted: false,
submissionSuccess: false,
allSubmissionsComplete: false,
};
this.handleSubmit = this.handleSubmit.bind(this);
this.declineCoaching = this.declineCoaching.bind(this);
this.patchUsingCoachingConsentForm = this.patchUsingCoachingConsentForm.bind(this);
}
componentDidMount() {
this.props.fetchSettings();
}
componentDidUpdate(prevProps, prevState) {
/*
When we are submitting the form, we're calling saveSettings 3 times, which causes
multiple parallel redux flows. Because of this we can't rely on just the redux states
being sent in through props. For instance if the coaching submission and name
submission happen in near parallel, the coaching flow could return errors in
formErrors and the name flow could overwrite the formErrors with an empty object.
To minimize disruption to the rest of the app, we're going to manage flow state from
within this component.
*/
// If a new error comes in, store it before the next redux call overwrites it.
let allFormErrors = {};
let allSubmissionsComplete = false;
// Collect new errors and add to state (will be cleared on new submission)
const newErrorsFound = (
this.props.formErrors !== prevProps.formErrors
&& Object.keys(this.props.formErrors).length > 0
);
if (newErrorsFound) {
allFormErrors = Object.assign({}, this.state.formErrors, this.props.formErrors);
}
// Check if all values from the form have confirmation values
if (
this.state.formSubmitted &&
this.props.confirmationValues.coaching &&
this.props.confirmationValues.name &&
this.props.confirmationValues.phone_number
) {
allSubmissionsComplete = true;
}
// Check if all values from the decline link have confirmation values
if (this.props.confirmationValues.coaching && this.state.declineSubmitted) {
allSubmissionsComplete = true;
}
if (newErrorsFound || (allSubmissionsComplete !== prevState.allSubmissionsComplete)) {
this.setState({
formErrors: allFormErrors,
allSubmissionsComplete,
});
}
}
sanitizeForwardingUrl(url) {
// Redirect to root of MFE if invalid next param is sent
return url && url.startsWith(getConfig().LMS_BASE_URL) ? url : `${getConfig().LMS_BASE_URL}/dashboard/`;
}
async patchUsingCoachingConsentForm(body) {
const { userId } = getAuthenticatedUser();
const requestUrl = `${getConfig().LMS_BASE_URL}/api/coaching/v1/coaching_consent/${userId}/`;
let formErrors = {};
const data = await getAuthenticatedHttpClient()
.patch(requestUrl, body)
.catch((error) => {
if (get(error, 'customAttributes.httpErrorResponseData')) {
formErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
} else {
formErrors = { full_name: 'Something went wrong. Please try again.' };
}
this.setState({
submissionSuccess: false,
formErrors,
formSubmitted: false,
});
});
if (get(data, 'status') === 200) {
this.setState({ submissionSuccess: true });
}
}
handleSubmit(e) {
async handleSubmit(e) {
e.preventDefault();
const fullName = e.target.fullName.value;
const phoneNumber = e.target.phoneNumber.value;
const body = {
coaching_consent: true,
consent_form_seen: true,
phone_number: phoneNumber,
full_name: fullName,
};
this.setState({
formErrors: {},
formSubmitted: true,
declineSubmitted: false,
}, () => this.patchUsingCoachingConsentForm(body));
});
// Must store target values or they disappear before the async function can use them.
const fullName = e.target.fullName.value;
const phoneNumber = e.target.phoneNumber.value;
const coachingValues = this.props.formValues.coaching;
// These will overwrite each other's redux states (see componentDidUpdate note)
this.props.saveSettings('name', fullName);
this.props.saveSettings('phone_number', phoneNumber);
this.props.saveSettings('coaching', {
...coachingValues,
phone_number: phoneNumber,
coaching_consent: true,
consent_form_seen: true,
});
}
declineCoaching(e) {
async declineCoaching(e) {
e.preventDefault();
const body = {
coaching_consent: false,
consent_form_seen: true,
};
this.setState({
formErrors: {},
formSubmitted: false,
declineSubmitted: true,
}, () => this.patchUsingCoachingConsentForm(body));
});
// Must store target values or they disappear before the async function can use them.
const coachingValues = this.props.formValues.coaching;
this.props.saveSettings('coaching', {
...coachingValues,
coaching_consent: false,
consent_form_seen: true,
});
}
renderView(currentView) {
@@ -141,7 +168,6 @@ 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..." />;
@@ -165,17 +191,18 @@ class CoachingConsent extends React.Component {
const { loaded } = this.props;
const formHasErrors = Object.keys(this.state.formErrors).length > 0;
let currentView = null;
// This amount of logic was making the template very hard to read, so I broke it out into views.
if (!loaded) {
currentView = VIEWS.NOT_LOADED;
} else if (this.state.formSubmitted && !formHasErrors) {
if (this.state.submissionSuccess) {
if (this.state.allSubmissionsComplete) {
currentView = VIEWS.SUCCESS;
} else {
currentView = VIEWS.SUCCESS_PENDING;
}
} else if (this.state.declineSubmitted && !formHasErrors) {
if (this.state.submissionSuccess) {
if (this.state.allSubmissionsComplete) {
currentView = VIEWS.DECLINED;
} else {
currentView = VIEWS.DECLINE_PENDING;
@@ -233,8 +260,6 @@ AutoRedirect.propTypes = {
CoachingConsent.defaultProps = {
loaded: false,
saveState: undefined,
profileDataManager: null,
};
CoachingConsent.propTypes = {
@@ -259,10 +284,10 @@ CoachingConsent.propTypes = {
phone_number: PropTypes.object,
}).isRequired,
fetchSettings: PropTypes.func.isRequired,
saveState: PropTypes.string,
profileDataManager: PropTypes.string,
saveSettings: PropTypes.func.isRequired,
};
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 can expect a message via email or SMS in the coming days.",
defaultMessage: "You're signed up for coaching. You will receive a text message confirmation.",
description: 'Text announcing that you have signed up and will receive texts',
},
'account.settings.coaching.consent.success.continue': {
@@ -56,11 +56,6 @@ const messages = defineMessages({
defaultMessage: 'Start my course',
description: 'Text that the user will be sent back to the courseware',
},
'account.settings.coaching.managed.support': {
id: 'account.settings.coaching.managed.support',
defaultMessage: 'support',
description: 'website support',
},
});
export default messages;

View File

@@ -1,29 +1,15 @@
import React from 'react';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Input, Button, Hyperlink } from '@edx/paragon';
import PropTypes from 'prop-types';
import Alert from '../Alert';
import messages from './CoachingConsent.messages';
const ErrorMessage = props => (
<div className="alert-warning mb-2">{props.message}</div>
);
const ManagedProfileAlert = ({ profileDataManager }) => (
<Alert className="alert alert-primary" role="alert">
<FormattedMessage
id="account.settings.coaching.managed.alert"
defaultMessage="Your name is managed by {managerTitle}. Contact your administrator for help."
description="alert message informing the user their account data is managed by a third party"
values={{
managerTitle: <b>{profileDataManager}</b>,
}}
/>
</Alert>
);
const CoachingForm = props => (
<div className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg">
<h2 className="h2">
@@ -33,17 +19,12 @@ const CoachingForm = props => (
<div>
<form onSubmit={props.onSubmit}>
<div className="py-3">
{
!!props.profileDataManager &&
<ManagedProfileAlert profileDataManager={props.profileDataManager} />
}
<ErrorMessage message={props.formErrors.full_name} />
<ErrorMessage message={props.formErrors.name} />
<label className="h6" htmlFor="fullName">{props.intl.formatMessage(messages['account.settings.coaching.consent.label.name'])}</label>
<Input
type="text"
name="full-name"
id="fullName"
disabled={!!props.profileDataManager}
defaultValue={props.formValues.name}
/>
</div>
@@ -52,7 +33,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="phone_number"
name="full-name"
id="phoneNumber"
defaultValue={props.formValues.phone_number}
/>
@@ -106,11 +87,10 @@ CoachingForm.propTypes = {
}).isRequired,
formErrors: PropTypes.shape({
coaching: PropTypes.string,
full_name: PropTypes.string,
name: PropTypes.string,
phone_number: PropTypes.string,
}),
redirectUrl: PropTypes.string.isRequired,
profileDataManager: PropTypes.string.isRequired,
};
ErrorMessage.defaultProps = {
@@ -121,9 +101,4 @@ 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, saveMultipleSettings } from '../data/actions';
import { saveSettings, updateDraft } from '../data/actions';
import EditableField from '../EditableField';
@@ -18,25 +18,7 @@ const CoachingToggle = props => (
label={props.intl.formatMessage(messages['account.settings.field.phone_number'])}
emptyLabel={props.intl.formatMessage(messages['account.settings.field.phone_number.empty'])}
onChange={props.updateDraft}
onSubmit={() => {
const { coaching } = props;
if (coaching.coaching_consent === true) {
return props.saveMultipleSettings([
{
formId: 'coaching',
commitValues: {
...coaching,
phone_number: props.phone_number,
},
},
{
formId: 'phone_number',
commitValues: props.phone_number,
},
]);
}
return props.saveSettings('phone_number', props.phone_number);
}}
onSubmit={props.saveSettings}
/>
<ValidationFormGroup
for="coachingConsent"
@@ -55,11 +37,9 @@ const CoachingToggle = props => (
value={props.coaching.coaching_consent}
onChange={async (e) => {
const { name } = e.target;
// eslint-disable-next-line camelcase
const { user, eligible_for_coaching } = props.coaching;
const value = {
user,
eligible_for_coaching,
...props.coaching,
phone_number: props.phone_number,
coaching_consent: e.target.checked,
};
props.saveSettings(name, value);
@@ -73,20 +53,18 @@ const CoachingToggle = props => (
CoachingToggle.defaultProps = {
phone_number: '',
error: '',
saveState: undefined,
};
CoachingToggle.propTypes = {
name: PropTypes.string.isRequired,
error: PropTypes.string,
coaching: PropTypes.shape({
coaching_consent: PropTypes.bool.isRequired,
coaching: PropTypes.objectOf(PropTypes.shape({
coaching_consent: PropTypes.string.isRequired,
user: PropTypes.number.isRequired,
eligible_for_coaching: PropTypes.bool.isRequired,
}).isRequired,
saveState: PropTypes.oneOf(['default', 'pending', 'complete', 'error']),
})).isRequired,
saveState: PropTypes.func.isRequired,
saveSettings: PropTypes.func.isRequired,
saveMultipleSettings: PropTypes.func.isRequired,
updateDraft: PropTypes.func.isRequired,
intl: intlShape.isRequired,
phone_number: PropTypes.string,
@@ -95,5 +73,4 @@ 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 to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.',
defaultMessage: 'MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available in English and Spanish languages. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.',
description: 'A tooltip explaining what coaching is and who it is for',
},
'account.settings.field.coaching_consent.error': {

View File

@@ -1,6 +1,5 @@
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import get from 'lodash.get';
/**
* get all settings related to the coaching plugin. Settings used
@@ -8,20 +7,21 @@ import get from 'lodash.get';
* @param {Number} userId users are identified in the api by LMS id
*/
export async function getCoachingPreferences(userId) {
let data = {};
let data = null;
try {
({ data } = await getAuthenticatedHttpClient()
.get(`${getConfig().LMS_BASE_URL}/api/coaching/v1/users/${userId}/`));
} catch (error) {
// If a user isn't active the API call will fail with a lack of credentials.
// Default values so the client doesn't fail if the user doesn't have an entry in the
// UserCoaching model yet, with the assumption that they'll be eligible for coaching
// when they hit this form.
data = {
coaching_consent: false,
user: userId,
eligible_for_coaching: false,
eligible_for_coaching: true,
consent_form_seen: false,
};
}
return data;
}
@@ -40,11 +40,9 @@ export async function patchCoachingPreferences(userId, commitValues) {
.catch((error) => {
const apiError = Object.create(error);
apiError.fieldErrors = JSON.parse(error.customAttributes.httpErrorResponseData);
if (get(apiError, 'fieldErrors.phone_number')) {
// eslint-disable-next-line prefer-destructuring
apiError.fieldErrors.coaching = apiError.fieldErrors.phone_number[0];
delete apiError.fieldErrors.phone_number;
}
// eslint-disable-next-line prefer-destructuring
apiError.fieldErrors.coaching = apiError.fieldErrors.phone_number[0];
delete apiError.fieldErrors.phone_number;
throw apiError;
});
return commitValues;

View File

@@ -1,104 +0,0 @@
import React from 'react';
import { Provider } from 'react-redux';
import renderer from 'react-test-renderer';
import { act } from 'react-dom/test-utils';
import configureStore from 'redux-mock-store';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import * as auth from '@edx/frontend-platform/auth';
import CoachingConsent from '../CoachingConsent';
import * as selectors from '../../data/selectors';
jest.mock('@edx/frontend-platform/auth');
const IntlCoachingConsent = injectIntl(CoachingConsent);
jest.mock('../../data/selectors', () => {
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

@@ -1,282 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CoachingConsent disables name field on enterprise user 1`] = `
<main>
<div
className="w-100 d-flex justify-content-center align-items-center shadow coaching-header"
>
<img
alt="Logo"
className="logo"
src="icon/mock/path"
/>
</div>
<div
className="col-12 col-md-6 col-xl-5 mx-auto mt-4 p-5 shadow-lg"
>
<h2
className="h2"
>
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,7 +2,6 @@ import { AsyncActionType } from './utils';
export const FETCH_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'FETCH_SETTINGS');
export const SAVE_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'SAVE_SETTINGS');
export const SAVE_MULTIPLE_SETTINGS = new AsyncActionType('ACCOUNT_SETTINGS', 'SAVE_MULTIPLE_SETTINGS');
export const FETCH_TIME_ZONES = new AsyncActionType('ACCOUNT_SETTINGS', 'FETCH_TIME_ZONES');
export const SAVE_PREVIOUS_SITE_LANGUAGE = 'SAVE_PREVIOUS_SITE_LANGUAGE';
export const OPEN_FORM = 'OPEN_FORM';
@@ -100,25 +99,6 @@ export const savePreviousSiteLanguage = previousSiteLanguage => ({
payload: { previousSiteLanguage },
});
export const saveMultipleSettings = (settingsArray, form = null) => ({
type: SAVE_MULTIPLE_SETTINGS.BASE,
payload: { settingsArray, form },
});
export const saveMultipleSettingsBegin = () => ({
type: SAVE_MULTIPLE_SETTINGS.BEGIN,
});
export const saveMultipleSettingsSuccess = settingsArray => ({
type: SAVE_MULTIPLE_SETTINGS.SUCCESS,
payload: { settingsArray },
});
export const saveMultipleSettingsFailure = ({ fieldErrors, message }) => ({
type: SAVE_MULTIPLE_SETTINGS.FAILURE,
payload: { errors: fieldErrors, message },
});
// FETCH TIME_ZONE ACTIONS
export const fetchTimeZones = country => ({

View File

@@ -31,173 +31,4 @@ export const GENDER_OPTIONS = [
'o',
];
export const COUNTRY_WITH_STATES = 'US';
export const TRANSIFEX_LANGUAGE_BASE_URL = 'https://www.transifex.com/open-edx/edx-platform/language/';
const COUNTRY_STATES_MAP = {
CA: [
{ value: 'AB', label: 'Alberta' },
{ value: 'BC', label: 'British Columbia' },
{ value: 'MB', label: 'Manitoba' },
{ value: 'NB', label: 'New Brunswick' },
{ value: 'NL', label: 'Newfoundland and Labrador' },
{ value: 'NS', label: 'Nova Scotia' },
{ value: 'NT', label: 'Northwest Territories' },
{ value: 'NU', label: 'Nunavut' },
{ value: 'ON', label: 'Ontario' },
{ value: 'PE', label: 'Prince Edward Island' },
{ value: 'QC', label: 'Québec' },
{ value: 'SK', label: 'Saskatchewan' },
{ value: 'YT', label: 'Yukon' },
],
US: [
{ value: 'AL', label: 'Alabama' },
{ value: 'AK', label: 'Alaska' },
{ value: 'AZ', label: 'Arizona' },
{ value: 'AR', label: 'Arkansas' },
{ value: 'AA', label: 'Armed Forces Americas' },
{ value: 'AE', label: 'Armed Forces Europe' },
{ value: 'AP', label: 'Armed Forces Pacific' },
{ value: 'CA', label: 'California' },
{ value: 'CO', label: 'Colorado' },
{ value: 'CT', label: 'Connecticut' },
{ value: 'DE', label: 'Delaware' },
{ value: 'DC', label: 'District Of Columbia' },
{ value: 'FL', label: 'Florida' },
{ value: 'GA', label: 'Georgia' },
{ value: 'HI', label: 'Hawaii' },
{ value: 'ID', label: 'Idaho' },
{ value: 'IL', label: 'Illinois' },
{ value: 'IN', label: 'Indiana' },
{ value: 'IA', label: 'Iowa' },
{ value: 'KS', label: 'Kansas' },
{ value: 'KY', label: 'Kentucky' },
{ value: 'LA', label: 'Louisiana' },
{ value: 'ME', label: 'Maine' },
{ value: 'MD', label: 'Maryland' },
{ value: 'MA', label: 'Massachusetts' },
{ value: 'MI', label: 'Michigan' },
{ value: 'MN', label: 'Minnesota' },
{ value: 'MS', label: 'Mississippi' },
{ value: 'MO', label: 'Missouri' },
{ value: 'MT', label: 'Montana' },
{ value: 'NE', label: 'Nebraska' },
{ value: 'NV', label: 'Nevada' },
{ value: 'NH', label: 'New Hampshire' },
{ value: 'NJ', label: 'New Jersey' },
{ value: 'NM', label: 'New Mexico' },
{ value: 'NY', label: 'New York' },
{ value: 'NC', label: 'North Carolina' },
{ value: 'ND', label: 'North Dakota' },
{ value: 'OH', label: 'Ohio' },
{ value: 'OK', label: 'Oklahoma' },
{ value: 'OR', label: 'Oregon' },
{ value: 'PA', label: 'Pennsylvania' },
{ value: 'RI', label: 'Rhode Island' },
{ value: 'SC', label: 'South Carolina' },
{ value: 'SD', label: 'South Dakota' },
{ value: 'TN', label: 'Tennessee' },
{ value: 'TX', label: 'Texas' },
{ value: 'UT', label: 'Utah' },
{ value: 'VT', label: 'Vermont' },
{ value: 'VA', label: 'Virginia' },
{ value: 'WA', label: 'Washington' },
{ value: 'WV', label: 'West Virginia' },
{ value: 'WI', label: 'Wisconsin' },
{ value: 'WY', label: 'Wyoming' },
],
};
export function getStatesList(country) {
return country && COUNTRY_STATES_MAP[country.toUpperCase()];
}
export const 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,7 +7,6 @@ import {
SAVE_PREVIOUS_SITE_LANGUAGE,
UPDATE_DRAFT,
RESET_DRAFTS,
SAVE_MULTIPLE_SETTINGS,
} from './actions';
import { reducer as deleteAccountReducer, DELETE_ACCOUNT } from '../delete-account';
@@ -145,24 +144,6 @@ const reducer = (state = defaultState, action) => {
...state,
previousSiteLanguage: action.payload.previousSiteLanguage,
};
case SAVE_MULTIPLE_SETTINGS.BEGIN:
return {
...state,
saveState: 'pending',
};
case SAVE_MULTIPLE_SETTINGS.SUCCESS:
return {
...state,
saveState: 'complete',
};
case SAVE_MULTIPLE_SETTINGS.FAILURE:
return {
...state,
saveState: 'error',
errors: Object.assign({}, state.errors, action.payload.errors),
};
case FETCH_TIME_ZONES.SUCCESS:
return {
@@ -196,7 +177,6 @@ const reducer = (state = defaultState, action) => {
case RESET_PASSWORD.BEGIN:
case RESET_PASSWORD.SUCCESS:
case RESET_PASSWORD.FORBIDDEN:
return {
...state,
resetPassword: resetPasswordReducer(state.resetPassword, action),

View File

@@ -12,7 +12,6 @@ import {
fetchSettingsFailure,
closeForm,
SAVE_SETTINGS,
SAVE_MULTIPLE_SETTINGS,
saveSettingsBegin,
saveSettingsSuccess,
saveSettingsFailure,
@@ -20,9 +19,6 @@ import {
FETCH_TIME_ZONES,
fetchTimeZones,
fetchTimeZonesSuccess,
saveMultipleSettingsBegin,
saveMultipleSettingsSuccess,
saveMultipleSettingsFailure,
} from './actions';
// Sub-modules
@@ -104,35 +100,6 @@ 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));
@@ -142,7 +109,6 @@ 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,5 +1,6 @@
import { createSelector, createStructuredSelector } from 'reselect';
import { siteLanguageListSelector, siteLanguageOptionsSelector } from '../site-language';
import { siteLanguageOptionsSelector, siteLanguageListSelector } from '../site-language';
export const storeName = 'accountSettings';
@@ -166,7 +167,6 @@ export const coachingConsentPageSelector = createSelector(
accountSettingsSelector,
formValuesSelector,
activeAccountSelector,
profileDataManagerSelector,
saveStateSelector,
confirmationValuesSelector,
errorSelector,
@@ -174,7 +174,6 @@ export const coachingConsentPageSelector = createSelector(
accountSettings,
formValues,
activeAccount,
profileDataManager,
saveState,
confirmationValues,
errors,
@@ -183,25 +182,9 @@ export const coachingConsentPageSelector = createSelector(
loaded: accountSettings.loaded,
loadingError: accountSettings.loadingError,
isActive: activeAccount,
profileDataManager,
formValues,
saveState,
confirmationValues,
formErrors: errors,
}),
);
export const demographicsSectionSelector = createSelector(
formValuesSelector,
draftsSelector,
errorSelector,
(
formValues,
drafts,
errors,
) => ({
formValues,
drafts,
formErrors: errors,
}),
);

View File

@@ -1,14 +1,12 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import pick from 'lodash.pick';
import pickBy from 'lodash.pickby';
import omit from 'lodash.omit';
import isEmpty from 'lodash.isempty';
import { handleRequestError, unpackFieldErrors } from './utils';
import { getThirdPartyAuthProviders } from '../third-party-auth';
import { getCoachingPreferences, patchCoachingPreferences } from '../coaching/data/service';
import { getDemographics, patchDemographics } from '../demographics/data/service';
const SOCIAL_PLATFORMS = [
{ id: 'twitter', key: 'social_link_twitter' },
@@ -139,11 +137,12 @@ export async function getProfileDataManager(username, userRoles) {
const url = `${getConfig().LMS_BASE_URL}/enterprise/api/v1/enterprise-learner/?username=${username}`;
const { data } = await getAuthenticatedHttpClient().get(url).catch(handleRequestError);
if (data.results.length > 0) {
const enterprise = data.results[0] && data.results[0].enterprise_customer;
// To ensure that enterprise returned is current enterprise & it manages profile settings
if (enterprise && enterprise.sync_learner_profile_data) {
return enterprise.name;
if ('results' in data) {
for (let i = 0; i < data.results.length; i += 1) {
const enterprise = data.results[i].enterprise_customer;
if (enterprise.sync_learner_profile_data) {
return enterprise.name;
}
}
}
}
@@ -163,7 +162,6 @@ export async function getSettings(username, userRoles, userId) {
getProfileDataManager(username, userRoles),
getTimeZones(),
getConfig().COACHING_ENABLED && getCoachingPreferences(userId),
getConfig().ENABLE_DEMOGRAPHICS_COLLECTION && getDemographics(userId),
]);
return {
@@ -173,7 +171,6 @@ export async function getSettings(username, userRoles, userId) {
profileDataManager: results[3],
timeZones: results[4],
coaching: results[5],
...results[6], // demographics
};
}
@@ -187,11 +184,9 @@ export async function patchSettings(username, commitValues, userId) {
// user/v1/preferences where it does update. This is the one we use.
const preferenceKeys = ['time_zone'];
const coachingKeys = ['coaching'];
const isDemographicsKey = (value, key) => key.includes('demographics');
const accountCommitValues = omit(commitValues, preferenceKeys, coachingKeys);
const accountCommitValues = omit(commitValues, preferenceKeys);
const preferenceCommitValues = pick(commitValues, preferenceKeys);
const coachingCommitValues = pick(commitValues, coachingKeys);
const demographicsCommitValues = pickBy(commitValues, isDemographicsKey);
const patchRequests = [];
if (!isEmpty(accountCommitValues)) {
@@ -203,9 +198,6 @@ export async function patchSettings(username, commitValues, userId) {
if (!isEmpty(coachingCommitValues)) {
patchRequests.push(patchCoachingPreferences(userId, coachingCommitValues));
}
if (!isEmpty(demographicsCommitValues)) {
patchRequests.push(patchDemographics(userId, demographicsCommitValues));
}
const results = await Promise.all(patchRequests);
// Assigns in order of requests. Preference keys

View File

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

View File

@@ -12,7 +12,6 @@ describe('AsyncActionType', () => {
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
expect(actionType.FORBIDDEN).toBe('HOUSE_CATS__START_THE_RACE__FORBIDDEN');
});
});

View File

@@ -19,8 +19,6 @@ export class ConfirmationModal extends Component {
switch (reason) {
case 'empty-password':
return 'account.settings.delete.account.error.no.password';
case 'invalid-password':
return 'account.settings.delete.account.error.invalid.password';
default:
return 'account.settings.delete.account.error.unable.to.delete';
}

View File

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

View File

@@ -91,11 +91,6 @@ const messages = defineMessages({
defaultMessage: 'A password is required',
description: 'Error message when user has not entered their password',
},
'account.settings.delete.account.error.invalid.password': {
id: 'account.settings.delete.account.error.invalid.password',
defaultMessage: 'Password is incorrect',
description: 'Error message when user has entered incorrect password',
},
'account.settings.delete.account.error.unable.to.delete.details': {
id: 'account.settings.delete.account.error.unable.to.delete.details',
defaultMessage: 'Sorry, there was an error trying to process your request. Please try again later.',

View File

@@ -1,78 +0,0 @@
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

@@ -1,308 +0,0 @@
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

@@ -1,456 +0,0 @@
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

@@ -1,102 +0,0 @@
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

@@ -1,50 +0,0 @@
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

@@ -1,128 +0,0 @@
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

@@ -1,3713 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DemographicsSection should render 1`] = `
<div
className="account-section"
id="demographics-information"
>
<h2
className="section-heading"
>
Optional Information
</h2>
<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

@@ -1,24 +0,0 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons';
import Alert from '../Alert';
const RequestInProgressAlert = (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,7 +7,6 @@ import { StatefulButton } from '@edx/paragon';
import { resetPassword } from './data/actions';
import messages from './messages';
import ConfirmationAlert from './ConfirmationAlert';
import RequestInProgressAlert from './RequestInProgressAlert';
const ResetPassword = (props) => {
const { email, intl, status } = props;
@@ -44,7 +43,6 @@ const ResetPassword = (props) => {
/>
</p>
{status === 'complete' ? <ConfirmationAlert email={email} /> : null}
{status === 'forbidden' ? <RequestInProgressAlert /> : null}
</div>
);
};

View File

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

View File

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

View File

@@ -1,20 +1,12 @@
import { put, call, takeEvery } from 'redux-saga/effects';
import { resetPasswordBegin, resetPasswordForbidden, resetPasswordSuccess, RESET_PASSWORD } from './actions';
import { resetPasswordBegin, resetPasswordSuccess, RESET_PASSWORD } from './actions';
import { postResetPassword } from './service';
function* handleResetPassword(action) {
yield put(resetPasswordBegin());
try {
const response = yield call(postResetPassword, action.payload.email);
yield put(resetPasswordSuccess(response));
} catch (error) {
if (error.response && error.response.status === 403) {
yield put(resetPasswordForbidden(error));
} else {
throw error;
}
}
const response = yield call(postResetPassword, action.payload.email);
yield put(resetPasswordSuccess(response));
}
export default function* saga() {

View File

@@ -11,7 +11,6 @@
"account.settings.section.account.information": "Account Information",
"account.settings.section.account.information.description": "These settings include basic information about your account.",
"account.settings.section.profile.information": "Profile Information",
"account.settings.section.demographics.information": "Optional Information",
"account.settings.section.site.preferences": "Site Preferences",
"account.settings.section.linked.accounts": "Linked Accounts",
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to edX.",
@@ -34,9 +33,6 @@
"account.settings.field.country": "Country",
"account.settings.field.country.empty": "Add country",
"account.settings.field.country.options.empty": "Select a Country",
"account.settings.field.state": "State",
"account.settings.field.state.empty": "Add state",
"account.settings.field.state.options.empty": "Select a State",
"account.settings.field.site.language": "Site language",
"account.settings.field.site.language.help.text": "The language used throughout this site. This site is currently available in a limited number of languages.",
"account.settings.field.education": "Education",
@@ -89,14 +85,12 @@
"account.settings.coaching.consent.label.name": "Please confirm your name",
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
"account.settings.coaching.consent.success.header": "Success!",
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You will receive a text message confirmation.",
"account.settings.coaching.consent.success.continue": "Start my course",
"account.settings.coaching.managed.support": "support",
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
"account.settings.field.phone_number": "Phone Number",
"account.settings.field.phone_number.empty": "Add a phone number",
"account.settings.field.coaching_consent": "Coaching consent",
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.",
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available in English and Spanish languages. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.",
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
"account.settings.delete.account.header": "Delete My Account",
@@ -117,180 +111,19 @@
"account.settings.delete.account.modal.confirm.cancel": "Cancel",
"account.settings.delete.account.error.unable.to.delete": "Unable to delete account",
"account.settings.delete.account.error.no.password": "A password is required",
"account.settings.delete.account.error.invalid.password": "Password is incorrect",
"account.settings.delete.account.error.unable.to.delete.details": "Sorry, there was an error trying to process your request. Please try again later.",
"account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.",
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
"account.settings.delete.account.modal.after.button": "Close",
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
"account.settings.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.",
"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}."
"account.settings.sso.no.providers": "No accounts can be linked at this time."
}

View File

@@ -11,7 +11,6 @@
"account.settings.section.account.information": "Información de la cuenta",
"account.settings.section.account.information.description": "Estas configuraciones incluyen información básica sobre tu cuenta.",
"account.settings.section.profile.information": "Información del perfil",
"account.settings.section.demographics.information": "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.",
@@ -34,9 +33,6 @@
"account.settings.field.country": "País",
"account.settings.field.country.empty": "Agregar país",
"account.settings.field.country.options.empty": "Seleccionar un país",
"account.settings.field.state": "Estado",
"account.settings.field.state.empty": "Añada un estado",
"account.settings.field.state.options.empty": "Seleccionar un estado",
"account.settings.field.site.language": "Idioma del sitio",
"account.settings.field.site.language.help.text": "El idioma que se usará para el sitio. Actualmente solo hay disponibilidad de usar un número limitado de idiomas.",
"account.settings.field.education": "Educación",
@@ -78,26 +74,24 @@
"account.settings.editable.field.action.save": "Guardar",
"account.settings.editable.field.action.cancel": "Cancelar",
"account.settings.editable.field.action.edit": "Editar",
"account.settings.static.field.empty": "No hay valor establecido. Contacte su administrador {enterprise} para hacer cambios.",
"account.settings.static.field.empty.no.admin": "No hay valor establecido.",
"account.settings.coaching.consent.welcome.header": "Empecemos",
"account.settings.coaching.consent.welcome.subheader": "Estamos aquí para ustede desde el inicio hasta el final",
"account.settings.coaching.consent.description": "Los programas de MicroBachelors incluyen entrenamiento que se enfoca en su carrera, educación y cómo logrará resultados a través de la comunicación individual con un profesional experimentado. Si está interesado, proporcione la información a continuación y haga clic en \"Enviar\", y nuestro socio asesor se comunicará con usted por correo electrónico y / o mensaje de texto para ayudarlo a avanzar. Los términos y Condiciones aplican.*",
"account.settings.coaching.consent.text-messaging.disclaimer": "* Los servicios de entrenamiento se incluyen sin costo adicional para los alumnos con números de teléfono de EE. UU. El entrenamiento incluye mensajes de texto recurrentes. Se pueden aplicar tarifas por mensajes y datos. Envía STOP para cancelar la suscripción.",
"account.settings.coaching.consent.accept-coaching": "Registrarse para coaching",
"account.settings.coaching.consent.decline-coaching": "Prefiero no ser contactado con servicios de coaching gratuitos.",
"account.settings.coaching.consent.label.name": "Por favor confirme su nombre",
"account.settings.coaching.consent.label.phone-number": "Ingrese su número de teléfono móvil",
"account.settings.coaching.consent.success.header": "¡Éxito!",
"account.settings.coaching.consent.success.message": "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.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.delete.account.before.proceeding": "Antes de continuar, por favor {actionLink}.",
"account.settings.delete.account.header": "Eliminar mi cuenta",
"account.settings.delete.account.subheader": "¡Sentimos que te vayas!",
@@ -108,7 +102,7 @@
"account.settings.delete.account.text.change.instead": "En lugar de eso, ¿quieres cambiar tu correo electrónico, nombre o contraseña?",
"account.settings.delete.account.button": "Eliminar mi cuenta",
"account.settings.delete.account.please.activate": "activar su cuenta",
"account.settings.delete.account.please.unlink": "Desvincular todas las cuentas de redes sociales.",
"account.settings.delete.account.please.unlink": "unlink all social media accounts",
"account.settings.delete.account.modal.header": "¿Está seguro?",
"account.settings.delete.account.modal.text.1": "Has seleccionado “Eliminar mi cuenta”. La eliminación de tu cuenta y datos personales es permanente e irreversible. edX no será capaz de recuperar tu cuenta o los datos que se hayan borrado.",
"account.settings.delete.account.modal.text.2": "Si procedes, no será posible usar esta cuenta para tomar cursos ni en la aplicación móvil de edX, ni en edx.org, ni en cualquier otro sitio hospedado por edX. Esto incluye el acceso a edx.org desde el sistema de tu empleador o universidad, y el acceso a sitios privados ofrecidos por MIT Open Learning, Wharton Executive Education, y Harvard Medical School.",
@@ -117,180 +111,19 @@
"account.settings.delete.account.modal.confirm.cancel": "Cancelar",
"account.settings.delete.account.error.unable.to.delete": "No es posible eliminar esta cuenta",
"account.settings.delete.account.error.no.password": "Se requiere una contraseña",
"account.settings.delete.account.error.invalid.password": "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": "Hubo un problema al desconectar esta Cuenta. Si el problema persiste, contacte soporte.",
"account.settings.sso.account.disconnect.error": "There was a problem disconnecting this account. Contact support if the problem persists.",
"account.settings.sso.unlink.account": "Desvincular la cuenta de {accountName} ",
"account.settings.sso.no.providers": "No se pueden vincular cuentas en este momento.",
"id.verification.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}."
"account.settings.sso.no.providers": "No accounts can be linked at this time."
}

View File

@@ -11,7 +11,6 @@
"account.settings.section.account.information": "Account Information",
"account.settings.section.account.information.description": "These settings include basic information about your account.",
"account.settings.section.profile.information": "Profile Information",
"account.settings.section.demographics.information": "Optional Information",
"account.settings.section.site.preferences": "Site Preferences",
"account.settings.section.linked.accounts": "Linked Accounts",
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to edX.",
@@ -34,9 +33,6 @@
"account.settings.field.country": "Country",
"account.settings.field.country.empty": "Add country",
"account.settings.field.country.options.empty": "Select a Country",
"account.settings.field.state": "State",
"account.settings.field.state.empty": "Add state",
"account.settings.field.state.options.empty": "Select a State",
"account.settings.field.site.language": "Site language",
"account.settings.field.site.language.help.text": "The language used throughout this site. This site is currently available in a limited number of languages.",
"account.settings.field.education": "Education",
@@ -89,14 +85,12 @@
"account.settings.coaching.consent.label.name": "Please confirm your name",
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
"account.settings.coaching.consent.success.header": "Success!",
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You will receive a text message confirmation.",
"account.settings.coaching.consent.success.continue": "Start my course",
"account.settings.coaching.managed.support": "support",
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
"account.settings.field.phone_number": "Phone Number",
"account.settings.field.phone_number.empty": "Add a phone number",
"account.settings.field.coaching_consent": "Coaching consent",
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.",
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available in English and Spanish languages. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.",
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
"account.settings.delete.account.header": "Delete My Account",
@@ -117,180 +111,19 @@
"account.settings.delete.account.modal.confirm.cancel": "Cancel",
"account.settings.delete.account.error.unable.to.delete": "Unable to delete account",
"account.settings.delete.account.error.no.password": "A password is required",
"account.settings.delete.account.error.invalid.password": "Password is incorrect",
"account.settings.delete.account.error.unable.to.delete.details": "Sorry, there was an error trying to process your request. Please try again later.",
"account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.",
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
"account.settings.delete.account.modal.after.button": "Close",
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
"account.settings.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.",
"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}."
"account.settings.sso.no.providers": "No accounts can be linked at this time."
}

View File

@@ -11,7 +11,6 @@
"account.settings.section.account.information": "Account Information",
"account.settings.section.account.information.description": "These settings include basic information about your account.",
"account.settings.section.profile.information": "Profile Information",
"account.settings.section.demographics.information": "Optional Information",
"account.settings.section.site.preferences": "Site Preferences",
"account.settings.section.linked.accounts": "Linked Accounts",
"account.settings.section.linked.accounts.description": "You can link your identity accounts to simplify signing in to edX.",
@@ -34,9 +33,6 @@
"account.settings.field.country": "Country",
"account.settings.field.country.empty": "Add country",
"account.settings.field.country.options.empty": "Select a Country",
"account.settings.field.state": "State",
"account.settings.field.state.empty": "Add state",
"account.settings.field.state.options.empty": "Select a State",
"account.settings.field.site.language": "Site language",
"account.settings.field.site.language.help.text": "The language used throughout this site. This site is currently available in a limited number of languages.",
"account.settings.field.education": "Education",
@@ -89,14 +85,12 @@
"account.settings.coaching.consent.label.name": "Please confirm your name",
"account.settings.coaching.consent.label.phone-number": "Enter your mobile number",
"account.settings.coaching.consent.success.header": "Success!",
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You can expect a message via email or SMS in the coming days.",
"account.settings.coaching.consent.success.message": "You're signed up for coaching. You will receive a text message confirmation.",
"account.settings.coaching.consent.success.continue": "Start my course",
"account.settings.coaching.managed.support": "support",
"account.settings.coaching.managed.alert": "Your name is managed by {managerTitle}. Contact your administrator for help.",
"account.settings.field.phone_number": "Phone Number",
"account.settings.field.phone_number.empty": "Add a phone number",
"account.settings.field.coaching_consent": "Coaching consent",
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available to learners with U.S. mobile phone numbers. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.",
"account.settings.field.coaching_consent.tooltip": "MicroBachelors programs include text message based coaching that helps you pair educational experiences with your career goals through one-on-one advice. Coaching services are included at no additional cost, and are available in English and Spanish languages. Standard messaging rates apply. Text STOP at anytime to opt-out of messages.",
"account.settings.field.coaching_consent.error": "A valid US phone number is required to opt into coaching",
"account.settings.delete.account.before.proceeding": "Before proceeding, please {actionLink}.",
"account.settings.delete.account.header": "Delete My Account",
@@ -117,180 +111,19 @@
"account.settings.delete.account.modal.confirm.cancel": "Cancel",
"account.settings.delete.account.error.unable.to.delete": "Unable to delete account",
"account.settings.delete.account.error.no.password": "A password is required",
"account.settings.delete.account.error.invalid.password": "Password is incorrect",
"account.settings.delete.account.error.unable.to.delete.details": "Sorry, there was an error trying to process your request. Please try again later.",
"account.settings.delete.account.modal.after.header": "We're sorry to see you go! Your account will be deleted shortly.",
"account.settings.delete.account.modal.after.text": "Account deletion, including removal from email lists, may take a few weeks to fully process through our system. If you want to opt-out of emails before then, please unsubscribe from the footer of any email.",
"account.settings.delete.account.modal.after.button": "Close",
"account.settings.delete.account.text.3": "You may also lose access to verified certificates and other program credentials like MicroMasters certificates. If you want to make a copy of these for your records before proceeding with deletion, {actionLink}.",
"account.settings.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.",
"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}."
"account.settings.sso.no.providers": "No accounts can be linked at this time."
}

View File

@@ -1,90 +0,0 @@
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

@@ -1,37 +0,0 @@
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

@@ -1,28 +0,0 @@
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

@@ -1,346 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'id.verification.next': {
id: 'id.verification.next',
defaultMessage: 'Next',
description: 'Next button.',
},
'id.verification.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

@@ -1,90 +0,0 @@
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

@@ -1,84 +0,0 @@
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

@@ -1,27 +0,0 @@
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

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

View File

@@ -1,63 +0,0 @@
.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

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

View File

@@ -1,70 +0,0 @@
import qs from 'qs';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
/**
* Get ID verification status from LMS.
*
* Returns {
* status: String,
* expires: String|null,
* canVerify: Boolean,
* }
*/
export async function getExistingIdVerification() {
const url = `${getConfig().LMS_BASE_URL}/verify_student/status/`;
const requestConfig = {
headers: { Accept: 'application/json' },
};
try {
const response = await getAuthenticatedHttpClient().get(url, requestConfig);
return {
status: response.data.status || null,
expires: response.data.expires || null,
canVerify: response.data.can_verify || false,
};
} catch (e) {
return { status: null, expires: null, canVerify: false };
}
}
/**
* 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

@@ -1,59 +0,0 @@
/**
* This polyfill is from MDN:
* https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
*
* Their description:
* "Here's an example of using navigator.mediaDevices.getUserMedia(), with a
* polyfill to cope with older browsers. Note that this polyfill does not
* correct for legacy differences in constraints syntax, which means constraints
* won't work well across browsers. It is recommended to use the adapter.js
* polyfill instead, which does handle constraints."
*
* Despite the lack of support for differences in constraints we'll use this
* since it's small and simple and we don't have a need for constraints at the
* moment. I've added an export hasGetUserMediaSupport before the polyfill to
* help us understand support before making calls to getUserMedia.
*/
// IIFE to check getUserMedia support. Must be run before the polyfill.
const hasGetUserMediaSupport = (() => {
// Modern API
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
return true;
}
// Deprecated APIs
if (navigator.webkitGetUserMedia || navigator.mozGetUserMedia) {
return true;
}
return false;
})();
// Older browsers might not implement mediaDevices at all, so we set an empty object first
if (navigator.mediaDevices === undefined) {
navigator.mediaDevices = {};
}
// Some browsers partially implement mediaDevices. We can't just assign an object
// with getUserMedia as it would overwrite existing properties.
// Here, we will just add the getUserMedia property if it's missing.
if (navigator.mediaDevices.getUserMedia === undefined) {
// eslint-disable-next-line func-names
navigator.mediaDevices.getUserMedia = function (constraints) {
// First get ahold of the legacy getUserMedia, if present
const getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
// Some browsers just don't implement it - return a rejected promise with an error
// to keep a consistent interface
if (!getUserMedia) {
return Promise.reject(new Error('getUserMedia is not implemented in this browser'));
}
// Otherwise, wrap the call to the old navigator.getUserMedia with a Promise
return new Promise(((resolve, reject) => {
getUserMedia.call(navigator, constraints, resolve, reject);
}));
};
}
// eslint-disable-next-line import/prefer-default-export
export { hasGetUserMediaSupport };

View File

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

View File

@@ -1,43 +0,0 @@
import React, { useRef, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Redirect } from 'react-router';
import { useVerificationRedirectSlug } from '../routing-utilities';
export default function BasePanel({
children,
focusOnMount,
name,
title,
}) {
const headingRef = useRef();
// focus heading element on mount
useEffect(() => {
if (focusOnMount && headingRef.current) {
headingRef.current.focus();
}
}, []);
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

@@ -1,91 +0,0 @@
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

@@ -1,50 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import 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

@@ -1,52 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import 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

@@ -1,88 +0,0 @@
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

@@ -1,78 +0,0 @@
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

@@ -1,30 +0,0 @@
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

@@ -1,126 +0,0 @@
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

@@ -1,58 +0,0 @@
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import ImageFileUpload from '../ImageFileUpload';
import ImagePreview from '../ImagePreview';
import Camera from '../Camera';
import CameraHelp from '../CameraHelp';
import { IdVerificationContext, 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

@@ -1,59 +0,0 @@
import React, { useContext } from 'react';
import { Link } from 'react-router-dom';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useNextPanelSlug } from '../routing-utilities';
import BasePanel from './BasePanel';
import ImageFileUpload from '../ImageFileUpload';
import ImagePreview from '../ImagePreview';
import Camera from '../Camera';
import CameraHelp from '../CameraHelp';
import { IdVerificationContext, 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

@@ -1,48 +0,0 @@
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,7 +11,6 @@ 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';
@@ -19,11 +18,11 @@ import './index.scss';
import './assets/favicon.ico';
const HeaderFooterLayout = ({ children }) => (
<div className="d-flex flex-column" style={{ minHeight: '100vh' }}>
<div>
<Header />
<main className="flex-grow-1">
{children}
</main>
<main>
{children}
</main>
<Footer />
</div>
);
@@ -32,14 +31,11 @@ subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={configureStore()}>
<Switch>
<Route path="/coaching_consent" component={CoachingConsent} />
<Route path="coaching_consent" component={CoachingConsent} />
<HeaderFooterLayout>
<Switch>
<Route path="/id-verification" component={IdVerificationPage} />
<Route exact path="/" component={AccountSettingsPage} />
<Route path="/notfound" component={NotFoundPage} />
<Route path="*" component={NotFoundPage} />
</Switch>
<Route exact path="" component={AccountSettingsPage} />
<Route path="notfound" component={NotFoundPage} />
<Route path="*" component={NotFoundPage} />
</HeaderFooterLayout>
</Switch>
</AppProvider>,
@@ -64,8 +60,6 @@ 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,7 +8,6 @@ $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;
@@ -51,13 +50,3 @@ $fa-font-path: "~font-awesome/fonts";
font-size: 0.75rem;
}
}
.checkboxOption {
input:focus {
outline: -webkit-focus-ring-color auto 5px;
}
label {
font-weight: normal;
}
}